diff --git a/internal/integration_test/engine/adhoc_test.go b/internal/integration_test/engine/adhoc_test.go index 085bceb894..65609a4640 100644 --- a/internal/integration_test/engine/adhoc_test.go +++ b/internal/integration_test/engine/adhoc_test.go @@ -3,7 +3,6 @@ package adhoc import ( "context" _ "embed" - "fmt" "math" "strconv" "testing" @@ -14,13 +13,17 @@ import ( "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" - "github.com/tetratelabs/wazero/internal/watzero" + "github.com/tetratelabs/wazero/internal/wasm/binary" "github.com/tetratelabs/wazero/sys" ) // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") +const ( + i32, i64 = wasm.ValueTypeI32, wasm.ValueTypeI64 +) + var memoryCapacityPages = uint32(2) var compileConfig = wazero.NewCompileConfig(). @@ -77,8 +80,12 @@ var ( unreachableWasm []byte //go:embed testdata/recursive.wasm recursiveWasm []byte + //go:embed testdata/host_memory.wasm + hostMemoryWasm []byte //go:embed testdata/hugestack.wasm hugestackWasm []byte + //go:embed testdata/memory.wasm + memoryWasm []byte //go:embed testdata/reftype_imports.wasm reftypeImportsWasm []byte //go:embed testdata/overflow.wasm @@ -220,19 +227,7 @@ func testHostFuncMemory(t *testing.T, r wazero.Runtime) { require.NoError(t, err) defer host.Close(testCtx) - module, err := r.InstantiateModuleFromBinary(testCtx, wat2wasm(`(module $test - (import "" "store_int" - (func $store_int (param $offset i32) (param $val i64) (result (;errno;) i32))) - (memory $memory 1 1) - (export "memory" (memory $memory)) - (func (param i32) (param i64) (result i32) - local.get 0 - local.get 1 - call 0 - ) - ;; store_int is imported from the environment. - (export "store_int" (func 1)) - )`)) + module, err := r.InstantiateModuleFromBinary(testCtx, hostMemoryWasm) require.NoError(t, err) defer module.Close(testCtx) @@ -273,14 +268,7 @@ func testNestedGoContext(t *testing.T, r wazero.Runtime) { defer imported.Close(testCtx) // Instantiate a module that uses Wasm code to call the host function. - importing, err = r.InstantiateModuleFromBinary(testCtx, wat2wasm(fmt.Sprintf(`(module $%[1]s - (import "%[2]s" "outer" (func $outer (param i32) (result i32))) - (import "%[2]s" "inner" (func $inner (param i32) (result i32))) - (func $call_outer (param i32) (result i32) local.get 0 call $outer) - (export "call->outer" (func $call_outer)) - (func $call_inner (param i32) (result i32) local.get 0 call $inner) - (export "inner" (func $call_inner)) -)`, importingName, importedName))) + importing, err = r.InstantiateModuleFromBinary(testCtx, callOuterInnerWasm(t, importedName, importingName)) require.NoError(t, err) defer importing.Close(testCtx) @@ -310,22 +298,21 @@ func testHostFunctionContextParameter(t *testing.T, r wazero.Runtime) { }, } - imported, err := r.NewModuleBuilder(importedName).ExportFunctions(fns).Instantiate(testCtx, r) - require.NoError(t, err) - defer imported.Close(testCtx) - for test := range fns { t.Run(test, func(t *testing.T) { + imported, err := r.NewModuleBuilder(importedName). + ExportFunction("return_input", fns[test]). + Instantiate(testCtx, r) + require.NoError(t, err) + defer imported.Close(testCtx) + // Instantiate a module that uses Wasm code to call the host function. - importing, err = r.InstantiateModuleFromBinary(testCtx, wat2wasm(fmt.Sprintf(`(module $%[1]s - (import "%[2]s" "%[3]s" (func $%[3]s (param i32) (result i32))) - (func $call_%[3]s (param i32) (result i32) local.get 0 call $%[3]s) - (export "call->%[3]s" (func $call_%[3]s)) -)`, importingName, importedName, test))) + importing, err = r.InstantiateModuleFromBinary(testCtx, + callReturnImportWasm(t, importedName, importingName, i32)) require.NoError(t, err) defer importing.Close(testCtx) - results, err := importing.ExportedFunction("call->"+test).Call(testCtx, math.MaxUint32-1) + results, err := importing.ExportedFunction("call_return_input").Call(testCtx, math.MaxUint32-1) require.NoError(t, err) require.Equal(t, uint64(math.MaxUint32), results[0]) }) @@ -352,67 +339,121 @@ func testHostFunctionNumericParameter(t *testing.T, r wazero.Runtime) { }, } - imported, err := r.NewModuleBuilder(importedName).ExportFunctions(fns).Instantiate(testCtx, r) - require.NoError(t, err) - defer imported.Close(testCtx) - for _, test := range []struct { name string + vt wasm.ValueType input, expected uint64 }{ { name: "i32", + vt: i32, input: math.MaxUint32 - 1, expected: math.MaxUint32, }, { name: "i64", + vt: i64, input: math.MaxUint64 - 1, expected: math.MaxUint64, }, { name: "f32", + vt: wasm.ValueTypeF32, input: api.EncodeF32(math.MaxFloat32 - 1), expected: api.EncodeF32(math.MaxFloat32), }, { name: "f64", + vt: wasm.ValueTypeF64, input: api.EncodeF64(math.MaxFloat64 - 1), expected: api.EncodeF64(math.MaxFloat64), }, } { t.Run(test.name, func(t *testing.T) { + imported, err := r.NewModuleBuilder(importedName). + ExportFunction("return_input", fns[test.name]). + Instantiate(testCtx, r) + require.NoError(t, err) + defer imported.Close(testCtx) + // Instantiate a module that uses Wasm code to call the host function. - importing, err := r.InstantiateModuleFromBinary(testCtx, wat2wasm(fmt.Sprintf(`(module $%[1]s - (import "%[2]s" "%[3]s" (func $%[3]s (param %[3]s) (result %[3]s))) - (func $call_%[3]s (param %[3]s) (result %[3]s) local.get 0 call $%[3]s) - (export "call->%[3]s" (func $call_%[3]s)) -)`, importingName, importedName, test.name))) + importing, err := r.InstantiateModuleFromBinary(testCtx, + callReturnImportWasm(t, importedName, importingName, test.vt)) require.NoError(t, err) defer importing.Close(testCtx) - results, err := importing.ExportedFunction("call->"+test.name).Call(testCtx, test.input) + results, err := importing.ExportedFunction("call_return_input").Call(testCtx, test.input) require.NoError(t, err) require.Equal(t, test.expected, results[0]) }) } } -func callReturnImportWasm(importedModule, importingModule string) []byte { - return wat2wasm(fmt.Sprintf(`(module $%[1]s - ;; test an imported function by re-exporting it - (import "%[2]s" "return_input" (func $return_input (param i32) (result i32))) - (export "return_input" (func $return_input)) - - ;; test wasm, by calling an imported function - (func $call_return_import (param i32) (result i32) local.get 0 call $return_input) - (export "call_return_import" (func $call_return_import)) -)`, importingModule, importedModule)) +func callReturnImportWasm(t *testing.T, importedModule, importingModule string, vt wasm.ValueType) []byte { + // test an imported function by re-exporting it + module := &wasm.Module{ + TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{vt}, Results: []wasm.ValueType{vt}}}, + // (import "%[2]s" "return_input" (func $return_input (param i32) (result i32))) + ImportSection: []*wasm.Import{ + {Module: importedModule, Name: "return_input", Type: wasm.ExternTypeFunc, DescFunc: 0}, + }, + FunctionSection: []wasm.Index{0}, + ExportSection: []*wasm.Export{ + // (export "return_input" (func $return_input)) + {Name: "return_input", Type: wasm.ExternTypeFunc, Index: 0}, + // (export "call_return_input" (func $call_return_input)) + {Name: "call_return_input", Type: wasm.ExternTypeFunc, Index: 1}, + }, + // (func $call_return_input (param i32) (result i32) local.get 0 call $return_input) + CodeSection: []*wasm.Code{ + {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, + }, + NameSection: &wasm.NameSection{ + ModuleName: importingModule, + FunctionNames: wasm.NameMap{ + {Index: 0, Name: "return_input"}, + {Index: 1, Name: "call_return_input"}, + }, + }, + } + require.NoError(t, module.Validate(wasm.Features20220419)) + return binary.EncodeModule(module) } -func wat2wasm(wat string) []byte { - wasm, _ := watzero.Wat2Wasm(wat) - return wasm +func callOuterInnerWasm(t *testing.T, importedModule, importingModule string) []byte { + module := &wasm.Module{ + TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}}}, + // (import "%[2]s" "outer" (func $outer (param i32) (result i32))) + // (import "%[2]s" "inner" (func $inner (param i32) (result i32))) + ImportSection: []*wasm.Import{ + {Module: importedModule, Name: "outer", Type: wasm.ExternTypeFunc, DescFunc: 0}, + {Module: importedModule, Name: "inner", Type: wasm.ExternTypeFunc, DescFunc: 0}, + }, + FunctionSection: []wasm.Index{0, 0}, + ExportSection: []*wasm.Export{ + // (export "call->outer" (func $call_outer)) + {Name: "call->outer", Type: wasm.ExternTypeFunc, Index: 2}, + // (export "inner" (func $call_inner)) + {Name: "inner", Type: wasm.ExternTypeFunc, Index: 3}, + }, + CodeSection: []*wasm.Code{ + // (func $call_outer (param i32) (result i32) local.get 0 call $outer) + {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, + // (func $call_inner (param i32) (result i32) local.get 0 call $inner) + {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 1, wasm.OpcodeEnd}}, + }, + NameSection: &wasm.NameSection{ + ModuleName: importingModule, + FunctionNames: wasm.NameMap{ + {Index: 0, Name: "outer"}, + {Index: 1, Name: "inner"}, + {Index: 2, Name: "call_outer"}, + {Index: 3, Name: "call_inner"}, + }, + }, + } + require.NoError(t, module.Validate(wasm.Features20220419)) + return binary.EncodeModule(module) } func testCloseInFlight(t *testing.T, r wazero.Runtime) { @@ -423,25 +464,25 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) { }{ { // Ex. WASI proc_exit or AssemblyScript abort handler. name: "importing", - function: "call_return_import", + function: "call_return_input", closeImporting: 1, }, // TODO: A module that re-exports a function (ex "return_input") can call it after it is closed! { // Ex. A function that stops the runtime. name: "both", - function: "call_return_import", + function: "call_return_input", closeImporting: 1, closeImported: 2, }, { // Ex. WASI proc_exit or AssemblyScript abort handler. name: "importing", - function: "call_return_import", + function: "call_return_input", closeImporting: 1, closeImportedCode: true, }, { // Ex. WASI proc_exit or AssemblyScript abort handler. name: "importing", - function: "call_return_import", + function: "call_return_input", closeImporting: 1, closeImportedCode: true, closeImportingCode: true, @@ -449,7 +490,7 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) { // TODO: A module that re-exports a function (ex "return_input") can call it after it is closed! { // Ex. A function that stops the runtime. name: "both", - function: "call_return_import", + function: "call_return_input", closeImporting: 1, closeImported: 2, closeImportingCode: true, @@ -488,7 +529,7 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) { defer imported.Close(testCtx) // Import that module. - binary := callReturnImportWasm(imported.Name(), t.Name()+"-importing") + binary := callReturnImportWasm(t, imported.Name(), t.Name()+"-importing", i32) importingCode, err = r.CompileModule(testCtx, binary, compileConfig) require.NoError(t, err) @@ -517,23 +558,7 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) { func testMemOps(t *testing.T, r wazero.Runtime) { // Instantiate a module that manages its memory - memory, err := r.InstantiateModuleFromBinary(testCtx, wat2wasm(`(module $memory - (func $grow (param $delta i32) (result (;previous_size;) i32) local.get 0 memory.grow) - (func $size (result (;size;) i32) memory.size) - - (memory 0) - - (export "size" (func $size)) - (export "grow" (func $grow)) - (export "memory" (memory 0)) - - (func $store (param $offset i32) - local.get 0 ;; memory offset - i64.const 1 - i64.store - ) - (export "store" (func $store)) -)`)) + memory, err := r.InstantiateModuleFromBinary(testCtx, memoryWasm) require.NoError(t, err) defer memory.Close(testCtx) @@ -581,15 +606,21 @@ func testMemOps(t *testing.T, r wazero.Runtime) { } func testMultipleInstantiation(t *testing.T, r wazero.Runtime) { - compiled, err := r.CompileModule(testCtx, wat2wasm(`(module $test - (memory 1) - (func $store - i32.const 1 ;; memory offset - i64.const 1000 ;; expected value - i64.store - ) - (export "store" (func $store)) - )`), compileConfig) + bin := binary.EncodeModule(&wasm.Module{ + TypeSection: []*wasm.FunctionType{{}}, + FunctionSection: []wasm.Index{0}, + MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: 1, IsMaxEncoded: true}, + CodeSection: []*wasm.Code{{ + Body: []byte{ + wasm.OpcodeI32Const, 1, // i32.const 1 ;; memory offset + wasm.OpcodeI64Const, 0xe8, 0x7, // i64.const 1000 ;; expected value + wasm.OpcodeI64Store, 0x3, 0x0, // i64.store + wasm.OpcodeEnd, + }, + }}, + ExportSection: []*wasm.Export{{Name: "store"}}, + }) + compiled, err := r.CompileModule(testCtx, bin, compileConfig) require.NoError(t, err) defer compiled.Close(testCtx) diff --git a/internal/integration_test/engine/hammer_test.go b/internal/integration_test/engine/hammer_test.go index 16faca9066..91b2f830a4 100644 --- a/internal/integration_test/engine/hammer_test.go +++ b/internal/integration_test/engine/hammer_test.go @@ -35,7 +35,7 @@ func closeImportingModuleWhileInUse(t *testing.T, r wazero.Runtime) { require.NoError(t, importing.Close(testCtx)) // Prove a module can be redefined even with in-flight calls. - binary := callReturnImportWasm(imported.Name(), importing.Name()) + binary := callReturnImportWasm(t, imported.Name(), importing.Name(), i32) importing, err := r.InstantiateModuleFromBinary(testCtx, binary) require.NoError(t, err) return imported, importing @@ -55,7 +55,7 @@ func closeImportedModuleWhileInUse(t *testing.T, r wazero.Runtime) { require.NoError(t, err) // Redefine the importing module, which should link to the redefined host module. - binary := callReturnImportWasm(imported.Name(), importing.Name()) + binary := callReturnImportWasm(t, imported.Name(), importing.Name(), i32) importing, err = r.InstantiateModuleFromBinary(testCtx, binary) require.NoError(t, err) @@ -84,7 +84,7 @@ func closeModuleWhileInUse(t *testing.T, r wazero.Runtime, closeFn func(imported defer imported.Close(testCtx) // Import that module. - binary := callReturnImportWasm(imported.Name(), t.Name()+"-importing") + binary := callReturnImportWasm(t, imported.Name(), t.Name()+"-importing", i32) importing, err := r.InstantiateModuleFromBinary(testCtx, binary) require.NoError(t, err) defer importing.Close(testCtx) @@ -93,7 +93,7 @@ func closeModuleWhileInUse(t *testing.T, r wazero.Runtime, closeFn func(imported i := importing // pin the module used inside goroutines hammer.NewHammer(t, P, 1).Run(func(name string) { // In all cases, the importing module is closed, so the error should have that as its module name. - requireFunctionCallExits(t, i.Name(), i.ExportedFunction("call_return_import")) + requireFunctionCallExits(t, i.Name(), i.ExportedFunction("call_return_input")) }, func() { // When all functions are in-flight, re-assign the modules. imported, importing = closeFn(imported, importing) // Unblock all the calls @@ -107,7 +107,7 @@ func closeModuleWhileInUse(t *testing.T, r wazero.Runtime, closeFn func(imported } // If unloading worked properly, a new function call should route to the newly instantiated module. - requireFunctionCall(t, importing.ExportedFunction("call_return_import")) + requireFunctionCall(t, importing.ExportedFunction("call_return_input")) } func requireFunctionCall(t *testing.T, fn api.Function) { diff --git a/internal/integration_test/engine/testdata/host_memory.wasm b/internal/integration_test/engine/testdata/host_memory.wasm new file mode 100644 index 0000000000..e01f9b30f8 Binary files /dev/null and b/internal/integration_test/engine/testdata/host_memory.wasm differ diff --git a/internal/integration_test/engine/testdata/host_memory.wat b/internal/integration_test/engine/testdata/host_memory.wat new file mode 100644 index 0000000000..45b9aa26b8 --- /dev/null +++ b/internal/integration_test/engine/testdata/host_memory.wat @@ -0,0 +1,13 @@ +(module $test + (import "" "store_int" + (func $store_int (param $offset i32) (param $val i64) (result (;errno;) i32))) + (memory $memory 1 1) + (export "memory" (memory $memory)) + (func (param i32) (param i64) (result i32) + local.get 0 + local.get 1 + call 0 + ) + ;; store_int is imported from the environment. + (export "store_int" (func 1)) +) diff --git a/internal/integration_test/engine/testdata/memory.wasm b/internal/integration_test/engine/testdata/memory.wasm new file mode 100644 index 0000000000..c551be7f9d Binary files /dev/null and b/internal/integration_test/engine/testdata/memory.wasm differ diff --git a/internal/integration_test/engine/testdata/memory.wat b/internal/integration_test/engine/testdata/memory.wat new file mode 100644 index 0000000000..00a800e36e --- /dev/null +++ b/internal/integration_test/engine/testdata/memory.wat @@ -0,0 +1,17 @@ +(module $memory + (func $grow (param $delta i32) (result (;previous_size;) i32) local.get 0 memory.grow) + (func $size (result (;size;) i32) memory.size) + + (memory 0) + + (export "size" (func $size)) + (export "grow" (func $grow)) + (export "memory" (memory 0)) + + (func $store (param $offset i32) + local.get 0 ;; memory offset + i64.const 1 + i64.store + ) + (export "store" (func $store)) +) diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go index 626cf2534c..6baa8ae4aa 100644 --- a/internal/integration_test/spectest/spectest.go +++ b/internal/integration_test/spectest/spectest.go @@ -21,7 +21,6 @@ import ( "github.com/tetratelabs/wazero/internal/wasm" binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary" "github.com/tetratelabs/wazero/internal/wasmruntime" - "github.com/tetratelabs/wazero/internal/watzero" ) // TODO: complete porting this to wazero API @@ -316,51 +315,19 @@ func (c command) expectedError() (err error) { return } +// spectestWasm was generated by the following: +// +// cd testdata; wat2wasm --debug-names spectest.wat +// +//go:embed testdata/spectest.wasm +var spectestWasm []byte + // addSpectestModule adds a module that drops inputs and returns globals as 666 per the default test harness. // // See https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/imports.wast // See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25 func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace) { - w, err := watzero.Wat2Wasm(`(module $spectest -(; TODO - (global (export "global_i32") i32) - (global (export "global_i64") i64) - (global (export "global_f32") f32) - (global (export "global_f64") f64) - - (table (export "table") 10 20 funcref) -;) - -;; TODO: revisit inlining after #215 - - (memory 1 2) - (export "memory" (memory 0)) - -;; Note: the following aren't host functions that print to console as it would clutter it. These only drop the inputs. - (func) - (export "print" (func 0)) - - (func (param i32) local.get 0 drop) - (export "print_i32" (func 1)) - - (func (param i64) local.get 0 drop) - (export "print_i64" (func 2)) - - (func (param f32) local.get 0 drop) - (export "print_f32" (func 3)) - - (func (param f64) local.get 0 drop) - (export "print_f64" (func 4)) - - (func (param i32 f32) local.get 0 drop local.get 1 drop) - (export "print_i32_f32" (func 5)) - - (func (param f64 f64) local.get 0 drop local.get 1 drop) - (export "print_f64_f64" (func 6)) -)`) - require.NoError(t, err) - - mod, err := binaryformat.DecodeModule(w, wasm.Features20220419, wasm.MemorySizer) + mod, err := binaryformat.DecodeModule(spectestWasm, wasm.Features20220419, wasm.MemorySizer) require.NoError(t, err) // (global (export "global_i32") i32 (i32.const 666)) diff --git a/internal/integration_test/spectest/testdata/spectest.wasm b/internal/integration_test/spectest/testdata/spectest.wasm new file mode 100644 index 0000000000..786d506363 Binary files /dev/null and b/internal/integration_test/spectest/testdata/spectest.wasm differ diff --git a/internal/integration_test/spectest/testdata/spectest.wat b/internal/integration_test/spectest/testdata/spectest.wat new file mode 100644 index 0000000000..cff583b6d6 --- /dev/null +++ b/internal/integration_test/spectest/testdata/spectest.wat @@ -0,0 +1,37 @@ +(module $spectest +(; TODO + (global (export "global_i32") i32) + (global (export "global_i64") i64) + (global (export "global_f32") f32) + (global (export "global_f64") f64) + + (table (export "table") 10 20 funcref) +;) + +;; TODO: revisit inlining after #215 + + (memory 1 2) + (export "memory" (memory 0)) + +;; Note: the following aren't host functions that print to console as it would clutter it. These only drop the inputs. + (func) + (export "print" (func 0)) + + (func (param i32) local.get 0 drop) + (export "print_i32" (func 1)) + + (func (param i64) local.get 0 drop) + (export "print_i64" (func 2)) + + (func (param f32) local.get 0 drop) + (export "print_f32" (func 3)) + + (func (param f64) local.get 0 drop) + (export "print_f64" (func 4)) + + (func (param i32 f32) local.get 0 drop local.get 1 drop) + (export "print_i32_f32" (func 5)) + + (func (param f64 f64) local.get 0 drop local.get 1 drop) + (export "print_f64_f64" (func 6)) +) diff --git a/internal/integration_test/vs/time/go.mod b/internal/integration_test/vs/time/go.mod index 683367e01c..9db0691bf8 100644 --- a/internal/integration_test/vs/time/go.mod +++ b/internal/integration_test/vs/time/go.mod @@ -1,6 +1,6 @@ module github.com/tetratelabs/wazero/internal/integration_test/vs/clock -go 1.17 +go 1.18 require github.com/tetratelabs/wazero v0.0.0 diff --git a/internal/integration_test/vs/wasmedge/go.mod b/internal/integration_test/vs/wasmedge/go.mod index 2a269da26d..9ef478e325 100644 --- a/internal/integration_test/vs/wasmedge/go.mod +++ b/internal/integration_test/vs/wasmedge/go.mod @@ -1,6 +1,6 @@ module github.com/tetratelabs/wazero/internal/integration_test/vs/wasmedge -go 1.17 +go 1.18 require ( github.com/second-state/WasmEdge-go v0.9.2 diff --git a/internal/integration_test/vs/wasmer/go.mod b/internal/integration_test/vs/wasmer/go.mod index 33dc9904f1..76d110d557 100644 --- a/internal/integration_test/vs/wasmer/go.mod +++ b/internal/integration_test/vs/wasmer/go.mod @@ -1,6 +1,6 @@ module github.com/tetratelabs/wazero/internal/integration_test/vs/wasmer -go 1.17 +go 1.18 require ( github.com/tetratelabs/wazero v0.0.0 diff --git a/internal/integration_test/vs/wasmtime/go.mod b/internal/integration_test/vs/wasmtime/go.mod index 4aa2629de1..8741f9d807 100644 --- a/internal/integration_test/vs/wasmtime/go.mod +++ b/internal/integration_test/vs/wasmtime/go.mod @@ -1,6 +1,6 @@ module github.com/tetratelabs/wazero/internal/integration_test/vs/wasmtime -go 1.17 +go 1.18 require ( github.com/bytecodealliance/wasmtime-go v0.39.0 diff --git a/internal/watzero/internal/decoder.go b/internal/watzero/internal/decoder.go deleted file mode 100644 index f17fa5f5d4..0000000000 --- a/internal/watzero/internal/decoder.go +++ /dev/null @@ -1,787 +0,0 @@ -package internal - -import ( - "errors" - "fmt" - - "github.com/tetratelabs/wazero/internal/wasm" -) - -// parserPosition holds the positional state of a parser. Values are also useful as they allow you to do a reference -// search for all related code including parsers of that position. -type parserPosition byte - -const ( - positionInitial parserPosition = iota - positionFunc - positionType - positionParam - positionResult - positionModule - positionImport - positionImportFunc - positionMemory - positionExport - positionExportFunc - positionExportMemory - positionStart -) - -type callbackPosition byte - -const ( - // callbackPositionUnhandledToken is set on a token besides a paren. - callbackPositionUnhandledToken callbackPosition = iota - // callbackPositionUnhandledField is at the field name (tokenKeyword) which isn't "type", "param" or "result" - callbackPositionUnhandledField - // callbackPositionEndField is at the end (tokenRParen) of the field enclosing the type use. - callbackPositionEndField -) - -// moduleParser parses a single api.Module from WebAssembly 1.0 (20191205) Text format. -// -// Note: The indexNamespace of wasm.SectionIDMemory and wasm.SectionIDTable allow up-to-one item. For example, you -// cannot define both one import and one module-defined memory, rather one or the other (or none). Even if these rules -// 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 { - // 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 - - // pos is used to give an appropriate errorContext - pos parserPosition - - // currentModuleField holds the incremental state of a module-scoped field such as an import. - currentModuleField interface{} - - // typeNamespace represents the function type index namespace, which begins with the TypeSection and ends with any - // inlined type uses which had neither a type index, nor matched an existing type. Elements in the TypeSection can - // can declare symbolic IDs, such as "$v_v", which are resolved here (without the '$' prefix) - typeNamespace *indexNamespace - - // typeParser parses "param" and "result" fields in the TypeSection. - typeParser *typeParser - - // typeParser parses "type", "param" and "result" fields for type uses such as imported or module-defined - // functions. - typeUseParser *typeUseParser - - // funcNamespace represents the function index namespace, which begins with any wasm.ExternTypeFunc in the - // wasm.SectionIDImport followed by the wasm.SectionIDFunction. - // - // Non-abbreviated imported and module-defined functions can declare symbolic IDs, such as "$main", which are - // resolved here (without the '$' prefix). - funcNamespace *indexNamespace - - // funcParser parses the CodeSection for a given module-defined function. - funcParser *funcParser - - // memoryNamespace represents the memory index namespace, which begins with any wasm.ExternTypeMemory in - // the wasm.SectionIDImport followed by the wasm.SectionIDMemory. - // - // Non-abbreviated imported and module-defined memories can declare symbolic IDs, such as "$mem", which are resolved - // here (without the '$' prefix). - memoryNamespace *indexNamespace - - // memoryParser parses the MemorySection for a given module-defined memory. - memoryParser *memoryParser - - // unresolvedExports holds any exports whose type index wasn't resolvable when parsed. - unresolvedExports map[wasm.Index]*wasm.Export - - // field counts can be different from the count in a section when abbreviated imports exist. To give an accurate - // errorContext, we count explicitly. - fieldCountFunc uint32 - - exportedName map[string]struct{} -} - -// DecodeModule implements wasm.DecodeModule for the WebAssembly 1.0 (20191205) Text Format -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-format%E2%91%A0 -func DecodeModule( - source []byte, - enabledFeatures wasm.Features, - memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), -) (module *wasm.Module, err error) { - // TODO: when globals are supported, err on global vars if disabled - - // names are the wasm.Module NameSection - // - // * ModuleName: ex. "test" if (module $test) - // * FunctionNames: nil when neither imported nor module-defined functions had a name - // * LocalNames: nil when neither imported nor module-defined functions had named (param) fields. - names := &wasm.NameSection{} - module = &wasm.Module{NameSection: names} - p := newModuleParser(module, enabledFeatures, memorySizer) - p.source = source - - // A valid source must begin with the token '(', but it could be preceded by whitespace or comments. For this - // reason, we cannot enforce source[0] == '(', and instead need to start the lexer to check the first token. - line, col, err := lex(p.ensureLParen, p.source) - if err != nil { - return nil, &FormatError{line, col, p.errorContext(), err} - } - - // All identifier contexts are now bound, so resolveTypeUses any uses of symbolic identifiers into concrete indices. - if err = p.resolveTypeUses(module); err != nil { - return nil, err - } - if err = p.resolveTypeIndices(module); err != nil { - return nil, err - } - if err = p.resolveFunctionIndices(module); err != nil { - return nil, err - } - - // Don't set the name section unless we parsed a name! - if names.ModuleName == "" && names.FunctionNames == nil && names.LocalNames == nil { - module.NameSection = nil - } - return -} - -func newModuleParser( - module *wasm.Module, - enabledFeatures wasm.Features, - memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), -) *moduleParser { - p := moduleParser{module: module, enabledFeatures: enabledFeatures, - typeNamespace: newIndexNamespace(module.SectionElementCount), - funcNamespace: newIndexNamespace(module.SectionElementCount), - memoryNamespace: newIndexNamespace(module.SectionElementCount), - } - 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(memorySizer, p.memoryNamespace, p.endMemory) - return &p -} - -// ensureLParen errors unless a '(' is found as the text format must start with a field. -func (p *moduleParser) ensureLParen(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenLParen { - return nil, fmt.Errorf("expected '(', but parsed %s: %s", tok, tokenBytes) - } - return p.beginSourceField, nil -} - -// beginSourceField returns parseModuleName if the field name is "module". -func (p *moduleParser) beginSourceField(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenKeyword { - return nil, expectedField(tok) - } - - if string(tokenBytes) != "module" { - return nil, unexpectedFieldName(tokenBytes) - } - - p.pos = positionModule - return p.parseModuleName, nil -} - -// beginModuleField returns a parser according to the module field name (tokenKeyword), or errs if invalid. -func (p *moduleParser) beginModuleField(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok == tokenKeyword { - switch string(tokenBytes) { - case "type": - p.pos = positionType - return p.typeParser.begin, nil - case "import": - p.pos = positionImport - return p.parseImportModule, nil - case wasm.ExternTypeFuncName: - p.pos = positionFunc - return p.funcParser.begin, nil - case wasm.ExternTypeTableName: - return nil, fmt.Errorf("TODO: %s", tokenBytes) - case wasm.ExternTypeMemoryName: - if p.module.SectionElementCount(wasm.SectionIDMemory) > 0 { - return nil, moreThanOneInvalidInSection(wasm.SectionIDMemory) - } - p.pos = positionMemory - return p.memoryParser.begin, nil - case wasm.ExternTypeGlobalName: - return nil, fmt.Errorf("TODO: %s", tokenBytes) - case "export": - p.pos = positionExport - return p.parseExportName, nil - case "start": - if p.module.SectionElementCount(wasm.SectionIDStart) > 0 { - return nil, moreThanOneInvalidInSection(wasm.SectionIDStart) - } - p.pos = positionStart - return p.parseStart, nil - case "elem": - return nil, errors.New("TODO: elem") - case "data": - return nil, errors.New("TODO: data") - default: - return nil, unexpectedFieldName(tokenBytes) - } - } - return nil, expectedField(tok) -} - -// parseModuleName records the wasm.NameSection ModuleName, if present, and resumes with parseModule. -// -// Ex. A module name is present `(module $math)` -// -// records math --^ -// -// Ex. No module name `(module)` -// -// calls parseModule here --^ -func (p *moduleParser) parseModuleName(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - if tok == tokenID { // Ex. $Math - p.module.NameSection.ModuleName = string(stripDollar(tokenBytes)) - return p.parseModule, nil - } - return p.parseModule(tok, tokenBytes, line, col) -} - -// parseModule returns beginModuleField on the start of a field '(' or parseUnexpectedTrailingCharacters if the module -// is complete ')'. -func (p *moduleParser) parseModule(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenID: - return nil, fmt.Errorf("redundant ID %s", tokenBytes) - case tokenLParen: - return p.beginModuleField, nil - case tokenRParen: // end of module - p.pos = positionInitial - return p.parseUnexpectedTrailingCharacters, nil // only one module is allowed and nothing else - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// onType adds the current type into the TypeSection and returns parseModule to prepare for the next field. -func (p *moduleParser) onTypeEnd(ft *wasm.FunctionType) tokenParser { - p.module.TypeSection = append(p.module.TypeSection, ft) - p.pos = positionModule - return p.parseModule -} - -// parseImportModule returns parseImportName after recording the import module name, or errs if it couldn't be read. -// -// Ex. Imported module name is present `(import "Math" "PI" (func (result f32)))` -// -// records Math --^ ^ -// parseImportName resumes here --+ -// -// Ex. Imported module name is absent `(import (func (result f32)))` -// -// errs here --^ -func (p *moduleParser) parseImportModule(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenString: // Ex. "" or "Math" - module := string(tokenBytes[1 : len(tokenBytes)-1]) // unquote - p.currentModuleField = &wasm.Import{Module: module} - return p.parseImportName, nil - case tokenLParen, tokenRParen: - return nil, errors.New("missing module and name") - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// parseImportName returns parseImport after recording the import name, or errs if it couldn't be read. -// -// Ex. Import name is present `(import "Math" "PI" (func (result f32)))` -// -// starts here --^^ ^ -// records PI --+ | -// parseImport resumes here --+ -// -// Ex. Imported function name is absent `(import "Math" (func (result f32)))` -// -// errs here --+ -func (p *moduleParser) parseImportName(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenString: // Ex. "" or "PI" - name := string(tokenBytes[1 : len(tokenBytes)-1]) // unquote - (p.currentModuleField.(*wasm.Import)).Name = name - return p.parseImport, nil - case tokenLParen, tokenRParen: - return nil, errors.New("missing name") - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// parseImport returns beginImportDesc to determine the wasm.ExternType and dispatch accordingly. -func (p *moduleParser) parseImport(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenString: // Ex. (import "Math" "PI" "PI" - return nil, fmt.Errorf("redundant name: %s", tokenBytes[1:len(tokenBytes)-1]) // unquote - case tokenLParen: // start fields, ex. (func - return p.beginImportDesc, nil - case tokenRParen: // end of this import - return nil, errors.New("missing description field") // Ex. missing (func): (import "Math" "Pi") - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// beginImportDesc returns a parser according to the import field name (tokenKeyword), or errs if invalid. -func (p *moduleParser) beginImportDesc(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenKeyword { - return nil, expectedField(tok) - } - - switch string(tokenBytes) { - case wasm.ExternTypeFuncName: - if p.module.SectionElementCount(wasm.SectionIDFunction) > 0 { - return nil, importAfterModuleDefined(wasm.SectionIDFunction) - } - p.pos = positionImportFunc - return p.parseImportFuncID, nil - case wasm.ExternTypeTableName, wasm.ExternTypeMemoryName, wasm.ExternTypeGlobalName: - return nil, fmt.Errorf("TODO: %s", tokenBytes) - default: - return nil, unexpectedFieldName(tokenBytes) - } -} - -// parseImportFuncID records the ID of the current imported function, if present, and resumes with parseImportFunc. -// -// Ex. A function ID is present `(import "Math" "PI" (func $math.pi (result f32))` -// -// records math.pi here --^ -// parseImportFunc resumes here --^ -// -// Ex. No function ID `(import "Math" "PI" (func (result f32))` -// -// calls parseImportFunc here --^ -func (p *moduleParser) parseImportFuncID(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - if tok == tokenID { // Ex. $main - if name, err := p.funcNamespace.setID(tokenBytes); err != nil { - return nil, err - } else { - p.addFunctionName(name) - } - return p.parseImportFunc, nil - } - return p.parseImportFunc(tok, tokenBytes, line, col) -} - -// addFunctionName appends the current imported or module-defined function name to the wasm.NameSection iff it is not -// empty. -func (p *moduleParser) addFunctionName(name string) { - if name == "" { - return // there's no value in an empty name - } - na := &wasm.NameAssoc{Index: p.funcNamespace.count, Name: name} - p.module.NameSection.FunctionNames = append(p.module.NameSection.FunctionNames, na) -} - -// parseImportFunc passes control to the typeUseParser until any signature is read, then returns onImportFunc. -// -// Ex. `(import "Math" "PI" (func $math.pi (result f32)))` -// -// starts here --^ ^ -// onImportFunc resumes here --+ -// -// Ex. If there is no signature `(import "" "main" (func))` -// -// calls onImportFunc here ---^ -func (p *moduleParser) parseImportFunc(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - if tok == tokenID { // Ex. (func $main $main) - return nil, fmt.Errorf("redundant ID %s", tokenBytes) - } - return p.typeUseParser.begin(wasm.SectionIDImport, p.onImportFunc, tok, tokenBytes, line, col) -} - -// onImportFunc records the type index and local names of the current imported function, and increments -// funcNamespace as it is shared across imported and module-defined functions. Finally, this returns parseImportEnd to -// the current import into the ImportSection. -func (p *moduleParser) onImportFunc(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - i := p.currentModuleField.(*wasm.Import) - i.Type = wasm.ExternTypeFunc - i.DescFunc = typeIdx - p.addLocalNames(paramNames) - - p.funcNamespace.count++ - - switch pos { - case callbackPositionUnhandledToken: - return nil, unexpectedToken(tok, tokenBytes) - case callbackPositionUnhandledField: - return nil, unexpectedFieldName(tokenBytes) - case callbackPositionEndField: - p.pos = positionImport - return p.parseImportEnd, nil - } - return p.parseImportFuncEnd, nil -} - -// parseImportEnd adds the current import into the ImportSection and returns parseModule to prepare for the next field. -func (p *moduleParser) parseImportFuncEnd(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenRParen { - return nil, unexpectedToken(tok, tokenBytes) - } - - p.pos = positionImport - return p.parseImportEnd, nil -} - -// addLocalNames appends wasm.NameSection LocalNames for the current function. -func (p *moduleParser) addLocalNames(localNames wasm.NameMap) { - if localNames != nil { - na := &wasm.NameMapAssoc{Index: p.funcNamespace.count, NameMap: localNames} - p.module.NameSection.LocalNames = append(p.module.NameSection.LocalNames, na) - } -} - -// parseImportEnd adds the current import into the ImportSection and returns parseModule to prepare for the next field. -func (p *moduleParser) parseImportEnd(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenRParen { - return nil, unexpectedToken(tok, tokenBytes) - } - - p.module.ImportSection = append(p.module.ImportSection, p.currentModuleField.(*wasm.Import)) - p.currentModuleField = nil - p.pos = positionModule - return p.parseModule, nil -} - -// endFunc adds the type index, code and local names for the current function, and increments funcNamespace as it is -// shared across imported and module-defined functions. Finally, this returns parseModule to prepare for the next field. -func (p *moduleParser) endFunc(typeIdx wasm.Index, code *wasm.Code, name string, localNames wasm.NameMap) (tokenParser, error) { - p.addFunctionName(name) - p.module.FunctionSection = append(p.module.FunctionSection, typeIdx) - p.module.CodeSection = append(p.module.CodeSection, code) - p.addLocalNames(localNames) - - // Multiple funcs are allowed, so advance in case there's a next. - p.funcNamespace.count++ - p.fieldCountFunc++ - p.pos = positionModule - return p.parseModule, nil -} - -// endMemory adds the limits for the current memory, and increments memoryNamespace as it is shared across imported and -// module-defined memories. Finally, this returns parseModule to prepare for the next field. -func (p *moduleParser) endMemory(mem *wasm.Memory) tokenParser { - p.module.MemorySection = mem - p.pos = positionModule - return p.parseModule -} - -// parseExportName returns parseExport after recording the export name, or errs if it couldn't be read. -// -// Ex. Export name is present `(export "PI" (func 0))` -// -// starts here --^ ^ -// records PI --^ | -// parseExport resumes here --+ -// -// Ex. Export name is absent `(export (func 0))` -// -// errs here --^ -func (p *moduleParser) parseExportName(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenString: // Ex. "" or "PI" - name := string(tokenBytes[1 : len(tokenBytes)-1]) // strip quotes - if p.exportedName == nil { - p.exportedName = map[string]struct{}{} - } - if _, ok := p.exportedName[name]; ok { - return nil, fmt.Errorf("%q already exported", name) - } else { - p.exportedName[name] = struct{}{} - } - p.currentModuleField = &wasm.Export{Name: name} - return p.parseExport, nil - case tokenLParen, tokenRParen: - return nil, errors.New("missing name") - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// parseExport returns beginExportDesc to determine the wasm.ExternType and dispatch accordingly. -func (p *moduleParser) parseExport(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenString: // Ex. (export "PI" "PI" - return nil, fmt.Errorf("redundant name: %s", tokenBytes[1:len(tokenBytes)-1]) // unquote - case tokenLParen: // start fields, ex. (func - return p.beginExportDesc, nil - case tokenRParen: // end of this export - return nil, errors.New("missing description field") // Ex. missing (func): (export "Math" "Pi") - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// beginExportDesc returns a parser according to the export field name (tokenKeyword), or errs if invalid. -func (p *moduleParser) beginExportDesc(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenKeyword { - return nil, expectedField(tok) - } - - switch string(tokenBytes) { - case wasm.ExternTypeFuncName: - p.pos = positionExportFunc - return p.parseExportDesc, nil - case wasm.ExternTypeMemoryName: - p.pos = positionExportMemory - return p.parseExportDesc, nil - case wasm.ExternTypeTableName, wasm.ExternTypeGlobalName: - return nil, fmt.Errorf("TODO: %s", tokenBytes) - default: - return nil, unexpectedFieldName(tokenBytes) - } -} - -// parseExportDesc records the symbolic or numeric function index of the export target -func (p *moduleParser) parseExportDesc(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - var namespace *indexNamespace - e := p.currentModuleField.(*wasm.Export) - switch p.pos { - case positionExportFunc: - e.Type = wasm.ExternTypeFunc - namespace = p.funcNamespace - case positionExportMemory: - e.Type = wasm.ExternTypeMemory - namespace = p.memoryNamespace - default: - panic(fmt.Errorf("BUG: unhandled parsing state on parseExportDesc: %v", p.pos)) - } - typeIdx, resolved, err := namespace.parseIndex(wasm.SectionIDExport, 0, tok, tokenBytes, line, col) - if err != nil { - return nil, err - } - e.Index = typeIdx - - // All sections in wasm.Module are numeric indices except exports. Hence, we have to special-case here - if !resolved { - eIdx := p.module.SectionElementCount(wasm.SectionIDExport) - if p.unresolvedExports == nil { - p.unresolvedExports = map[wasm.Index]*wasm.Export{eIdx: e} - } else { - p.unresolvedExports[eIdx] = e - } - } - return p.parseExportDescEnd, nil -} - -// parseExportFuncEnd returns parseExportEnd to add the current export. -func (p *moduleParser) parseExportDescEnd(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenUN, tokenID: - return nil, errors.New("redundant index") - case tokenRParen: - p.pos = positionExport - return p.parseExportEnd, nil - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// parseImportEnd adds the current export into the ExportSection and returns parseModule to prepare for the next field. -func (p *moduleParser) parseExportEnd(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenRParen { - return nil, unexpectedToken(tok, tokenBytes) - } - - e := p.currentModuleField.(*wasm.Export) - p.currentModuleField = nil - p.module.ExportSection = append(p.module.ExportSection, e) - p.pos = positionModule - return p.parseModule, nil -} - -// parseStart returns parseStartEnd after recording the start function index, or errs if it couldn't be read. -func (p *moduleParser) parseStart(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - idx, _, err := p.funcNamespace.parseIndex(wasm.SectionIDStart, 0, tok, tokenBytes, line, col) - if err != nil { - return nil, err - } - p.module.StartSection = &idx - return p.parseStartEnd, nil -} - -// parseStartEnd returns parseModule to prepare for the next field. -func (p *moduleParser) parseStartEnd(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenUN, tokenID: - return nil, errors.New("redundant index") - case tokenRParen: - p.pos = positionModule - return p.parseModule, nil - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -func (p *moduleParser) parseUnexpectedTrailingCharacters(_ tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - return nil, fmt.Errorf("unexpected trailing characters: %s", tokenBytes) -} - -// resolveTypeIndices ensures any indices point are numeric or returns a FormatError if they cannot be bound. -func (p *moduleParser) resolveTypeIndices(module *wasm.Module) error { - for _, unresolved := range p.typeNamespace.unresolvedIndices { - target, err := p.typeNamespace.resolve(unresolved) - if err != nil { - return err - } - switch unresolved.section { - case wasm.SectionIDImport: - module.ImportSection[unresolved.idx].DescFunc = target - case wasm.SectionIDFunction: - module.FunctionSection[unresolved.idx] = target - default: - panic(unhandledSection(unresolved.section)) - } - } - return nil -} - -// resolveFunctionIndices ensures any indices point are numeric or returns a FormatError if they cannot be bound. -func (p *moduleParser) resolveFunctionIndices(module *wasm.Module) error { - for _, unresolved := range p.funcNamespace.unresolvedIndices { - target, err := p.funcNamespace.resolve(unresolved) - if err != nil { - return err - } - switch unresolved.section { - case wasm.SectionIDCode: - if target > 255 { - return errors.New("TODO: unresolved function indexes that don't fit in a byte") - } - module.CodeSection[unresolved.idx].Body[unresolved.bodyOffset] = byte(target) - case wasm.SectionIDExport: - p.unresolvedExports[unresolved.idx].Index = target - case wasm.SectionIDStart: - module.StartSection = &target - default: - panic(unhandledSection(unresolved.section)) - } - } - return nil -} - -// resolveTypeUses adds any missing inlined types, resolving any type indexes in the FunctionSection or ImportSection. -// This errs if any type index is unresolved, out of range or mismatches an inlined type use signature. -func (p *moduleParser) resolveTypeUses(module *wasm.Module) error { - inlinedToRealIdx := p.addInlinedTypes() - return p.resolveInlinedTypes(module, inlinedToRealIdx) -} - -func (p *moduleParser) resolveInlinedTypes(module *wasm.Module, inlinedToRealIdx map[wasm.Index]wasm.Index) error { - // Now look for all the uses of the inlined types and apply the mapping above - for _, i := range p.typeUseParser.inlinedTypeIndices { - switch i.section { - case wasm.SectionIDImport: - if i.typePos == nil { - module.ImportSection[i.idx].DescFunc = inlinedToRealIdx[i.inlinedIdx] - continue - } - - typeIdx := module.ImportSection[i.idx].DescFunc - if err := p.requireInlinedMatchesReferencedType(module.TypeSection, typeIdx, i); err != nil { - return err - } - case wasm.SectionIDFunction: - if i.typePos == nil { - module.FunctionSection[i.idx] = inlinedToRealIdx[i.inlinedIdx] - continue - } - - typeIdx := module.FunctionSection[i.idx] - if err := p.requireInlinedMatchesReferencedType(module.TypeSection, typeIdx, i); err != nil { - return err - } - default: - panic(unhandledSection(i.section)) - } - } - return nil -} - -func (p *moduleParser) requireInlinedMatchesReferencedType(typeSection []*wasm.FunctionType, typeIdx wasm.Index, i *inlinedTypeIndex) error { - inlined := p.typeUseParser.inlinedTypes[i.inlinedIdx] - if err := requireInlinedMatchesReferencedType(typeSection, typeIdx, inlined.Params, inlined.Results); err != nil { - var context string - switch i.section { - case wasm.SectionIDImport: - context = fmt.Sprintf("module.import[%d].func", i.idx) - case wasm.SectionIDFunction: - context = fmt.Sprintf("module.func[%d]", i.idx) - default: - panic(unhandledSection(i.section)) - } - return &FormatError{Line: i.typePos.line, Col: i.typePos.col, Context: context, cause: err} - } - return nil -} - -// addInlinedTypes adds any inlined types missing from the module TypeSection and returns an index mapping the inlined -// index to real index in the TypeSection. This avoids adding or looking up a type twice when it has multiple type uses. -func (p *moduleParser) addInlinedTypes() map[wasm.Index]wasm.Index { - inlinedTypeCount := len(p.typeUseParser.inlinedTypes) - if inlinedTypeCount == 0 { - return nil - } - - inlinedToRealIdx := make(map[wasm.Index]wasm.Index, inlinedTypeCount) -INLINED: - for idx, inlined := range p.typeUseParser.inlinedTypes { - inlinedIdx := wasm.Index(idx) - - // A type can be defined after its type use. Ex. (module (func (param i32)) (type (func (param i32))) - // This uses an inner loop to avoid creating a large map for an edge case. - for realIdx, t := range p.module.TypeSection { - if t.EqualsSignature(inlined.Params, inlined.Results) { - inlinedToRealIdx[inlinedIdx] = wasm.Index(realIdx) - continue INLINED - } - } - - // When we get here, this means the inlined type is not in the TypeSection, yet, so add it. - inlinedToRealIdx[inlinedIdx] = p.typeNamespace.count - p.module.TypeSection = append(p.module.TypeSection, inlined) - p.typeNamespace.count++ - } - return inlinedToRealIdx -} - -func (p *moduleParser) errorContext() string { - switch p.pos { - case positionInitial: - return "" - case positionModule: - return "module" - case positionType: - idx := p.module.SectionElementCount(wasm.SectionIDType) - return fmt.Sprintf("module.type[%d]%s", idx, p.typeParser.errorContext()) - case positionImport, positionImportFunc: // TODO: table, memory or global - idx := p.module.SectionElementCount(wasm.SectionIDImport) - if p.pos == positionImport { - return fmt.Sprintf("module.import[%d]", idx) - } - return fmt.Sprintf("module.import[%d].%s%s", idx, wasm.ExternTypeFuncName, p.typeUseParser.errorContext()) - case positionFunc: - idx := p.fieldCountFunc - return fmt.Sprintf("module.%s[%d]%s", wasm.ExternTypeFuncName, idx, p.typeUseParser.errorContext()) - case positionMemory: - return fmt.Sprintf("module.%s[0]", wasm.ExternTypeMemoryName) - case positionExport, positionExportFunc: // TODO: table, memory or global - idx := p.module.SectionElementCount(wasm.SectionIDExport) - if p.pos == positionExport { - return fmt.Sprintf("module.export[%d]", idx) - } - return fmt.Sprintf("module.export[%d].%s", idx, wasm.ExternTypeFuncName) - case positionStart: - return "module.start" - default: // parserPosition is an enum, we expect to have handled all cases above. panic if we didn't - panic(fmt.Errorf("BUG: unhandled parsing state on errorContext: %v", p.pos)) - } -} diff --git a/internal/watzero/internal/decoder_test.go b/internal/watzero/internal/decoder_test.go deleted file mode 100644 index d58bd2070a..0000000000 --- a/internal/watzero/internal/decoder_test.go +++ /dev/null @@ -1,2178 +0,0 @@ -package internal - -import ( - _ "embed" - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/wasm" -) - -func TestDecodeModule(t *testing.T) { - zero := uint32(0) - localGet0End := []byte{wasm.OpcodeLocalGet, 0x00, wasm.OpcodeEnd} - - tests := []struct { - name, input string - expected *wasm.Module - }{ - { - name: "empty", - input: "(module)", - expected: &wasm.Module{}, - }, - { - name: "only name", - input: "(module $tools)", - expected: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "tools"}}, - }, - { - name: "type funcs same param types", - input: `(module - (type (func (param i32) (param i64) (result i32))) - (type (func) (; here to ensure sparse indexes work ;)) - (type (func (param i32) (param i64) (result i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i64_i32, v_v, i32i64_i32}, - }, - }, - { - name: "type func empty after inlined", // ensures the parser was reset properly - input: `(module - (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (param i32 i32 i32 i32) (result i32))) - (type (func)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{ - v_v, // module types are always before inlined types - i32i32i32i32_i32, - }, - ImportSection: []*wasm.Import{{ - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "wasi.fd_write"}}, - }, - }, - }, - { - 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 - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{ - Module: "foo", Name: "bar", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - }, - }, - { - name: "import func redundant", - input: `(module - (type (func)) - (import "foo" "bar" (func)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{ - Module: "foo", Name: "bar", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - }, - }, - { - name: "import func redundant - late", - input: `(module - (import "foo" "bar" (func)) - (type (func)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{ - Module: "foo", Name: "bar", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - }, - }, - { - name: "import func redundant - two late", // pun intended - input: `(module - (import "foo" "bar" (func)) - (import "baz" "qux" (func)) - (type (func)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{ - Module: "foo", Name: "bar", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, { - Module: "baz", Name: "qux", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - }, - }, - { - name: "import func empty after non-empty", // ensures the parser was reset properly - input: `(module - (type (func (param i32) (param i32) (param i32) (param i32) (result i32))) - (import "foo" "bar" (func)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32_i32, {}}, - ImportSection: []*wasm.Import{{ - Module: "foo", Name: "bar", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }}, - }, - }, - { - name: "import func empty twice", - input: "(module (import \"foo\" \"bar\" (func)) (import \"baz\" \"qux\" (func)))", // ok empty sig - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{ - Module: "foo", Name: "bar", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, { - Module: "baz", Name: "qux", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - }, - }, - { - name: "import func inlined type", - input: `(module - (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (param i32) (param i32) (param i32) (param i32) (result i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32_i32}, - ImportSection: []*wasm.Import{{ - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "wasi.fd_write"}}, - }, - }, - }, - { - name: "import func inlined type - abbreviated", - input: `(module - (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (param i32 i32 i32 i32) (result i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32_i32}, - ImportSection: []*wasm.Import{{ - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "wasi.fd_write"}}, - }, - }, - }, - { - name: "func call - index - after import", - input: `(module - (import "" "" (func)) - (func) - (func call 1) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{Type: wasm.ExternTypeFunc, DescFunc: 0}}, - FunctionSection: []wasm.Index{0, 0}, - CodeSection: []*wasm.Code{ - {Body: end}, {Body: []byte{wasm.OpcodeCall, 0x01, wasm.OpcodeEnd}}, - }, - }, - }, - { - name: "func sign-extension", - input: `(module - (func (param i64) (result i64) local.get 0 i64.extend16_s) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i64_i64}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{ - {Body: []byte{wasm.OpcodeLocalGet, 0x00, wasm.OpcodeI64Extend16S, wasm.OpcodeEnd}}, - }, - }, - }, - { - name: "func nontrapping-float-to-int-conversions", - input: `(module - (func (param f32) (result i32) local.get 0 i32.trunc_sat_f32_s) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{f32_i32}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF32S, - wasm.OpcodeEnd, - }}}, - }, - }, - { - // Spec says expand abbreviations first. It doesn't explicitly say you can't mix forms. - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#abbreviations%E2%91%A0 - name: "import func inlined type - mixed abbreviated", - input: `(module - (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (param i32) (param i32 i32) (param i32) (result i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32_i32}, - ImportSection: []*wasm.Import{{ - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "wasi.fd_write"}}, - }, - }, - }, - { - name: "import func inlined type no result", - input: `(module - (import "wasi_snapshot_preview1" "proc_exit" (func $wasi.proc_exit (param i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32_v}, - ImportSection: []*wasm.Import{{ - Module: "wasi_snapshot_preview1", Name: "proc_exit", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "wasi.proc_exit"}}, - }, - }, - }, - { - name: "import func inlined type no param", - input: `(module (import "" "" (func (result i32))))`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_i32}, - ImportSection: []*wasm.Import{{Type: wasm.ExternTypeFunc, DescFunc: 0}}, - }, - }, - { - name: "import func inlined type different param types", - input: `(module - (import "wasi_snapshot_preview1" "path_open" (func $wasi.path_open (param i32) (param i32) (param i32) (param i32) (param i32) (param i64) (param i64) (param i32) (param i32) (result i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32i32i64i64i32i32_i32}, - ImportSection: []*wasm.Import{{ - Module: "wasi_snapshot_preview1", Name: "path_open", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "wasi.path_open"}}, - }, - }, - }, - { - name: "import func inlined type different param types - abbreviated", - input: `(module - (import "wasi_snapshot_preview1" "path_open" (func $wasi.path_open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32i32i64i64i32i32_i32}, - ImportSection: []*wasm.Import{{ - Module: "wasi_snapshot_preview1", Name: "path_open", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "wasi.path_open"}}, - }, - }, - }, - { - name: "multiple import func different inlined type", - input: `(module - (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (param i32 i32) (result i32))) - (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (param i32 i32 i32 i32) (result i32))) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32_i32, i32i32i32i32_i32}, - ImportSection: []*wasm.Import{{ - Module: "wasi_snapshot_preview1", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, { - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "wasi.args_sizes_get"}, - &wasm.NameAssoc{Index: 1, Name: "wasi.fd_write"}, - }, - }, - }, - }, - { - name: "multiple import func different type - ID index", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (type $i32i32_i32 (func (param i32 i32) (result i32))) - (type $i32i32i32i32_i32 (func (param i32 i32 i32 i32) (result i32))) - (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (type $i32i32_i32))) - (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (type $i32i32i32i32_i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{ - v_v, - {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}, - {Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}}, - }, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, { - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 2, - }, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - {Index: 0, Name: "wasi.args_sizes_get"}, - {Index: 1, Name: "wasi.fd_write"}, - }, - }, - }, - }, - { - name: "multiple import func different type - numeric index", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (type (func (param i32 i32) (result i32))) - (type (func (param i32 i32 i32 i32) (result i32))) - (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (type 1))) - (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (type 2))) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v, i32i32_i32, i32i32i32i32_i32}, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, { - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 2, - }, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "wasi.args_sizes_get"}, - &wasm.NameAssoc{Index: 1, Name: "wasi.fd_write"}, - }, - }, - }, - }, - { - name: "multiple import func same inlined type", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (import "wasi_snapshot_preview1" "environ_get" (func $wasi.environ_get (param i32 i32) (result i32))) - (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (param i32 i32) (result i32))) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v, i32i32_i32}, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "environ_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, { - Module: "wasi_snapshot_preview1", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "wasi.environ_get"}, - &wasm.NameAssoc{Index: 1, Name: "wasi.args_sizes_get"}, - }, - }, - }, - }, - { - name: "multiple import func same type index", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (type (func (param i32 i32) (result i32))) - (import "wasi_snapshot_preview1" "environ_get" (func $wasi.environ_get (type 1))) - (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (type 1))) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v, i32i32_i32}, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "environ_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, { - Module: "wasi_snapshot_preview1", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "wasi.environ_get"}, - &wasm.NameAssoc{Index: 1, Name: "wasi.args_sizes_get"}, - }, - }, - }, - }, - { - name: "multiple import func same type index - type after import", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (import "wasi_snapshot_preview1" "environ_get" (func $wasi.environ_get (type 1))) - (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (type 1))) - (type (func (param i32 i32) (result i32))) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v, i32i32_i32}, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "environ_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, { - Module: "wasi_snapshot_preview1", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "wasi.environ_get"}, - &wasm.NameAssoc{Index: 1, Name: "wasi.args_sizes_get"}, - }, - }, - }, - }, - { - name: "import func param IDs", - input: "(module (import \"Math\" \"Mul\" (func $mul (param $x i32) (param $y i64) (result i32))))", - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i64_i32}, - ImportSection: []*wasm.Import{{ - Module: "Math", Name: "Mul", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "mul"}}, - LocalNames: wasm.IndirectNameMap{ - {Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}, {Index: 1, Name: "y"}}}, - }, - }, - }, - }, - { - name: "import funcs same param types different names", - input: `(module - (import "Math" "Mul" (func $mul (param $x i32) (param $y i64) (result i32))) - (import "Math" "Add" (func $add (param $l i32) (param $r i64) (result i32))) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i64_i32}, - ImportSection: []*wasm.Import{{ - Module: "Math", Name: "Mul", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, { - Module: "Math", Name: "Add", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{{Index: 0, Name: "mul"}, {Index: 1, Name: "add"}}, - LocalNames: wasm.IndirectNameMap{ - {Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}, {Index: 1, Name: "y"}}}, - {Index: 1, NameMap: wasm.NameMap{{Index: 0, Name: "l"}, {Index: 1, Name: "r"}}}, - }, - }, - }, - }, - { - name: "import func mixed param IDs", // Verifies we can handle less param fields than Params - input: "(module (import \"\" \"\" (func (param i32 i32) (param $v i32) (param i64) (param $t f32))))", - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{ - {Params: []wasm.ValueType{i32, i32, i32, i64, f32}}, - }, - ImportSection: []*wasm.Import{{Type: wasm.ExternTypeFunc, DescFunc: 0}}, - NameSection: &wasm.NameSection{ - LocalNames: wasm.IndirectNameMap{ - {Index: 0, NameMap: wasm.NameMap{{Index: wasm.Index(2), Name: "v"}, {Index: wasm.Index(4), Name: "t"}}}, - }, - }, - }, - }, - { - name: "multiple import func with different inlined type", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (import "wasi_snapshot_preview1" "path_open" (func $wasi.path_open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32))) - (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (param i32 i32 i32 i32) (result i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{ - v_v, - {Params: []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32}, Results: []wasm.ValueType{i32}}, - {Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}}, - }, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "path_open", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, { - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 2, - }, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - {Index: 0, Name: "wasi.path_open"}, - {Index: 1, Name: "wasi.fd_write"}, - }, - }, - }, - }, - { - name: "import func inlined type match - index", - input: `(module - (type $i32 (func (param i32))) - (import "foo" "bar" (func (type 0) (param i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}}, - ImportSection: []*wasm.Import{{ - Module: "foo", Name: "bar", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - }, - }, - { - name: "import func inlined type match - index - late", - input: `(module - (import "foo" "bar" (func (type 0) (param i32))) - (type $i32 (func (param i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}}, - ImportSection: []*wasm.Import{{ - Module: "foo", Name: "bar", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - }, - }, - { - name: "import func inlined type match - ID", - input: `(module - (type $i32 (func (param i32))) - (import "foo" "bar" (func (type $i32) (param i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}}, - ImportSection: []*wasm.Import{{ - Module: "foo", Name: "bar", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - }, - }, - { - name: "import func inlined type match - ID - late", - input: `(module - (import "foo" "bar" (func (type $i32) (param i32))) - (type $i32 (func (param i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}}, - ImportSection: []*wasm.Import{{ - Module: "foo", Name: "bar", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - }, - }, - { - 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 - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - }, - }, - { - name: "func redundant empty type", - input: `(module - (type (func)) - (func) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - }, - }, - { - name: "func redundant empty type - late", - input: `(module - (func) - (type (func)) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - }, - }, - { - name: "func redundant type - two late", // pun intended - input: `(module - (func) - (func) - (type (func)) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0, 0}, - CodeSection: []*wasm.Code{{Body: end}, {Body: end}}, - }, - }, - { - name: "func empty after non-empty", // ensures the parser was reset properly - input: `(module - (type (func (param i32) (param i32) (param i32) (param i32) (result i32) )) - (func) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32_i32, {}}, - FunctionSection: []wasm.Index{1}, - CodeSection: []*wasm.Code{{Body: end}}, - }, - }, - { - name: "func empty twice", - input: "(module (func) (func))", // ok empty sig - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0, 0}, - CodeSection: []*wasm.Code{{Body: end}, {Body: end}}, - }, - }, - { - name: "func inlined type", - input: `(module - (func $wasi.fd_write (param i32) (param i32) (param i32) (param i32) (result i32) local.get 0 ) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32_i32}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "wasi.fd_write"}}, - }, - }, - }, - { - name: "func inlined type - abbreviated", - input: `(module - (func $wasi.fd_write (param i32 i32 i32 i32) (result i32) local.get 0) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32_i32}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "wasi.fd_write"}}, - }, - }, - }, - { - // Spec says expand abbreviations first. It doesn't explicitly say you can't mix forms. - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#abbreviations%E2%91%A0 - name: "func inlined type - mixed abbreviated", - input: `(module - (func $wasi.fd_write (param i32) (param i32 i32) (param i32) (result i32) local.get 0) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32_i32}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "wasi.fd_write"}}, - }, - }, - }, - { - name: "func inlined type no result", - input: `(module - (func $runtime.proc_exit (param i32)) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32_v}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "runtime.proc_exit"}}, - }, - }, - }, - { - name: "func inlined type no param", - input: `(module (func (result i32) local.get 0))`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_i32}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - }, - }, - { - name: "func inlined type different param types", - input: `(module - (func $runtime.path_open (param i32) (param i32) (param i32) (param i32) (param i32) (param i64) (param i64) (param i32) (param i32) (result i32) local.get 0) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32i32i64i64i32i32_i32}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "runtime.path_open"}}, - }, - }, - }, - { - name: "func inlined type different param types - abbreviated", - input: `(module - (func $runtime.path_open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32) local.get 0) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32i32i32i32i64i64i32i32_i32}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "runtime.path_open"}}, - }, - }, - }, - { - name: "multiple func different inlined type", - input: `(module - (func $runtime.args_sizes_get (param i32 i32) (result i32) local.get 0) - (func $runtime.fd_write (param i32 i32 i32 i32) (result i32) local.get 0) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{i32i32_i32, i32i32i32i32_i32}, - FunctionSection: []wasm.Index{0, 1}, - CodeSection: []*wasm.Code{{Body: localGet0End}, {Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "runtime.args_sizes_get"}, - &wasm.NameAssoc{Index: 1, Name: "runtime.fd_write"}, - }, - }, - }, - }, - { - name: "multiple func with different inlined type", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (func $runtime.path_open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32) local.get 0) - (func $runtime.fd_write (param i32 i32 i32 i32) (result i32) local.get 0) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{ - v_v, - {Params: []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32}, Results: []wasm.ValueType{i32}}, - {Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}}, - }, - FunctionSection: []wasm.Index{1, 2}, - CodeSection: []*wasm.Code{{Body: localGet0End}, {Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - {Index: 0, Name: "runtime.path_open"}, - {Index: 1, Name: "runtime.fd_write"}, - }, - }, - }, - }, - { - name: "multiple func different type - ID index", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (type $i32i32_i32 (func (param i32 i32) (result i32))) - (type $i32i32i32i32_i32 (func (param i32 i32 i32 i32) (result i32))) - (func $runtime.args_sizes_get (type $i32i32_i32) local.get 0) - (func $runtime.fd_write (type $i32i32i32i32_i32) local.get 0) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{ - v_v, - {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}, - {Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}}, - }, - FunctionSection: []wasm.Index{1, 2}, - CodeSection: []*wasm.Code{{Body: localGet0End}, {Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - {Index: 0, Name: "runtime.args_sizes_get"}, - {Index: 1, Name: "runtime.fd_write"}, - }, - }, - }, - }, - { - name: "multiple func different type - numeric index", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (type (func (param i32 i32) (result i32) )) - (type (func (param i32 i32 i32 i32) (result i32) )) - (func $runtime.args_sizes_get (type 1) local.get 0 ) - (func $runtime.fd_write (type 2) local.get 0 ) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v, i32i32_i32, i32i32i32i32_i32}, - FunctionSection: []wasm.Index{1, 2}, - CodeSection: []*wasm.Code{{Body: localGet0End}, {Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "runtime.args_sizes_get"}, - &wasm.NameAssoc{Index: 1, Name: "runtime.fd_write"}, - }, - }, - }, - }, - { - name: "func inlined type match - index", - input: `(module - (type $i32 (func (param i32))) - (func (type 0) (param i32)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{codeEnd}, - }, - }, - { - name: "func inlined type match - index - late", - input: `(module - (func (type 0) (param i32)) - (type $i32 (func (param i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{codeEnd}, - }, - }, - { - name: "func inlined type match - ID", - input: `(module - (type $i32 (func (param i32))) - (func (type $i32) (param i32)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{codeEnd}, - }, - }, - { - name: "func inlined type match - ID - late", - input: `(module - (func (type $i32) (param i32)) - (type $i32 (func (param i32))) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{codeEnd}, - }, - }, - { - name: "mixed func same inlined type", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (import "wasi_snapshot_preview1" "args_get" (func $runtime.args_get (param i32 i32) (result i32) )) - (func $runtime.args_sizes_get (param i32 i32) (result i32) local.get 0) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v, i32i32_i32}, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, - }, - FunctionSection: []wasm.Index{1}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "runtime.args_get"}, - &wasm.NameAssoc{Index: 1, Name: "runtime.args_sizes_get"}, - }, - }, - }, - }, - { - name: "mixed func same type index", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (type (func (param i32 i32) (result i32) )) - (import "wasi_snapshot_preview1" "args_get" (func $runtime.args_get (type 1))) - (func $runtime.args_sizes_get (type 1) local.get 0) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v, i32i32_i32}, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, - }, - FunctionSection: []wasm.Index{1}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "runtime.args_get"}, - &wasm.NameAssoc{Index: 1, Name: "runtime.args_sizes_get"}, - }, - }, - }, - }, - { - name: "mixed func same type index - type after import", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (import "wasi_snapshot_preview1" "args_get" (func $runtime.args_get (type 1))) - (func $runtime.args_sizes_get (type 1) local.get 0) - (type (func (param i32 i32) (result i32) )) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v, i32i32_i32}, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, - }, - FunctionSection: []wasm.Index{1}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "runtime.args_get"}, - &wasm.NameAssoc{Index: 1, Name: "runtime.args_sizes_get"}, - }, - }, - }, - }, - { - name: "mixed func signature needs match", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (type (func (param i32 i32) (result i32) )) - (import "wasi_snapshot_preview1" "args_get" (func $runtime.args_get (type 1) (param i32 i32) (result i32))) - (func $runtime.args_sizes_get (type 1) (param i32 i32) (result i32) local.get 0) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v, i32i32_i32}, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, - }, - FunctionSection: []wasm.Index{1}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "runtime.args_get"}, - &wasm.NameAssoc{Index: 1, Name: "runtime.args_sizes_get"}, - }, - }, - }, - }, - { - name: "mixed func signature needs match - type after import", - input: `(module - (type (func) (; ensures no false match on index 0 ;)) - (import "wasi_snapshot_preview1" "args_get" (func $runtime.args_get (type 1) (param i32 i32) (result i32))) - (func $runtime.args_sizes_get (type 1) (param i32 i32) (result i32) local.get 0) - (type (func (param i32 i32) (result i32) )) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v, i32i32_i32}, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_get", - Type: wasm.ExternTypeFunc, - DescFunc: 1, - }, - }, - FunctionSection: []wasm.Index{1}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - &wasm.NameAssoc{Index: 0, Name: "runtime.args_get"}, - &wasm.NameAssoc{Index: 1, Name: "runtime.args_sizes_get"}, - }, - }, - }, - }, - { - name: "func param IDs", - input: "(module (func $one (param $x i32) (param $y i32) (result i32) local.get 0))", - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: localGet0End}}, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "one"}}, - LocalNames: wasm.IndirectNameMap{ - {Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}, {Index: 1, Name: "y"}}}, - }, - }, - }, - }, - { - name: "funcs same param types different names", - input: `(module - (func (param $x i32) (param $y i32) (result i32) local.get 0) - (func (param $l i32) (param $r i32) (result i32) local.get 0) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}}, - FunctionSection: []wasm.Index{0, 0}, - CodeSection: []*wasm.Code{{Body: localGet0End}, {Body: localGet0End}}, - NameSection: &wasm.NameSection{ - LocalNames: wasm.IndirectNameMap{ - {Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}, {Index: 1, Name: "y"}}}, - {Index: 1, NameMap: wasm.NameMap{{Index: 0, Name: "l"}, {Index: 1, Name: "r"}}}, - }, - }, - }, - }, - { - name: "func mixed param IDs", // Verifies we can handle less param fields than Params - input: "(module (func (param i32 i32) (param $v i32) (param i64) (param $t f32)))", - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32, i32, i64, f32}}}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - NameSection: &wasm.NameSection{ - LocalNames: wasm.IndirectNameMap{ - {Index: 0, NameMap: wasm.NameMap{{Index: wasm.Index(2), Name: "v"}, {Index: wasm.Index(4), Name: "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 - (func) - (func) - (func call 1) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0, 0, 0}, - CodeSection: []*wasm.Code{ - {Body: end}, {Body: end}, {Body: []byte{wasm.OpcodeCall, 0x01, wasm.OpcodeEnd}}, - }, - }, - }, - { - name: "func call - index - late", - input: `(module - (func) - (func call 1) - (func) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0, 0, 0}, - CodeSection: []*wasm.Code{ - {Body: end}, {Body: []byte{wasm.OpcodeCall, 0x01, wasm.OpcodeEnd}}, {Body: end}, - }, - }, - }, - { - name: "func call - ID", - input: `(module - (func) - (func $main) - (func call $main) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0, 0, 0}, - CodeSection: []*wasm.Code{ - {Body: end}, {Body: end}, {Body: []byte{wasm.OpcodeCall, 0x01, wasm.OpcodeEnd}}, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{{Index: 1, Name: "main"}}, - }, - }, - }, - { - name: "func call - ID - late", - input: `(module - (func) - (func call $main) - (func $main) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0, 0, 0}, - CodeSection: []*wasm.Code{ - {Body: end}, {Body: []byte{wasm.OpcodeCall, 0x02, wasm.OpcodeEnd}}, {Body: end}, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{{Index: 2, Name: "main"}}, - }, - }, - }, - { - name: "memory", - input: "(module (memory 1))", - expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: wasm.MemoryLimitPages}, - }, - }, - { - name: "memory ID", - input: "(module (memory $mem 1))", - expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: wasm.MemoryLimitPages}, - }, - }, - { - name: "export imported func", - input: `(module - (import "foo" "bar" (func $bar)) - (export "bar" (func $bar)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{ - {Module: "foo", Name: "bar", Type: wasm.ExternTypeFunc, DescFunc: 0}, - }, - ExportSection: []*wasm.Export{ - {Name: "bar", Type: wasm.ExternTypeFunc, Index: 0}, - }, - NameSection: &wasm.NameSection{FunctionNames: wasm.NameMap{{Index: 0, Name: "bar"}}}, - }, - }, - { - name: "export imported func - numeric", - input: `(module - (import "foo" "bar" (func)) - (export "bar" (func 0)) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{ - {Module: "foo", Name: "bar", Type: wasm.ExternTypeFunc, DescFunc: 0}, - }, - ExportSection: []*wasm.Export{ - {Name: "bar", Type: wasm.ExternTypeFunc, Index: 0}, - }, - }, - }, - { - name: "export imported func twice", - input: `(module - (import "foo" "bar" (func $bar)) - (export "foo" (func $bar)) - (export "bar" (func $bar)) - )`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{ - {Module: "foo", Name: "bar", Type: wasm.ExternTypeFunc, DescFunc: 0}, - }, - ExportSection: []*wasm.Export{ - {Name: "foo", Type: wasm.ExternTypeFunc, Index: 0}, - {Name: "bar", Type: wasm.ExternTypeFunc, Index: 0}, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "bar"}}, - }, - }, - }, - { - name: "export different func", - input: `(module - (import "foo" "bar" (func $bar)) - (func $qux) - (export "foo" (func $bar)) - (export "bar" (func $qux)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{Module: "foo", Name: "bar", Type: wasm.ExternTypeFunc, DescFunc: 0}}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - ExportSection: []*wasm.Export{ - {Name: "foo", Type: wasm.ExternTypeFunc, Index: 0}, - {Name: "bar", Type: wasm.ExternTypeFunc, Index: 1}, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - {Index: 0, Name: "bar"}, - {Index: 1, Name: "qux"}, - }, - }, - }, - }, - { - name: "export different func - late", - input: `(module - (export "foo" (func $bar)) - (export "bar" (func $qux)) - (import "foo" "bar" (func $bar)) - (func $qux) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{Module: "foo", Name: "bar", Type: wasm.ExternTypeFunc, DescFunc: 0}}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - ExportSection: []*wasm.Export{ - {Name: "foo", Type: wasm.ExternTypeFunc, Index: 0}, - {Name: "bar", Type: wasm.ExternTypeFunc, Index: 1}, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - {Index: 0, Name: "bar"}, - {Index: 1, Name: "qux"}, - }, - }, - }, - }, - { - name: "export different func - numeric", - input: `(module - (import "foo" "bar" (func)) - (func) - (export "foo" (func 0)) - (export "bar" (func 1)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{Module: "foo", Name: "bar", Type: wasm.ExternTypeFunc, DescFunc: 0}}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - ExportSection: []*wasm.Export{ - {Name: "foo", Type: wasm.ExternTypeFunc, Index: 0}, - {Name: "bar", Type: wasm.ExternTypeFunc, Index: 1}, - }, - }, - }, - { - name: "export different func - numeric - late", - input: `(module - (export "foo" (func 0)) - (export "bar" (func 1)) - (import "foo" "bar" (func)) - (func) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{Module: "foo", Name: "bar", Type: wasm.ExternTypeFunc, DescFunc: 0}}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - ExportSection: []*wasm.Export{ - {Name: "foo", Type: wasm.ExternTypeFunc, Index: 0}, - {Name: "bar", Type: wasm.ExternTypeFunc, Index: 1}, - }, - }, - }, - { - name: "exported func with instructions", - input: `(module - ;; from https://github.com/summerwind/the-art-of-webassembly-go/blob/main/chapter1/addint/addint.wat - (func $addInt ;; TODO: function exports (export "AddInt") - (param $value_1 i32) (param $value_2 i32) - (result i32) - local.get 0 ;; TODO: instruction variables $value_1 - local.get 1 ;; TODO: instruction variables $value_2 - i32.add - ) - (export "AddInt" (func $addInt)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{ - {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}, - }, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{ - {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeI32Add, wasm.OpcodeEnd}}, - }, - ExportSection: []*wasm.Export{ - {Name: "AddInt", Type: wasm.ExternTypeFunc, Index: 0}, - }, - NameSection: &wasm.NameSection{ - FunctionNames: wasm.NameMap{{Index: 0, Name: "addInt"}}, - LocalNames: wasm.IndirectNameMap{ - {Index: 0, NameMap: wasm.NameMap{ - {Index: 0, Name: "value_1"}, - {Index: 1, Name: "value_2"}, - }}, - }, - }, - }, - }, - { - name: "export memory - numeric", - input: `(module - (memory 0) - (export "foo" (memory 0)) -)`, - expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryLimitPages}, - ExportSection: []*wasm.Export{ - {Name: "foo", Type: wasm.ExternTypeMemory, Index: 0}, - }, - }, - }, - { - name: "export memory - numeric - late", - input: `(module - (export "foo" (memory 0)) - (memory 0) -)`, - expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryLimitPages}, - ExportSection: []*wasm.Export{ - {Name: "foo", Type: wasm.ExternTypeMemory, Index: 0}, - }, - }, - }, - { - name: "export empty and non-empty name", - input: `(module - (func) - (func) - (func) - (export "" (func 2)) - (export "a" (func 1)) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0, 0, 0}, - CodeSection: []*wasm.Code{{Body: end}, {Body: end}, {Body: end}}, - ExportSection: []*wasm.Export{ - {Name: "", Type: wasm.ExternTypeFunc, Index: wasm.Index(2)}, - {Name: "a", Type: wasm.ExternTypeFunc, Index: 1}, - }, - }, - }, - { - name: "export memory - ID", - input: `(module - (memory $mem 1) - (export "memory" (memory $mem)) -)`, - expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: wasm.MemoryLimitPages}, - ExportSection: []*wasm.Export{ - {Name: "memory", Type: wasm.ExternTypeMemory, Index: 0}, - }, - }, - }, - { - name: "start imported function by ID", - input: `(module - (import "" "hello" (func $hello)) - (start $hello) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{ - Module: "", Name: "hello", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - StartSection: &zero, - NameSection: &wasm.NameSection{FunctionNames: wasm.NameMap{{Index: 0, Name: "hello"}}}, - }, - }, - { - name: "start imported function by index", - input: `(module - (import "" "hello" (func)) - (start 0) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - ImportSection: []*wasm.Import{{ - Module: "", Name: "hello", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }}, - StartSection: &zero, - }, - }, - { - name: "start function by ID", - input: `(module - (func $hello) - (start $hello) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - StartSection: &zero, - NameSection: &wasm.NameSection{FunctionNames: wasm.NameMap{{Index: 0, Name: "hello"}}}, - }, - }, - { - name: "start function by ID - late", - input: `(module - (start $hello) - (func $hello) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - StartSection: &zero, - NameSection: &wasm.NameSection{FunctionNames: wasm.NameMap{{Index: 0, Name: "hello"}}}, - }, - }, - { - name: "start function by index", - input: `(module - (func) - (start 0) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - StartSection: &zero, - }, - }, - { - name: "start function by index - late", - input: `(module - (start 0) - (func) -)`, - expected: &wasm.Module{ - TypeSection: []*wasm.FunctionType{v_v}, - FunctionSection: []wasm.Index{0}, - CodeSection: []*wasm.Code{{Body: end}}, - StartSection: &zero, - }, - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - m, err := DecodeModule([]byte(tc.input), wasm.Features20220419, wasm.MemorySizer) - require.NoError(t, err) - require.Equal(t, tc.expected, m) - }) - } -} - -func TestParseModule_Errors(t *testing.T) { - tests := []struct { - name, input string - expectedErr string - }{ - { - name: "forgot parens", - input: "module", - expectedErr: "1:1: expected '(', but parsed keyword: module", - }, - { - name: "no module", - input: "()", - expectedErr: "1:2: expected field, but parsed )", - }, - { - name: "invalid", - input: "module", - expectedErr: "1:1: expected '(', but parsed keyword: module", - }, - { - name: "not module", - input: "(moodule)", - expectedErr: "1:2: unexpected field: moodule", - }, - { - name: "double module", - input: "(module) (module)", - expectedErr: "1:10: unexpected trailing characters: (", - }, - { - name: "module invalid name", - input: "(module test)", // must start with $ - expectedErr: "1:9: unexpected keyword: test in module", - }, - { - name: "module invalid field", - input: "(module (test))", - expectedErr: "1:10: unexpected field: test in module", - }, - { - name: "module double ID", - input: "(module $foo $bar)", - expectedErr: "1:14: redundant ID $bar in module", - }, - { - name: "module empty field", - input: "(module $foo ())", - expectedErr: "1:15: expected field, but parsed ) in module", - }, - { - name: "module trailing )", - input: "(module $foo ))", - expectedErr: "1:15: found ')' before '('", - }, - { - name: "module name after import", - input: "(module (import \"\" \"\" (func) $Math)", - expectedErr: "1:30: unexpected ID: $Math in module.import[0]", - }, - { - 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))", - expectedErr: "1:16: missing module and name in module.import[0]", - }, - { - name: "import with desc, but no module", - input: "(module (import (func))", - expectedErr: "1:17: missing module and name in module.import[0]", - }, - { - name: "import missing name", - input: "(module (import \"\"))", - expectedErr: "1:19: missing name in module.import[0]", - }, - { - name: "import name not a string", - input: "(module (import \"\" 0))", - expectedErr: "1:20: unexpected uN: 0 in module.import[0]", - }, - { - name: "import with desc, no name", - input: "(module (import \"\" (func)))", - expectedErr: "1:20: missing name in module.import[0]", - }, - { - name: "import unquoted module", - input: "(module (import foo bar))", - expectedErr: "1:17: unexpected keyword: foo in module.import[0]", - }, - { - name: "import double name", - input: "(module (import \"foo\" \"bar\" \"baz\")", - expectedErr: "1:29: redundant name: baz in module.import[0]", - }, - { - name: "import invalid token after name", - input: "(module (import \"foo\" \"bar\" $baz)", - expectedErr: "1:29: unexpected ID: $baz in module.import[0]", - }, - { - name: "import missing desc", - input: "(module (import \"foo\" \"bar\"))", - expectedErr: "1:28: missing description field in module.import[0]", - }, - { - name: "import empty desc", - input: "(module (import \"foo\" \"bar\"())", - expectedErr: "1:29: expected field, but parsed ) in module.import[0]", - }, - { - name: "import wrong end", - input: "(module (import \"foo\" \"bar\" (func) \"\"))", - expectedErr: "1:36: unexpected string: \"\" in module.import[0]", - }, - { - name: "import not desc field", - input: "(module (import \"foo\" \"bar\" ($func)))", - expectedErr: "1:30: expected field, but parsed ID in module.import[0]", - }, - { - name: "import wrong desc field", - input: "(module (import \"foo\" \"bar\" (funk)))", - expectedErr: "1:30: unexpected field: funk in module.import[0]", - }, - { - name: "import func invalid name", - input: "(module (import \"foo\" \"bar\" (func baz)))", - expectedErr: "1:35: unexpected keyword: baz in module.import[0].func", - }, - { - name: "import func invalid token", - input: "(module (import \"foo\" \"bar\" (func ($param))))", - expectedErr: "1:36: unexpected ID: $param in module.import[0].func", - }, - { - name: "import func double ID", - input: "(module (import \"foo\" \"bar\" (func $baz $qux)))", - expectedErr: "1:40: redundant ID $qux in module.import[0].func", - }, - { - name: "import func clash ID", - input: "(module (import \"\" \"\" (func $main)) (import \"\" \"\" (func $main)))", - expectedErr: "1:57: duplicate ID $main in module.import[1].func", - }, - { - name: "import func param second ID", - input: "(module (import \"\" \"\" (func (param $x $y i32) ))", - expectedErr: "1:39: redundant ID $y in module.import[0].func.param[0]", - }, - { - name: "import func param ID in abbreviation", - input: "(module (import \"\" \"\" (func (param $x i32 i64) ))", - expectedErr: "1:43: cannot assign IDs to parameters in abbreviated form in module.import[0].func.param[0]", - }, - { - name: "import func param ID clash", - 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 wrong param0 type", - input: "(module (import \"\" \"\" (func (param f65))))", - expectedErr: "1:36: unknown type: f65 in module.import[0].func.param[0]", - }, - { - name: "import func wrong param1 type", - input: "(module (import \"\" \"\" (func (param i32) (param f65))))", - expectedErr: "1:48: unknown type: f65 in module.import[0].func.param[1]", - }, - { - 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 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[0]", - }, - { - name: "import func wrong param token", - input: "(module (import \"\" \"\" (func (param () ))))", - expectedErr: "1:36: unexpected '(' in module.import[0].func.param[0]", - }, - { - name: "import func wrong result token", - input: "(module (import \"\" \"\" (func (result () ))))", - expectedErr: "1:37: unexpected '(' in module.import[0].func.result[0]", - }, - { - name: "import func ID after param", - input: "(module (import \"\" \"\" (func (param i32) $main)))", - expectedErr: "1:41: unexpected ID: $main in module.import[0].func", - }, - { - name: "import func ID after result", - input: "(module (import \"\" \"\" (func (result i32) $main)))", - expectedErr: "1:42: unexpected ID: $main in module.import[0].func", - }, - { - name: "import func type duplicate index", - input: "(module (import \"\" \"\" (func (type 9 2)))", - expectedErr: "1:37: redundant index in module.import[0].func.type", - }, - { - name: "import func duplicate type", - input: "(module (import \"\" \"\" (func (type 9) (type 2)))", - expectedErr: "1:39: redundant type in module.import[0].func", - }, - { - name: "import func type mismatch", - input: `(module (type (func)) (import "" "" (func (type 0) (result i32)))`, - expectedErr: "1:64: inlined type doesn't match module.type[0].func in module.import[0].func", - }, - { - name: "import func type mismatch - late", - input: "(module (import \"\" \"\" (func (type 0) (result i32))) (type (func)))", - expectedErr: "1:35: inlined type doesn't match module.type[0].func in module.import[0].func", - }, - { - name: "import func type invalid", - input: "(module (import \"\" \"\" (func (type \"0\")))", - expectedErr: "1:35: unexpected string: \"0\" in module.import[0].func.type", - }, - { - name: "import func param after result", - input: "(module (import \"\" \"\" (func (result i32) (param i32))))", - expectedErr: "1:43: param after result in module.import[0].func", - }, - { - name: "import func double desc", - input: "(module (import \"foo\" \"bar\" (func $main) (func $mein)))", - expectedErr: "1:42: unexpected '(' in module.import[0]", - }, - { - name: "import func wrong end", - input: "(module (import \"foo\" \"bar\" (func \"\")))", - expectedErr: "1:35: unexpected string: \"\" in module.import[0].func", - }, - { - name: "import func points nowhere", - input: "(module (import \"foo\" \"bar\" (func (type $v_v))))", - expectedErr: "1:41: unknown ID $v_v", - }, - { - name: "import func after func", - input: "(module (func) (import \"\" \"\" (func)))", - expectedErr: "1:31: import after module-defined function in module.import[0]", - }, - { - name: "func invalid name", - input: "(module (func baz)))", - expectedErr: "1:15: unsupported instruction: baz in module.func[0]", - }, - { - name: "func invalid token", - input: "(module (func ($param)))", - expectedErr: "1:16: unexpected ID: $param in module.func[0]", - }, - { - name: "func double param ID", - input: "(module (func $baz $qux)))", - expectedErr: "1:20: redundant ID $qux in module.func[0]", - }, - { - name: "func param ID clash", - input: "(module (func (param $x i32) (param i32) (param $x i32)))", - expectedErr: "1:49: duplicate ID $x in module.func[0].param[2]", - }, - { - name: "func param second ID", - input: "(module (func (param $x $y i32) ))", - expectedErr: "1:25: redundant ID $y in module.func[0].param[0]", - }, - { - name: "func param ID in abbreviation", - 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 wrong param0 type", - input: "(module (func (param f65)))", - expectedErr: "1:22: unknown type: f65 in module.func[0].param[0]", - }, - { - name: "func wrong param1 type", - input: "(module (func (param i32) (param f65)))", - expectedErr: "1:34: unknown type: f65 in module.func[0].param[1]", - }, - { - 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 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[0]", - }, - { - name: "func wrong param token", - input: "(module (func (param () )))", - expectedErr: "1:22: unexpected '(' in module.func[0].param[0]", - }, - { - name: "func wrong result token", - input: "(module (func (result () )))", - expectedErr: "1:23: unexpected '(' in module.func[0].result[0]", - }, - { - name: "func ID after param", - input: "(module (func (param i32) $main))", - expectedErr: "1:27: unexpected ID: $main in module.func[0]", - }, - { - name: "func wrong end", - input: "(module (func $main \"\"))", - expectedErr: "1:21: unexpected string: \"\" in module.func[0]", - }, - { - name: "clash on func ID", - input: "(module (func $main) (func $main)))", - expectedErr: "1:28: duplicate ID $main in module.func[1]", - }, - { - name: "func ID clashes with import func ID", - input: "(module (import \"\" \"\" (func $main)) (func $main)))", - expectedErr: "1:43: duplicate ID $main in module.func[0]", - }, - { - name: "func ID after result", - input: "(module (func (result i32) $main)))", - expectedErr: "1:28: unexpected ID: $main in module.func[0]", - }, - { - name: "func type duplicate index", - input: "(module (func (type 9 2)))", - expectedErr: "1:23: redundant index in module.func[0].type", - }, - { - name: "func duplicate type", - input: "(module (func (type 9) (type 2)))", - expectedErr: "1:25: redundant type in module.func[0]", - }, - { - name: "func type mismatch", - input: "(module (type (func)) (func (type 0) (result i32))", - expectedErr: "1:50: inlined type doesn't match module.type[0].func in module.func[0]", - }, - { - name: "func type mismatch - late", - input: "(module (func (type 0) (result i32)) (type (func)))", - expectedErr: "1:21: inlined type doesn't match module.type[0].func in module.func[0]", - }, - { - name: "func type invalid", - input: "(module (func (type \"0\")))", - expectedErr: "1:21: unexpected string: \"0\" in module.func[0].type", - }, - { - name: "func param after result", - input: "(module (func (result i32) (param i32)))", - expectedErr: "1:29: param after result in module.func[0]", - }, - { - name: "func points nowhere", - input: "(module (func (type $v_v)))", - expectedErr: "1:21: unknown ID $v_v", - }, - { - name: "func call unresolved - index", - input: `(module - (func) - (func call 2) - )`, - expectedErr: "3:15: index 2 is out of range [0..1] in module.code[1].body[1]", - }, - { - name: "func call unresolved - ID", - input: `(module - (func $main) - (func call $mein) - )`, - expectedErr: "3:15: unknown ID $mein in module.code[1].body[1]", - }, - { - name: "func sign-extension disabled", - 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]", - }, - { - name: "func nontrapping-float-to-int-conversions disabled", - input: `(module - (func (param f32) (result i32) local.get 0 i32.trunc_sat_f32_s) - )`, - expectedErr: "2:47: i32.trunc_sat_f32_s invalid as feature \"nontrapping-float-to-int-conversion\" is disabled in module.func[0]", - }, - { - name: "memory over max", - input: "(module (memory 1 70000))", - expectedErr: "1:19: max 70000 pages (4 Gi) over limit of 65536 pages (4 Gi) in module.memory[0]", - }, - { - name: "second memory", - input: "(module (memory 1) (memory 1))", - expectedErr: "1:21: at most one memory allowed in module", - }, - { - name: "export duplicates empty name", - input: `(module - (func) - (func) - (export "" (func 0)) - (export "" (memory 1)) -)`, - expectedErr: `5:13: "" already exported in module.export[1]`, - }, - { - name: "export duplicates name", - input: `(module - (func) - (func) - (export "a" (func 0)) - (export "a" (memory 1)) -)`, - expectedErr: `5:13: "a" already exported in module.export[1]`, - }, - { - name: "export double name", - input: "(module (export \"PI\" \"PI\" (func main)))", - expectedErr: "1:22: redundant name: PI in module.export[0]", - }, - { - name: "export wrong name", - input: "(module (export PI (func $main)))", - expectedErr: "1:17: unexpected reserved: PI in module.export[0]", - }, - { - name: "export wrong end", - input: "(module (export \"PI\" (func $main) \"\"))", - expectedErr: "1:35: unexpected string: \"\" in module.export[0]", - }, - { - name: "export missing name", - input: "(module (export (func)))", - expectedErr: "1:17: missing name in module.export[0]", - }, - { - name: "export missing desc", - input: "(module (export \"foo\")))", - expectedErr: "1:22: missing description field in module.export[0]", - }, - { - name: "export not desc", - input: "(module (export \"foo\" $func))", - expectedErr: "1:23: unexpected ID: $func in module.export[0]", - }, - { - name: "export not desc field", - input: "(module (export \"foo\" ($func)))", - expectedErr: "1:24: expected field, but parsed ID in module.export[0]", - }, - { - name: "export wrong desc field", - input: "(module (export \"foo\" (funk)))", - expectedErr: "1:24: unexpected field: funk in module.export[0]", - }, - { - name: "export func missing index", - input: "(module (export \"PI\" (func)))", - expectedErr: "1:27: missing index in module.export[0].func", - }, - { - name: "export func double index", - input: "(module (export \"PI\" (func $main $main)))", - expectedErr: "1:34: redundant index in module.export[0].func", - }, - { - name: "export func wrong index", - input: "(module (export \"PI\" (func main)))", - expectedErr: "1:28: unexpected keyword: main in module.export[0].func", - }, - { - name: "export func wrong end", - input: "(module (export \"PI\" (func $main \"\")))", - expectedErr: "1:34: unexpected string: \"\" in module.export[0].func", - }, - { - name: "export func points out of range", - input: `(module - (import "" "hello" (func)) - (export "PI" (func 1)) -)`, - expectedErr: "3:21: index 1 is out of range [0..0] in module.exports[0].func", - }, - { - name: "export func points nowhere", - input: "(module (export \"PI\" (func $main)))", - expectedErr: "1:28: unknown ID $main in module.exports[0].func", - }, - { - name: "start missing index", - input: "(module (start))", - expectedErr: "1:15: missing index in module.start", - }, - { - name: "start double index", - input: "(module (start $main $main))", - expectedErr: "1:22: redundant index in module.start", - }, - { - name: "start wrong end", - input: "(module (start $main \"\"))", - expectedErr: "1:22: unexpected string: \"\" in module.start", - }, - { - name: "double start", - input: "(module (start $main) (start $main))", - expectedErr: "1:24: at most one start allowed in module", - }, - { - name: "wrong start", - input: "(module (start main))", - expectedErr: "1:16: unexpected keyword: main in module.start", - }, - { - name: "start points out of range", - input: `(module - (import "" "hello" (func)) - (start 1) -)`, - expectedErr: "3:9: index 1 is out of range [0..0] in module.start", - }, - { - name: "start points nowhere", - input: "(module (start $main))", - expectedErr: "1:16: unknown ID $main in module.start", - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - _, err := DecodeModule([]byte(tc.input), wasm.Features20191205, wasm.MemorySizer) - require.EqualError(t, err, tc.expectedErr) - }) - } -} - -func TestModuleParser_ErrorContext(t *testing.T) { - p := newModuleParser(&wasm.Module{}, 0, wasm.MemorySizer) - tests := []struct { - input string - pos parserPosition - expected string - }{ - {input: "initial", pos: positionInitial, expected: ""}, - {input: "module", pos: positionModule, expected: "module"}, - {input: "module import", pos: positionImport, expected: "module.import[0]"}, - {input: "module import func", pos: positionImportFunc, expected: "module.import[0].func"}, - {input: "module func", pos: positionFunc, expected: "module.func[0]"}, - {input: "module memory", pos: positionMemory, expected: "module.memory[0]"}, - {input: "module export", pos: positionExport, expected: "module.export[0]"}, - {input: "module export func", pos: positionExportFunc, expected: "module.export[0].func"}, - {input: "start", pos: positionStart, expected: "module.start"}, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.input, func(t *testing.T) { - p.pos = tc.pos - require.Equal(t, tc.expected, p.errorContext()) - }) - } -} diff --git a/internal/watzero/internal/errors.go b/internal/watzero/internal/errors.go deleted file mode 100644 index ec7c02d7c1..0000000000 --- a/internal/watzero/internal/errors.go +++ /dev/null @@ -1,76 +0,0 @@ -package internal - -import ( - "fmt" - - "github.com/tetratelabs/wazero/internal/wasm" -) - -// FormatError allows control over the format of errors parsing the WebAssembly Text Format. -type FormatError struct { - // Line is the source line number determined by unescaped '\n' characters of the error or EOF - Line uint32 - // Col is the UTF-8 column number of the error or EOF - Col uint32 - // Context is where symbolically the error occurred. Ex "imports[1].func" - Context string - cause error -} - -func (e *FormatError) Error() string { - if e.Context == "" { // error starting the file - return fmt.Sprintf("%d:%d: %v", e.Line, e.Col, e.cause) - } - return fmt.Sprintf("%d:%d: %v in %s", e.Line, e.Col, e.cause, e.Context) -} - -func (e *FormatError) Unwrap() error { - return e.cause -} - -func unexpectedFieldName(tokenBytes []byte) error { - return fmt.Errorf("unexpected field: %s", tokenBytes) -} - -func expectedField(tok tokenType) error { - return fmt.Errorf("expected field, but parsed %s", tok) -} - -func unexpectedToken(tok tokenType, tokenBytes []byte) error { - switch tok { - case tokenLParen, tokenRParen: - return fmt.Errorf("unexpected '%s'", tok) - default: - return fmt.Errorf("unexpected %s: %s", tok, tokenBytes) - } -} - -// importAfterModuleDefined is the failure for the condition "all imports must occur before any regular definition", -// which applies regardless of abbreviation. -// -// Ex. Both of these fail because an import can only be declared when SectionIDFunction is empty. -// `(func) (import "" "" (func))` which is the same as `(func) (import "" "" (func))` -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#modules%E2%91%A0%E2%91%A2 -func importAfterModuleDefined(section wasm.SectionID) error { - return fmt.Errorf("import after module-defined %s", wasm.SectionIDName(section)) -} - -// moreThanOneInvalidInSection allows enforcement of section size limits. -// -// Ex. All of these fail because they result in two memories. -// * `(module (memory 1) (memory 1))` -// * `(module (memory 1) (import "" "" (memory 1)))` -// - Note the latter expands to the same as the former: `(import "" "" (memory 1))` -// -// * `(module (import "" "" (memory 1)) (import "" "" (memory 1)))` -// -// 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 fmt.Errorf("at most one %s allowed", wasm.SectionIDName(section)) -} - -func unhandledSection(section wasm.SectionID) error { - return fmt.Errorf("BUG: unhandled %s", wasm.SectionIDName(section)) -} diff --git a/internal/watzero/internal/errors_test.go b/internal/watzero/internal/errors_test.go deleted file mode 100644 index 43043d3633..0000000000 --- a/internal/watzero/internal/errors_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package internal - -import ( - "errors" - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/wasm" -) - -func TestFormatError_Error(t *testing.T) { - t.Run("with context", func(t *testing.T) { - require.EqualError(t, &FormatError{ - Line: 1, - Col: 2, - Context: "start", - cause: errors.New("invalid token"), - }, "1:2: invalid token in start") - }) - t.Run("no context", func(t *testing.T) { - require.EqualError(t, &FormatError{ - Line: 1, - Col: 2, - cause: errors.New("invalid token"), - }, "1:2: invalid token") - }) -} - -func TestFormatError_Unwrap(t *testing.T) { - t.Run("cause", func(t *testing.T) { - cause := errors.New("invalid token") - formatErr := &FormatError{ - Line: 1, - Col: 2, - Context: "start", - cause: cause, - } - require.Equal(t, cause, formatErr.Unwrap()) - }) - t.Run("no cause", func(t *testing.T) { - formatErr := &FormatError{ - Line: 1, - Col: 2, - Context: "start", - } - require.Nil(t, formatErr.Unwrap()) - }) -} - -func TestUnexpectedToken(t *testing.T) { - tests := []struct { - input tokenType - inputBytes []byte - expected string - }{ - {tokenKeyword, []byte{'a'}, "unexpected keyword: a"}, - {tokenUN, []byte{'1'}, "unexpected uN: 1"}, - {tokenSN, []byte{'-', '1'}, "unexpected sN: -1"}, - {tokenFN, []byte{'1', '.', '1'}, "unexpected fN: 1.1"}, - {tokenString, []byte{'"', 'a', '"'}, "unexpected string: \"a\""}, - {tokenID, []byte{'$', 'a'}, "unexpected ID: $a"}, - {tokenLParen, []byte{'('}, "unexpected '('"}, - {tokenRParen, []byte{')'}, "unexpected ')'"}, - {tokenReserved, []byte{'0', '$'}, "unexpected reserved: 0$"}, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.input.String(), func(t *testing.T) { - require.Equal(t, tc.expected, unexpectedToken(tc.input, tc.inputBytes).Error()) - }) - } -} - -func TestUnhandledSection(t *testing.T) { - require.Equal(t, "BUG: unhandled function", unhandledSection(wasm.SectionIDFunction).Error()) -} - -func TestUnexpectedFieldName(t *testing.T) { - require.Equal(t, "unexpected field: moodule", unexpectedFieldName([]byte("moodule")).Error()) -} - -func TestExpectedField(t *testing.T) { - require.Equal(t, "expected field, but parsed sN", expectedField(tokenSN).Error()) -} diff --git a/internal/watzero/internal/func_parser.go b/internal/watzero/internal/func_parser.go deleted file mode 100644 index 2f3125404b..0000000000 --- a/internal/watzero/internal/func_parser.go +++ /dev/null @@ -1,387 +0,0 @@ -package internal - -import ( - "errors" - "fmt" - - "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/internal/leb128" - "github.com/tetratelabs/wazero/internal/u64" - "github.com/tetratelabs/wazero/internal/wasm" -) - -func newFuncParser(enabledFeatures wasm.Features, typeUseParser *typeUseParser, funcNamespace *indexNamespace, onFunc onFunc) *funcParser { - return &funcParser{enabledFeatures: enabledFeatures, typeUseParser: typeUseParser, funcNamespace: funcNamespace, onFunc: onFunc} -} - -type onFunc func(typeIdx wasm.Index, code *wasm.Code, name string, localNames wasm.NameMap) (tokenParser, error) - -// funcParser parses any instructions and dispatches to onFunc. -// -// Ex. `(module (func (nop)))` -// -// begin here --^ ^ -// end calls onFunc here --+ -// -// Note: funcParser is reusable. The caller resets via begin. -type funcParser struct { - // enabledFeatures should be set to moduleParser.enabledFeatures - enabledFeatures wasm.Features - - // onFunc is called when complete parsing the body. Unless testing, this should be moduleParser.onFuncEnd - onFunc onFunc - - // typeUseParser is described by moduleParser.typeUseParser - typeUseParser *typeUseParser - - // funcNamespace is described by moduleParser.funcNamespace - funcNamespace *indexNamespace - - currentName string - currentTypeIdx wasm.Index - currentParamNames wasm.NameMap - - // currentBody is the current function body encoded in WebAssembly 1.0 (20191205) binary format - currentBody []byte -} - -// end indicates the end of instructions in this function body -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#expressions%E2%91%A0 -var end = []byte{wasm.OpcodeEnd} -var codeEnd = &wasm.Code{Body: end} - -// begin should be called after reaching the wasm.ExternTypeFuncName keyword in a module field. Parsing -// continues until onFunc or error. -// -// This stage records the ID of the current function, if present, and resumes with onFunc. -// -// Ex. A func ID is present `(func $main nop)` -// -// records main --^ ^ -// parseFunc resumes here --+ -// -// Ex. No func ID `(func nop)` -// -// calls parseFunc --^ -func (p *funcParser) begin(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - if tok == tokenID { // Ex. $main - if id, err := p.funcNamespace.setID(tokenBytes); err != nil { - return nil, err - } else { - p.currentName = id - } - return p.parseFunc, nil - } - p.currentName = "" - return p.parseFunc(tok, tokenBytes, line, col) -} - -// parseFunc passes control to the typeUseParser until any signature is read, then funcParser until and locals or body -// are read. Finally, this finishes via endFunc. -// -// Ex. `(module (func $math.pi (result f32))` -// -// begin here --^ ^ -// endFunc resumes here --+ -// -// Ex. `(module (func $math.pi (result f32) (local i32) )` -// -// begin here --^ ^ ^ -// funcParser.afterTypeUse resumes here --+ | -// endFunc resumes here --+ -// -// Ex. If there is no signature `(func)` -// -// calls endFunc here ---^ -func (p *funcParser) parseFunc(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - if tok == tokenID { // Ex. (func $main $main) - return nil, fmt.Errorf("redundant ID %s", tokenBytes) - } - - return p.typeUseParser.begin(wasm.SectionIDFunction, p.afterTypeUse, tok, tokenBytes, line, col) -} - -// afterTypeUse is a tokenParser that starts after a type use. -// -// The onFunc field is invoked once any instructions are written into currentBody. -// -// Ex. Given the source `(module (func nop))` -// -// afterTypeUse starts here --^ ^ -// calls onFunc here --+ -func (p *funcParser) afterTypeUse(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - switch pos { - case callbackPositionEndField: - return p.onFunc(typeIdx, codeEnd, p.currentName, paramNames) - case callbackPositionUnhandledField: - return sExpressionsUnsupported(tok, tokenBytes, line, col) - } - - p.currentBody = nil - p.currentTypeIdx = typeIdx - p.currentParamNames = paramNames - return p.beginFieldOrInstruction(tok, tokenBytes, line, col) -} - -func sExpressionsUnsupported(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 "local": - return nil, errors.New("TODO: local") - } - return nil, fmt.Errorf("TODO: s-expressions are not yet supported: %s", tokenBytes) -} - -func (p *funcParser) beginFieldOrInstruction(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenLParen: - return sExpressionsUnsupported, nil - case tokenRParen: - return p.end() - case tokenKeyword: - return p.beginInstruction(tokenBytes) - } - return nil, unexpectedToken(tok, tokenBytes) -} - -// beginInstruction parses the token into an opcode and dispatches accordingly. If there are none, this calls onFunc. -func (p *funcParser) beginInstruction(tokenBytes []byte) (next tokenParser, err error) { - var opCode wasm.Opcode - switch string(tokenBytes) { - 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 wasm.OpcodeDropName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-parametricmathsfdrop - opCode = wasm.OpcodeDrop - next = p.beginFieldOrInstruction - case wasm.OpcodeUnreachableName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-instr-control - opCode = wasm.OpcodeUnreachable - next = p.beginFieldOrInstruction - - case wasm.OpcodeF32ConstName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-instr-numeric - opCode = wasm.OpcodeF32Const - next = p.parseF32 - case wasm.OpcodeF64ConstName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-instr-numeric - opCode = wasm.OpcodeF64Const - next = p.parseF64 - - 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 wasm.OpcodeI32SubName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-instr-numeric - opCode = wasm.OpcodeI32Sub - next = p.beginFieldOrInstruction - 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.OpcodeI32LoadName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instructions%E2%91%A8 - return p.encodeMemArgOp(wasm.OpcodeI32Load, alignment32) - case wasm.OpcodeI32StoreName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instructions%E2%91%A8 - return p.encodeMemArgOp(wasm.OpcodeI32Store, alignment32) - 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.encodeMemArgOp(wasm.OpcodeI64Load, alignment64) - case wasm.OpcodeI64StoreName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instructions%E2%91%A8 - return p.encodeMemArgOp(wasm.OpcodeI64Store, alignment64) - 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 - case wasm.OpcodeMemoryGrowName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instructions%E2%91%A6 - p.currentBody = append(p.currentBody, wasm.OpcodeMemoryGrow, 0x00) // reserved arg0 - return p.beginFieldOrInstruction, nil - case wasm.OpcodeMemorySizeName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instructions%E2%91%A6 - p.currentBody = append(p.currentBody, wasm.OpcodeMemorySize, 0x00) // reserved arg0 - return p.beginFieldOrInstruction, nil - - // 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 - - // next are nontrapping-float-to-int-conversion - // See https://github.com/WebAssembly/spec/blob/main/proposals/nontrapping-float-to-int-conversion/Overview.md - - case wasm.OpcodeI32TruncSatF32SName: - opCode = wasm.OpcodeMiscPrefix - p.currentBody = append(p.currentBody, opCode, wasm.OpcodeMiscI32TruncSatF32S) - case wasm.OpcodeI32TruncSatF32UName: - opCode = wasm.OpcodeMiscPrefix - p.currentBody = append(p.currentBody, opCode, wasm.OpcodeMiscI32TruncSatF32U) - case wasm.OpcodeI32TruncSatF64SName: - opCode = wasm.OpcodeMiscPrefix - p.currentBody = append(p.currentBody, opCode, wasm.OpcodeMiscI32TruncSatF64S) - case wasm.OpcodeI32TruncSatF64UName: - opCode = wasm.OpcodeMiscPrefix - p.currentBody = append(p.currentBody, opCode, wasm.OpcodeMiscI32TruncSatF64U) - case wasm.OpcodeI64TruncSatF32SName: - opCode = wasm.OpcodeMiscPrefix - p.currentBody = append(p.currentBody, opCode, wasm.OpcodeMiscI64TruncSatF32S) - case wasm.OpcodeI64TruncSatF32UName: - opCode = wasm.OpcodeMiscPrefix - p.currentBody = append(p.currentBody, opCode, wasm.OpcodeMiscI64TruncSatF32U) - case wasm.OpcodeI64TruncSatF64SName: - opCode = wasm.OpcodeMiscPrefix - p.currentBody = append(p.currentBody, opCode, wasm.OpcodeMiscI64TruncSatF64S) - case wasm.OpcodeI64TruncSatF64UName: - opCode = wasm.OpcodeMiscPrefix - p.currentBody = append(p.currentBody, opCode, wasm.OpcodeMiscI64TruncSatF64U) - - default: - return nil, fmt.Errorf("unsupported instruction: %s", tokenBytes) - } - - // Guard >1.0 feature sign-extension-ops - if opCode >= wasm.OpcodeI32Extend8S && opCode <= wasm.OpcodeI64Extend32S { - if err = p.enabledFeatures.Require(wasm.FeatureSignExtensionOps); err != nil { - return nil, fmt.Errorf("%s invalid as %v", tokenBytes, err) - } - } - - // Guard >1.0 feature nontrapping-float-to-int-conversion - if opCode == wasm.OpcodeMiscPrefix { - if err = p.enabledFeatures.Require(wasm.FeatureNonTrappingFloatToIntConversion); err != nil { - return nil, fmt.Errorf("%s invalid as %v", tokenBytes, err) - } - return p.beginFieldOrInstruction, nil - } - - p.currentBody = append(p.currentBody, opCode) - return next, nil -} - -const ( - // alignment32 is because it is 32bit is 2^2 bytes - alignment32 = 2 - // alignment64 is because it is 64bit is 2^3 bytes - alignment64 = 3 -) - -func (p *funcParser) encodeMemArgOp(oc wasm.Opcode, alignment byte) (tokenParser, error) { - offset := byte(0) // offset=0 because that's the default - p.currentBody = append(p.currentBody, oc, alignment, offset) - return p.beginFieldOrInstruction, nil -} - -// end invokes onFunc to continue parsing -func (p *funcParser) end() (tokenParser, error) { - var code *wasm.Code - if p.currentBody == nil { - code = codeEnd - } else { - code = &wasm.Code{Body: append(p.currentBody, wasm.OpcodeEnd)} - } - return p.onFunc(p.currentTypeIdx, code, p.currentName, p.currentParamNames) -} - -// parseF32 parses a wasm.ValueTypeF32 and appends it to the currentBody. -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#floating-point%E2%91%A4 -func (p *funcParser) parseF32(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenUN { - return nil, unexpectedToken(tok, tokenBytes) - } - if i, overflow := decodeUint32(tokenBytes); overflow { // TODO: negative hex nan inf and actual float! - return nil, fmt.Errorf("f32 outside range of uint32: %s", tokenBytes) - } else { - p.currentBody = append(p.currentBody, u64.LeBytes(api.EncodeF32(float32(i)))...) - } - return p.beginFieldOrInstruction, nil -} - -// parseF64 parses a wasm.ValueTypeF64 and appends it to the currentBody. -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#floating-point%E2%91%A4 -func (p *funcParser) parseF64(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenUN { - return nil, unexpectedToken(tok, tokenBytes) - } - if i, overflow := decodeUint64(tokenBytes); overflow { // TODO: negative hex nan inf and actual float! - return nil, fmt.Errorf("f64 outside range of uint64: %s", tokenBytes) - } else { - p.currentBody = append(p.currentBody, u64.LeBytes(api.EncodeF64(float64(i)))...) - } - return p.beginFieldOrInstruction, nil -} - -// parseI32 parses a wasm.ValueTypeI32 and appends it to the currentBody. -func (p *funcParser) parseI32(tok tokenType, tokenBytes []byte, _, _ 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, _, _ 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) { - bodyOffset := uint32(len(p.currentBody)) - idx, resolved, err := p.funcNamespace.parseIndex(wasm.SectionIDCode, bodyOffset, tok, tokenBytes, line, col) - if err != nil { - return nil, err - } - if !resolved && tok == tokenID { - p.currentBody = append(p.currentBody, 0) // will be replaced later - } else { - p.currentBody = append(p.currentBody, leb128.EncodeUint32(idx)...) - } - return p.beginFieldOrInstruction, nil -} - -// TODO: port this to the indexNamespace -func (p *funcParser) parseLocalIndex(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenUN: // Ex. 1 - i, overflow := decodeUint32(tokenBytes) - if overflow { - return nil, fmt.Errorf("index outside range of uint32: %s", tokenBytes) - } - p.currentBody = append(p.currentBody, leb128.EncodeUint32(i)...) - - // TODO: it is possible this is out of range in the index. Local out-of-range is caught in Store.Initialize, but - // we can provide a better area since we can access the line and col info. However, since the local index starts - // with parameters and they are on the type, and that type may be defined after the function, it can get hairy. - // To handle this neatly means doing a quick check to see if the type is already present and immediately - // validate. If the type isn't yet present, we can save off the context for late validation, noting that we - // should probably save off the instruction count or at least the current opcode to help with the error message. - return p.beginFieldOrInstruction, nil - case tokenID: // Ex $y - return nil, errors.New("TODO: index variables are not yet supported") - } - return nil, unexpectedToken(tok, tokenBytes) -} diff --git a/internal/watzero/internal/func_parser_test.go b/internal/watzero/internal/func_parser_test.go deleted file mode 100644 index 5e581dcbb6..0000000000 --- a/internal/watzero/internal/func_parser_test.go +++ /dev/null @@ -1,553 +0,0 @@ -package internal - -import ( - "errors" - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/wasm" -) - -func TestFuncParser(t *testing.T) { - tests := []struct { - name, source string - expected *wasm.Code - }{ - { - name: "empty", - source: "(func)", - expected: &wasm.Code{Body: []byte{wasm.OpcodeEnd}}, - }, - { - name: "local.get", - source: "(func local.get 0)", - expected: &wasm.Code{Body: []byte{wasm.OpcodeLocalGet, 0x00, wasm.OpcodeEnd}}, - }, - { - name: "local.get, drop", - source: "(func local.get 0 drop)", - expected: &wasm.Code{Body: []byte{wasm.OpcodeLocalGet, 0x00, wasm.OpcodeDrop, wasm.OpcodeEnd}}, - }, - { - name: "local.get twice", - source: "(func local.get 0 local.get 1)", - expected: &wasm.Code{Body: []byte{wasm.OpcodeLocalGet, 0x00, wasm.OpcodeLocalGet, 0x01, wasm.OpcodeEnd}}, - }, - { - name: "local.get twice and add", - source: "(func local.get 0 local.get 1 i32.add)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeLocalGet, 0x01, - wasm.OpcodeI32Add, - wasm.OpcodeEnd, - }}, - }, - { - name: "f32.const", - source: "(func f32.const 306)", - expected: &wasm.Code{Body: []byte{wasm.OpcodeF32Const, 0x0, 0x0, 0x99, 0x43, 0x0, 0x0, 0x0, 0x0, wasm.OpcodeEnd}}, - }, - { - name: "f64.const", - source: "(func f64.const 356)", - expected: &wasm.Code{Body: []byte{wasm.OpcodeF64Const, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x76, 0x40, wasm.OpcodeEnd}}, - }, - { - name: "i32.const", - source: "(func i32.const 306)", - expected: &wasm.Code{Body: []byte{wasm.OpcodeI32Const, 0xb2, 0x02, wasm.OpcodeEnd}}, - }, - { - name: "i32.add", - source: "(func i32.const 2 i32.const 1 i32.add)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeI32Const, 0x02, wasm.OpcodeI32Const, 0x01, wasm.OpcodeI32Add, wasm.OpcodeEnd, - }}, - }, - { - name: "i32.sub", - source: "(func i32.const 2 i32.const 1 i32.sub)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeI32Const, 0x02, wasm.OpcodeI32Const, 0x01, wasm.OpcodeI32Sub, wasm.OpcodeEnd, - }}, - }, - { - name: "i32.load", - source: "(func i32.const 8 i32.load)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeI32Const, 8, // memory offset to load - wasm.OpcodeI32Load, 0x2, 0x0, // load alignment=2 (natural alignment) staticOffset=0 - wasm.OpcodeEnd, - }}, - }, - { - name: "i32.store", - source: "(func i32.const 8 i32.const 37 i32.store)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeI32Const, 8, // dynamic memory offset to store - wasm.OpcodeI32Const, 37, // value to store - wasm.OpcodeI32Store, 0x2, 0x0, // load alignment=2 (natural alignment) staticOffset=0 - 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, // 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, - }}, - }, - { - name: "memory.grow", - source: "(func i32.const 2 memory.grow drop)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeI32Const, 2, // how many pages to grow - wasm.OpcodeMemoryGrow, 0, // memory index zero - wasm.OpcodeDrop, // drop the previous page count (or -1 if grow failed) - wasm.OpcodeEnd, - }}, - }, - { - name: "memory.size", - source: "(func memory.size drop)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeMemorySize, 0, // memory index zero - wasm.OpcodeDrop, // drop the page count - 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, - wasm.OpcodeEnd, - }}, - }, - { - name: "i32.extend16_s", - source: "(func (param i32) local.get 0 i32.extend16_s)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeI32Extend16S, - wasm.OpcodeEnd, - }}, - }, - { - name: "i64.extend8_s", - source: "(func (param i64) local.get 0 i64.extend8_s)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeI64Extend8S, - wasm.OpcodeEnd, - }}, - }, - { - name: "i64.extend16_s", - source: "(func (param i64) local.get 0 i64.extend16_s)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeI64Extend16S, - wasm.OpcodeEnd, - }}, - }, - { - name: "i64.extend32_s", - source: "(func (param i64) local.get 0 i64.extend32_s)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeI64Extend32S, - wasm.OpcodeEnd, - }}, - }, - - // Below are changes to test/core/conversions.wast from the commit that added "nontrapping-float-to-int-conversions" support. - // See https://github.com/WebAssembly/spec/commit/c8fd933fa51eb0b511bce027b573aef7ee373726 - - { - name: "i32.trunc_sat_f32_s", - source: "(func (param f32) (result i32) local.get 0 i32.trunc_sat_f32_s)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF32S, - wasm.OpcodeEnd, - }}, - }, - { - name: "i32.trunc_sat_f32_u", - source: "(func (param f32) (result i32) local.get 0 i32.trunc_sat_f32_u)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF32U, - wasm.OpcodeEnd, - }}, - }, - { - name: "i32.trunc_sat_f64_s", - source: "(func (param f64) (result i32) local.get 0 i32.trunc_sat_f64_s)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF64S, - wasm.OpcodeEnd, - }}, - }, - { - name: "i32.trunc_sat_f64_u", - source: "(func (param f64) (result i32) local.get 0 i32.trunc_sat_f64_u)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF64U, - wasm.OpcodeEnd, - }}, - }, - { - name: "i64.trunc_sat_f32_s", - source: "(func (param f32) (result i64) local.get 0 i64.trunc_sat_f32_s)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI64TruncSatF32S, - wasm.OpcodeEnd, - }}, - }, - { - name: "i64.trunc_sat_f32_u", - source: "(func (param f32) (result i64) local.get 0 i64.trunc_sat_f32_u)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI64TruncSatF32U, - wasm.OpcodeEnd, - }}, - }, - { - name: "i64.trunc_sat_f64_s", - source: "(func (param f64) (result i64) local.get 0 i64.trunc_sat_f64_s)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI64TruncSatF64S, - wasm.OpcodeEnd, - }}, - }, - { - name: "i64.trunc_sat_f64_u", - source: "(func (param f64) (result i64) local.get 0 i64.trunc_sat_f64_u)", - expected: &wasm.Code{Body: []byte{ - wasm.OpcodeLocalGet, 0x00, - wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI64TruncSatF64U, - wasm.OpcodeEnd, - }}, - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - var parsedCode *wasm.Code - var setFunc onFunc = func(typeIdx wasm.Index, code *wasm.Code, name string, localNames wasm.NameMap) (tokenParser, error) { - parsedCode = code - return parseErr, nil - } - - module := &wasm.Module{} - fp := newFuncParser(wasm.Features20220419, &typeUseParser{module: module}, newIndexNamespace(module.SectionElementCount), setFunc) - require.NoError(t, parseFunc(fp, tc.source)) - require.Equal(t, tc.expected, parsedCode) - }) - } -} - -func TestFuncParser_Call_Unresolved(t *testing.T) { - tests := []struct { - name, source string - expectedCode *wasm.Code - expectedUnresolvedIndex *unresolvedIndex - }{ - { - name: "index zero", - source: "(func call 0)", - expectedCode: &wasm.Code{Body: []byte{wasm.OpcodeCall, 0x00, wasm.OpcodeEnd}}, - expectedUnresolvedIndex: &unresolvedIndex{ - section: wasm.SectionIDCode, - bodyOffset: 1, // second byte is the position Code.Body - targetIdx: 0, // zero is literally the intended index. because targetID isn't set, this will be read - line: 1, col: 12, - }, - }, - { - name: "index", - source: "(func call 2)", - expectedCode: &wasm.Code{Body: []byte{wasm.OpcodeCall, 0x02, wasm.OpcodeEnd}}, - expectedUnresolvedIndex: &unresolvedIndex{ - section: wasm.SectionIDCode, - bodyOffset: 1, // second byte is the position Code.Body - targetIdx: 2, - line: 1, col: 12, - }, - }, - { - name: "ID", - source: "(func call $main)", - expectedCode: &wasm.Code{Body: []byte{wasm.OpcodeCall, 0x00, wasm.OpcodeEnd}}, - expectedUnresolvedIndex: &unresolvedIndex{ - section: wasm.SectionIDCode, - bodyOffset: 1, // second byte is the position Code.Body - targetID: "main", - line: 1, col: 12, - }, - }, - } - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - var parsedCode *wasm.Code - var setFunc onFunc = func(typeIdx wasm.Index, code *wasm.Code, name string, localNames wasm.NameMap) (tokenParser, error) { - parsedCode = code - return parseErr, nil - } - - module := &wasm.Module{} - 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) - }) - } -} - -func TestFuncParser_Call_Resolved(t *testing.T) { - tests := []struct { - name, source string - expected *wasm.Code - }{ - { - name: "index zero", - source: "(func call 0)", - expected: &wasm.Code{Body: []byte{wasm.OpcodeCall, 0x00, wasm.OpcodeEnd}}, - }, - { - name: "index", - source: "(func call 2)", - expected: &wasm.Code{Body: []byte{wasm.OpcodeCall, 0x02, wasm.OpcodeEnd}}, - }, - { - name: "ID", - source: "(func call $main)", - expected: &wasm.Code{Body: []byte{wasm.OpcodeCall, 0x02, wasm.OpcodeEnd}}, - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - funcNamespace := newIndexNamespace(func(sectionID wasm.SectionID) uint32 { - require.Equal(t, wasm.SectionIDFunction, sectionID) - return 0 - }) - _, err := funcNamespace.setID([]byte("$not_main")) - require.NoError(t, err) - funcNamespace.count++ - - _, err = funcNamespace.setID([]byte("$also_not_main")) - require.NoError(t, err) - funcNamespace.count++ - - _, err = funcNamespace.setID([]byte("$main")) - require.NoError(t, err) - funcNamespace.count++ - - _, err = funcNamespace.setID([]byte("$still_not_main")) - require.NoError(t, err) - funcNamespace.count++ - - var parsedCode *wasm.Code - var setFunc onFunc = func(typeIdx wasm.Index, code *wasm.Code, name string, localNames wasm.NameMap) (tokenParser, error) { - parsedCode = code - return parseErr, nil - } - - fp := newFuncParser(wasm.Features20220419, &typeUseParser{module: &wasm.Module{}}, funcNamespace, setFunc) - require.NoError(t, parseFunc(fp, tc.source)) - require.Equal(t, tc.expected, parsedCode) - }) - } -} - -func TestFuncParser_Errors(t *testing.T) { - tests := []struct { - name, source string - expectedErr string - }{ - { - name: "not field", - source: "(func ($local.get 1))", - expectedErr: "1:8: unexpected ID: $local.get", - }, - { - name: "local.get wrong value", - source: "(func local.get a)", - expectedErr: "1:17: unexpected keyword: a", - }, - { - name: "local.get symbolic ID", - source: "(func local.get $y)", - expectedErr: "1:17: TODO: index variables are not yet supported", - }, - { - name: "local.get overflow", - source: "(func local.get 4294967296)", - expectedErr: "1:17: index outside range of uint32: 4294967296", - }, - { - name: "f32.const overflow", - source: "(func f32.const 4294967296)", - expectedErr: "1:17: f32 outside range of uint32: 4294967296", - }, - { - name: "f64.const overflow", - source: "(func f64.const 18446744073709551616)", - expectedErr: "1:17: f64 outside range of uint64: 18446744073709551616", - }, - { - 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 loop)", - expectedErr: "1:7: unsupported instruction: loop", - }, - { - name: "s-expressions not yet supported", - source: "(func (f64.const 1.1))", - expectedErr: "1:8: TODO: s-expressions are not yet supported: f64.const", - }, - { - name: "param after result", - source: "(func (result i32) (param i32))", - expectedErr: "1:21: param after result", - }, - { - name: "duplicate result", - source: "(func (result i32) (result i32))", - 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", - }, - { - 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", - }, - { - 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", - }, - { - 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", - }, - { - 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", - }, - { - name: "i32.trunc_sat_f32_s disabled", - source: "(func (param f32) (result i32) local.get 0 i32.trunc_sat_f32_s)", - expectedErr: "1:44: i32.trunc_sat_f32_s invalid as feature \"nontrapping-float-to-int-conversion\" is disabled", - }, - { - name: "i32.trunc_sat_f32_u disabled", - source: "(func (param f32) (result i32) local.get 0 i32.trunc_sat_f32_u)", - expectedErr: "1:44: i32.trunc_sat_f32_u invalid as feature \"nontrapping-float-to-int-conversion\" is disabled", - }, - { - name: "i32.trunc_sat_f64_s disabled", - source: "(func (param f64) (result i32) local.get 0 i32.trunc_sat_f64_s)", - expectedErr: "1:44: i32.trunc_sat_f64_s invalid as feature \"nontrapping-float-to-int-conversion\" is disabled", - }, - { - name: "i32.trunc_sat_f64_u disabled", - source: "(func (param f64) (result i32) local.get 0 i32.trunc_sat_f64_u)", - expectedErr: "1:44: i32.trunc_sat_f64_u invalid as feature \"nontrapping-float-to-int-conversion\" is disabled", - }, - { - name: "i64.trunc_sat_f32_s disabled", - source: "(func (param f32) (result i64) local.get 0 i64.trunc_sat_f32_s)", - expectedErr: "1:44: i64.trunc_sat_f32_s invalid as feature \"nontrapping-float-to-int-conversion\" is disabled", - }, - { - name: "i64.trunc_sat_f32_u disabled", - source: "(func (param f32) (result i64) local.get 0 i64.trunc_sat_f32_u)", - expectedErr: "1:44: i64.trunc_sat_f32_u invalid as feature \"nontrapping-float-to-int-conversion\" is disabled", - }, - { - name: "i64.trunc_sat_f64_s disabled", - source: "(func (param f64) (result i64) local.get 0 i64.trunc_sat_f64_s)", - expectedErr: "1:44: i64.trunc_sat_f64_s invalid as feature \"nontrapping-float-to-int-conversion\" is disabled", - }, - { - name: "i64.trunc_sat_f64_u disabled", - source: "(func (param f64) (result i64) local.get 0 i64.trunc_sat_f64_u)", - expectedErr: "1:44: i64.trunc_sat_f64_u invalid as feature \"nontrapping-float-to-int-conversion\" is disabled", - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - module := &wasm.Module{} - fp := newFuncParser(wasm.Features20191205, &typeUseParser{module: module}, newIndexNamespace(module.SectionElementCount), failOnFunc) - require.EqualError(t, parseFunc(fp, tc.source), tc.expectedErr) - }) - } -} - -var failOnFunc onFunc = func(typeIdx wasm.Index, code *wasm.Code, name string, localNames wasm.NameMap) (tokenParser, error) { - return nil, errors.New("unexpected to call onFunc on error") -} - -func parseFunc(fp *funcParser, source string) error { - line, col, err := lex(skipTokens(2, fp.begin), []byte(source)) // skip the leading (func - if err != nil { - err = &FormatError{Line: line, Col: col, cause: err} - } - return err -} diff --git a/internal/watzero/internal/index_namespace.go b/internal/watzero/internal/index_namespace.go deleted file mode 100644 index 964fb1fb26..0000000000 --- a/internal/watzero/internal/index_namespace.go +++ /dev/null @@ -1,194 +0,0 @@ -package internal - -import ( - "errors" - "fmt" - - "github.com/tetratelabs/wazero/internal/wasm" -) - -// newIndexNamespace sectionElementCount parameter should be wasm.Module SectionElementCount unless testing. -func newIndexNamespace(sectionElementCount func(wasm.SectionID) uint32) *indexNamespace { - return &indexNamespace{sectionElementCount: sectionElementCount, idToIdx: map[string]wasm.Index{}} -} - -// indexNamespace contains the count in an index namespace and any association of symbolic IDs to numeric indices. -// -// The Web Assembly Text Format allows use of symbolic identifiers, ex "$main", instead of numeric indices for most -// sections, notably types, functions and parameters. For example, if a function is defined with the tokenID "$main", -// the start section can use that symbolic ID instead of its numeric offset in the code section. These IDs require two -// features, uniqueness and lookup, as implemented with a map. The key is stripped of the leading '$' to match other -// tools, as described in stripDollar -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-context -type indexNamespace struct { - sectionElementCount func(wasm.SectionID) uint32 - - unresolvedIndices []*unresolvedIndex - - // count is the count of items in this namespace - count uint32 - - // idToIdx resolves symbolic identifiers, such as "v_v" to a numeric index in the appropriate section, such - // as '2'. Duplicate identifiers are not allowed by specification. - // - // Note: This is not encoded in the wasm.NameSection as there is no type name section in WebAssembly 1.0 (20191205) - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-context - idToIdx map[string]wasm.Index -} - -// setID ensures the given tokenID is unique within this context and raises an error if not. The resulting mapping is -// stripped of the leading '$' to match other tools, as described in stripDollar. -func (i *indexNamespace) setID(idToken []byte) (string, error) { - name, err := i.requireNoID(idToken) - if err != nil { - return name, err - } - i.idToIdx[name] = i.count - return name, nil -} - -// hasID checks to see if this tokenID is unique within this context and returns an error. The result string is -// stripped of the leading '$' to match other tools, as described in stripDollar. -func (i *indexNamespace) requireNoID(idToken []byte) (string, error) { - name := string(stripDollar(idToken)) - if _, ok := i.idToIdx[name]; ok { - return name, fmt.Errorf("duplicate ID %s", idToken) - } - return name, nil -} - -// parseIndex is a tokenParser called in a field that can only contain a symbolic identifier or raw numeric index. -func (i *indexNamespace) parseIndex(section wasm.SectionID, bodyOffset uint32, tok tokenType, tokenBytes []byte, line, col uint32) (targetIdx wasm.Index, resolved bool, err error) { - switch tok { - case tokenUN: // Ex. 2 - if i, overflow := decodeUint32(tokenBytes); overflow { - return 0, false, fmt.Errorf("index outside range of uint32: %s", tokenBytes) - } else { - targetIdx = i - } - - if targetIdx < i.count { - resolved = true - } else { - i.recordOutOfRange(section, bodyOffset, targetIdx, line, col) - } - case tokenID: // Ex. $main - targetID := string(stripDollar(tokenBytes)) - if targetIdx, resolved = i.idToIdx[targetID]; !resolved { - i.recordUnresolved(section, bodyOffset, targetID, line, col) - } - return - case tokenRParen: - err = errors.New("missing index") - default: - err = unexpectedToken(tok, tokenBytes) - } - return -} - -// recordUnresolved records an ID, such as "main", is not yet resolvable. -// -// See unresolvedIndex for parameter descriptions -func (i *indexNamespace) recordUnresolved(section wasm.SectionID, bodyOffset uint32, targetID string, line, col uint32) { - idx := i.sectionElementCount(section) - i.unresolvedIndices = append(i.unresolvedIndices, &unresolvedIndex{section: section, idx: idx, bodyOffset: bodyOffset, targetID: targetID, line: line, col: col}) -} - -// recordUnresolved records numeric index is currently out of bounds. -// -// See unresolvedIndex for parameter descriptions -func (i *indexNamespace) recordOutOfRange(section wasm.SectionID, bodyOffset uint32, targetIdx wasm.Index, line, col uint32) { - idx := i.sectionElementCount(section) - i.unresolvedIndices = append(i.unresolvedIndices, &unresolvedIndex{section: section, idx: idx, bodyOffset: bodyOffset, targetIdx: targetIdx, line: line, col: col}) -} - -// unresolvedIndex is symbolic ID, such as "main", or its equivalent numeric value, such as "2". -// -// Note: section, idx, line and col used for lazy validation of index. These are attached to an error if later parsed to -// be invalid (ex an unknown function or out-of-bound index). -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#indices%E2%91%A4 -type unresolvedIndex struct { - // section is the primary index of what's targeting this index (in the api.Module) - section wasm.SectionID - - // idx is slice position in the section - idx wasm.Index - - // bodyOffset is only used when section is wasm.SectionIDCode and identifies the offset in wasm.Code Body. - bodyOffset uint32 - - // id is set when its corresponding token is tokenID to a symbolic identifier index. Ex. main - // - // Ex. This is t0 from (import "Math" "PI" (func (type $t0))), when (type $t0 (func ...)) does not yet exist. - // - // Note: After parsing, convert this to a numeric index via requireIndex - targetID string - - // numeric is set when its corresponding token is tokenUN is a wasm.Index. Ex. 3 - // - // Ex. If this is 32 from (import "Math" "PI" (func (type 32))), but there are only 10 types defined in the module. - // - // Note: To avoid conflating unset with the valid index zero, only read this value when ID is unset. - // Note: After parsing, check this is in range via requireIndexInRange - targetIdx wasm.Index - - // line is the line in the source where the index was defined. - line uint32 - - // col is the column on the line where the index was defined. - col uint32 -} - -// resolve ensures the idx points to a valid numeric function index or returns a FormatError if it cannot be bound. -// -// Failure cases are when a symbolic identifier points nowhere or a numeric index is out of range. -// Ex. (start $t0) exists, but there's no import or module-defined function with that name. -// -// or (start 32) exists, but there are only 10 functions. -func (i *indexNamespace) resolve(unresolved *unresolvedIndex) (wasm.Index, error) { - if unresolved.targetID == "" { // already bound to a numeric index, but we have to verify it is in range - if err := requireIndexInRange(unresolved.targetIdx, i.count); err != nil { - return 0, unresolved.formatErr(err) - } - return unresolved.targetIdx, nil - } - numeric, err := i.requireIndex(unresolved.targetID) - if err != nil { - return 0, unresolved.formatErr(err) - } - return numeric, nil -} - -func (i *indexNamespace) requireIndex(id string) (wasm.Index, error) { - if numeric, ok := i.idToIdx[id]; ok { - return numeric, nil - } - return 0, fmt.Errorf("unknown ID $%s", id) // re-attach '$' as that was in the text format! -} - -func requireIndexInRange(index wasm.Index, count uint32) error { - if index >= count { - if count == 0 { - return fmt.Errorf("index %d is not in range due to empty namespace", index) - } - return fmt.Errorf("index %d is out of range [0..%d]", index, count-1) - } - return nil -} - -func (d *unresolvedIndex) formatErr(err error) error { - // This check allows us to defer Sprintf until there's an error, and reuse the same logic for non-indexed types. - var context string - switch d.section { - case wasm.SectionIDCode: - context = fmt.Sprintf("module.code[%d].body[%d]", d.idx, d.bodyOffset) - case wasm.SectionIDExport: - context = fmt.Sprintf("module.exports[%d].func", d.idx) - case wasm.SectionIDStart: - context = "module.start" - } - return &FormatError{d.line, d.col, context, err} -} diff --git a/internal/watzero/internal/index_namespace_test.go b/internal/watzero/internal/index_namespace_test.go deleted file mode 100644 index ec703813d8..0000000000 --- a/internal/watzero/internal/index_namespace_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package internal - -import ( - "errors" - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/wasm" -) - -func TestIndexNamespace_SetId(t *testing.T) { - in := newIndexNamespace(func(_ wasm.SectionID) uint32 { - t.Fail() - return 0 - }) - t.Run("set when empty", func(t *testing.T) { - id, err := in.setID([]byte("$x")) - require.NoError(t, err) - require.Equal(t, "x", id) // strips "$" to be like the name section - require.Equal(t, map[string]wasm.Index{"x": wasm.Index(0)}, in.idToIdx) - }) - t.Run("set when exists fails", func(t *testing.T) { - _, err := in.setID([]byte("$x")) - - // error reflects the original '$' prefix - require.EqualError(t, err, "duplicate ID $x") - require.Equal(t, map[string]wasm.Index{"x": wasm.Index(0)}, in.idToIdx) // no change - }) -} - -func TestIndexNamespace_Resolve(t *testing.T) { - for _, tt := range []struct { - name string - namespace *indexNamespace - id string - numeric wasm.Index - expected wasm.Index - expectedErr string - }{ - { - name: "numeric exists", - namespace: &indexNamespace{count: 2}, - numeric: 1, - expected: 1, - }, - { - name: "numeric, but empty", - namespace: &indexNamespace{count: 0}, - numeric: 0, - expectedErr: "3:4: index 0 is not in range due to empty namespace", - }, - { - name: "numeric out of range", - namespace: &indexNamespace{count: 2}, - numeric: 2, - expectedErr: "3:4: index 2 is out of range [0..1]", - }, - { - name: "ID exists", - namespace: &indexNamespace{idToIdx: map[string]wasm.Index{"x": 1}}, - id: "x", - expected: 1, - }, - { - name: "ID, but empty", - namespace: &indexNamespace{idToIdx: map[string]wasm.Index{}}, - id: "X", - expectedErr: "3:4: unknown ID $X", - }, - { - name: "ID doesn't exist", - namespace: &indexNamespace{idToIdx: map[string]wasm.Index{"x": 1}}, - id: "X", - expectedErr: "3:4: unknown ID $X", - }, - } { - tc := tt - t.Run(tc.name, func(t *testing.T) { - ui := &unresolvedIndex{section: wasm.SectionIDFunction, idx: 1, targetID: tc.id, targetIdx: tc.numeric, line: 3, col: 4} - index, err := tc.namespace.resolve(ui) - if tc.expectedErr == "" { - require.NoError(t, err) - require.Equal(t, tc.expected, index) - } else { - require.EqualError(t, err, tc.expectedErr) - } - }) - } -} - -func TestIndexNamespace_RequireIndex(t *testing.T) { - for _, tt := range []struct { - name string - namespace *indexNamespace - id string - expected wasm.Index - expectedErr string - }{ - {name: "exists", namespace: &indexNamespace{idToIdx: map[string]wasm.Index{"x": 1}}, id: "x", expected: 1}, - {name: "empty", namespace: &indexNamespace{idToIdx: map[string]wasm.Index{}}, id: "X", expectedErr: "unknown ID $X"}, - {name: "doesn't exist", namespace: &indexNamespace{idToIdx: map[string]wasm.Index{"x": 1}}, id: "X", expectedErr: "unknown ID $X"}, - } { - tc := tt - t.Run(tc.name, func(t *testing.T) { - index, err := tc.namespace.requireIndex(tc.id) - if tc.expectedErr == "" { - require.NoError(t, err) - require.Equal(t, tc.expected, index) - } else { - require.EqualError(t, err, tc.expectedErr) - } - }) - } -} - -func TestUnresolvedIndex_FormatError(t *testing.T) { - for _, tt := range []struct { - section wasm.SectionID - expectedErr string - }{ - {section: wasm.SectionIDCode, expectedErr: "3:4: bomb in module.code[1].body[2]"}, - {section: wasm.SectionIDExport, expectedErr: "3:4: bomb in module.exports[1].func"}, - {section: wasm.SectionIDStart, expectedErr: "3:4: bomb in module.start"}, - } { - tc := tt - t.Run(wasm.SectionIDName(tc.section), func(t *testing.T) { - ui := &unresolvedIndex{section: tc.section, idx: 1, bodyOffset: 2, targetID: "X", targetIdx: 2, line: 3, col: 4} - require.EqualError(t, ui.formatErr(errors.New("bomb")), tc.expectedErr) - }) - } -} - -func TestRequireIndexInRange(t *testing.T) { - for _, tt := range []struct { - name string - index, count uint32 - expectedErr string - }{ - {name: "in range: index 0", index: 0, count: 1}, - {name: "in range: index positive", index: 0, count: 1}, - {name: "out of range: index 0", index: 0, count: 0, expectedErr: "index 0 is not in range due to empty namespace"}, - {name: "out of range: index equals count", index: 3, count: 3, expectedErr: "index 3 is out of range [0..2]"}, - {name: "out of range: index greater than count", index: 4, count: 3, expectedErr: "index 4 is out of range [0..2]"}, - } { - tc := tt - t.Run(tc.name, func(t *testing.T) { - err := requireIndexInRange(tc.index, tc.count) - if tc.expectedErr != "" { - require.EqualError(t, err, tc.expectedErr) - } else { - require.Nil(t, err) - } - }) - } -} diff --git a/internal/watzero/internal/lexer.go b/internal/watzero/internal/lexer.go deleted file mode 100644 index ebd335b191..0000000000 --- a/internal/watzero/internal/lexer.go +++ /dev/null @@ -1,274 +0,0 @@ -package internal - -import ( - "errors" - "fmt" - "unicode/utf8" -) - -// tokenParser parses the current token and returns a parser for the next. -// -// * tokenType is the token type -// * tokenBytes are the UTF-8 bytes representing the token. Do not modify this. -// * line is the source line number determined by unescaped '\n' characters. -// * col is the UTF-8 column number. -// -// Returning an error will short-circuit any future invocations. -// -// Note: Do not include the line and column number in a parsing error as that will be attached automatically. Line and -// column are here for storing the source location, such as for use in runtime stack traces. -type tokenParser func(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) - -// TODO: since S-expressions are common and also multiple nesting levels in fields, ex. (import (func)), think about a -// special result of popCount which pops one or two RParens. This could inline skipping parens, which have no error -// possibility unless there are extra tokens. - -var ( - constantLParen = []byte{'('} - constantRParen = []byte{')'} -) - -// lex invokes the parser function for the given source. This function returns when the source is exhausted or an error -// occurs. -// -// Here's a description of the return values: -// * line is the source line number determined by unescaped '\n' characters of the error or EOF -// * col is the UTF-8 column number of the error or EOF -// * err is an error invoking the parser, dangling block comments or unexpected characters. -func lex(parser tokenParser, source []byte) (line, col uint32, err error) { - // i is the source index to begin reading, inclusive. - i := 0 - // end is the source index to stop reading, exclusive. - end := len(source) - line = 1 - col = 1 - - // Web assembly expressions are grouped by parenthesis, even the minimal example "(module)". We track nesting level - // to help report problems instead of bubbling to the parser layer. - parenDepth := 0 - - // Block comments, ex. (; comment ;), can span multiple lines and also nest, ex. (; one (; two ;) ). - // There may be no block comments, but we declare the variable that tracks them here, as it is more efficient vs - // inline processing. - blockCommentDepth := 0 - - for ; i < end; i, col = i+1, col+1 { - b1 := source[i] - - // The spec does not consider newlines apart from '\n'. Notably, a bare '\r' is not a newline here. - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-comment - if b1 == '\n' { - line++ - col = 0 // for loop will + 1 - continue // next line - } - - if b1 == ' ' || b1 == '\t' || b1 == '\r' { // fast path ASCII whitespace - continue // next whitespace - } - - // Handle parens and comments, noting block comments, ex. "(; look! ;)", can be nested. - switch b1 { - case '(': - peek := i + 1 - if peek == end { // invalid regardless of block comment or not. nothing opens at EOF! - return line, col, errors.New("found '(' at end of input") - } - if source[peek] == ';' { // next block comment - i = peek // continue after "(;" - col++ - blockCommentDepth++ - continue - } else if blockCommentDepth == 0 { // Fast path left paren token at the expense of code duplication. - if parser, err = parser(tokenLParen, constantLParen, line, col); err != nil { - return line, col, err - } - parenDepth++ - continue - } - case ')': - if blockCommentDepth == 0 { // Fast path right paren token at the expense of code duplication. - if parenDepth == 0 { - return line, col, errors.New("found ')' before '('") - } - if parser, err = parser(tokenRParen, constantRParen, line, col); err != nil { - return line, col, err - } - parenDepth-- - continue - } - case ';': // possible line comment or block comment end - peek := i + 1 - if peek < end { - b2 := source[peek] - if blockCommentDepth > 0 && b2 == ')' { - i = peek // continue after ";)" - col++ - blockCommentDepth-- - continue - } - - if b2 == ';' { // line comment - // Start after ";;" and run until the end. Note UTF-8 (multi-byte) characters are allowed. - peek++ - col++ - - LineComment: - for peek < end { - peeked := source[peek] - if peeked == '\n' { - break LineComment // EOL bookkeeping will proceed on the next iteration - } - - col++ - s := utf8Size[peeked] // While unlikely, it is possible the byte peeked is invalid unicode - if s == 0 { - return line, col, fmt.Errorf("found an invalid byte in line comment: 0x%x", peeked) - } - peek = peek + s - } - - // -1 because for loop will + 1: This optimizes speed of tokenization over line comments. - i = peek - 1 // at the '\n' - continue // end of line comment - } - } - } - - // non-ASCII is only supported in comments. Check UTF-8 size as we may need to set position > column! - if blockCommentDepth > 0 { - s := utf8Size[b1] // While unlikely, it is possible the current byte is invalid unicode - if s == 0 { - return line, col, fmt.Errorf("found an invalid byte in block comment: 0x%x", b1) - } - i = i + s - 1 // -1 because for loop will + 1: This optimizes speed of tokenization over block comments. - continue - } - - // One design-affecting constraint is that all tokens begin and end with a 7-bit ASCII character. This - // simplifies line and column counting and how to detect the end of a token. - // - // Note: Even though string allows unicode contents it is enclosed by ASCII double-quotes ("). - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#characters%E2%91%A0 - - tok := firstTokenByte[b1] - // Track positions passed to the parser - b := i // the start position of the token (fixed) - peek := i + 1 // when finished scanning, this becomes end (the position after the token). - // line == line because no token is allowed to include an unescaped '\n' - c := col // the start column of the token (fixed) - - switch tok { - // case tokenLParen, tokenRParen: // min/max 1 byte - case tokenSN: // min 2 bytes for sign and number; ambiguous: could be tokenFN - return line, c, errors.New("TODO: signed") - case tokenUN: // min 1 byte; ambiguous when >=3 bytes as could be tokenFN - if peek < end { - peeked := source[peek] - if peeked == 'x' { - return line, col, errors.New("TODO: hex") - } - Number: - // Start after the number and run until the end. Note all allowed characters are single byte. - for ; peek < end; peek++ { - switch source[peek] { - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_': - i = peek - col++ - default: - break Number // end of this token (or malformed, which the next loop will notice) - } - } - } - case tokenString: // min 2 bytes for empty string ("") - hitQuote := false - // Start at the second character and run until the end. Note UTF-8 (multi-byte) characters are allowed. - String: - for peek < end { - peeked := source[peek] - if peeked == '"' { // TODO: escaping and banning disallowed characters like newlines. - hitQuote = true - break String - } - - col++ - s := utf8Size[peeked] // While unlikely, it is possible the current byte is invalid unicode - if s == 0 { - return line, col, fmt.Errorf("found an invalid byte in string token: 0x%x", peeked) - } - peek = peek + s - } - - if !hitQuote { - return line, col, errors.New("expected end quote") - } - - i = peek - // set the position to after the quote - peek++ - col++ - case tokenKeyword, tokenID, tokenReserved: // min 1 byte; end with zero or more idChar - // Start after the first character and run until the end. Note all allowed characters are single byte. - IdChars: - for ; peek < end; peek++ { - if !idChar[source[peek]] { - break IdChars // end of this token (or malformed, which the next loop will notice) - } - col++ - } - i = peek - 1 - default: - if b1 > 0x7F { // non-ASCII - r, _ := utf8.DecodeRune(source[line:]) - return line, col, fmt.Errorf("expected an ASCII character, not %s", string(r)) - } - return line, col, fmt.Errorf("unexpected character %s", string(b1)) - } - - // Unsigned floating-point constants for infinity or canonical NaN (not a number) clash with keyword - // representation. For example, "nan" and "inf" are floating-point constants, while "nano" and "info" are - // possible keywords. See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#floating-point%E2%91%A6 - // - // TODO: Ex. inf nan nan:0xfffffffffffff or nan:0x400000 - - if parser, err = parser(tok, source[b:peek], line, c); err != nil { - return line, c, err - } - } - - if blockCommentDepth > 0 { - return line, col, errors.New("expected block comment end ';)', but reached end of input") - } - if parenDepth > 0 { - return line, col, errors.New("expected ')', but reached end of input") - } - return line, col, nil -} - -// utf8Size returns the size of the UTF-8 rune based on its first byte, or zero. -// -// Note: The null byte (0x00) is here as it is valid in string tokens and comments. See WebAssembly/spec#1372 -// -// Note: We don't validate the subsequent bytes make a well-formed UTF-8 rune intentionally for performance and to keep -// lexing allocation free. Meanwhile, the impact is that we might skip over malformed bytes. -var utf8Size = [256]int{ - // 1 2 3 4 5 6 7 8 9 A B C D E F - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x00-0x0F - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x10-0x1F - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x20-0x2F - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x30-0x3F - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40-0x4F - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x50-0x5F - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60-0x6F - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x70-0x7F - // 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x80-0x8F - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x90-0x9F - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xA0-0xAF - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xB0-0xBF - 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xC0-0xCF - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xD0-0xDF - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 0xE0-0xEF - 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xF0-0xFF -} diff --git a/internal/watzero/internal/lexer_test.go b/internal/watzero/internal/lexer_test.go deleted file mode 100644 index 7a1b4e71db..0000000000 --- a/internal/watzero/internal/lexer_test.go +++ /dev/null @@ -1,667 +0,0 @@ -package internal - -import ( - "fmt" - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" -) - -// exampleWat was at one time in the wasmtime repo under cranelift. We added a unicode comment for fun! -const exampleWat = `(module - ;; 私たちはフィボナッチ数列を使います。何故ならみんなやってるからです。 - (memory 1) - (func $main (local i32 i32 i32 i32) - (set_local 0 (i32.const 0)) - (set_local 1 (i32.const 1)) - (set_local 2 (i32.const 1)) - (set_local 3 (i32.const 0)) - (block - (loop - (br_if 1 (i32.gt_s (get_local 0) (i32.const 5))) - (set_local 3 (get_local 2)) - (set_local 2 (i32.add (get_local 2) (get_local 1))) - (set_local 1 (get_local 3)) - (set_local 0 (i32.add (get_local 0) (i32.const 1))) - (br 0) - ) - ) - (i32.store (i32.const 0) (get_local 2)) - ) - (start $main) - (data (i32.const 0) "0000") -)` - -// TestLex_Example is intentionally verbose to catch line/column/position bugs -func TestLex_Example(t *testing.T) { - require.Equal(t, []*token{ - {tokenLParen, 1, 1, "("}, - {tokenKeyword, 1, 2, "module"}, - {tokenLParen, 3, 3, "("}, - {tokenKeyword, 3, 4, "memory"}, - {tokenUN, 3, 11, "1"}, - {tokenRParen, 3, 12, ")"}, - {tokenLParen, 4, 3, "("}, - {tokenKeyword, 4, 4, "func"}, - {tokenID, 4, 9, "$main"}, - {tokenLParen, 4, 15, "("}, - {tokenKeyword, 4, 16, "local"}, - {tokenKeyword, 4, 22, "i32"}, - {tokenKeyword, 4, 26, "i32"}, - {tokenKeyword, 4, 30, "i32"}, - {tokenKeyword, 4, 34, "i32"}, - {tokenRParen, 4, 37, ")"}, - {tokenLParen, 5, 5, "("}, - {tokenKeyword, 5, 6, "set_local"}, - {tokenUN, 5, 16, "0"}, - {tokenLParen, 5, 18, "("}, - {tokenKeyword, 5, 19, "i32.const"}, - {tokenUN, 5, 29, "0"}, - {tokenRParen, 5, 30, ")"}, - {tokenRParen, 5, 31, ")"}, - {tokenLParen, 6, 5, "("}, - {tokenKeyword, 6, 6, "set_local"}, - {tokenUN, 6, 16, "1"}, - {tokenLParen, 6, 18, "("}, - {tokenKeyword, 6, 19, "i32.const"}, - {tokenUN, 6, 29, "1"}, - {tokenRParen, 6, 30, ")"}, - {tokenRParen, 6, 31, ")"}, - {tokenLParen, 7, 5, "("}, - {tokenKeyword, 7, 6, "set_local"}, - {tokenUN, 7, 16, "2"}, - {tokenLParen, 7, 18, "("}, - {tokenKeyword, 7, 19, "i32.const"}, - {tokenUN, 7, 29, "1"}, - {tokenRParen, 7, 30, ")"}, - {tokenRParen, 7, 31, ")"}, - {tokenLParen, 8, 5, "("}, - {tokenKeyword, 8, 6, "set_local"}, - {tokenUN, 8, 16, "3"}, - {tokenLParen, 8, 18, "("}, - {tokenKeyword, 8, 19, "i32.const"}, - {tokenUN, 8, 29, "0"}, - {tokenRParen, 8, 30, ")"}, - {tokenRParen, 8, 31, ")"}, - {tokenLParen, 9, 5, "("}, - {tokenKeyword, 9, 6, "block"}, - {tokenLParen, 10, 5, "("}, - {tokenKeyword, 10, 6, "loop"}, - {tokenLParen, 11, 9, "("}, - {tokenKeyword, 11, 10, "br_if"}, - {tokenUN, 11, 16, "1"}, - {tokenLParen, 11, 18, "("}, - {tokenKeyword, 11, 19, "i32.gt_s"}, - {tokenLParen, 11, 28, "("}, - {tokenKeyword, 11, 29, "get_local"}, - {tokenUN, 11, 39, "0"}, - {tokenRParen, 11, 40, ")"}, - {tokenLParen, 11, 42, "("}, - {tokenKeyword, 11, 43, "i32.const"}, - {tokenUN, 11, 53, "5"}, - {tokenRParen, 11, 54, ")"}, - {tokenRParen, 11, 55, ")"}, - {tokenRParen, 11, 56, ")"}, - {tokenLParen, 12, 9, "("}, - {tokenKeyword, 12, 10, "set_local"}, - {tokenUN, 12, 20, "3"}, - {tokenLParen, 12, 22, "("}, - {tokenKeyword, 12, 23, "get_local"}, - {tokenUN, 12, 33, "2"}, - {tokenRParen, 12, 34, ")"}, - {tokenRParen, 12, 35, ")"}, - {tokenLParen, 13, 9, "("}, - {tokenKeyword, 13, 10, "set_local"}, - {tokenUN, 13, 20, "2"}, - {tokenLParen, 13, 22, "("}, - {tokenKeyword, 13, 23, "i32.add"}, - {tokenLParen, 13, 31, "("}, - {tokenKeyword, 13, 32, "get_local"}, - {tokenUN, 13, 42, "2"}, - {tokenRParen, 13, 43, ")"}, - {tokenLParen, 13, 45, "("}, - {tokenKeyword, 13, 46, "get_local"}, - {tokenUN, 13, 56, "1"}, - {tokenRParen, 13, 57, ")"}, - {tokenRParen, 13, 58, ")"}, - {tokenRParen, 13, 59, ")"}, - {tokenLParen, 14, 9, "("}, - {tokenKeyword, 14, 10, "set_local"}, - {tokenUN, 14, 20, "1"}, - {tokenLParen, 14, 22, "("}, - {tokenKeyword, 14, 23, "get_local"}, - {tokenUN, 14, 33, "3"}, - {tokenRParen, 14, 34, ")"}, - {tokenRParen, 14, 35, ")"}, - {tokenLParen, 15, 9, "("}, - {tokenKeyword, 15, 10, "set_local"}, - {tokenUN, 15, 20, "0"}, - {tokenLParen, 15, 22, "("}, - {tokenKeyword, 15, 23, "i32.add"}, - {tokenLParen, 15, 31, "("}, - {tokenKeyword, 15, 32, "get_local"}, - {tokenUN, 15, 42, "0"}, - {tokenRParen, 15, 43, ")"}, - {tokenLParen, 15, 45, "("}, - {tokenKeyword, 15, 46, "i32.const"}, - {tokenUN, 15, 56, "1"}, - {tokenRParen, 15, 57, ")"}, - {tokenRParen, 15, 58, ")"}, - {tokenRParen, 15, 59, ")"}, - {tokenLParen, 16, 9, "("}, - {tokenKeyword, 16, 10, "br"}, - {tokenUN, 16, 13, "0"}, - {tokenRParen, 16, 14, ")"}, - {tokenRParen, 17, 5, ")"}, - {tokenRParen, 18, 5, ")"}, - {tokenLParen, 19, 5, "("}, - {tokenKeyword, 19, 6, "i32.store"}, - {tokenLParen, 19, 16, "("}, - {tokenKeyword, 19, 17, "i32.const"}, - {tokenUN, 19, 27, "0"}, - {tokenRParen, 19, 28, ")"}, - {tokenLParen, 19, 30, "("}, - {tokenKeyword, 19, 31, "get_local"}, - {tokenUN, 19, 41, "2"}, - {tokenRParen, 19, 42, ")"}, - {tokenRParen, 19, 43, ")"}, - {tokenRParen, 20, 3, ")"}, - {tokenLParen, 21, 3, "("}, - {tokenKeyword, 21, 4, "start"}, - {tokenID, 21, 10, "$main"}, - {tokenRParen, 21, 15, ")"}, - {tokenLParen, 22, 3, "("}, - {tokenKeyword, 22, 4, "data"}, - {tokenLParen, 22, 9, "("}, - {tokenKeyword, 22, 10, "i32.const"}, - {tokenUN, 22, 20, "0"}, - {tokenRParen, 22, 21, ")"}, - {tokenString, 22, 23, "\"0000\""}, - {tokenRParen, 22, 29, ")"}, - {tokenRParen, 23, 1, ")"}, - }, lexTokens(t, exampleWat)) -} - -func TestLex(t *testing.T) { - tests := []struct { - name string - input string - expected []*token - }{ - { - name: "empty", - input: "", - }, - { - name: "only parens", - input: "()", - expected: []*token{{tokenLParen, 1, 1, "("}, {tokenRParen, 1, 2, ")"}}, - }, - { - name: "shortest keywords", - input: "a z", - expected: []*token{{tokenKeyword, 1, 1, "a"}, {tokenKeyword, 1, 3, "z"}}, - }, - { - name: "shortest tokens - EOL", - input: "(a)\n", - expected: []*token{{tokenLParen, 1, 1, "("}, {tokenKeyword, 1, 2, "a"}, {tokenRParen, 1, 3, ")"}}, - }, - { - name: "only tokens", - input: "(module)", - expected: []*token{{tokenLParen, 1, 1, "("}, {tokenKeyword, 1, 2, "module"}, {tokenRParen, 1, 8, ")"}}, - }, - { - name: "only white space characters", - input: " \t\r\n", - }, - { - name: "after white space characters - EOL", - input: " \t\na", - expected: []*token{{tokenKeyword, 2, 1, "a"}}, - }, - { - name: "after white space characters - Windows EOL", - input: " \t\r\na", - expected: []*token{{tokenKeyword, 2, 1, "a"}}, - }, - { - name: "only line comment - EOL before EOF", - input: ";; TODO\n", - }, - { - name: "only line comment - EOF", - input: ";; TODO", - }, - { - name: "only unicode line comment - EOF", - input: ";; брэд-ЛГТМ", - }, - { - name: "after line comment", - input: ";; TODO\na", - expected: []*token{{tokenKeyword, 2, 1, "a"}}, - }, - { - name: "double line comment", - input: ";; TODO\n;; YOLO\na", - expected: []*token{{tokenKeyword, 3, 1, "a"}}, - }, - { - name: "after unicode line comment", - input: ";; брэд-ЛГТМ\na", - expected: []*token{{tokenKeyword, 2, 1, "a"}}, - }, - { - name: "after line comment - Windows EOL", - input: ";; TODO\r\na", - expected: []*token{{tokenKeyword, 2, 1, "a"}}, - }, - { - name: "after redundant line comment", - input: ";;;; TODO\na", - expected: []*token{{tokenKeyword, 2, 1, "a"}}, - }, - { - name: "after line commenting out block comment", - input: ";; TODO (; ;)\na", - expected: []*token{{tokenKeyword, 2, 1, "a"}}, - }, - { - name: "after line commenting out open block comment", - input: ";; TODO (;\na", - expected: []*token{{tokenKeyword, 2, 1, "a"}}, - }, - { - name: "after line commenting out close block comment", - input: ";; TODO ;)\na", - expected: []*token{{tokenKeyword, 2, 1, "a"}}, - }, - { - name: "only block comment - EOL before EOF", - input: "(; TODO ;)\n", - }, - { - name: "only block comment - Windows EOL before EOF", - input: "(; TODO ;)\r\n", - }, - { - name: "only block comment - EOF", - input: "(; TODO ;)", - }, - { - name: "double block comment", - input: "(; TODO ;)(; YOLO ;)a", - expected: []*token{{tokenKeyword, 1, 21, "a"}}, - }, - { - name: "double block comment - EOL", - input: "(; TODO ;)\n(; YOLO ;)\na", - expected: []*token{{tokenKeyword, 3, 1, "a"}}, - }, - { - name: "after block comment", - input: "(; TODO ;)a", - expected: []*token{{tokenKeyword, 1, 11, "a"}}, - }, - { - name: "only nested block comment - EOL before EOF", - input: "(; TODO (; (YOLO) ;) ;)\n", - }, - { - name: "only nested block comment - EOF", - input: "(; TODO (; (YOLO) ;) ;)", - }, - { - name: "only unicode block comment - EOF", - input: "(; брэд-ЛГТМ ;)", - }, - { - name: "after nested block comment", - input: "(; TODO (; (YOLO) ;) ;)a", - expected: []*token{{tokenKeyword, 1, 24, "a"}}, - }, - { - name: "after nested block comment - EOL", - input: "(; TODO (; (YOLO) ;) ;)\n a", - expected: []*token{{tokenKeyword, 2, 2, "a"}}, - }, - { - name: "after nested block comment - Windows EOL", - input: "(; TODO (; (YOLO) ;) ;)\r\n a", - expected: []*token{{tokenKeyword, 2, 2, "a"}}, - }, - { - name: "white space between parens", - input: "( )", - expected: []*token{{tokenLParen, 1, 1, "("}, {tokenRParen, 1, 3, ")"}}, - }, - { - name: "nested parens", - input: "(())", - expected: []*token{ - {tokenLParen, 1, 1, "("}, - {tokenLParen, 1, 2, "("}, - {tokenRParen, 1, 3, ")"}, - {tokenRParen, 1, 4, ")"}, - }, - }, - { - name: "empty string", - input: `""`, - expected: []*token{{tokenString, 1, 1, `""`}}, - }, - { - name: "unicode string", - input: "\"брэд-ЛГТМ\"", - expected: []*token{{tokenString, 1, 1, "\"брэд-ЛГТМ\""}}, - }, - { - name: "string inside tokens with newline", - input: "(\"\n\")", // TODO newline char isn't actually allowed unless escaped! - expected: []*token{{tokenLParen, 1, 1, "("}, {tokenString, 1, 2, "\"\n\""}, {tokenRParen, 1, 5, ")"}}, - }, - { - name: "unsigned shortest - EOL", - input: "1\n", - expected: []*token{{tokenUN, 1, 1, "1"}}, - }, - { - name: "unsigned shortest - EOF", - input: "1", - expected: []*token{{tokenUN, 1, 1, "1"}}, - }, - { - name: "unsigned shortest inside tokens", - input: "(1)", - expected: []*token{{tokenLParen, 1, 1, "("}, {tokenUN, 1, 2, "1"}, {tokenRParen, 1, 3, ")"}}, - }, - { - name: "unsigned shortest then string", - input: `1"1"`, - expected: []*token{{tokenUN, 1, 1, "1"}, {tokenString, 1, 2, `"1"`}}, - }, - { - name: "unsigned - EOL", - input: "123\n", - expected: []*token{{tokenUN, 1, 1, "123"}}, - }, - { - name: "unsigned - EOF", - input: "123", - expected: []*token{{tokenUN, 1, 1, "123"}}, - }, - { - name: "unsigned inside tokens", - input: "(123)", - expected: []*token{{tokenLParen, 1, 1, "("}, {tokenUN, 1, 2, "123"}, {tokenRParen, 1, 5, ")"}}, - }, - { - name: "unsigned then string", - input: `123"123"`, - expected: []*token{{tokenUN, 1, 1, "123"}, {tokenString, 1, 4, `"123"`}}, - }, - { - name: "unsigned then keyword", - input: "1a", // whitespace is optional between tokens, and a keyword can be single-character! - expected: []*token{{tokenUN, 1, 1, "1"}, {tokenKeyword, 1, 2, "a"}}, - }, - { - name: "0x80 in block comment", - input: "(; \000);)", - }, - { - name: "0x80 in block comment unicode", - input: "(; 私\000);)", - }, - { - name: "0x80 in line comment", - input: ";; \000", - }, - { - name: "0x80 in string", - input: "\" \000\"", - expected: []*token{{tokenString, 1, 1, "\" \000\""}}, - }, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - require.Equal(t, tc.expected, lexTokens(t, tc.input)) - }) - } -} - -func TestLex_Errors(t *testing.T) { - tests := []struct { - name string - parser tokenParser - input []byte - expectedLine, expectedCol uint32 - expectedErr string - }{ - { - name: "close paren before open paren", - input: []byte(")("), - expectedLine: 1, - expectedCol: 1, - expectedErr: "found ')' before '('", - }, - { - name: "unbalanced nesting", - input: []byte("(()"), - expectedLine: 1, - expectedCol: 4, - expectedErr: "expected ')', but reached end of input", - }, - { - name: "open paren at end of input", - input: []byte("("), - expectedLine: 1, - expectedCol: 1, - expectedErr: "found '(' at end of input", - }, - { - name: "begin block comment at end of input", - input: []byte("(;"), - expectedLine: 1, - expectedCol: 3, - expectedErr: "expected block comment end ';)', but reached end of input", - }, - { - name: "half line comment", - input: []byte("; TODO"), - expectedLine: 1, - expectedCol: 1, - expectedErr: "unexpected character ;", - }, - { - name: "open block comment", - input: []byte("(; TODO"), - expectedLine: 1, - expectedCol: 8, - expectedErr: "expected block comment end ';)', but reached end of input", - }, - { - name: "close block comment", - input: []byte(";) TODO"), - expectedLine: 1, - expectedCol: 1, - - expectedErr: "unexpected character ;", - }, - { - name: "unbalanced nested block comment", - input: []byte("(; TODO (; (YOLO) ;)"), - expectedLine: 1, - expectedCol: 21, - expectedErr: "expected block comment end ';)', but reached end of input", - }, - { - name: "dangling unicode", - input: []byte(" 私"), - expectedLine: 1, - expectedCol: 2, - expectedErr: "expected an ASCII character, not 私", - }, - { - name: "truncated string", - input: []byte("\"hello"), - expectedLine: 1, - expectedCol: 6, - expectedErr: "expected end quote", - }, - { - name: "0x80 in block comment", - input: []byte("(; \200)"), - expectedLine: 1, - expectedCol: 4, - expectedErr: "found an invalid byte in block comment: 0x80", - }, - { - name: "0x80 in block comment unicode", - input: []byte("(; 私\200)"), - expectedLine: 1, - expectedCol: 5, - expectedErr: "found an invalid byte in block comment: 0x80", - }, - { - name: "0x80 in line comment", - input: []byte(";; \200"), - expectedLine: 1, - expectedCol: 4, - expectedErr: "found an invalid byte in line comment: 0x80", - }, - { - name: "0x80 in line comment unicode", - input: []byte(";; 私\200"), - expectedLine: 1, - expectedCol: 5, - expectedErr: "found an invalid byte in line comment: 0x80", - }, - { - name: "0x80 in string", - input: []byte("\" \200\""), - expectedLine: 1, - expectedCol: 3, - expectedErr: "found an invalid byte in string token: 0x80", - }, - { - name: "parser error: lParen", - input: []byte(" (module)"), - parser: (&errorOnTokenParser{tokenLParen}).parse, - expectedLine: 1, - expectedCol: 2, - expectedErr: "unexpected '('", - }, - { - name: "parser error: keyword", - input: []byte(" (module)"), - parser: (&errorOnTokenParser{tokenKeyword}).parse, - expectedLine: 1, - expectedCol: 3, - expectedErr: "unexpected keyword: module", - }, - { - name: "parser error: rParen", - input: []byte(" (module)"), - parser: (&errorOnTokenParser{tokenRParen}).parse, - expectedLine: 1, - expectedCol: 9, - expectedErr: "unexpected ')'", - }, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - parser := tc.parser - if parser == nil { - parser = parseNoop - } - line, col, err := lex(parser, tc.input) - require.Equal(t, tc.expectedLine, line) - require.Equal(t, tc.expectedCol, col) - require.EqualError(t, err, tc.expectedErr) - }) - } -} - -func lexTokens(t *testing.T, input string) []*token { - p := &collectTokenParser{} - line, col, err := lex(p.parse, []byte(input)) - require.NoError(t, err, "%d:%d: %s", line, col, err) - return p.tokens -} - -type errorOnTokenParser struct{ tok tokenType } - -func (e *errorOnTokenParser) parse(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - if tok != e.tok { - return e.parse, nil - } - return parseErr(tok, tokenBytes, line, col) -} - -type collectTokenParser struct{ tokens []*token } - -func (c *collectTokenParser) parse(tok tokenType, tokenUTF8 []byte, line, col uint32) (tokenParser, error) { - c.tokens = append(c.tokens, &token{tokenType: tok, line: line, col: col, token: string(tokenUTF8)}) - return c.parse, nil -} - -type collectTokenTypeParser struct{ tokenTypes []tokenType } - -func (c *collectTokenTypeParser) parse(tok tokenType, _ []byte, _, _ uint32) (tokenParser, error) { - c.tokenTypes = append(c.tokenTypes, tok) - return c.parse, nil -} - -type noopTokenParser struct{} - -func (n *noopTokenParser) parse(_ tokenType, _ []byte, _, _ uint32) (tokenParser, error) { - return n.parse, nil -} - -var parseNoop = (&noopTokenParser{}).parse - -type errTokenParser struct{} - -func (n *errTokenParser) parse(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - return nil, unexpectedToken(tok, tokenBytes) -} - -var parseErr = (&errTokenParser{}).parse - -type skipTokenParser struct { - count uint32 - next tokenParser -} - -func (s *skipTokenParser) parse(_ tokenType, _ []byte, _, _ uint32) (tokenParser, error) { - s.count-- - if s.count == 0 { - return s.next, nil - } - return s.parse, nil -} - -// skipTokens is a hack because lex tracks parens, so they may need to be skipped. Also, some parsers need to skip past -// a field name. -func skipTokens(count uint32, next tokenParser) tokenParser { - out := &skipTokenParser{count: count, next: next} - return out.parse -} - -type token struct { - tokenType - line, col uint32 - token string -} - -// String helps format to allow copy/pasting of expected values -func (t *token) String() string { - return fmt.Sprintf("{%s, %d, %d, %q}", t.tokenType, t.line, t.col, t.token) -} diff --git a/internal/watzero/internal/memory_parser.go b/internal/watzero/internal/memory_parser.go deleted file mode 100644 index fbc5336430..0000000000 --- a/internal/watzero/internal/memory_parser.go +++ /dev/null @@ -1,127 +0,0 @@ -package internal - -import ( - "errors" - "fmt" - - "github.com/tetratelabs/wazero/internal/wasm" -) - -func newMemoryParser( - memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), - memoryNamespace *indexNamespace, - onMemory onMemory, -) *memoryParser { - return &memoryParser{memorySizer: memorySizer, memoryNamespace: memoryNamespace, onMemory: onMemory} -} - -type onMemory func(*wasm.Memory) tokenParser - -// memoryParser parses an api.Memory from and dispatches to onMemory. -// -// Ex. `(module (memory 0 1024))` -// -// starts here --^ ^ -// onMemory resumes here --+ -// -// Note: memoryParser is reusable. The caller resets via begin. -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memories%E2%91%A7 -type memoryParser struct { - memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) - - memoryNamespace *indexNamespace - - // onMemory is invoked on end - onMemory onMemory - - // currentMin is reset on begin and read onMemory - currentMemory *wasm.Memory -} - -// begin should be called after reaching the wasm.ExternTypeMemoryName keyword in a module field. Parsing -// continues until onMemory or error. -// -// This stage records the ID of the current memory, if present, and resumes with beginMin. -// -// Ex. A memory ID is present `(memory $mem 0)` -// -// records mem --^ ^ -// beginMin resumes here --+ -// -// Ex. No memory ID `(memory 0)` -// -// calls beginMin --^ -func (p *memoryParser) begin(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - p.currentMemory = &wasm.Memory{} - if tok == tokenID { // Ex. $mem - if _, err := p.memoryNamespace.setID(tokenBytes); err != nil { - return nil, err - } - return p.beginMin, nil - } - return p.beginMin(tok, tokenBytes, line, col) -} - -// beginMin looks for the minimum memory size and proceeds with beginMax, or errs on any other token. -func (p *memoryParser) beginMin(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenID: // Ex.(memory $rf32 $rf32 - return nil, fmt.Errorf("redundant ID %s", tokenBytes) - case tokenUN: - mem := p.currentMemory - if min, err := decodePages("min", tokenBytes); err != nil { - return nil, err - } else { - mem.Min, mem.Cap, mem.Max = p.memorySizer(min, nil) - if err = mem.Validate(); err != nil { - return nil, err - } - } - return p.beginMax, nil - case tokenRParen: - return nil, errors.New("missing min") - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// beginMax looks for the max memory size and returns end. If this is an ')' end completes the memory. Otherwise, this -// errs on any other token. -func (p *memoryParser) beginMax(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - switch tok { - case tokenUN: - mem := p.currentMemory - if max, err := decodePages("max", tokenBytes); err != nil { - return nil, err - } else { - mem.Min, mem.Cap, mem.Max = p.memorySizer(p.currentMemory.Min, &max) - mem.IsMaxEncoded = true - if err = mem.Validate(); err != nil { - return nil, err - } - } - return p.end, nil - case tokenRParen: - return p.end(tok, tokenBytes, line, col) - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -func decodePages(fieldName string, tokenBytes []byte) (uint32, error) { - i, overflow := decodeUint32(tokenBytes) - if overflow { - return 0, fmt.Errorf("%s %d pages (%s) over limit of %d pages (%s)", fieldName, - i, wasm.PagesToUnitOfBytes(i), wasm.MemoryLimitPages, wasm.PagesToUnitOfBytes(wasm.MemoryLimitPages)) - } - return i, nil -} - -// end increments the memory namespace and calls onMemory with the current limits -func (p *memoryParser) end(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenRParen { - return nil, unexpectedToken(tok, tokenBytes) - } - p.memoryNamespace.count++ - return p.onMemory(p.currentMemory), nil -} diff --git a/internal/watzero/internal/memory_parser_test.go b/internal/watzero/internal/memory_parser_test.go deleted file mode 100644 index 72ba3fc42d..0000000000 --- a/internal/watzero/internal/memory_parser_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package internal - -import ( - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/wasm" -) - -func TestMemoryParser(t *testing.T) { - zero := uint32(0) - max := wasm.MemoryLimitPages - tests := []struct { - name string - input string - expected *wasm.Memory - expectedID string - }{ - { - name: "min 0", - input: "(memory 0)", - expected: &wasm.Memory{Max: max}, - }, - { - name: "min 0, max 0", - input: "(memory 0 0)", - expected: &wasm.Memory{Max: zero, IsMaxEncoded: true}, - }, - { - name: "min largest", - input: "(memory 65536)", - expected: &wasm.Memory{Min: max, Cap: max, Max: max}, - }, - { - name: "min largest ID", - input: "(memory $mem 65536)", - expected: &wasm.Memory{Min: max, Cap: max, Max: max}, - expectedID: "mem", - }, - { - name: "min 0, max largest", - input: "(memory 0 65536)", - expected: &wasm.Memory{Max: max, IsMaxEncoded: true}, - }, - { - name: "min largest max largest", - input: "(memory 65536 65536)", - expected: &wasm.Memory{Min: max, Cap: max, Max: max, IsMaxEncoded: true}, - }, - { - name: "min largest max largest ID", - input: "(memory $mem 65536 65536)", - expected: &wasm.Memory{Min: max, Cap: max, Max: max, IsMaxEncoded: true}, - expectedID: "mem", - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - memoryNamespace := newIndexNamespace(func(sectionID wasm.SectionID) uint32 { - require.Equal(t, wasm.SectionIDMemory, sectionID) - return 0 - }) - parsed, tp, err := parseMemoryType(memoryNamespace, tc.input) - require.NoError(t, err) - require.Equal(t, tc.expected, parsed) - require.Equal(t, uint32(1), tp.memoryNamespace.count) - if tc.expectedID == "" { - require.Zero(t, len(tp.memoryNamespace.idToIdx), "expected no indices") - } else { - // Since the parser was initially empty, the expected index of the parsed memory is 0 - require.Equal(t, map[string]wasm.Index{tc.expectedID: wasm.Index(0)}, tp.memoryNamespace.idToIdx) - } - }) - } -} - -func TestMemoryParser_Errors(t *testing.T) { - tests := []struct{ name, input, expectedErr string }{ - { - name: "invalid token", - input: "(memory \"0\")", - expectedErr: "unexpected string: \"0\"", - }, - { - name: "not ID", - input: "(memory mem)", - expectedErr: "unexpected keyword: mem", - }, - { - name: "redundant ID", - input: "(memory $mem $0)", - expectedErr: "redundant ID $0", - }, - { - name: "invalid after ID", - input: "(memory $mem frank)", - expectedErr: "unexpected keyword: frank", - }, - { - name: "missing min", - input: "(memory)", - expectedErr: "missing min", - }, - { - name: "invalid after min", - input: "(memory 0 $0)", - expectedErr: "unexpected ID: $0", - }, - { - name: "invalid after max", - input: "(memory 0 0 $0)", - expectedErr: "unexpected ID: $0", - }, - { - name: "max < min", - input: "(memory 1 0)", - expectedErr: "min 1 pages (64 Ki) > max 0 pages (0 Ki)", - }, - { - name: "min > limit", - input: "(memory 4294967295)", - expectedErr: "min 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)", - }, - { - name: "max > limit", - input: "(memory 0 4294967295)", - expectedErr: "max 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)", - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - memoryNamespace := newIndexNamespace(func(sectionID wasm.SectionID) uint32 { - require.Equal(t, wasm.SectionIDMemory, sectionID) - return 0 - }) - parsed, _, err := parseMemoryType(memoryNamespace, tc.input) - require.EqualError(t, err, tc.expectedErr) - require.Nil(t, parsed) - }) - } - - t.Run("duplicate ID", func(t *testing.T) { - memoryNamespace := newIndexNamespace(func(sectionID wasm.SectionID) uint32 { - require.Equal(t, wasm.SectionIDMemory, sectionID) - return 0 - }) - _, err := memoryNamespace.setID([]byte("$mem")) - require.NoError(t, err) - memoryNamespace.count++ - - parsed, _, err := parseMemoryType(memoryNamespace, "(memory $mem 1024)") - require.EqualError(t, err, "duplicate ID $mem") - require.Nil(t, parsed) - }) -} - -func parseMemoryType(memoryNamespace *indexNamespace, input string) (*wasm.Memory, *memoryParser, error) { - var parsed *wasm.Memory - var setFunc onMemory = func(mem *wasm.Memory) tokenParser { - parsed = mem - return parseErr - } - tp := newMemoryParser(wasm.MemorySizer, memoryNamespace, setFunc) - // memoryParser starts after the '(memory', so we need to eat it first! - _, _, err := lex(skipTokens(2, tp.begin), []byte(input)) - return parsed, tp, err -} diff --git a/internal/watzero/internal/numbers.go b/internal/watzero/internal/numbers.go deleted file mode 100644 index 4bfa638f85..0000000000 --- a/internal/watzero/internal/numbers.go +++ /dev/null @@ -1,89 +0,0 @@ -package internal - -// decodeUint32 decodes an uint32 from a tokenUN or returns false on overflow -// -// Note: Bit length interpretation is not defined at the lexing layer, so this may fail on overflow due to invalid -// length known at the parsing layer. -// -// Note: This is similar to, but cannot use strconv.Atoi because WebAssembly allows underscore characters in numeric -// representation. See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#integers%E2%91%A6 -func decodeUint32(tokenBytes []byte) (uint32, bool) { // TODO: hex - // The max ASCII length of a uint32 is 10 (length of 4294967295). If we are at that length we can overflow. - // - // Note: There's chance we are at length 10 due to underscores, but easier to take the slow path for either reason. - if len(tokenBytes) > 9 { - v, overflow := decodeUint64(tokenBytes) - if overflow { - return 0, overflow - } - if v > 0xffffffff { - return 0, true - } - return uint32(v), false - } - - // If we are here, we know we cannot overflow uint32. The only valid characters in tokenUN are 0-9 and underscore. - var n uint32 - for _, ch := range tokenBytes { - if ch == '_' { - continue - } - n = n*10 + uint32(ch-'0') - } - return n, false -} - -// decodeUint64 is like decodeUint32, but for uint64 -func decodeUint64(tokenBytes []byte) (uint64, bool) { // TODO: hex - // The max ASCII length of a uint64 is 20 (length of 18446744073709551615). If we are at that length we can overflow. - // - // Note: There's chance we are at length 20 due to underscores, but easier to take the slow path for either reason. - if len(tokenBytes) > 19 { - return decodeUint64SlowPath(tokenBytes) - } - - // If we are here, we know we cannot overflow uint64. The only valid characters in tokenUN are 0-9 and underscore. - var n uint64 - for _, ch := range tokenBytes { - if ch == '_' { - continue - } - n = n*10 + uint64(ch-'0') - } - return n, false -} - -// decodeUint64SlowPath implements decodeUint64 for the slow path when there's a chance the result cannot fit. -// Notably, this strips underscore characters first, so that the string length can be used to assess overflows. -func decodeUint64SlowPath(tokenBytes []byte) (uint64, bool) { - // We have a possible overflow, but won't know for sure due to underscores until we strip them. This strips any - // underscores from the token in-place. - n := 0 - for _, b := range tokenBytes { - if b != '_' { - tokenBytes[n] = b - n++ - } - } - tokenBytes = tokenBytes[:n] - - // Now, we know there are only numbers and no underscores. This means the ASCII length is insightful. - switch len(tokenBytes) { - case 19: // cannot overflow - return decodeUint64(tokenBytes) - case 20: // only overflows depending on the last number - first19, overflow := decodeUint64(tokenBytes[0:19]) - if overflow { - return 0, false // impossible unless someone used this with an unvalidated token - } - last := uint64(tokenBytes[19] - '0') - - // Remember the largest uint64 encoded in ASCII is 20 characters: "1844674407370955161" followed by "5" - if first19 > 1844674407370955161 /* first 19 chars of largest uint64 */ || last > 5 { - return 0, true - } - return first19*10 + last, false - default: // must overflow - return 0, true - } -} diff --git a/internal/watzero/internal/numbers_test.go b/internal/watzero/internal/numbers_test.go deleted file mode 100644 index fa334edda8..0000000000 --- a/internal/watzero/internal/numbers_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package internal - -import ( - "math" - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" -) - -var maxUint32 = uint32(math.MaxUint32) - -func TestDecodeUint32(t *testing.T) { - for _, tt := range []struct { - name, input string - expected uint32 - expectedOverflow bool - }{ - {name: "zero", input: "0", expected: 0}, - {name: "largest uint16", input: "65535", expected: 0xffff}, - {name: "largest uint16 with underscores", input: "6_5_5_3_5", expected: 0xffff}, - {name: "under largest uint32 by factor of 10", input: "429496729", expected: 429496729}, - {name: "under largest uint32 by factor of 10 with underscores", input: "4_2_9_4_9_6_7_2_9", expected: 429496729}, - {name: "largest uint32", input: "4294967295", expected: maxUint32}, - {name: "largest uint32 with underscores", input: "4_2_9_4_9_6_7_2_9_5", expected: maxUint32}, - {name: "overflow by one", input: "4294967296", expectedOverflow: true}, - {name: "overflow by one with underscores", input: "4_2_9_4_9_6_7_2_9_6", expectedOverflow: true}, - {name: "overflow by factor of 10", input: "42949672950", expectedOverflow: true}, - {name: "overflow by factor of 10 with underscores", input: "4_2_9_4_9_6_7_2_9_5_0", expectedOverflow: true}, - } { - tc := tt - t.Run(tc.name, func(t *testing.T) { - actual, overflow := decodeUint32([]byte(tc.input)) - require.Equal(t, tc.expected, actual) - require.Equal(t, tc.expectedOverflow, overflow) - }) - } -} - -func TestDecodeUint64(t *testing.T) { - for _, tt := range []struct { - name, input string - expected uint64 - expectedOverflow bool - }{ - {name: "zero", input: "0", expected: 0}, - {name: "largest uint32", input: "4294967295", expected: uint64(maxUint32)}, - {name: "under largest uint64 by factor of 10", input: "1844674407370955161", expected: 1844674407370955161}, - {name: "under largest uint64 by factor of 10 with underscores", input: "1_8_4_4_6_7_4_4_0_7_3_7_0_9_5_5_1_6_1", expected: 1844674407370955161}, - {name: "largest uint64", input: "18446744073709551615", expected: 0xffffffffffffffff}, - {name: "largest uint64 with underscores", input: "1_8_4_4_6_7_4_4_0_7_3_7_0_9_5_5_1_6_1_5", expected: 0xffffffffffffffff}, - {name: "overflow by one", input: "18446744073709551616", expectedOverflow: true}, - {name: "overflow by one with underscores", input: "1_8_4_4_6_7_4_4_0_7_3_7_0_9_5_5_1_6_1_6", expectedOverflow: true}, - {name: "overflow by factor of 10", input: "184467440737095516150", expectedOverflow: true}, - {name: "overflow by factor of 10 with underscores", input: "1_8_4_4_6_7_4_4_0_7_3_7_0_9_5_5_1_6_1_5_0", expectedOverflow: true}, - } { - tc := tt - t.Run(tc.name, func(t *testing.T) { - actual, overflow := decodeUint64([]byte(tc.input)) - require.Equal(t, tc.expected, actual) - require.Equal(t, tc.expectedOverflow, overflow) - }) - } -} diff --git a/internal/watzero/internal/token.go b/internal/watzero/internal/token.go deleted file mode 100644 index 32185fe458..0000000000 --- a/internal/watzero/internal/token.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -// token is the set of tokens defined by the WebAssembly Text Format 1.0 -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#tokens%E2%91%A0 -type tokenType byte - -const ( - tokenInvalid tokenType = iota - // tokenKeyword is a potentially empty sequence of idChar characters prefixed by a lowercase letter. - // - // For example, in the below, 'local.get' 'i32.const' and 'i32.lt_s' are keywords: - // local.get $y - // i32.const 6 - // i32.lt_s - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-keyword - tokenKeyword - - // tokenUN is an unsigned integer in decimal or hexadecimal notation, optionally separated by underscores. - // - // For example, the following tokens represent the same number: 10 - // (i32.const 10) - // (i32.const 1_0) - // (i32.const 0x0a) - // (i32.const 0x0_A) - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-int - tokenUN - - // tokenSN is a signed integer in decimal or hexadecimal notation, optionally separated by underscores. - // - // For example, the following tokens represent the same number: 10 - // (i32.const +10) - // (i32.const +1_0) - // (i32.const +0x0a) - // (i32.const +0x0_A) - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-int - tokenSN - - // tokenFN represents an IEEE-754 floating point number in decimal or hexadecimal notation, optionally separated by - // underscores. This also includes special constants for infinity ('inf') and NaN ('nan'). - // - // For example, the right-hand side of the following S-expressions are all valid floating point tokens: - // (f32.const +nan) - // (f64.const -nan:0xfffffffffffff) - // (f64.const -inf) - // (f64.const +0x0.0p0) - // (f32.const 0.0e0) - // (i32.const +0x0_A) - // (f32.const 1.e10) - // (f64.const 0x1.fff_fff_fff_fff_fp+1_023) - // (f64.const 1.7976931348623157e+308) - tokenFN - - // tokenString is a UTF-8 sequence enclosed by quotation marks, representing an encoded byte string. A tokenString - // can contain any character except ASCII control characters, quotation marks ('"') and backslash ('\'): these must - // be escaped. tokenString characters correspond to UTF-8 encoding except the special case of '\hh', which allows - // raw bytes expressed as hexadecimal. - // - // For example, the following tokens represent the same raw bytes: 0xe298ba0a - // (data (i32.const 0) "☺\n") - // (data (i32.const 0) "\u{263a}\u{0a}") - // (data (i32.const 0) "\e2\98\ba\0a") - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#strings%E2%91%A0 - tokenString - - // tokenID is a sequence of idChar characters prefixed by a '$': - // - // For example, in the below, '$y' is an ID: - // local.get $y - // i32.const 6 - // i32.lt_s - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-id - tokenID - - // tokenLParen is a left paren: '(' - tokenLParen - - // tokenRParen is a right paren: ')' - tokenRParen - - // tokenReserved is a sequence of idChar characters which are neither a tokenID nor a tokenString. - // - // For example, '0$y' is a tokenReserved, because it doesn't start with a letter or '$'. - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-reserved - tokenReserved -) - -// tokenNames is index-coordinated with tokenType -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#tokens%E2%91%A0 for the naming choices. -var tokenNames = [...]string{ - "invalid", - "keyword", - "uN", - "sN", - "fN", - "string", - "ID", - "(", - ")", - "reserved", -} - -// String returns the string name of this token. -func (t tokenType) String() string { - return tokenNames[t] -} - -// constants below help format a somewhat readable lookup table that eases identification of tokens. -const ( - // xx is an invalid token start byte - xx = tokenInvalid - // xs is the start of tokenString ('"') - xs = tokenString - // xi is the start of tokenID ('$') - xi = tokenID - // lp is the start of tokenLParen ('(') - lp = tokenLParen - // rp is the start of tokenRParen (')') - rp = tokenRParen - // un is the start of a tokenUN (or tokenFN) - un = tokenUN - // sn is the start of a tokenSN (or tokenFN) - sn = tokenSN - // xk is the start of a tokenKeyword - xk = tokenKeyword - // xr is the start of tokenReserved (a valid, but not defined above). - xr = tokenReserved -) - -// firstTokenByte is information about the firstTokenByte byte in a token. All expected token starts are ASCII, but we -// switch to avoid a range check. -var firstTokenByte = [256]tokenType{ - // 1 2 3 4 5 6 7 8 9 A B C D E F - xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x00-0x0F - xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x10-0x1F - xx, xr, xs, xr, xi, xr, xr, xr, lp, rp, xr, sn, xx, sn, xr, xr, // 0x20-0x2F - un, un, un, un, un, un, un, un, un, un, xr, xx, xr, xr, xr, xr, // 0x30-0x3F - xr, xr, xr, xr, xr, xr, xr, xr, xr, xr, xr, xr, xr, xr, xr, xr, // 0x40-0x4F - xr, xr, xr, xr, xr, xr, xr, xr, xr, xr, xr, xx, xr, xx, xr, xr, // 0x50-0x5F - xr, xk, xk, xk, xk, xk, xk, xk, xk, xk, xk, xk, xk, xk, xk, xk, // 0x60-0x6F - xk, xk, xk, xk, xk, xk, xk, xk, xk, xk, xk, xx, xr, xx, xr, xx, // 0x70-0x7F -} - -// idChar is a printable ASCII character that does not contain a space, quotation mark, comma, semicolon, or bracket. -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-idchar -var idChar = buildIdChars() - -func buildIdChars() (result [256]bool) { - for i := 0; i < 128; i++ { - result[i] = _idChar(byte(i)) - } - return -} - -func _idChar(ch byte) bool { - switch ch { - case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '/', ':', '<', '=', '>', '?', '@', '\\', '^', '_', '`', '|', '~': - return true - } - switch { - case ch >= '0' && ch <= '9': - fallthrough - case ch >= 'a' && ch <= 'z': - fallthrough - case ch >= 'A' && ch <= 'Z': - return true - } - return false -} - -// stripDollar returns the input without a leading '$' -// -// The WebAssembly 1.0 specification includes support for naming modules, functions, locals and tables via the custom -// 'name' section: https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namesec However, how this round-trips between the text and -// binary format is not discussed. -// -// We know that in the text format names must be dollar-sign prefixed to conform with tokenID conventions. However, we -// don't know if the user's intent was a dollar-sign or not. For example, a function written in a higher level language, -// targeting the binary format may end up prefixed with '$' for other reasons. -// -// This round-tripping concern materializes when a function written in the text format is transpiled into the binary -// format (ex via `wat2wasm --debug-names`). For example, if a module name was encoded literally in the binary custom -// 'name' section as "$Math", wabt tools would prefix it again, resulting in "$$Math". -// https://github.com/WebAssembly/wabt/blob/e59cf9369004a521814222afbc05ae6b59446cd5/src/binary-reader-ir.cc#L1279 -// -// Until the standard clarifies round-tripping concerns between the text and binary format, we chop off the leading '$' -// when reading any names from the text format. This prevents awkwardness while wabt tools are in use. -func stripDollar(tokenID []byte) []byte { - return tokenID[1:] // we don't check for leading '$' because we know the call sites must have one per tokenID -} diff --git a/internal/watzero/internal/token_test.go b/internal/watzero/internal/token_test.go deleted file mode 100644 index cad3b23394..0000000000 --- a/internal/watzero/internal/token_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package internal - -import ( - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" -) - -func TestTokenType_String(t *testing.T) { - tests := []struct { - input tokenType - expected string - }{ - {tokenKeyword, "keyword"}, - {tokenUN, "uN"}, - {tokenSN, "sN"}, - {tokenFN, "fN"}, - {tokenString, "string"}, - {tokenID, "ID"}, - {tokenLParen, "("}, - {tokenRParen, ")"}, - {tokenReserved, "reserved"}, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.expected, func(t *testing.T) { - require.Equal(t, tc.expected, tc.input.String()) - }) - } -} - -func TestStripDollar(t *testing.T) { - require.Equal(t, []byte{'1'}, stripDollar([]byte{'$', '1'})) -} diff --git a/internal/watzero/internal/type_parser.go b/internal/watzero/internal/type_parser.go deleted file mode 100644 index 289ad79bd8..0000000000 --- a/internal/watzero/internal/type_parser.go +++ /dev/null @@ -1,335 +0,0 @@ -package internal - -import ( - "errors" - "fmt" - - "github.com/tetratelabs/wazero/internal/wasm" -) - -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 - -// typeParser parses a wasm.Type from and dispatches to onType. -// -// Ex. `(module (type (func (param i32) (result i64)))` -// -// starts here --^ ^ -// onType resumes here --+ -// -// 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 - onType onType - - // pos is used to give an appropriate errorContext - pos parserPosition - - // currentType is reset on begin and complete onType - currentType *wasm.FunctionType - - // 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 - - // 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 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. - // - // See https://github.com/WebAssembly/spec/issues/1411 - parsedParamID bool -} - -// begin should be called after reaching the "type" keyword in a module field. Parsing continues until onType or error. -// -// This stage records the ID of the current type, if present, and resumes with tryBeginFunc. -// -// Ex. A type ID is present `(type $t0 (func (result i32)))` -// -// records t0 --^ ^ -// tryBeginFunc resumes here --+ -// -// Ex. No type ID `(type (func (result i32)))` -// -// calls tryBeginFunc --^ -func (p *typeParser) begin(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - p.currentType = &wasm.FunctionType{} - if tok == tokenID { // Ex. $v_v - if _, err := p.typeNamespace.setID(tokenBytes); err != nil { - return nil, err - } - return p.tryBeginFunc, nil - } - return p.tryBeginFunc(tok, tokenBytes, line, col) -} - -// tryBeginFunc begins a field on '(' by returning beginFunc, or errs on any other token. -func (p *typeParser) tryBeginFunc(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenID: // Ex.(type $rf32 $rf32 - return nil, fmt.Errorf("redundant ID %s", tokenBytes) - case tokenLParen: - return p.beginFunc, nil - case tokenRParen: // end of this type - return nil, errors.New("missing func field") // Ex. (type) - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// beginFunc returns a parser according to the type field name (tokenKeyword), or errs if invalid. -func (p *typeParser) beginFunc(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenKeyword { - return nil, expectedField(tok) - } - - if string(tokenBytes) != wasm.ExternTypeFuncName { - return nil, unexpectedFieldName(tokenBytes) - } - - p.pos = positionFunc - return p.parseFunc, nil -} - -// parseFunc passes control to the typeParser until any signature is read, then returns parseFuncEnd. -// -// Ex. `(module (type $rf32 (func (result f32))))` -// -// starts here --^ ^ -// parseFuncEnd resumes here --+ -// -// Ex. If there is no signature `(module (type $rf32 ))` -// -// calls parseFuncEnd here ---^ -func (p *typeParser) parseFunc(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - switch tok { - case tokenLParen: - return p.beginParamOrResult, nil // start fields, ex. (param or (result - case tokenRParen: // empty - return p.parseFuncEnd(tok, tokenBytes, line, col) - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// parseFuncEnd completes the wasm.ExternTypeFuncName field and returns end -func (p *typeParser) parseFuncEnd(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenRParen { - return nil, unexpectedToken(tok, tokenBytes) - } - p.pos = positionInitial - return p.end, nil -} - -// end increments the type namespace and calls onType with the current type -func (p *typeParser) end(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenRParen { - return nil, unexpectedToken(tok, tokenBytes) - } - p.typeNamespace.count++ - return p.onType(p.currentType), nil -} - -// beginParamOrResult decides which tokenParser to use based on its field name: "param" or "result". -func (p *typeParser) beginParamOrResult(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - if tok != tokenKeyword { - return nil, unexpectedToken(tok, tokenBytes) - } - - p.parsedParamType = false - - switch string(tokenBytes) { - case "param": - p.pos = positionParam - p.parsedParamID = false - return p.parseParamID, nil - case "result": - p.currentField = 0 // reset - p.pos = positionResult - return p.parseResult, nil - default: - return nil, unexpectedFieldName(tokenBytes) - } -} - -// parseMoreParamsOrResult looks for a '(', and if present returns beginParamOrResult to continue the type. Otherwise, -// it calls onType. -func (p *typeParser) parseMoreParamsOrResult(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - if tok == tokenLParen { - p.pos = positionFunc - return p.beginParamOrResult, nil - } - 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)` -// -// ^ -// parseParam resumes here --+ -// -// Ex. No param ID `(param i32)` -// -// calls parseParam --^ -func (p *typeParser) parseParamID(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - if tok == tokenID { // Ex. $len - p.parsedParamID = true - return p.parseParam, nil - } - return p.parseParam(tok, tokenBytes, line, col) -} - -// parseParam records value type and continues if it is an abbreviated form with multiple value types. When complete, -// this returns parseMoreParamsOrResult. -// -// Ex. One param type is present `(param i32)` -// -// records i32 --^ ^ -// parseMoreParamsOrResult resumes 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 - return nil, fmt.Errorf("redundant ID %s", tokenBytes) - case tokenKeyword: // Ex. i32 - vt, err := parseValueType(tokenBytes) - if err != nil { - return nil, err - } - 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.parsedParamType = true - return p.parseParam, nil - case tokenRParen: // end of this field - // since multiple param fields are valid, ex `(func (param i32) (param i64))`, prepare for any next. - p.currentField++ - p.pos = positionFunc - return p.parseMoreParamsOrResult, nil - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// 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 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 - } - } - 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 - // since multiple result fields are valid, ex `(func (result i32) (result i64))`, prepare for any next. - p.currentField++ - p.pos = positionFunc - return p.parseMoreResults, nil - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -func (p *typeParser) errorContext() string { - switch p.pos { - case positionFunc: - return ".func" - case positionParam: - return fmt.Sprintf(".func.param[%d]", p.currentField) - case positionResult: - return fmt.Sprintf(".func.result[%d]", p.currentField) - } - return "" -} - -func parseValueType(tokenBytes []byte) (wasm.ValueType, error) { - t := string(tokenBytes) - switch t { - case "i32": - return wasm.ValueTypeI32, nil - case "i64": - return wasm.ValueTypeI64, nil - case "f32": - return wasm.ValueTypeF32, nil - case "f64": - return wasm.ValueTypeF64, nil - default: - return 0, fmt.Errorf("unknown type: %s", t) - } -} diff --git a/internal/watzero/internal/type_parser_test.go b/internal/watzero/internal/type_parser_test.go deleted file mode 100644 index 963397633c..0000000000 --- a/internal/watzero/internal/type_parser_test.go +++ /dev/null @@ -1,432 +0,0 @@ -package internal - -import ( - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/wasm" -) - -var ( - 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}} - f32_i32 = &wasm.FunctionType{Params: []wasm.ValueType{f32}, Results: []wasm.ValueType{i32}} - 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}} - i32i64_i32 = &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32}} - i32i32i32i32_i32 = &wasm.FunctionType{ - Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}} - i32i32i32i32i32i64i64i32i32_i32 = &wasm.FunctionType{ - Params: []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32}, - Results: []wasm.ValueType{i32}, - } -) - -func TestTypeParser(t *testing.T) { - tests := []struct { - name string - input string - expected *wasm.FunctionType - expectedID string - }{ - { - name: "empty", - input: "(type (func))", - expected: &wasm.FunctionType{}, - }, - { - name: "empty - ID", - input: "(type $v_v (func))", - expected: &wasm.FunctionType{}, - expectedID: "v_v", - }, - { - name: "param no result", - input: "(type (func (param i32)))", - expected: i32_v, - }, - { - name: "param no result - ID", - input: "(type $i32_v (func (param i32)))", - expected: i32_v, - expectedID: "i32_v", - }, - { - name: "result no param", - input: "(type (func (result i32)))", - expected: v_i32, - }, - { - name: "result no param - ID", - input: "(type $v_i32 (func (result i32)))", - 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)))", - expected: i32i64_v, - }, - { - name: "mixed param no result - ID", - input: "(type $i32i64_v (func (param i32) (param i64)))", - expected: i32i64_v, - expectedID: "i32i64_v", - }, - { - name: "mixed param result", - input: "(type (func (param i32) (param i64) (result i32)))", - expected: i32i64_i32, - }, - { - name: "mixed param result - ID", - input: "(type $i32i64_i32 (func (param i32) (param i64) (result i32)))", - expected: i32i64_i32, - expectedID: "i32i64_i32", - }, - { - name: "abbreviated param result", - input: "(type (func (param i32 i64) (result i32)))", - expected: i32i64_i32, - }, - { - name: "mixed param abbreviation", // Verifies we can handle less param fields than param types - 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 { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - typeNamespace := newIndexNamespace(func(sectionID wasm.SectionID) uint32 { - require.Equal(t, wasm.SectionIDType, sectionID) - return 0 - }) - parsed, tp, err := parseFunctionType(wasm.Features20220419, typeNamespace, tc.input) - require.NoError(t, err) - require.Equal(t, tc.expected, parsed) - require.Equal(t, uint32(1), tp.typeNamespace.count) - if tc.expectedID == "" { - require.Zero(t, len(tp.typeNamespace.idToIdx), "expected no indices") - } else { - // Since the parser was initially empty, the expected index of the parsed type is 0 - require.Equal(t, map[string]wasm.Index{tc.expectedID: wasm.Index(0)}, tp.typeNamespace.idToIdx) - } - }) - } -} - -func TestTypeParser_Errors(t *testing.T) { - tests := []struct { - name, input, expectedErr string - enabledFeatures wasm.Features - }{ - { - name: "invalid token", - input: "(type \"0\")", - expectedErr: "unexpected string: \"0\"", - }, - { - name: "missing desc", - input: "(type)", - expectedErr: "missing func field", - }, - { - name: "not desc field", - input: "(type ($func))", - expectedErr: "expected field, but parsed ID", - }, - { - name: "wrong desc field", - input: "(type (funk))", - expectedErr: "unexpected field: funk", - }, - { - name: "second ID", - input: "(type $v_v $v_v func())", - expectedErr: "redundant ID $v_v", - }, - { - name: "func repeat", - input: "(type (func) (func))", - expectedErr: "unexpected '('", - }, - { - name: "func not field", - input: "(type (func ($param i64))", - expectedErr: "unexpected ID: $param", - }, - { - name: "func not param or result field", - input: "(type (func (type 0))", - expectedErr: "unexpected field: type", - }, - { - name: "param wrong type", - input: "(type (func (param i33)))", - expectedErr: "unknown type: i33", - }, - { - name: "param ID in abbreviation", - input: "(type (func (param $x i32 i64) )", - expectedErr: "cannot assign IDs to parameters in abbreviated form", - }, - { - name: "param second ID", - input: "(type (func (param $x $x i64) )", - expectedErr: "redundant ID $x", - }, - { - name: "param wrong end", - input: "(type (func (param i64 \"\")))", - expectedErr: "unexpected string: \"\"", - }, - { - name: "result has no ID", - input: "(type (func (result $x i64)))", - expectedErr: "unexpected ID: $x", - }, - { - name: "result wrong type", - 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.Features20220419, - expectedErr: "unknown type: i33", - }, - { - name: "result second redundant type wrong", - input: "(type (func (result i32) (result i32 i33)))", - enabledFeatures: wasm.Features20220419, - 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 \"\")))", - expectedErr: "unexpected string: \"\"", - }, - { - name: "func has no ID", - input: "(type (func $v_v )))", - expectedErr: "unexpected ID: $v_v", - }, - { - name: "func invalid token", - input: "(type (func \"0\")))", - expectedErr: "unexpected string: \"0\"", - }, - { - name: "wrong end", - input: "(type (func) \"\")", - expectedErr: "unexpected string: \"\"", - }, - { - name: "wrong end - after func", - input: "(type (func (param i32) \"\")))", - expectedErr: "unexpected string: \"\"", - }, - } - - for _, tt := range tests { - 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(enabledFeatures, typeNamespace, tc.input) - require.EqualError(t, err, tc.expectedErr) - require.Nil(t, parsed) - }) - } - - t.Run("duplicate ID", func(t *testing.T) { - typeNamespace := newIndexNamespace(func(sectionID wasm.SectionID) uint32 { - require.Equal(t, wasm.SectionIDType, sectionID) - return 0 - }) - _, err := typeNamespace.setID([]byte("$v_v")) - require.NoError(t, err) - typeNamespace.count++ - - parsed, _, err := parseFunctionType(wasm.Features20191205, typeNamespace, "(type $v_v (func))") - require.EqualError(t, err, "duplicate ID $v_v") - require.Nil(t, parsed) - }) -} - -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(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 -} - -func TestParseValueType(t *testing.T) { - tests := []struct { - input string - expected wasm.ValueType - }{ - {input: "i32", expected: wasm.ValueTypeI32}, - {input: "i64", expected: wasm.ValueTypeI64}, - {input: "f32", expected: wasm.ValueTypeF32}, - {input: "f64", expected: wasm.ValueTypeF64}, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.input, func(t *testing.T) { - m, err := parseValueType([]byte(tc.input)) - require.NoError(t, err) - require.Equal(t, tc.expected, m) - }) - } - t.Run("unknown type", func(t *testing.T) { - _, err := parseValueType([]byte("f65")) - require.EqualError(t, err, "unknown type: f65") - }) -} - -func TestTypeParser_ErrorContext(t *testing.T) { - p := typeParser{currentField: 3, currentType: &wasm.FunctionType{}} - tests := []struct { - input string - pos parserPosition - expected string - }{ - {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[3]"}, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.input, func(t *testing.T) { - p.pos = tc.pos - require.Equal(t, tc.expected, p.errorContext()) - }) - } -} diff --git a/internal/watzero/internal/typeuse_parser.go b/internal/watzero/internal/typeuse_parser.go deleted file mode 100644 index 4172d3fe00..0000000000 --- a/internal/watzero/internal/typeuse_parser.go +++ /dev/null @@ -1,516 +0,0 @@ -package internal - -import ( - "errors" - "fmt" - - "github.com/tetratelabs/wazero/internal/wasm" -) - -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. -// -// * typeIdx if unresolved, this is replaced in moduleParser.resolveTypeUses -// * paramNames is nil unless IDs existed on at least one "param" field. -// * pos is the context used to determine which tokenParser to return -// -// Note: this is called when neither a "param" nor a "result" field are parsed, or on any field following a "param" -// that is not a "result": pos clarifies this. -type onTypeUse func(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) - -// typeUseParser parses an inlined type from a field such wasm.ExternTypeFuncName and calls onTypeUse. -// -// Ex. `(import "Math" "PI" (func $math.pi (result f32)))` -// -// starts here --^ ^ -// onTypeUse resumes here --+ -// -// Note: Unlike normal parsers, this is not used for an entire field (enclosed by parens). Rather, this only handles -// "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 - - typeNamespace *indexNamespace - - section wasm.SectionID - - // inlinedTypes are anonymous types defined by signature, which at the time of definition didn't match a - // module-defined type. Ex. `(param i32)` in `(import (func (param i32)))` - // - // Note: The Text Format requires imports first, not types first. This resolution has to be done later. The impact - // of this is types here can be ignored if later discovered to be an explicitly defined type. - // - // For example, here the `(param i32)` type is doesn't match a module type with the same signature until later: - // (module (import (func (param i32))) (type (func (param i32))))` - inlinedTypes []*wasm.FunctionType - - inlinedTypeIndices []*inlinedTypeIndex - - // onTypeUse is invoked on end - onTypeUse onTypeUse - - // pos is used to give an appropriate errorContext - pos parserPosition - - // parsedTypeField is set when there is a "type" field in the current type use. - parsedTypeField bool - - // currentTypeIndex should be read when parsedTypeField is true - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#type-uses%E2%91%A0 - currentTypeIndex wasm.Index - - // currentTypeIndexUnresolved is set when the currentTypeIndex was not in the api.Module TypeSection - currentTypeIndexUnresolved *lineCol - - // currentInlinedType is reset on begin and complete onTypeUse - currentInlinedType *wasm.FunctionType - - // paramIndex enforce the uniqueness constraint on param ID - paramIndex map[string]struct{} - - // paramNames are the paramIndex formatted for the wasm.NameSection LocalNames - paramNames wasm.NameMap - - // 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 - - // 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 currentField had an ID. Ex. (param $x i32) - parsedParamID bool -} - -// begin should be called after reading any ID in a field that contains a type use. Parsing starts with the returned -// beginTypeParamOrResult and continues until onTypeUse or error. This should be called regardless of the tokenType -// to ensure a valid empty type use is associated with the section index, if needed. -// -// Ex. Given the source `(module (import (func $main (param i32))))` -// -// beginTypeParamOrResult starts here --^ ^ -// onTypeUse resumes here --+ -// -// Ex. Given the source `(module (func $main (result i32) (local.get 0))` -// -// beginTypeParamOrResult starts here --^ ^ -// onTypeUse resumes here --+ -func (p *typeUseParser) begin(section wasm.SectionID, onTypeUse onTypeUse, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - pos := callbackPositionUnhandledToken - p.pos = positionInitial // to ensure errorContext reports properly - switch tok { - case tokenLParen: - p.section = section - p.onTypeUse = onTypeUse - return p.beginTypeParamOrResult, nil - case tokenRParen: - pos = callbackPositionEndField - } - return onTypeUse(p.emptyTypeIndex(section), nil, pos, tok, tokenBytes, line, col) -} - -// v_v is a nullary function type (void -> void) -var v_v = &wasm.FunctionType{} - -// inlinedTypeIndex searches for any existing empty type to re-use -func (p *typeUseParser) emptyTypeIndex(section wasm.SectionID) wasm.Index { - for i, t := range p.module.TypeSection { - if t == v_v { - return wasm.Index(i) - } - } - - foundEmpty := false - var inlinedIdx wasm.Index - for i, t := range p.inlinedTypes { - if t == v_v { - foundEmpty = true - inlinedIdx = wasm.Index(i) - } - } - - if !foundEmpty { - inlinedIdx = wasm.Index(len(p.inlinedTypes)) - p.inlinedTypes = append(p.inlinedTypes, v_v) - } - - // typePos is not needed on empty as there's nothing to verify - idx := p.module.SectionElementCount(section) - i := &inlinedTypeIndex{section: section, idx: idx, inlinedIdx: inlinedIdx, typePos: nil} - p.inlinedTypeIndices = append(p.inlinedTypeIndices, i) - return 0 // substitute index that will be replaced later -} - -// beginTypeParamOrResult decides which tokenParser to use based on its field name: "type", "param" or "result". -func (p *typeUseParser) beginTypeParamOrResult(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - p.currentInlinedType = nil - if len(p.paramIndex) > 0 { - p.paramIndex = nil - p.paramNames = nil - } - p.currentField = 0 - p.parsedTypeField = false - if tok == tokenKeyword && string(tokenBytes) == "type" { - p.pos = positionType - p.parsedTypeField = true - return p.parseType, nil - } - p.pos = positionInitial - return p.beginParamOrResult(tok, tokenBytes, line, col) -} - -// parseType parses a type index inside the "type" field. If not yet in the TypeSection, the position is recorded for -// resolution later. Finally, this returns parseTypeEnd to finish the field. -func (p *typeUseParser) parseType(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - idx, resolved, err := p.typeNamespace.parseIndex(p.section, 0, tok, tokenBytes, line, col) - if err != nil { - return nil, err - } - - p.currentTypeIndex = idx - if !resolved { // save the source position in case there's an inlined use after this field and it doesn't match. - p.currentTypeIndexUnresolved = &lineCol{line: line, col: col} - } - return p.parseTypeEnd, nil -} - -// parseTypeEnd is the last parser of a "type" field. As "param" or "result" fields can follow, this returns -// parseMoreParamsOrResult to continue the type use. -func (p *typeUseParser) parseTypeEnd(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenUN, tokenID: - return nil, errors.New("redundant index") - case tokenRParen: - p.pos = positionInitial - return p.parseMoreParamsOrResult, nil - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// beginParamOrResult decides which tokenParser to use based on its field name: "param" or "result". -func (p *typeUseParser) beginParamOrResult(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - if tok != tokenKeyword { - return nil, unexpectedToken(tok, tokenBytes) - } - - p.parsedParamType = false - switch string(tokenBytes) { - case "param": - p.pos = positionParam - p.parsedParamID = false - return p.parseParamID, nil - case "result": - p.currentField = 0 // reset - p.pos = positionResult - return p.parseResult, nil - case "type": - return nil, errors.New("redundant type") - default: - return p.end(callbackPositionUnhandledField, tok, tokenBytes, line, col) - } -} - -// parseMoreParamsOrResult looks for a '(', and if present returns beginParamOrResult to continue the type. Otherwise, -// it calls parseEnd. -func (p *typeUseParser) parseMoreParamsOrResult(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - if tok == tokenLParen { - return p.beginParamOrResult, nil - } - 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)` -// -// ^ -// parseParam resumes here --+ -// -// Ex. No param ID `(param i32)` -// -// calls parseParam --^ -func (p *typeUseParser) parseParamID(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - if tok == tokenID { // Ex. $len - if err := p.setParamID(tokenBytes); err != nil { - return nil, err - } - p.parsedParamID = true - return p.parseParam, nil - } - return p.parseParam(tok, tokenBytes, line, col) -} - -// setParamID adds the normalized ('$' stripped) parameter ID to the paramIndex and the wasm.NameSection. -func (p *typeUseParser) setParamID(idToken []byte) error { - // 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 { - idx = wasm.Index(len(p.currentInlinedType.Params)) - } - - id := string(stripDollar(idToken)) - if p.paramIndex == nil { - p.paramIndex = map[string]struct{}{id: {}} - } else if _, ok := p.paramIndex[id]; ok { - return fmt.Errorf("duplicate ID $%s", id) - } else { - p.paramIndex[id] = struct{}{} - } - p.paramNames = append(p.paramNames, &wasm.NameAssoc{Index: idx, Name: id}) - return nil -} - -// parseParam records value type and continues if it is an abbreviated form with multiple value types. When complete, -// this returns parseMoreParamsOrResult. -// -// Ex. One param type is present `(param i32)` -// -// records i32 --^ ^ -// parseMoreParamsOrResult resumes here --+ -// -// Ex. Multiple param types are present `(param i32 i64)` -// -// records i32 --^ ^ ^ -// records i32 --+ | -// parseMoreParamsOrResult resumes here --+ -func (p *typeUseParser) parseParam(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) { - switch tok { - case tokenID: // Ex. $len - return nil, fmt.Errorf("redundant ID %s", tokenBytes) - case tokenKeyword: // Ex. i32 - vt, err := parseValueType(tokenBytes) - if err != nil { - return nil, err - } - if p.parsedParamType && p.parsedParamID { - return nil, errors.New("cannot assign IDs to parameters in abbreviated form") - } - if p.currentInlinedType == nil { - p.currentInlinedType = &wasm.FunctionType{Params: []wasm.ValueType{vt}} - } else { - p.currentInlinedType.Params = append(p.currentInlinedType.Params, vt) - } - p.parsedParamType = true - return p.parseParam, nil - case tokenRParen: // end of this field - // since multiple param fields are valid, ex `(func (param i32) (param i64))`, prepare for any next. - p.currentField++ - p.pos = positionInitial - return p.parseMoreParamsOrResult, nil - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -// 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 && 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 - } - } - vt, err := parseValueType(tokenBytes) - if err != nil { - return nil, err - } - if p.currentInlinedType == nil { - p.currentInlinedType = &wasm.FunctionType{Results: []wasm.ValueType{vt}} - } else { - p.currentInlinedType.Results = append(p.currentInlinedType.Results, vt) - } - return p.parseResult, nil - case tokenRParen: // end of this field - // since multiple result fields are valid, ex `(func (result i32) (result i64))`, prepare for any next. - p.currentField++ - p.pos = positionInitial - return p.parseMoreResults, nil - default: - return nil, unexpectedToken(tok, tokenBytes) - } -} - -func (p *typeUseParser) parseEnd(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - pos := callbackPositionUnhandledToken - if tok == tokenRParen { - pos = callbackPositionEndField - } - return p.end(pos, tok, tokenBytes, line, col) -} - -func (p *typeUseParser) errorContext() string { - switch p.pos { - case positionType: - return ".type" - case positionParam: - return fmt.Sprintf(".param[%d]", p.currentField) - case positionResult: - return fmt.Sprintf(".result[%d]", p.currentField) - } - return "" -} - -// lineCol saves source positions in case a FormatError needs to be raised later -type lineCol struct { - // line is FormatError.Line - line uint32 - // col is FormatError.Col - col uint32 -} - -// end invokes onTypeUse to continue parsing -func (p *typeUseParser) end(pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (parser tokenParser, err error) { - // Record the potentially inlined type if needed and invoke onTypeUse with the parsed index - var typeIdx wasm.Index - if p.parsedTypeField { - typeIdx, err = p.typeFieldIndex() - if err != nil { - return nil, err - } - } else if p.currentInlinedType == nil { // no type was found - typeIdx = p.emptyTypeIndex(p.section) - } else { // There was no explicitly defined type, so search for any existing ones to re-use - typeIdx = p.inlinedTypeIndex() - } - - // Invoke the onTypeUse hook with the current token - return p.onTypeUse(typeIdx, p.paramNames, pos, tok, tokenBytes, line, col) -} - -// inlinedTypeIndex searches for any existing type to re-use -func (p *typeUseParser) inlinedTypeIndex() wasm.Index { - it := p.currentInlinedType - for i, t := range p.module.TypeSection { - if t.EqualsSignature(it.Params, it.Results) { - return wasm.Index(i) - } - } - - p.maybeAddInlinedType(it) - return 0 // substitute index that will be replaced later -} - -func (p *typeUseParser) typeFieldIndex() (wasm.Index, error) { - if p.currentInlinedType == nil { - p.currentTypeIndexUnresolved = nil // no need to resolve a signature - return p.currentTypeIndex, nil - } - - typeIdx := p.currentTypeIndex - if p.currentTypeIndexUnresolved == nil { - // the type was resolved successfully, validate if needed and return it. - params, results := p.currentInlinedType.Params, p.currentInlinedType.Results - if err := requireInlinedMatchesReferencedType(p.module.TypeSection, typeIdx, params, results); err != nil { - return 0, err - } - } else { - // If we parsed param or results, we need to verify the signature against them once the type is known. - p.maybeAddInlinedType(p.currentInlinedType) - p.currentTypeIndexUnresolved = nil - } - return typeIdx, nil -} - -// maybeAddInlinedType records that the current type use had an inlined declaration. It is added to the inlinedTypes, if -// it didn't already exist. -func (p *typeUseParser) maybeAddInlinedType(it *wasm.FunctionType) { - for i, t := range p.inlinedTypes { - if t.EqualsSignature(it.Params, it.Results) { - p.recordInlinedType(wasm.Index(i)) - return - } - } - - // If we didn't find a match, we need to insert an inlined type to use it. - p.recordInlinedType(wasm.Index(len(p.inlinedTypes))) - p.inlinedTypes = append(p.inlinedTypes, it) -} - -type inlinedTypeIndex struct { - section wasm.SectionID - idx wasm.Index - inlinedIdx wasm.Index - typePos *lineCol -} - -func (p *typeUseParser) recordInlinedType(inlinedIdx wasm.Index) { - idx := p.module.SectionElementCount(p.section) - i := &inlinedTypeIndex{section: p.section, idx: idx, inlinedIdx: inlinedIdx, typePos: p.currentTypeIndexUnresolved} - p.inlinedTypeIndices = append(p.inlinedTypeIndices, i) -} - -// requireInlinedMatchesReferencedType satisfies the following rule: -// -// >> If inline declarations are given, then their types must match the referenced function type. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#type-uses%E2%91%A0 -func requireInlinedMatchesReferencedType(typeSection []*wasm.FunctionType, index wasm.Index, params, results []wasm.ValueType) error { - if !typeSection[index].EqualsSignature(params, results) { - return fmt.Errorf("inlined type doesn't match module.type[%d].func", index) - } - return nil -} diff --git a/internal/watzero/internal/typeuse_parser_test.go b/internal/watzero/internal/typeuse_parser_test.go deleted file mode 100644 index ac152f1207..0000000000 --- a/internal/watzero/internal/typeuse_parser_test.go +++ /dev/null @@ -1,677 +0,0 @@ -package internal - -import ( - "errors" - "fmt" - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/wasm" -) - -type typeUseParserTest struct { - name string - input string - expectedInlinedType *wasm.FunctionType - expectedTypeIdx wasm.Index - expectedParamNames wasm.NameMap - - expectedOnTypeUsePosition callbackPosition - expectedOnTypeUseToken tokenType - expectedTrailingTokens []tokenType -} - -func TestTypeUseParser_InlinesTypesWhenNotYetAdded(t *testing.T) { - tests := []*typeUseParserTest{ - { - name: "empty", - input: "()", - expectedInlinedType: v_v, - }, - { - name: "param no result", - input: "((param i32))", - expectedInlinedType: i32_v, - }, - { - name: "param no result - ID", - input: "((param $x i32))", - expectedInlinedType: i32_v, - expectedParamNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "x"}}, - }, - { - name: "result no param", - input: "((result i32))", - expectedInlinedType: v_i32, - }, - { - name: "mixed param no result", - input: "((param i32) (param i64))", - expectedInlinedType: i32i64_v, - }, - { - name: "mixed param no result - ID", - 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", - input: "((param i32) (param i64) (result i32))", - expectedInlinedType: i32i64_i32, - }, - { - name: "mixed param result - ID", - 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", - input: "((param i32 i64) (result i32))", - expectedInlinedType: i32i64_i32, - }, - { - name: "mixed param abbreviation", // Verifies we can handle less param fields than param types - 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(wasm.Features20220419, 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) - require.Equal(t, []*wasm.FunctionType{tc.expectedInlinedType}, tp.inlinedTypes) - } - }) -} - -func TestTypeUseParser_UnresolvedType(t *testing.T) { - tests := []*typeUseParserTest{ - { - name: "unresolved type - index", - input: "((type 1))", - expectedTypeIdx: 1, - }, - { - name: "unresolved type - ID", - input: "((type $v_v))", - expectedTypeIdx: 0, - }, - { - name: "unresolved type - index - match", - input: "((type 3) (param i32 i64) (result i32))", - expectedTypeIdx: 3, - expectedInlinedType: i32i64_i32, - }, - { - name: "unresolved type - ID - match", - 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(wasm.Features20220419, module, newIndexNamespace(module.SectionElementCount)) - return tp, func(t *testing.T) { - require.NotNil(t, tp.typeNamespace.unresolvedIndices) - if tc.expectedInlinedType == nil { - require.Zero(t, len(tp.inlinedTypes), "expected no inlinedTypes") - } else { - require.Equal(t, tc.expectedInlinedType, tp.inlinedTypes[0]) - } - } - }) -} - -func TestTypeUseParser_ReuseExistingType(t *testing.T) { - tests := []*typeUseParserTest{ - { - name: "match existing - result", - input: "((result i32))", - expectedTypeIdx: 0, - }, - { - name: "match existing - nullary", - input: "()", - expectedTypeIdx: 1, - }, - { - name: "match existing - param", - input: "((param i32))", - expectedTypeIdx: 2, - }, - { - name: "match existing - param and result", - input: "((param i32 i64) (result i32))", - expectedTypeIdx: 3, - }, - { - name: "type field index - result", - input: "((type 0))", - expectedTypeIdx: 0, - }, - { - name: "type field ID - result", - input: "((type $v_i32))", - expectedTypeIdx: 0, - }, - { - name: "type field ID - result - match", - input: "((type $v_i32) (result i32))", - expectedTypeIdx: 0, - }, - { - name: "type field index - nullary", - input: "((type 1))", - expectedTypeIdx: 1, - }, - { - name: "type field ID - nullary", - input: "((type $v_v))", - expectedTypeIdx: 1, - }, - { - name: "type field index - param", - input: "((type 2))", - expectedTypeIdx: 2, - }, - { - name: "type field ID - param", - input: "((type $i32_v))", - expectedTypeIdx: 2, - }, - { - name: "type field ID - param - match", - input: "((type $i32_v) (param i32))", - expectedTypeIdx: 2, - }, - { - name: "type field index - param and result", - input: "((type 3))", - expectedTypeIdx: 3, - }, - { - name: "type field ID - param and result", - input: "((type $i32i64_i32))", - expectedTypeIdx: 3, - }, - { - name: "type field ID - param and result - matched", - input: "((type $i32i64_i32) (param i32 i64) (result i32))", - expectedTypeIdx: 3, - }, - } - runTypeUseParserTests(t, tests, func(tc *typeUseParserTest) (*typeUseParser, func(t *testing.T)) { - // Add types to cover the main ways types uses are declared - module := &wasm.Module{TypeSection: []*wasm.FunctionType{v_i32, v_v, i32_v, i32i64_i32}} - typeNamespace := newIndexNamespace(module.SectionElementCount) - _, err := typeNamespace.setID([]byte("$v_i32")) - require.NoError(t, err) - typeNamespace.count++ - - _, err = typeNamespace.setID([]byte("$v_v")) - require.NoError(t, err) - typeNamespace.count++ - - _, err = typeNamespace.setID([]byte("$i32_v")) - require.NoError(t, err) - typeNamespace.count++ - - _, err = typeNamespace.setID([]byte("$i32i64_i32")) - require.NoError(t, err) - typeNamespace.count++ - - tp := newTypeUseParser(wasm.Features20220419, module, typeNamespace) - return tp, func(t *testing.T) { - require.Zero(t, len(tp.typeNamespace.unresolvedIndices)) - require.Zero(t, len(tp.inlinedTypes)) - require.Zero(t, len(tp.inlinedTypeIndices)) - } - }) -} - -func TestTypeUseParser_ReuseExistingInlinedType(t *testing.T) { - tests := []*typeUseParserTest{ - { - name: "match existing - result", - input: "((result i32))", - expectedInlinedType: v_i32, - }, - { - name: "nullary", - input: "()", - expectedInlinedType: v_v, - }, - { - name: "param", - input: "((param i32))", - expectedInlinedType: i32_v, - }, - { - name: "param and result", - 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(wasm.Features20220419, 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.input, ignoreTypeUse)) - - return tp, func(t *testing.T) { - // verify it wasn't duplicated - require.Equal(t, []*wasm.FunctionType{i32i64_v, tc.expectedInlinedType}, tp.inlinedTypes) - // last two inlined types are the same - require.Equal(t, tp.inlinedTypeIndices[1].inlinedIdx, tp.inlinedTypeIndices[2].inlinedIdx) - } - }) -} - -func TestTypeUseParser_BeginResets(t *testing.T) { - tests := []*typeUseParserTest{ - { - name: "result", - input: "((result i32))", - expectedInlinedType: v_i32, - }, - { - name: "nullary", - input: "()", - expectedInlinedType: v_v, - }, - { - name: "param", - input: "((param i32))", - expectedInlinedType: i32_v, - }, - { - name: "param and result", - input: "((param i32 i32) (result i32))", - expectedInlinedType: i32i32_i32, - }, - { - name: "param and result - with IDs", - 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(wasm.Features20220419, 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.input, ignoreTypeUse)) - - return tp, func(t *testing.T) { - // this is the second inlined type - require.Equal(t, []*wasm.FunctionType{i32i64_i32, tc.expectedInlinedType}, tp.inlinedTypes) - } - }) -} - -type typeUseTestFunc func(*typeUseParserTest) (*typeUseParser, func(t *testing.T)) - -// To prevent having to maintain a lot of tests to cover the necessary dimensions, this generates combinations that -// can happen in a type use. -// Ex. (func ) (func (local i32)) and (func nop) are all empty type uses, and we need to hit all three cases -func runTypeUseParserTests(t *testing.T, tests []*typeUseParserTest, tf typeUseTestFunc) { - moreTests := make([]*typeUseParserTest, 0, len(tests)*2) - for _, tt := range tests { - tt.expectedOnTypeUsePosition = callbackPositionEndField - tt.expectedOnTypeUseToken = tokenRParen // at the end of the field ')' - tt.expectedTrailingTokens = nil - - kt := *tt // copy - kt.name = fmt.Sprintf("%s - trailing keyword", tt.name) - 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} - moreTests = append(moreTests, &kt) - - ft := *tt // copy - ft.name = fmt.Sprintf("%s - trailing field", tt.name) - 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) - } - - for _, tt := range append(tests, moreTests...) { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - var parsedTypeIdx wasm.Index - var parsedParamNames wasm.NameMap - p := &collectTokenTypeParser{} - var setTypeUse onTypeUse = func(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - parsedTypeIdx = typeIdx - parsedParamNames = paramNames - require.Equal(t, tc.expectedOnTypeUsePosition, pos) - require.Equal(t, tc.expectedOnTypeUseToken, tok) - return p.parse, nil - } - - tp, test := tf(tc) - 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) - test(t) - }) - } -} - -func TestTypeUseParser_Errors(t *testing.T) { - tests := []struct { - name, input, expectedErr string - enabledFeatures wasm.Features - }{ - { - name: "not param", - input: "((param i32) ($param i32))", - expectedErr: "1:15: unexpected ID: $param", - }, - { - name: "param wrong type", - input: "((param i33))", - expectedErr: "1:9: unknown type: i33", - }, - { - name: "param ID in abbreviation", - input: "((param $x i32 i64) ", - expectedErr: "1:16: cannot assign IDs to parameters in abbreviated form", - }, - { - name: "param second ID", - input: "((param $x $x i64) ", - expectedErr: "1:12: redundant ID $x", - }, - { - name: "param duplicate ID", - input: "((param $x i32) (param $x i64) ", - expectedErr: "1:24: duplicate ID $x", - }, - { - name: "param wrong end", - input: `((param i64 ""))`, - expectedErr: "1:13: unexpected string: \"\"", - }, - { - name: "result has no ID", - input: "((result $x i64) ", - expectedErr: "1:10: unexpected ID: $x", - }, - { - name: "result wrong type", - input: "((result i33))", - expectedErr: "1:10: unknown type: i33", - }, - { - 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.Features20220419, - expectedErr: "1:23: unknown type: i33", - }, - { - name: "result second redundant type wrong", - input: "((result i32) (result i32 i33))", - enabledFeatures: wasm.Features20220419, - 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", - input: "((result i64 \"\"))", - expectedErr: "1:14: unexpected string: \"\"", - }, - { - name: "type missing index", - input: "((type))", - expectedErr: "1:7: missing index", - }, - { - name: "type wrong token", - input: "((type v_v))", - expectedErr: "1:8: unexpected keyword: v_v", - }, - { - name: "type redundant", - input: "((type 0) (type 1))", - expectedErr: "1:12: redundant type", - }, - { - name: "type second index", - input: "((type 0 1))", - expectedErr: "1:10: redundant index", - }, - { - name: "type overflow index", - input: "((type 4294967296))", - expectedErr: "1:8: index outside range of uint32: 4294967296", - }, - { - name: "type second ID", - input: "((type $v_v $v_v i64) ", - expectedErr: "1:13: redundant index", - }, - - { - name: "type wrong end", - input: `((type 0 ""))`, - expectedErr: "1:10: unexpected string: \"\"", - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - enabledFeatures := tc.enabledFeatures - if enabledFeatures == 0 { - enabledFeatures = wasm.Features20191205 - } - module := &wasm.Module{} - tp := newTypeUseParser(enabledFeatures, module, newIndexNamespace(module.SectionElementCount)) - err := parseTypeUse(tp, tc.input, failOnTypeUse) - require.EqualError(t, err, tc.expectedErr) - }) - } -} - -func TestTypeUseParser_FailsMatch(t *testing.T) { - // Add types to cover the main ways types uses are declared - module := &wasm.Module{TypeSection: []*wasm.FunctionType{v_v, i32i64_i32}} - typeNamespace := newIndexNamespace(module.SectionElementCount) - _, err := typeNamespace.setID([]byte("$v_v")) - require.NoError(t, err) - typeNamespace.count++ - - _, err = typeNamespace.setID([]byte("$i32i64_i32")) - require.NoError(t, err) - typeNamespace.count++ - - tp := newTypeUseParser(wasm.Features20220419, module, typeNamespace) - tests := []struct{ name, source, expectedErr string }{ - { - name: "nullary index", - source: "((type 0)(param i32))", - expectedErr: "1:21: inlined type doesn't match module.type[0].func", - }, - { - name: "nullary ID", - source: "((type $v_v)(param i32))", - expectedErr: "1:24: inlined type doesn't match module.type[0].func", - }, - { - name: "arity index - fails match", - source: "((type 1)(param i32))", - expectedErr: "1:21: inlined type doesn't match module.type[1].func", - }, - { - name: "arity ID - fails match", - source: "((type $i32i64_i32)(param i32))", - expectedErr: "1:31: inlined type doesn't match module.type[1].func", - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - require.EqualError(t, parseTypeUse(tp, tc.source, failOnTypeUse), tc.expectedErr) - }) - } -} - -var ignoreTypeUse onTypeUse = func(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - return parseNoop, nil -} - -var failOnTypeUse onTypeUse = func(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - return nil, errors.New("unexpected to call onTypeUse on error") -} - -func parseTypeUse(tp *typeUseParser, source string, onTypeUse onTypeUse) error { - var parser tokenParser = func(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { - return tp.begin(wasm.SectionIDFunction, onTypeUse, tok, tokenBytes, line, col) - } - - line, col, err := lex(skipTokens(1, parser), []byte(source)) - if err != nil { - err = &FormatError{Line: line, Col: col, cause: err} - } - return err -} - -func TestTypeUseParser_ErrorContext(t *testing.T) { - p := typeUseParser{currentField: 3} - tests := []struct { - source string - pos parserPosition - expected string - }{ - {source: "initial", pos: positionInitial, expected: ""}, - {source: "param", pos: positionParam, expected: ".param[3]"}, - {source: "result", pos: positionResult, expected: ".result[3]"}, - {source: "type", pos: positionType, expected: ".type"}, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.source, func(t *testing.T) { - p.pos = tc.pos - require.Equal(t, tc.expected, p.errorContext()) - }) - } -} diff --git a/internal/watzero/testdata/example.wat b/internal/watzero/testdata/example.wat deleted file mode 100644 index f4fa1d41a8..0000000000 --- a/internal/watzero/testdata/example.wat +++ /dev/null @@ -1,53 +0,0 @@ -;; This example contains currently supported functionality in the text format, used primarily for benchmarking. -(module $example ;; module name - ;; explicit type with param IDs which are to be ignored per WebAssembly/spec#1412 - (type $i32i32_i32 (func (param $i i32) (param $j i32) (result i32))) - - ;; type use by symbolic ID, but no param names - (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (type $i32i32_i32))) - ;; type use on an import func which adds param names on anonymous type - ;; TODO correct the param names - (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write - (param $fd i32) (param $iovs_ptr i32) (param $iovs_len i32) (param $nwritten_ptr i32) (result i32))) - - ;; func call referencing a func not defined, yet - (func $call_hello call $hello) - - ;; type use referencing a type not defined, yet - (func $hello (type 1)) - (type (func)) - - ;; start function referencing a function by symbolic ID - (start $hello) - - ;; export a function before it was defined, given its symbolic ID - (export "AddInt" (func $addInt)) - ;; export a function before it was defined, with an empty name - (export "" (func 3)) - - ;; from https://github.com/summerwind/the-art-of-webassembly-go/blob/main/chapter1/addint/addint.wat - (func $addInt ;; TODO: function exports (export "AddInt") - (param $value_1 i32) (param $value_2 i32) - (result i32) - local.get 0 ;; TODO: instruction variables $value_1 - local.get 1 ;; TODO: instruction variables $value_2 - i32.add - ) - - ;; export a memory before it was defined, given its symbolic ID - (export "mem" (memory $mem)) - (memory $mem 1 3) - - ;; 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 "nontrapping-float-to-int-conversion" - ;; https://github.com/WebAssembly/spec/blob/main/proposals/nontrapping-float-to-int-conversion/Overview.md - (func (param f32) (result i32) local.get 0 i32.trunc_sat_f32_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/internal/watzero/watzero.go b/internal/watzero/watzero.go deleted file mode 100644 index 6b98770ac7..0000000000 --- a/internal/watzero/watzero.go +++ /dev/null @@ -1,23 +0,0 @@ -package watzero - -import ( - internalwasm "github.com/tetratelabs/wazero/internal/wasm" - "github.com/tetratelabs/wazero/internal/wasm/binary" - "github.com/tetratelabs/wazero/internal/watzero/internal" -) - -// Wat2Wasm converts the WebAssembly Text Format (%.wat) into the Binary Format (%.wasm). -// This function returns when the input is exhausted or an error occurs. -// -// Here's a description of the return values: -// - result is the module parsed or nil on error -// - err is an error invoking the parser, dangling block comments or unexpected characters. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-format%E2%91%A0 -func Wat2Wasm(wat string) ([]byte, error) { - if m, err := internal.DecodeModule([]byte(wat), internalwasm.Features20220419, internalwasm.MemorySizer); err != nil { - return nil, err - } else { - return binary.EncodeModule(m), nil - } -} diff --git a/internal/watzero/watzero_test.go b/internal/watzero/watzero_test.go deleted file mode 100644 index bbd3531473..0000000000 --- a/internal/watzero/watzero_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package watzero - -import ( - _ "embed" - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/wasm" - "github.com/tetratelabs/wazero/internal/wasm/binary" -) - -// example holds the latest supported features as described in the comments of exampleWat -var example = newExample() - -// exampleWat is different from exampleWat because the parser doesn't yet support all features. -// -//go:embed testdata/example.wat -var exampleWat string - -func newExample() *wasm.Module { - three := wasm.Index(3) - f32, i32, i64 := wasm.ValueTypeF32, wasm.ValueTypeI32, wasm.ValueTypeI64 - return &wasm.Module{ - TypeSection: []*wasm.FunctionType{ - {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1}, - {}, - {Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 4, ResultNumInUint64: 1}, - {Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}, ParamNumInUint64: 1, ResultNumInUint64: 1}, - {Params: []wasm.ValueType{f32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1}, - {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}, ParamNumInUint64: 2, ResultNumInUint64: 2}, - }, - ImportSection: []*wasm.Import{ - { - Module: "wasi_snapshot_preview1", Name: "args_sizes_get", - Type: wasm.ExternTypeFunc, - DescFunc: 0, - }, { - Module: "wasi_snapshot_preview1", Name: "fd_write", - Type: wasm.ExternTypeFunc, - DescFunc: 2, - }, - }, - FunctionSection: []wasm.Index{wasm.Index(1), wasm.Index(1), wasm.Index(0), wasm.Index(3), wasm.Index(4), wasm.Index(5)}, - 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, 0x00, - wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF32S, - wasm.OpcodeEnd, - }}, - {Body: []byte{wasm.OpcodeLocalGet, 1, wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}}, - }, - MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: three, IsMaxEncoded: true}, - ExportSection: []*wasm.Export{ - {Name: "AddInt", Type: wasm.ExternTypeFunc, Index: wasm.Index(4)}, - {Name: "", Type: wasm.ExternTypeFunc, Index: wasm.Index(3)}, - {Name: "mem", Type: wasm.ExternTypeMemory, Index: wasm.Index(0)}, - {Name: "swap", Type: wasm.ExternTypeFunc, Index: wasm.Index(7)}, - }, - StartSection: &three, - NameSection: &wasm.NameSection{ - ModuleName: "example", - FunctionNames: wasm.NameMap{ - {Index: wasm.Index(0), Name: "wasi.args_sizes_get"}, - {Index: wasm.Index(1), Name: "wasi.fd_write"}, - {Index: wasm.Index(2), Name: "call_hello"}, - {Index: wasm.Index(3), Name: "hello"}, - {Index: wasm.Index(4), Name: "addInt"}, - {Index: wasm.Index(7), Name: "swap"}, - }, - LocalNames: wasm.IndirectNameMap{ - {Index: wasm.Index(1), NameMap: wasm.NameMap{ - {Index: wasm.Index(0), Name: "fd"}, - {Index: wasm.Index(1), Name: "iovs_ptr"}, - {Index: wasm.Index(2), Name: "iovs_len"}, - {Index: wasm.Index(3), Name: "nwritten_ptr"}, - }}, - {Index: wasm.Index(4), NameMap: wasm.NameMap{ - {Index: wasm.Index(0), Name: "value_1"}, - {Index: wasm.Index(1), Name: "value_2"}, - }}, - }, - }, - } -} - -func BenchmarkWat2Wasm(b *testing.B) { - for i := 0; i < b.N; i++ { - if _, err := Wat2Wasm(exampleWat); err != nil { - b.Fatal(err) - } - } -} - -func TestWat2Wasm(t *testing.T) { - wasm, err := Wat2Wasm(exampleWat) - require.NoError(t, err) - require.Equal(t, binary.EncodeModule(example), wasm) -} diff --git a/internal/wazeroir/compiler_test.go b/internal/wazeroir/compiler_test.go index f0315219eb..0097c48e32 100644 --- a/internal/wazeroir/compiler_test.go +++ b/internal/wazeroir/compiler_test.go @@ -10,8 +10,6 @@ import ( "github.com/tetratelabs/wazero/internal/leb128" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" - binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary" - "github.com/tetratelabs/wazero/internal/watzero" ) // ctx is an arbitrary, non-default context. @@ -43,8 +41,12 @@ func TestCompile(t *testing.T) { enabledFeatures wasm.Features }{ { - name: "nullary", - module: requireModuleText(t, `(module (func))`), + name: "nullary", + module: &wasm.Module{ + TypeSection: []*wasm.FunctionType{v_v}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeEnd}}}, + }, expected: &CompilationResult{ Operations: []Operation{ // begin with params: [] &OperationBr{Target: &BranchTarget{}}, // return! @@ -104,9 +106,11 @@ func TestCompile(t *testing.T) { }, { name: "identity", - module: requireModuleText(t, `(module - (func (param $x i32) (result i32) local.get 0) -)`), + module: &wasm.Module{ + TypeSection: []*wasm.FunctionType{i32_i32}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}}}, + }, expected: &CompilationResult{ Operations: []Operation{ // begin with params: [$x] &OperationPick{Depth: 0}, // [$x, $x] @@ -185,9 +189,13 @@ func TestCompile(t *testing.T) { }, { name: "memory.grow", // Ex to expose ops to grow memory - module: requireModuleText(t, `(module - (func (param $delta i32) (result (;previous_size;) i32) local.get 0 memory.grow) -)`), + module: &wasm.Module{ + TypeSection: []*wasm.FunctionType{i32_i32}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{{Body: []byte{ + wasm.OpcodeLocalGet, 0, wasm.OpcodeMemoryGrow, 0, wasm.OpcodeEnd, + }}}, + }, expected: &CompilationResult{ Operations: []Operation{ // begin with params: [$delta] &OperationPick{Depth: 0}, // [$delta, $delta] @@ -390,10 +398,14 @@ func TestCompile_MultiValue(t *testing.T) { }{ { name: "swap", - module: requireModuleText(t, `(module - (func (param $x i32) (param $y i32) (result i32 i32) local.get 1 local.get 0) -)`), - + module: &wasm.Module{ + TypeSection: []*wasm.FunctionType{i32i32_i32i32}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{{Body: []byte{ + // (func (param $x i32) (param $y i32) (result i32 i32) local.get 1 local.get 0) + wasm.OpcodeLocalGet, 1, wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd, + }}}, + }, expected: &CompilationResult{ Operations: []Operation{ // begin with params: [$x, $y] &OperationPick{Depth: 0}, // [$x, $y, $y] @@ -455,10 +467,14 @@ func TestCompile_MultiValue(t *testing.T) { }, { name: "call.wast - $const-i32-i64", - module: requireModuleText(t, `(module - (func $const-i32-i64 (result i32 i64) i32.const 306 i64.const 356) -)`), - + module: &wasm.Module{ + TypeSection: []*wasm.FunctionType{_i32i64}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{{Body: []byte{ + // (func $const-i32-i64 (result i32 i64) i32.const 306 i64.const 356) + wasm.OpcodeI32Const, 0xb2, 0x2, wasm.OpcodeI64Const, 0xe4, 0x2, wasm.OpcodeEnd, + }}}, + }, expected: &CompilationResult{ Operations: []Operation{ // begin with params: [] &OperationConstI32{Value: 306}, // [306] @@ -661,9 +677,14 @@ func TestCompile_MultiValue(t *testing.T) { // TestCompile_NonTrappingFloatToIntConversion picks an arbitrary operator from "nontrapping-float-to-int-conversion". func TestCompile_NonTrappingFloatToIntConversion(t *testing.T) { - module := requireModuleText(t, `(module - (func (param f32) (result i32) local.get 0 i32.trunc_sat_f32_s) -)`) + module := &wasm.Module{ + TypeSection: []*wasm.FunctionType{f32_i32}, + FunctionSection: []wasm.Index{0}, + // (func (param f32) (result i32) local.get 0 i32.trunc_sat_f32_s) + CodeSection: []*wasm.Code{{Body: []byte{ + wasm.OpcodeLocalGet, 0, wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF32S, wasm.OpcodeEnd, + }}}, + } expected := &CompilationResult{ Operations: []Operation{ // begin with params: [$0] @@ -692,9 +713,13 @@ func TestCompile_NonTrappingFloatToIntConversion(t *testing.T) { // TestCompile_SignExtensionOps picks an arbitrary operator from "sign-extension-ops". func TestCompile_SignExtensionOps(t *testing.T) { - module := requireModuleText(t, `(module - (func (param i32) (result i32) local.get 0 i32.extend8_s) -)`) + module := &wasm.Module{ + TypeSection: []*wasm.FunctionType{i32_i32}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{{Body: []byte{ + wasm.OpcodeLocalGet, 0, wasm.OpcodeI32Extend8S, wasm.OpcodeEnd, + }}}, + } expected := &CompilationResult{ Operations: []Operation{ // begin with params: [$0] @@ -726,14 +751,6 @@ func requireCompilationResult(t *testing.T, enabledFeatures wasm.Features, expec require.Equal(t, expected, res[0]) } -func requireModuleText(t *testing.T, wat string) *wasm.Module { - binary, err := watzero.Wat2Wasm(wat) - require.NoError(t, err) - m, err := binaryformat.DecodeModule(binary, wasm.Features20220419, wasm.MemorySizer) - require.NoError(t, err) - return m -} - func TestCompile_CallIndirectNonZeroTableIndex(t *testing.T) { module := &wasm.Module{ TypeSection: []*wasm.FunctionType{v_v, v_v, v_v},