594 lines
19 KiB
Go
594 lines
19 KiB
Go
package compiler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"testing"
|
|
"unsafe"
|
|
|
|
"github.com/tetratelabs/wazero/api"
|
|
"github.com/tetratelabs/wazero/experimental"
|
|
"github.com/tetratelabs/wazero/internal/bitpack"
|
|
"github.com/tetratelabs/wazero/internal/platform"
|
|
"github.com/tetratelabs/wazero/internal/testing/require"
|
|
"github.com/tetratelabs/wazero/internal/wasm"
|
|
)
|
|
|
|
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
|
|
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
|
|
|
|
// requireSupportedOSArch is duplicated also in the platform package to ensure no cyclic dependency.
|
|
func requireSupportedOSArch(t *testing.T) {
|
|
if !platform.CompilerSupported() {
|
|
t.Skip()
|
|
}
|
|
}
|
|
|
|
type fakeFinalizer map[*compiledModule]func(module *compiledModule)
|
|
|
|
func (f fakeFinalizer) setFinalizer(obj interface{}, finalizer interface{}) {
|
|
cf := obj.(*compiledModule)
|
|
if _, ok := f[cf]; ok { // easier than adding a field for testing.T
|
|
panic(fmt.Sprintf("BUG: %v already had its finalizer set", cf))
|
|
}
|
|
f[cf] = finalizer.(func(*compiledModule))
|
|
}
|
|
|
|
func TestCompiler_CompileModule(t *testing.T) {
|
|
t.Run("ok", func(t *testing.T) {
|
|
e := NewEngine(testCtx, api.CoreFeaturesV1, nil).(*engine)
|
|
ff := fakeFinalizer{}
|
|
e.setFinalizer = ff.setFinalizer
|
|
|
|
okModule := &wasm.Module{
|
|
TypeSection: []wasm.FunctionType{{}},
|
|
FunctionSection: []wasm.Index{0, 0, 0, 0},
|
|
CodeSection: []wasm.Code{
|
|
{Body: []byte{wasm.OpcodeEnd}},
|
|
{Body: []byte{wasm.OpcodeEnd}},
|
|
{Body: []byte{wasm.OpcodeEnd}},
|
|
{Body: []byte{wasm.OpcodeEnd}},
|
|
},
|
|
ID: wasm.ModuleID{},
|
|
}
|
|
|
|
err := e.CompileModule(testCtx, okModule, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
// Compiling same module shouldn't be compiled again, but instead should be cached.
|
|
err = e.CompileModule(testCtx, okModule, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
compiled, ok := e.codes[okModule.ID]
|
|
require.True(t, ok)
|
|
require.Equal(t, len(okModule.FunctionSection), len(compiled.functions))
|
|
|
|
// Pretend the finalizer executed, by invoking them one-by-one.
|
|
for k, v := range ff {
|
|
v(k)
|
|
}
|
|
})
|
|
|
|
t.Run("fail", func(t *testing.T) {
|
|
errModule := &wasm.Module{
|
|
TypeSection: []wasm.FunctionType{{}},
|
|
FunctionSection: []wasm.Index{0, 0, 0},
|
|
CodeSection: []wasm.Code{
|
|
{Body: []byte{wasm.OpcodeEnd}},
|
|
{Body: []byte{wasm.OpcodeEnd}},
|
|
{Body: []byte{wasm.OpcodeCall}}, // Call instruction without immediate for call target index is invalid and should fail to compile.
|
|
},
|
|
ID: wasm.ModuleID{},
|
|
}
|
|
|
|
e := NewEngine(testCtx, api.CoreFeaturesV1, nil).(*engine)
|
|
err := e.CompileModule(testCtx, errModule, nil, false)
|
|
require.EqualError(t, err, "failed to lower func[2]: handling instruction: apply stack failed for call: reading immediates: EOF")
|
|
|
|
// On the compilation failure, the compiled functions must not be cached.
|
|
_, ok := e.codes[errModule.ID]
|
|
require.False(t, ok)
|
|
})
|
|
}
|
|
|
|
func TestCompiler_Releasecode_Panic(t *testing.T) {
|
|
captured := require.CapturePanic(func() {
|
|
releaseCompiledModule(&compiledModule{
|
|
compiledCode: &compiledCode{
|
|
executable: makeCodeSegment(1, 2),
|
|
},
|
|
})
|
|
})
|
|
require.Contains(t, captured.Error(), "compiler: failed to munmap code segment")
|
|
}
|
|
|
|
// Ensures that value stack and call-frame stack are allocated on heap which
|
|
// allows us to safely access to their data region from native code.
|
|
// See comments on initialStackSize and initialCallFrameStackSize.
|
|
func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
|
|
enabledFeatures := api.CoreFeaturesV1
|
|
e := newEngine(enabledFeatures, nil)
|
|
s := wasm.NewStore(enabledFeatures, e)
|
|
|
|
const hostModuleName = "env"
|
|
const hostFnName = "grow_and_shrink_goroutine_stack"
|
|
hostFn := func() {
|
|
// This function aggressively grow the goroutine stack by recursively
|
|
// calling the function many times.
|
|
callNum := 1000
|
|
var growGoroutineStack func()
|
|
growGoroutineStack = func() {
|
|
if callNum != 0 {
|
|
callNum--
|
|
growGoroutineStack()
|
|
}
|
|
}
|
|
growGoroutineStack()
|
|
|
|
// Trigger relocation of goroutine stack because at this point we have the majority of
|
|
// goroutine stack unused after recursive call.
|
|
runtime.GC()
|
|
}
|
|
hm, err := wasm.NewHostModule(
|
|
hostModuleName,
|
|
[]string{hostFnName},
|
|
map[string]*wasm.HostFunc{hostFnName: {ExportName: hostFnName, Code: wasm.Code{GoFunc: hostFn}}},
|
|
enabledFeatures,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
err = s.Engine.CompileModule(testCtx, hm, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
typeIDs, err := s.GetFunctionTypeIDs(hm.TypeSection)
|
|
require.NoError(t, err)
|
|
|
|
_, err = s.Instantiate(testCtx, hm, hostModuleName, nil, typeIDs)
|
|
require.NoError(t, err)
|
|
|
|
const stackCorruption = "value_stack_corruption"
|
|
const callStackCorruption = "call_stack_corruption"
|
|
const expectedReturnValue = 0x1
|
|
m := &wasm.Module{
|
|
ImportFunctionCount: 1,
|
|
TypeSection: []wasm.FunctionType{
|
|
{Params: []wasm.ValueType{}, Results: []wasm.ValueType{wasm.ValueTypeI32}, ResultNumInUint64: 1},
|
|
{Params: []wasm.ValueType{}, Results: []wasm.ValueType{}},
|
|
},
|
|
FunctionSection: []wasm.Index{
|
|
wasm.Index(0),
|
|
wasm.Index(0),
|
|
wasm.Index(0),
|
|
},
|
|
CodeSection: []wasm.Code{
|
|
{
|
|
// value_stack_corruption
|
|
Body: []byte{
|
|
wasm.OpcodeCall, 0, // Call host function to shrink Goroutine stack
|
|
// We expect this value is returned, but if the stack is allocated on
|
|
// goroutine stack, we write this expected value into the old-location of
|
|
// stack.
|
|
wasm.OpcodeI32Const, expectedReturnValue,
|
|
wasm.OpcodeEnd,
|
|
},
|
|
},
|
|
{
|
|
// call_stack_corruption
|
|
Body: []byte{
|
|
wasm.OpcodeCall, 3, // Call the wasm function below.
|
|
// At this point, call stack's memory looks like [call_stack_corruption, index3]
|
|
// With this function call it should end up [call_stack_corruption, host func]
|
|
// but if the call-frame stack is allocated on goroutine stack, we exit the native code
|
|
// with [call_stack_corruption, index3] (old call frame stack) with HostCall status code,
|
|
// and end up trying to call index3 as a host function which results in nil pointer exception.
|
|
wasm.OpcodeCall, 0,
|
|
wasm.OpcodeI32Const, expectedReturnValue,
|
|
wasm.OpcodeEnd,
|
|
},
|
|
},
|
|
{Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}},
|
|
},
|
|
ImportSection: []wasm.Import{{Module: hostModuleName, Name: hostFnName, DescFunc: 1}},
|
|
ImportPerModule: map[string][]*wasm.Import{
|
|
hostModuleName: {{Module: hostModuleName, Name: hostFnName, DescFunc: 1}},
|
|
},
|
|
ExportSection: []wasm.Export{
|
|
{Type: wasm.ExternTypeFunc, Index: 1, Name: stackCorruption},
|
|
{Type: wasm.ExternTypeFunc, Index: 2, Name: callStackCorruption},
|
|
},
|
|
Exports: map[string]*wasm.Export{
|
|
stackCorruption: {Type: wasm.ExternTypeFunc, Index: 1, Name: stackCorruption},
|
|
callStackCorruption: {Type: wasm.ExternTypeFunc, Index: 2, Name: callStackCorruption},
|
|
},
|
|
ID: wasm.ModuleID{1},
|
|
}
|
|
|
|
err = s.Engine.CompileModule(testCtx, m, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
typeIDs, err = s.GetFunctionTypeIDs(m.TypeSection)
|
|
require.NoError(t, err)
|
|
|
|
mi, err := s.Instantiate(testCtx, m, t.Name(), nil, typeIDs)
|
|
require.NoError(t, err)
|
|
|
|
for _, fnName := range []string{stackCorruption, callStackCorruption} {
|
|
fnName := fnName
|
|
t.Run(fnName, func(t *testing.T) {
|
|
ret, err := mi.ExportedFunction(fnName).Call(testCtx)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, uint32(expectedReturnValue), uint32(ret[0]))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCallEngine_builtinFunctionTableGrow(t *testing.T) {
|
|
ce := &callEngine{
|
|
stack: []uint64{
|
|
0xff, // pseudo-ref
|
|
1, // num
|
|
// Table Index = 0 (lower 32-bits), but the higher bits (32-63) are all sets,
|
|
// which happens if the previous value on that stack location was 64-bit wide.
|
|
0xffffffff << 32,
|
|
},
|
|
stackContext: stackContext{stackPointer: 3},
|
|
}
|
|
|
|
table := &wasm.TableInstance{References: []wasm.Reference{}, Min: 10}
|
|
ce.builtinFunctionTableGrow([]*wasm.TableInstance{table})
|
|
|
|
require.Equal(t, 1, len(table.References))
|
|
require.Equal(t, uintptr(0xff), table.References[0])
|
|
}
|
|
|
|
func ptrAsUint64(f *function) uint64 {
|
|
return uint64(uintptr(unsafe.Pointer(f)))
|
|
}
|
|
|
|
func TestCallEngine_deferredOnCall(t *testing.T) {
|
|
s := &wasm.Module{
|
|
FunctionSection: []wasm.Index{0, 1, 2},
|
|
CodeSection: []wasm.Code{{}, {}, {}},
|
|
TypeSection: []wasm.FunctionType{{}, {}, {}},
|
|
}
|
|
f1 := &function{
|
|
funcType: &wasm.FunctionType{ParamNumInUint64: 2},
|
|
parent: &compiledFunction{parent: &compiledCode{source: s}, index: 0},
|
|
}
|
|
f2 := &function{
|
|
funcType: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3},
|
|
parent: &compiledFunction{parent: &compiledCode{source: s}, index: 1},
|
|
}
|
|
f3 := &function{
|
|
funcType: &wasm.FunctionType{ResultNumInUint64: 1},
|
|
parent: &compiledFunction{parent: &compiledCode{source: s}, index: 2},
|
|
}
|
|
|
|
ce := &callEngine{
|
|
stack: []uint64{
|
|
0xff, 0xff, // dummy argument for f1
|
|
0, 0, 0, 0,
|
|
0xcc, 0xcc, // local variable for f1.
|
|
// <----- stack base point of f2 (top) == index 8.
|
|
0xaa, 0xaa, 0xdeadbeaf, // dummy argument for f2 (0xaa, 0xaa) and the reserved slot for result 0xdeadbeaf)
|
|
0, 0, ptrAsUint64(f1), 0, // callFrame
|
|
0xcc, 0xcc, 0xcc, // local variable for f2.
|
|
// <----- stack base point of f3 (top) == index 18
|
|
0xdeadbeaf, // the reserved slot for result 0xdeadbeaf) from f3.
|
|
0, 8 << 3, ptrAsUint64(f2), 0, // callFrame
|
|
},
|
|
stackContext: stackContext{
|
|
stackBasePointerInBytes: 18 << 3, // currently executed function (f3)'s base pointer.
|
|
stackPointer: 0xff, // dummy supposed to be reset to zero.
|
|
},
|
|
moduleContext: moduleContext{
|
|
fn: f3, // currently executed function (f3)!
|
|
moduleInstance: nil,
|
|
},
|
|
}
|
|
|
|
beforeRecoverStack := ce.stack
|
|
|
|
err := ce.deferredOnCall(context.Background(), &wasm.ModuleInstance{}, errors.New("some error"))
|
|
require.EqualError(t, err, `some error (recovered by wazero)
|
|
wasm stack trace:
|
|
.$2()
|
|
.$1()
|
|
.$0()`)
|
|
|
|
// After recover, the state of callEngine must be reset except that the underlying slices must be intact
|
|
// for the subsequent calls to avoid additional allocations on each call.
|
|
require.Equal(t, uint64(0), ce.stackBasePointerInBytes)
|
|
require.Equal(t, uint64(0), ce.stackPointer)
|
|
require.Equal(t, nil, ce.moduleInstance)
|
|
require.Equal(t, beforeRecoverStack, ce.stack)
|
|
|
|
// Keep f1, f2, and f3 alive until we reach here, as we access these functions from the uint64 raw pointers in the stack.
|
|
// In practice, they are guaranteed to be alive as they are held by moduleContext.
|
|
runtime.KeepAlive(f1)
|
|
runtime.KeepAlive(f2)
|
|
runtime.KeepAlive(f3)
|
|
}
|
|
|
|
func TestCallEngine_initializeStack(t *testing.T) {
|
|
const i32 = wasm.ValueTypeI32
|
|
const stackSize = 10
|
|
const initialVal = ^uint64(0)
|
|
tests := []struct {
|
|
name string
|
|
funcType *wasm.FunctionType
|
|
args []uint64
|
|
expStackPointer uint64
|
|
expStack [stackSize]uint64
|
|
}{
|
|
{
|
|
name: "no param/result",
|
|
funcType: &wasm.FunctionType{},
|
|
expStackPointer: callFrameDataSizeInUint64,
|
|
expStack: [stackSize]uint64{
|
|
0, 0, 0, // zeroed call frame
|
|
initialVal, initialVal, initialVal, initialVal, initialVal, initialVal, initialVal,
|
|
},
|
|
},
|
|
{
|
|
name: "no result",
|
|
funcType: &wasm.FunctionType{
|
|
Params: []wasm.ValueType{i32, i32},
|
|
ParamNumInUint64: 2,
|
|
},
|
|
args: []uint64{0xdeadbeaf, 0xdeadbeaf},
|
|
expStackPointer: callFrameDataSizeInUint64 + 2,
|
|
expStack: [stackSize]uint64{
|
|
0xdeadbeaf, 0xdeadbeaf, // arguments
|
|
0, 0, 0, // zeroed call frame
|
|
initialVal, initialVal, initialVal, initialVal, initialVal,
|
|
},
|
|
},
|
|
{
|
|
name: "no param",
|
|
funcType: &wasm.FunctionType{
|
|
Results: []wasm.ValueType{i32, i32, i32},
|
|
ResultNumInUint64: 3,
|
|
},
|
|
expStackPointer: callFrameDataSizeInUint64 + 3,
|
|
expStack: [stackSize]uint64{
|
|
initialVal, initialVal, initialVal, // reserved slots for results
|
|
0, 0, 0, // zeroed call frame
|
|
initialVal, initialVal, initialVal, initialVal,
|
|
},
|
|
},
|
|
{
|
|
name: "params > results",
|
|
funcType: &wasm.FunctionType{
|
|
Params: []wasm.ValueType{i32, i32, i32, i32, i32},
|
|
ParamNumInUint64: 5,
|
|
Results: []wasm.ValueType{i32, i32, i32},
|
|
ResultNumInUint64: 3,
|
|
},
|
|
args: []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf},
|
|
expStackPointer: callFrameDataSizeInUint64 + 5,
|
|
expStack: [stackSize]uint64{
|
|
0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf,
|
|
0, 0, 0, // zeroed call frame
|
|
initialVal, initialVal,
|
|
},
|
|
},
|
|
{
|
|
name: "params == results",
|
|
funcType: &wasm.FunctionType{
|
|
Params: []wasm.ValueType{i32, i32, i32, i32, i32},
|
|
ParamNumInUint64: 5,
|
|
Results: []wasm.ValueType{i32, i32, i32, i32, i32},
|
|
ResultNumInUint64: 5,
|
|
},
|
|
args: []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf},
|
|
expStackPointer: callFrameDataSizeInUint64 + 5,
|
|
expStack: [stackSize]uint64{
|
|
0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf,
|
|
0, 0, 0, // zeroed call frame
|
|
initialVal, initialVal,
|
|
},
|
|
},
|
|
{
|
|
name: "params < results",
|
|
funcType: &wasm.FunctionType{
|
|
Params: []wasm.ValueType{i32, i32, i32},
|
|
ParamNumInUint64: 3,
|
|
Results: []wasm.ValueType{i32, i32, i32, i32, i32},
|
|
ResultNumInUint64: 5,
|
|
},
|
|
args: []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf},
|
|
expStackPointer: callFrameDataSizeInUint64 + 5,
|
|
expStack: [stackSize]uint64{
|
|
0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf,
|
|
initialVal, initialVal, // reserved for results
|
|
0, 0, 0, // zeroed call frame
|
|
initialVal, initialVal,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
initialStack := make([]uint64, stackSize)
|
|
for i := range initialStack {
|
|
initialStack[i] = initialVal
|
|
}
|
|
ce := &callEngine{stack: initialStack}
|
|
ce.initializeStack(tc.funcType, tc.args)
|
|
require.Equal(t, tc.expStackPointer, ce.stackPointer)
|
|
require.Equal(t, tc.expStack[:], ce.stack)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_callFrameOffset(t *testing.T) {
|
|
require.Equal(t, 1, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 0, ResultNumInUint64: 1}))
|
|
require.Equal(t, 10, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 5, ResultNumInUint64: 10}))
|
|
require.Equal(t, 100, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 50, ResultNumInUint64: 100}))
|
|
require.Equal(t, 1, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 1, ResultNumInUint64: 0}))
|
|
require.Equal(t, 10, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 10, ResultNumInUint64: 5}))
|
|
require.Equal(t, 100, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 100, ResultNumInUint64: 50}))
|
|
}
|
|
|
|
type stackEntry struct {
|
|
def api.FunctionDefinition
|
|
}
|
|
|
|
func assertStackIterator(t *testing.T, it experimental.StackIterator, expected []stackEntry) {
|
|
var actual []stackEntry
|
|
for it.Next() {
|
|
actual = append(actual, stackEntry{def: it.Function().Definition()})
|
|
}
|
|
require.Equal(t, expected, actual)
|
|
}
|
|
|
|
func TestCallEngine_builtinFunctionFunctionListenerBefore(t *testing.T) {
|
|
currentContext := context.Background()
|
|
|
|
f := &function{
|
|
funcType: &wasm.FunctionType{ParamNumInUint64: 3},
|
|
parent: &compiledFunction{
|
|
listener: mockListener{
|
|
before: func(ctx context.Context, _ api.Module, def api.FunctionDefinition, params []uint64, stackIterator experimental.StackIterator) {
|
|
require.Equal(t, currentContext, ctx)
|
|
require.Equal(t, []uint64{2, 3, 4}, params)
|
|
assertStackIterator(t, stackIterator, []stackEntry{{def: def}})
|
|
},
|
|
},
|
|
index: 0,
|
|
parent: &compiledCode{source: &wasm.Module{
|
|
FunctionSection: []wasm.Index{0},
|
|
CodeSection: []wasm.Code{{}},
|
|
TypeSection: []wasm.FunctionType{{}},
|
|
}},
|
|
},
|
|
}
|
|
ce := &callEngine{
|
|
stack: []uint64{0, 1, 2, 3, 4, 0, 0, 0},
|
|
stackContext: stackContext{stackBasePointerInBytes: 16},
|
|
}
|
|
ce.builtinFunctionFunctionListenerBefore(currentContext, &wasm.ModuleInstance{}, f)
|
|
}
|
|
|
|
func TestCallEngine_builtinFunctionFunctionListenerAfter(t *testing.T) {
|
|
currentContext := context.Background()
|
|
f := &function{
|
|
funcType: &wasm.FunctionType{ResultNumInUint64: 1},
|
|
parent: &compiledFunction{
|
|
listener: mockListener{
|
|
after: func(ctx context.Context, mod api.Module, def api.FunctionDefinition, results []uint64) {
|
|
require.Equal(t, currentContext, ctx)
|
|
require.Equal(t, []uint64{5}, results)
|
|
},
|
|
},
|
|
index: 0,
|
|
parent: &compiledCode{source: &wasm.Module{
|
|
FunctionSection: []wasm.Index{0},
|
|
CodeSection: []wasm.Code{{}},
|
|
TypeSection: []wasm.FunctionType{{}},
|
|
}},
|
|
},
|
|
}
|
|
|
|
ce := &callEngine{
|
|
stack: []uint64{0, 1, 2, 3, 4, 5},
|
|
stackContext: stackContext{stackBasePointerInBytes: 40},
|
|
}
|
|
ce.builtinFunctionFunctionListenerAfter(currentContext, &wasm.ModuleInstance{}, f)
|
|
}
|
|
|
|
type mockListener struct {
|
|
before func(context.Context, api.Module, api.FunctionDefinition, []uint64, experimental.StackIterator)
|
|
after func(context.Context, api.Module, api.FunctionDefinition, []uint64)
|
|
abort func(context.Context, api.Module, api.FunctionDefinition, error)
|
|
}
|
|
|
|
func (m mockListener) Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, params []uint64, stackIterator experimental.StackIterator) {
|
|
if m.before != nil {
|
|
m.before(ctx, mod, def, params, stackIterator)
|
|
}
|
|
}
|
|
|
|
func (m mockListener) After(ctx context.Context, mod api.Module, def api.FunctionDefinition, results []uint64) {
|
|
if m.after != nil {
|
|
m.after(ctx, mod, def, results)
|
|
}
|
|
}
|
|
|
|
func (m mockListener) Abort(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error) {
|
|
if m.abort != nil {
|
|
m.abort(ctx, mod, def, err)
|
|
}
|
|
}
|
|
|
|
func TestFunction_getSourceOffsetInWasmBinary(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pc, exp uint64
|
|
codeInitialAddress uintptr
|
|
srcMap sourceOffsetMap
|
|
}{
|
|
{name: "not found", srcMap: sourceOffsetMap{}},
|
|
{
|
|
name: "first IR",
|
|
pc: 4000,
|
|
codeInitialAddress: 3999,
|
|
srcMap: sourceOffsetMap{
|
|
irOperationOffsetsInNativeBinary: bitpack.NewOffsetArray([]uint64{
|
|
0 /*4000-3999=1 exists here*/, 5, 8, 15,
|
|
}),
|
|
irOperationSourceOffsetsInWasmBinary: bitpack.NewOffsetArray([]uint64{
|
|
10, 100, 800, 12344,
|
|
}),
|
|
},
|
|
exp: 10,
|
|
},
|
|
{
|
|
name: "middle",
|
|
pc: 100,
|
|
codeInitialAddress: 90,
|
|
srcMap: sourceOffsetMap{
|
|
irOperationOffsetsInNativeBinary: bitpack.NewOffsetArray([]uint64{
|
|
0, 5, 8 /*100-90=10 exists here*/, 15,
|
|
}),
|
|
irOperationSourceOffsetsInWasmBinary: bitpack.NewOffsetArray([]uint64{
|
|
10, 100, 800, 12344,
|
|
}),
|
|
},
|
|
exp: 800,
|
|
},
|
|
{
|
|
name: "last",
|
|
pc: 9999,
|
|
codeInitialAddress: 8999,
|
|
srcMap: sourceOffsetMap{
|
|
irOperationOffsetsInNativeBinary: bitpack.NewOffsetArray([]uint64{
|
|
0, 5, 8, 15, /*9999-8999=1000 exists here*/
|
|
}),
|
|
irOperationSourceOffsetsInWasmBinary: bitpack.NewOffsetArray([]uint64{
|
|
10, 100, 800, 12344,
|
|
}),
|
|
},
|
|
exp: 12344,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
f := function{
|
|
parent: &compiledFunction{sourceOffsetMap: tc.srcMap},
|
|
codeInitialAddress: tc.codeInitialAddress,
|
|
}
|
|
|
|
actual := f.getSourceOffsetInWasmBinary(tc.pc)
|
|
require.Equal(t, tc.exp, actual)
|
|
})
|
|
}
|
|
}
|