From 5add4dc0bff6db8e101ece6ed771509fcdde6b6f Mon Sep 17 00:00:00 2001 From: Achille Date: Sat, 6 May 2023 08:19:27 -0700 Subject: [PATCH] experimental: add wazerotest package (#1437) --- experimental/experimental.go | 10 +- experimental/logging/log_listener_test.go | 23 + experimental/wazerotest/wazerotest.go | 727 +++++++++++++++++++++ experimental/wazerotest/wazerotest_test.go | 167 +++++ 4 files changed, 922 insertions(+), 5 deletions(-) create mode 100644 experimental/wazerotest/wazerotest.go create mode 100644 experimental/wazerotest/wazerotest_test.go diff --git a/experimental/experimental.go b/experimental/experimental.go index 1d917af7..47022f20 100644 --- a/experimental/experimental.go +++ b/experimental/experimental.go @@ -12,11 +12,11 @@ import ( type InternalModule interface { api.Module - // NumGlobal returns the count of all globals in the - // module. + // NumGlobal returns the count of all globals in the module. NumGlobal() int - // Global provides a read-only view for a given global - // index. Panics if idx > GlobalsCount(). - Global(idx int) api.Global + // Global provides a read-only view for a given global index. + // + // The methods panics if i is out of bounds. + Global(i int) api.Global } diff --git a/experimental/logging/log_listener_test.go b/experimental/logging/log_listener_test.go index 8ed79258..7aa684c8 100644 --- a/experimental/logging/log_listener_test.go +++ b/experimental/logging/log_listener_test.go @@ -9,6 +9,7 @@ import ( "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/experimental/logging" + "github.com/tetratelabs/wazero/experimental/wazerotest" "github.com/tetratelabs/wazero/internal/testing/require" wasi "github.com/tetratelabs/wazero/internal/wasip1" "github.com/tetratelabs/wazero/internal/wasm" @@ -347,3 +348,25 @@ func Test_loggingListener_indentation(t *testing.T) { <-- `, out.String()) } + +func BenchmarkLoggingListener(b *testing.B) { + module := wazerotest.NewModule( + wazerotest.NewMemory(0), + wazerotest.NewFunction(func(ctx context.Context, mod api.Module) {}), + ) + + function := module.Function(0) + factory := logging.NewLoggingListenerFactory(discard{}) + listener := factory.NewListener(function.Definition()) + + stack := []wazerotest.StackFrame{ + {Function: function}, + } + + wazerotest.BenchmarkFunctionListener(b, module, stack, listener) +} + +type discard struct{} + +func (discard) Write(b []byte) (int, error) { return len(b), nil } +func (discard) WriteString(s string) (int, error) { return len(s), nil } diff --git a/experimental/wazerotest/wazerotest.go b/experimental/wazerotest/wazerotest.go new file mode 100644 index 00000000..953a31b4 --- /dev/null +++ b/experimental/wazerotest/wazerotest.go @@ -0,0 +1,727 @@ +package wazerotest + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "math" + "reflect" + "strconv" + "sync" + "sync/atomic" + "testing" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/internalapi" + "github.com/tetratelabs/wazero/sys" +) + +const ( + exitStatusMarker = 1 << 63 +) + +// Module is an implementation of the api.Module interface, it represents a +// WebAssembly module. +type Module struct { + internalapi.WazeroOnlyType + exitStatus uint64 + + // The module name that will be returned by calling the Name method. + ModuleName string + + // The list of functions of the module. Functions with a non-empty export + // names will be exported by the module. + Functions []*Function + + // The list of globals of the module. Global + Globals []*Global + + // The program memory. If non-nil, the memory is automatically exported as + // "memory". + ExportMemory *Memory + + once sync.Once + exportedFunctions map[string]api.Function + exportedFunctionDefinitions map[string]api.FunctionDefinition + exportedGlobals map[string]api.Global + exportedMemoryDefinitions map[string]api.MemoryDefinition +} + +// NewModule constructs a Module object with the given memory and function list. +func NewModule(memory *Memory, functions ...*Function) *Module { + return &Module{Functions: functions, ExportMemory: memory} +} + +func (m *Module) String() string { + return "module[" + m.ModuleName + "]" +} + +func (m *Module) Name() string { + return m.ModuleName +} + +func (m *Module) Memory() api.Memory { + if m.ExportMemory != nil { + m.once.Do(m.initialize) + return m.ExportMemory + } + return nil +} + +func (m *Module) ExportedFunction(name string) api.Function { + m.once.Do(m.initialize) + return m.exportedFunctions[name] +} + +func (m *Module) ExportedFunctionDefinitions() map[string]api.FunctionDefinition { + m.once.Do(m.initialize) + return m.exportedFunctionDefinitions +} + +func (m *Module) ExportedMemory(name string) api.Memory { + if m.ExportMemory != nil && name == "memory" { + m.once.Do(m.initialize) + return m.ExportMemory + } + return nil +} + +func (m *Module) ExportedMemoryDefinitions() map[string]api.MemoryDefinition { + m.once.Do(m.initialize) + return m.exportedMemoryDefinitions +} + +func (m *Module) ExportedGlobal(name string) api.Global { + m.once.Do(m.initialize) + return m.exportedGlobals[name] +} + +func (m *Module) NumFunction() int { + return len(m.Functions) +} + +func (m *Module) Function(i int) api.Function { + m.once.Do(m.initialize) + return m.Functions[i] +} + +func (m *Module) NumGlobal() int { + return len(m.Globals) +} + +func (m *Module) Global(i int) api.Global { + m.once.Do(m.initialize) + return m.Globals[i] +} + +func (m *Module) Close(ctx context.Context) error { + return m.CloseWithExitCode(ctx, 0) +} + +func (m *Module) CloseWithExitCode(ctx context.Context, exitCode uint32) error { + atomic.CompareAndSwapUint64(&m.exitStatus, 0, exitStatusMarker|uint64(exitCode)) + return nil +} + +func (m *Module) ExitStatus() (exitCode uint32, exited bool) { + exitStatus := atomic.LoadUint64(&m.exitStatus) + return uint32(exitStatus), exitStatus != 0 +} + +func (m *Module) initialize() { + m.exportedFunctions = make(map[string]api.Function) + m.exportedFunctionDefinitions = make(map[string]api.FunctionDefinition) + m.exportedGlobals = make(map[string]api.Global) + m.exportedMemoryDefinitions = make(map[string]api.MemoryDefinition) + + for index, function := range m.Functions { + for _, exportName := range function.ExportNames { + m.exportedFunctions[exportName] = function + m.exportedFunctionDefinitions[exportName] = function.Definition() + } + function.module = m + function.index = index + } + + for _, global := range m.Globals { + for _, exportName := range global.ExportNames { + m.exportedGlobals[exportName] = global + } + } + + if m.ExportMemory != nil { + m.ExportMemory.module = m + m.exportedMemoryDefinitions["memory"] = m.ExportMemory.Definition() + } +} + +// Global is an implementation of the api.Global interface, it represents a +// global in a WebAssembly module. +type Global struct { + internalapi.WazeroOnlyType + + // Type of the global value, used to interpret bits of the Value field. + ValueType api.ValueType + + // Value of the global packed in a 64 bits field. + Value uint64 + + // List of names that the globla is exported as. + ExportNames []string +} + +func (g *Global) String() string { + switch g.ValueType { + case api.ValueTypeI32: + return strconv.FormatInt(int64(api.DecodeI32(g.Value)), 10) + case api.ValueTypeI64: + return strconv.FormatInt(int64(g.Value), 10) + case api.ValueTypeF32: + return strconv.FormatFloat(float64(api.DecodeF32(g.Value)), 'g', -1, 32) + case api.ValueTypeF64: + return strconv.FormatFloat(api.DecodeF64(g.Value), 'g', -1, 64) + default: + return "0x" + strconv.FormatUint(g.Value, 16) + } +} + +func (g *Global) Type() api.ValueType { + return g.ValueType +} + +func (g *Global) Get() uint64 { + return g.Value +} + +func GlobalI32(value int32, export ...string) *Global { + return &Global{ValueType: api.ValueTypeI32, Value: api.EncodeI32(value), ExportNames: export} +} + +func GlobalI64(value int64, export ...string) *Global { + return &Global{ValueType: api.ValueTypeI64, Value: api.EncodeI64(value), ExportNames: export} +} + +func GlobalF32(value float32, export ...string) *Global { + return &Global{ValueType: api.ValueTypeF32, Value: api.EncodeF32(value), ExportNames: export} +} + +func GlobalF64(value float64, export ...string) *Global { + return &Global{ValueType: api.ValueTypeF64, Value: api.EncodeF64(value), ExportNames: export} +} + +// Function is an implementation of the api.Function interface, it represents +// a function in a WebAssembly module. +// +// Until accessed through a Module's method, the function definition's +// ModuleName method returns an empty string and its Index method returns 0. +type Function struct { + internalapi.WazeroOnlyType + + // GoModuleFunction may be set to a non-nil value to allow calling of the + // function via Call or CallWithStack. + // + // It is the user's responsibility to ensure that the signature of this + // implementation matches the ParamTypes and ResultTypes fields. + GoModuleFunction api.GoModuleFunction + + // Type lists representing the function signature. Those fields should be + // set for the function to be properly constructed. The Function's Call + // and CallWithStack methods will error if those fields are nil. + ParamTypes []api.ValueType + ResultTypes []api.ValueType + + // Sets of names associated with the function. It is valid to leave those + // names empty, they are only used for debugging purposes. + FunctionName string + DebugName string + ParamNames []string + ResultNames []string + ExportNames []string + + // Lazily initialized when accessed through the module. + module *Module + index int +} + +// NewFunction constructs a Function object from a Go function. +// +// The function fn must accept at least two arguments of type context.Context +// and api.Module. Any other arguments and return values must be of type uint32, +// uint64, int32, int64, float32, or float64. The call panics if fn is not a Go +// functionn or has an unsupported signature. +func NewFunction(fn any) *Function { + functionType := reflect.TypeOf(fn) + functionValue := reflect.ValueOf(fn) + + paramTypes := make([]api.ValueType, functionType.NumIn()-2) + paramFuncs := make([]func(uint64) reflect.Value, len(paramTypes)) + + resultTypes := make([]api.ValueType, functionType.NumOut()) + resultFuncs := make([]func(reflect.Value) uint64, len(resultTypes)) + + for i := range paramTypes { + var paramType api.ValueType + var paramFunc func(uint64) reflect.Value + + switch functionType.In(i + 2).Kind() { + case reflect.Uint32: + paramType = api.ValueTypeI32 + paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(api.DecodeU32(v)) } + case reflect.Uint64: + paramType = api.ValueTypeI64 + paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(v) } + case reflect.Int32: + paramType = api.ValueTypeI32 + paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(api.DecodeI32(v)) } + case reflect.Int64: + paramType = api.ValueTypeI64 + paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(int64(v)) } + case reflect.Float32: + paramType = api.ValueTypeF32 + paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(api.DecodeF32(v)) } + case reflect.Float64: + paramType = api.ValueTypeF64 + paramFunc = func(v uint64) reflect.Value { return reflect.ValueOf(api.DecodeF64(v)) } + default: + panic("cannot construct wasm function from go function of type " + functionType.String()) + } + + paramTypes[i] = paramType + paramFuncs[i] = paramFunc + } + + for i := range resultTypes { + var resultType api.ValueType + var resultFunc func(reflect.Value) uint64 + + switch functionType.Out(i).Kind() { + case reflect.Uint32: + resultType = api.ValueTypeI32 + resultFunc = func(v reflect.Value) uint64 { return v.Uint() } + case reflect.Uint64: + resultType = api.ValueTypeI64 + resultFunc = func(v reflect.Value) uint64 { return v.Uint() } + case reflect.Int32: + resultType = api.ValueTypeI32 + resultFunc = func(v reflect.Value) uint64 { return api.EncodeI32(int32(v.Int())) } + case reflect.Int64: + resultType = api.ValueTypeI64 + resultFunc = func(v reflect.Value) uint64 { return api.EncodeI64(v.Int()) } + case reflect.Float32: + resultType = api.ValueTypeF32 + resultFunc = func(v reflect.Value) uint64 { return api.EncodeF32(float32(v.Float())) } + case reflect.Float64: + resultType = api.ValueTypeF64 + resultFunc = func(v reflect.Value) uint64 { return api.EncodeF64(v.Float()) } + default: + panic("cannot construct wasm function from go function of type " + functionType.String()) + } + + resultTypes[i] = resultType + resultFuncs[i] = resultFunc + } + + return &Function{ + GoModuleFunction: api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) { + in := make([]reflect.Value, 2+len(paramFuncs)) + in[0] = reflect.ValueOf(ctx) + in[1] = reflect.ValueOf(mod) + for i, param := range paramFuncs { + in[i+2] = param(stack[i]) + } + out := functionValue.Call(in) + for i, result := range resultFuncs { + stack[i] = result(out[i]) + } + }), + ParamTypes: paramTypes, + ResultTypes: resultTypes, + } +} + +var ( + errMissingFunctionSignature = errors.New("missing function signature") + errMissingFunctionModule = errors.New("missing function module") + errMissingFunctionImplementation = errors.New("missing function implementation") +) + +func (f *Function) Definition() api.FunctionDefinition { + return functionDefinition{function: f} +} + +func (f *Function) Call(ctx context.Context, params ...uint64) ([]uint64, error) { + stackLen := len(f.ParamTypes) + if stackLen < len(f.ResultTypes) { + stackLen = len(f.ResultTypes) + } + stack := make([]uint64, stackLen) + copy(stack, params) + err := f.CallWithStack(ctx, stack) + if err != nil { + for i := range stack { + stack[i] = 0 + } + } + return stack[:len(f.ResultTypes)], err +} + +func (f *Function) CallWithStack(ctx context.Context, stack []uint64) error { + if f.ParamTypes == nil || f.ResultTypes == nil { + return errMissingFunctionSignature + } + if f.GoModuleFunction == nil { + return errMissingFunctionImplementation + } + if f.module == nil { + return errMissingFunctionModule + } + if exitCode, exited := f.module.ExitStatus(); exited { + return sys.NewExitError(exitCode) + } + f.GoModuleFunction.Call(ctx, f.module, stack) + return nil +} + +type functionDefinition struct { + internalapi.WazeroOnlyType + function *Function +} + +func (def functionDefinition) Name() string { + return def.function.FunctionName +} + +func (def functionDefinition) DebugName() string { + if def.function.DebugName != "" { + return def.function.DebugName + } + return fmt.Sprintf("%s.$%d", def.ModuleName(), def.Index()) +} + +func (def functionDefinition) GoFunction() any { + return def.function.GoModuleFunction +} + +func (def functionDefinition) ParamTypes() []api.ValueType { + return def.function.ParamTypes +} + +func (def functionDefinition) ParamNames() []string { + return def.function.ParamNames +} + +func (def functionDefinition) ResultTypes() []api.ValueType { + return def.function.ResultTypes +} + +func (def functionDefinition) ResultNames() []string { + return def.function.ResultNames +} + +func (def functionDefinition) ModuleName() string { + if def.function.module != nil { + return def.function.module.ModuleName + } + return "" +} + +func (def functionDefinition) Index() uint32 { + return uint32(def.function.index) +} + +func (def functionDefinition) Import() (moduleName, name string, isImport bool) { + return +} + +func (def functionDefinition) ExportNames() []string { + return def.function.ExportNames +} + +// Memory is an implementation of the api.Memory interface, representing the +// memory of a WebAssembly module. +type Memory struct { + internalapi.WazeroOnlyType + + // Byte slices holding the memory pages. + // + // It is the user's repsonsibility to ensure that the length of this byte + // slice is a multiple of the page size. + Bytes []byte + + // Min and max number of memory pages which may be held in this memory. + // + // Leaving Max to zero means no upper bound. + Min uint32 + Max uint32 + + // Lazily initialized when accessed through the module. + module *Module +} + +// NewMemory constructs a Memory object with a buffer of the given size, aligned +// to the closest multiple of the page size. +func NewMemory(size int) *Memory { + numPages := (size + (PageSize - 1)) / PageSize + return &Memory{ + Bytes: make([]byte, numPages*PageSize), + Min: uint32(numPages), + } +} + +// NewFixedMemory constructs a Memory object of the given size. The returned +// memory is configured with a max limit to prevent growing beyond its initial +// size. +func NewFixedMemory(size int) *Memory { + memory := NewMemory(size) + memory.Max = memory.Min + return memory +} + +// The PageSize constant defines the size of WebAssembly memory pages in bytes. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#page-size +const PageSize = 65536 + +func (m *Memory) Definition() api.MemoryDefinition { + return memoryDefinition{memory: m} +} + +func (m *Memory) Size() uint32 { + return uint32(len(m.Bytes)) +} + +func (m *Memory) Grow(deltaPages uint32) (previousPages uint32, ok bool) { + previousPages = uint32(len(m.Bytes) / PageSize) + numPages := previousPages + deltaPages + if m.Max != 0 && numPages > m.Max { + return previousPages, false + } + bytes := make([]byte, PageSize*numPages) + copy(bytes, m.Bytes) + m.Bytes = bytes + return previousPages, true +} + +func (m *Memory) ReadByte(offset uint32) (byte, bool) { + if m.isOutOfRange(offset, 1) { + return 0, false + } + return m.Bytes[offset], true +} + +func (m *Memory) ReadUint16Le(offset uint32) (uint16, bool) { + if m.isOutOfRange(offset, 2) { + return 0, false + } + return binary.LittleEndian.Uint16(m.Bytes[offset:]), true +} + +func (m *Memory) ReadUint32Le(offset uint32) (uint32, bool) { + if m.isOutOfRange(offset, 4) { + return 0, false + } + return binary.LittleEndian.Uint32(m.Bytes[offset:]), true +} + +func (m *Memory) ReadUint64Le(offset uint32) (uint64, bool) { + if m.isOutOfRange(offset, 8) { + return 0, false + } + return binary.LittleEndian.Uint64(m.Bytes[offset:]), true +} + +func (m *Memory) ReadFloat32Le(offset uint32) (float32, bool) { + v, ok := m.ReadUint32Le(offset) + return math.Float32frombits(v), ok +} + +func (m *Memory) ReadFloat64Le(offset uint32) (float64, bool) { + v, ok := m.ReadUint64Le(offset) + return math.Float64frombits(v), ok +} + +func (m *Memory) Read(offset, length uint32) ([]byte, bool) { + if m.isOutOfRange(offset, length) { + return nil, false + } + return m.Bytes[offset : offset+length : offset+length], true +} + +func (m *Memory) WriteByte(offset uint32, value byte) bool { + if m.isOutOfRange(offset, 1) { + return false + } + m.Bytes[offset] = value + return true +} + +func (m *Memory) WriteUint16Le(offset uint32, value uint16) bool { + if m.isOutOfRange(offset, 2) { + return false + } + binary.LittleEndian.PutUint16(m.Bytes[offset:], value) + return true +} + +func (m *Memory) WriteUint32Le(offset uint32, value uint32) bool { + if m.isOutOfRange(offset, 4) { + return false + } + binary.LittleEndian.PutUint32(m.Bytes[offset:], value) + return true +} + +func (m *Memory) WriteUint64Le(offset uint32, value uint64) bool { + if m.isOutOfRange(offset, 4) { + return false + } + binary.LittleEndian.PutUint64(m.Bytes[offset:], value) + return true +} + +func (m *Memory) WriteFloat32Le(offset uint32, value float32) bool { + return m.WriteUint32Le(offset, math.Float32bits(value)) +} + +func (m *Memory) WriteFloat64Le(offset uint32, value float64) bool { + return m.WriteUint64Le(offset, math.Float64bits(value)) +} + +func (m *Memory) Write(offset uint32, value []byte) bool { + if m.isOutOfRange(offset, uint32(len(value))) { + return false + } + copy(m.Bytes[offset:], value) + return true +} + +func (m *Memory) WriteString(offset uint32, value string) bool { + if m.isOutOfRange(offset, uint32(len(value))) { + return false + } + copy(m.Bytes[offset:], value) + return true +} + +func (m *Memory) isOutOfRange(offset, length uint32) bool { + size := m.Size() + return offset >= size || length > size || offset > (size-length) +} + +type memoryDefinition struct { + internalapi.WazeroOnlyType + memory *Memory +} + +func (def memoryDefinition) ModuleName() string { + if def.memory.module != nil { + return def.memory.module.ModuleName + } + return "" +} + +func (def memoryDefinition) Index() uint32 { + return 0 +} + +func (def memoryDefinition) Import() (moduleName, name string, isImport bool) { + return +} + +func (def memoryDefinition) ExportNames() []string { + if def.memory.module != nil { + return []string{"memory"} + } + return nil +} + +func (def memoryDefinition) Min() uint32 { + return def.memory.Min +} + +func (def memoryDefinition) Max() (uint32, bool) { + return def.memory.Max, def.memory.Max != 0 +} + +// StackFrame represents a frame on the call stack. +type StackFrame struct { + Function api.Function + Params []uint64 + Results []uint64 +} + +type stackIterator struct { + stack []StackFrame + fndef []api.FunctionDefinition + index int +} + +func (si *stackIterator) Next() bool { + si.index++ + return si.index < len(si.stack) +} + +func (si *stackIterator) FunctionDefinition() api.FunctionDefinition { + return si.fndef[si.index] +} + +func (si *stackIterator) Parameters() []uint64 { + return si.stack[si.index].Params +} + +func (si *stackIterator) reset() { + si.index = 0 +} + +func newStackIterator(stack []StackFrame) *stackIterator { + si := &stackIterator{ + stack: stack, + fndef: make([]api.FunctionDefinition, len(stack)), + } + // The size of functionDefinition is only one pointer which should allow + // the compiler to optimize the conversion to api.FunctionDefinition; but + // the presence of internal.WazeroOnlyType, despite being defined as an + // empty struct, forces a heap allocation that we amortize by caching the + // result. + for i, frame := range stack { + si.fndef[i] = frame.Function.Definition() + } + return si +} + +var ( + _ experimental.InternalModule = (*Module)(nil) + + _ api.Module = (*Module)(nil) + _ api.Function = (*Function)(nil) + _ api.Global = (*Global)(nil) +) + +// BenchmarkFunctionListener implements a benchmark for function listeners. +// +// The benchmark calls Before and After methods repeatedly using the provided +// module an stack frames to invoke the methods. +// +// The stack frame is a representation of the call stack that the Before method +// will be invoked with. The top of the stack is stored at index zero. The stack +// must contain at least one frame or the benchmark will fail. +func BenchmarkFunctionListener(b *testing.B, module *Module, stack []StackFrame, listener experimental.FunctionListener) { + if len(stack) == 0 { + b.Error("cannot benchmark function listener with an empty stack") + return + } + + functionDefinition := stack[0].Function.Definition() + functionParams := stack[0].Params + functionResults := stack[0].Results + stackIterator := newStackIterator(stack) + ctx := context.Background() + + for i := 0; i < b.N; i++ { + stackIterator.reset() + callContext := listener.Before(ctx, module, functionDefinition, functionParams, stackIterator) + listener.After(callContext, module, functionDefinition, nil, functionResults) + } +} diff --git a/experimental/wazerotest/wazerotest_test.go b/experimental/wazerotest/wazerotest_test.go new file mode 100644 index 00000000..39f280aa --- /dev/null +++ b/experimental/wazerotest/wazerotest_test.go @@ -0,0 +1,167 @@ +package wazerotest + +import ( + "bytes" + "context" + "reflect" + "testing" + + "github.com/tetratelabs/wazero/api" +) + +func TestNewFunction(t *testing.T) { + tests := []struct { + in any + out *Function + }{ + { + in: func(ctx context.Context, mod api.Module) {}, + out: &Function{ + ParamTypes: []api.ValueType{}, + ResultTypes: []api.ValueType{}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module, v uint32) {}, + out: &Function{ + ParamTypes: []api.ValueType{api.ValueTypeI32}, + ResultTypes: []api.ValueType{}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module, v uint64) {}, + out: &Function{ + ParamTypes: []api.ValueType{api.ValueTypeI64}, + ResultTypes: []api.ValueType{}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module, v int32) {}, + out: &Function{ + ParamTypes: []api.ValueType{api.ValueTypeI32}, + ResultTypes: []api.ValueType{}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module, v int64) {}, + out: &Function{ + ParamTypes: []api.ValueType{api.ValueTypeI64}, + ResultTypes: []api.ValueType{}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module, v float32) {}, + out: &Function{ + ParamTypes: []api.ValueType{api.ValueTypeF32}, + ResultTypes: []api.ValueType{}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module, v float64) {}, + out: &Function{ + ParamTypes: []api.ValueType{api.ValueTypeF64}, + ResultTypes: []api.ValueType{}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module) uint32 { return 0 }, + out: &Function{ + ParamTypes: []api.ValueType{}, + ResultTypes: []api.ValueType{api.ValueTypeI32}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module) uint64 { return 0 }, + out: &Function{ + ParamTypes: []api.ValueType{}, + ResultTypes: []api.ValueType{api.ValueTypeI64}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module) int32 { return 0 }, + out: &Function{ + ParamTypes: []api.ValueType{}, + ResultTypes: []api.ValueType{api.ValueTypeI32}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module) int64 { return 0 }, + out: &Function{ + ParamTypes: []api.ValueType{}, + ResultTypes: []api.ValueType{api.ValueTypeI64}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module) float32 { return 0 }, + out: &Function{ + ParamTypes: []api.ValueType{}, + ResultTypes: []api.ValueType{api.ValueTypeF32}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module) float64 { return 0 }, + out: &Function{ + ParamTypes: []api.ValueType{}, + ResultTypes: []api.ValueType{api.ValueTypeF64}, + }, + }, + + { + in: func(ctx context.Context, mod api.Module, _ uint32, _ uint64, _ int32, _ int64, _ float32, _ float64) (_ uint32, _ uint64, _ int32, _ int64, _ float32, _ float64) { + return + }, + out: &Function{ + ParamTypes: []api.ValueType{ + api.ValueTypeI32, api.ValueTypeI64, + api.ValueTypeI32, api.ValueTypeI64, + api.ValueTypeF32, api.ValueTypeF64, + }, + ResultTypes: []api.ValueType{ + api.ValueTypeI32, api.ValueTypeI64, + api.ValueTypeI32, api.ValueTypeI64, + api.ValueTypeF32, api.ValueTypeF64, + }, + }, + }, + } + + for _, test := range tests { + t.Run(reflect.TypeOf(test.in).String(), func(t *testing.T) { + f := NewFunction(test.in) + if !bytes.Equal(test.out.ParamTypes, f.ParamTypes) { + t.Errorf("invalid parameter types: want=%v got=%v", test.out.ParamTypes, f.ParamTypes) + } + if !bytes.Equal(test.out.ResultTypes, f.ResultTypes) { + t.Errorf("invalid result types: want=%v got=%v", test.out.ResultTypes, f.ResultTypes) + } + }) + } +} + +func TestNewMemory(t *testing.T) { + if memory := NewMemory(1); len(memory.Bytes) != PageSize { + t.Error("memory buffer not aligned on page size") + } +} + +func TestNewFixedMemory(t *testing.T) { + if memory := NewFixedMemory(1); len(memory.Bytes) != PageSize { + t.Error("memory buffer not aligned on page size") + } else if memory.Min != 1 { + t.Error("invalid min memory size:", memory.Min) + } else if memory.Max != 1 { + t.Error("invalid max memory size:", memory.Max) + } +}