github.com/tetratelabs/wazero@v1.2.1/internal/engine/compiler/engine_test.go (about)

     1  package compiler
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"runtime"
     9  	"testing"
    10  	"unsafe"
    11  
    12  	"github.com/tetratelabs/wazero/api"
    13  	"github.com/tetratelabs/wazero/experimental"
    14  	"github.com/tetratelabs/wazero/experimental/logging"
    15  	"github.com/tetratelabs/wazero/internal/bitpack"
    16  	"github.com/tetratelabs/wazero/internal/platform"
    17  	"github.com/tetratelabs/wazero/internal/testing/enginetest"
    18  	"github.com/tetratelabs/wazero/internal/testing/require"
    19  	"github.com/tetratelabs/wazero/internal/wasm"
    20  )
    21  
    22  // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
    23  var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
    24  
    25  var (
    26  	// et is used for tests defined in the enginetest package.
    27  	et              = &engineTester{}
    28  	functionLog     bytes.Buffer
    29  	listenerFactory = logging.NewLoggingListenerFactory(&functionLog)
    30  )
    31  
    32  // engineTester implements enginetest.EngineTester.
    33  type engineTester struct{}
    34  
    35  // ListenerFactory implements the same method as documented on enginetest.EngineTester.
    36  func (e engineTester) ListenerFactory() experimental.FunctionListenerFactory {
    37  	return listenerFactory
    38  }
    39  
    40  // NewEngine implements the same method as documented on enginetest.EngineTester.
    41  func (e engineTester) NewEngine(enabledFeatures api.CoreFeatures) wasm.Engine {
    42  	return newEngine(enabledFeatures, nil)
    43  }
    44  
    45  func TestCompiler_Engine_NewModuleEngine(t *testing.T) {
    46  	defer functionLog.Reset()
    47  	requireSupportedOSArch(t)
    48  	enginetest.RunTestEngineNewModuleEngine(t, et)
    49  }
    50  
    51  func TestCompiler_MemoryGrowInRecursiveCall(t *testing.T) {
    52  	defer functionLog.Reset()
    53  	enginetest.RunTestEngineMemoryGrowInRecursiveCall(t, et)
    54  }
    55  
    56  func TestCompiler_ModuleEngine_LookupFunction(t *testing.T) {
    57  	defer functionLog.Reset()
    58  	enginetest.RunTestModuleEngineLookupFunction(t, et)
    59  }
    60  
    61  func TestCompiler_ModuleEngine_Call(t *testing.T) {
    62  	defer functionLog.Reset()
    63  	requireSupportedOSArch(t)
    64  	enginetest.RunTestModuleEngineCall(t, et)
    65  	require.Equal(t, `
    66  --> .$0(1,2)
    67  <-- (1,2)
    68  `, "\n"+functionLog.String())
    69  }
    70  
    71  func TestCompiler_ModuleEngine_CallWithStack(t *testing.T) {
    72  	defer functionLog.Reset()
    73  	requireSupportedOSArch(t)
    74  	enginetest.RunTestModuleEngineCallWithStack(t, et)
    75  	require.Equal(t, `
    76  --> .$0(1,2)
    77  <-- (1,2)
    78  `, "\n"+functionLog.String())
    79  }
    80  
    81  func TestCompiler_ModuleEngine_Call_HostFn(t *testing.T) {
    82  	defer functionLog.Reset()
    83  	requireSupportedOSArch(t)
    84  	enginetest.RunTestModuleEngineCallHostFn(t, et)
    85  }
    86  
    87  func TestCompiler_ModuleEngine_Call_Errors(t *testing.T) {
    88  	defer functionLog.Reset()
    89  	requireSupportedOSArch(t)
    90  	enginetest.RunTestModuleEngine_Call_Errors(t, et)
    91  
    92  	// TODO: Currently, the listener doesn't get notified on errors as they are
    93  	// implemented with panic. This means the end hooks aren't make resulting
    94  	// in dangling logs like this:
    95  	//	==> host.host_div_by(-1)
    96  	// instead of seeing a return like
    97  	//	<== DivByZero
    98  	require.Equal(t, `
    99  --> imported.div_by.wasm(1)
   100  <-- 1
   101  --> imported.div_by.wasm(1)
   102  <-- 1
   103  --> imported.div_by.wasm(0)
   104  --> imported.div_by.wasm(1)
   105  <-- 1
   106  --> imported.call->div_by.go(-1)
   107  	==> host.div_by.go(-1)
   108  --> imported.call->div_by.go(1)
   109  	==> host.div_by.go(1)
   110  	<== 1
   111  <-- 1
   112  --> importing.call_import->call->div_by.go(0)
   113  	--> imported.call->div_by.go(0)
   114  		==> host.div_by.go(0)
   115  --> importing.call_import->call->div_by.go(1)
   116  	--> imported.call->div_by.go(1)
   117  		==> host.div_by.go(1)
   118  		<== 1
   119  	<-- 1
   120  <-- 1
   121  --> importing.call_import->call->div_by.go(-1)
   122  	--> imported.call->div_by.go(-1)
   123  		==> host.div_by.go(-1)
   124  --> importing.call_import->call->div_by.go(1)
   125  	--> imported.call->div_by.go(1)
   126  		==> host.div_by.go(1)
   127  		<== 1
   128  	<-- 1
   129  <-- 1
   130  --> importing.call_import->call->div_by.go(0)
   131  	--> imported.call->div_by.go(0)
   132  		==> host.div_by.go(0)
   133  --> importing.call_import->call->div_by.go(1)
   134  	--> imported.call->div_by.go(1)
   135  		==> host.div_by.go(1)
   136  		<== 1
   137  	<-- 1
   138  <-- 1
   139  `, "\n"+functionLog.String())
   140  }
   141  
   142  func TestCompiler_ModuleEngine_Memory(t *testing.T) {
   143  	defer functionLog.Reset()
   144  	requireSupportedOSArch(t)
   145  	enginetest.RunTestModuleEngineMemory(t, et)
   146  }
   147  
   148  func TestCompiler_BeforeListenerStackIterator(t *testing.T) {
   149  	enginetest.RunTestModuleEngineBeforeListenerStackIterator(t, et)
   150  }
   151  
   152  func TestCompiler_BeforeListenerStackIteratorSourceOffset(t *testing.T) {
   153  	enginetest.RunTestModuleEngineStackIteratorOffset(t, et)
   154  }
   155  
   156  func TestCompiler_BeforeListenerGlobals(t *testing.T) {
   157  	enginetest.RunTestModuleEngineBeforeListenerGlobals(t, et)
   158  }
   159  
   160  // requireSupportedOSArch is duplicated also in the platform package to ensure no cyclic dependency.
   161  func requireSupportedOSArch(t *testing.T) {
   162  	if !platform.CompilerSupported() {
   163  		t.Skip()
   164  	}
   165  }
   166  
   167  type fakeFinalizer map[*compiledModule]func(module *compiledModule)
   168  
   169  func (f fakeFinalizer) setFinalizer(obj interface{}, finalizer interface{}) {
   170  	cf := obj.(*compiledModule)
   171  	if _, ok := f[cf]; ok { // easier than adding a field for testing.T
   172  		panic(fmt.Sprintf("BUG: %v already had its finalizer set", cf))
   173  	}
   174  	f[cf] = finalizer.(func(*compiledModule))
   175  }
   176  
   177  func TestCompiler_CompileModule(t *testing.T) {
   178  	t.Run("ok", func(t *testing.T) {
   179  		e := et.NewEngine(api.CoreFeaturesV1).(*engine)
   180  		ff := fakeFinalizer{}
   181  		e.setFinalizer = ff.setFinalizer
   182  
   183  		okModule := &wasm.Module{
   184  			TypeSection:     []wasm.FunctionType{{}},
   185  			FunctionSection: []wasm.Index{0, 0, 0, 0},
   186  			CodeSection: []wasm.Code{
   187  				{Body: []byte{wasm.OpcodeEnd}},
   188  				{Body: []byte{wasm.OpcodeEnd}},
   189  				{Body: []byte{wasm.OpcodeEnd}},
   190  				{Body: []byte{wasm.OpcodeEnd}},
   191  			},
   192  			ID: wasm.ModuleID{},
   193  		}
   194  
   195  		err := e.CompileModule(testCtx, okModule, nil, false)
   196  		require.NoError(t, err)
   197  
   198  		// Compiling same module shouldn't be compiled again, but instead should be cached.
   199  		err = e.CompileModule(testCtx, okModule, nil, false)
   200  		require.NoError(t, err)
   201  
   202  		compiled, ok := e.codes[okModule.ID]
   203  		require.True(t, ok)
   204  		require.Equal(t, len(okModule.FunctionSection), len(compiled.functions))
   205  
   206  		// Pretend the finalizer executed, by invoking them one-by-one.
   207  		for k, v := range ff {
   208  			v(k)
   209  		}
   210  	})
   211  
   212  	t.Run("fail", func(t *testing.T) {
   213  		errModule := &wasm.Module{
   214  			TypeSection:     []wasm.FunctionType{{}},
   215  			FunctionSection: []wasm.Index{0, 0, 0},
   216  			CodeSection: []wasm.Code{
   217  				{Body: []byte{wasm.OpcodeEnd}},
   218  				{Body: []byte{wasm.OpcodeEnd}},
   219  				{Body: []byte{wasm.OpcodeCall}}, // Call instruction without immediate for call target index is invalid and should fail to compile.
   220  			},
   221  			ID: wasm.ModuleID{},
   222  		}
   223  
   224  		e := et.NewEngine(api.CoreFeaturesV1).(*engine)
   225  		err := e.CompileModule(testCtx, errModule, nil, false)
   226  		require.EqualError(t, err, "failed to lower func[2]: handling instruction: apply stack failed for call: reading immediates: EOF")
   227  
   228  		// On the compilation failure, the compiled functions must not be cached.
   229  		_, ok := e.codes[errModule.ID]
   230  		require.False(t, ok)
   231  	})
   232  }
   233  
   234  func TestCompiler_Releasecode_Panic(t *testing.T) {
   235  	captured := require.CapturePanic(func() {
   236  		releaseCompiledModule(&compiledModule{
   237  			executable: makeCodeSegment(1, 2),
   238  		})
   239  	})
   240  	require.Contains(t, captured.Error(), "compiler: failed to munmap code segment")
   241  }
   242  
   243  // Ensures that value stack and call-frame stack are allocated on heap which
   244  // allows us to safely access to their data region from native code.
   245  // See comments on initialStackSize and initialCallFrameStackSize.
   246  func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
   247  	enabledFeatures := api.CoreFeaturesV1
   248  	e := newEngine(enabledFeatures, nil)
   249  	s := wasm.NewStore(enabledFeatures, e)
   250  
   251  	const hostModuleName = "env"
   252  	const hostFnName = "grow_and_shrink_goroutine_stack"
   253  	hostFn := func() {
   254  		// This function aggressively grow the goroutine stack by recursively
   255  		// calling the function many times.
   256  		callNum := 1000
   257  		var growGoroutineStack func()
   258  		growGoroutineStack = func() {
   259  			if callNum != 0 {
   260  				callNum--
   261  				growGoroutineStack()
   262  			}
   263  		}
   264  		growGoroutineStack()
   265  
   266  		// Trigger relocation of goroutine stack because at this point we have the majority of
   267  		// goroutine stack unused after recursive call.
   268  		runtime.GC()
   269  	}
   270  	hm, err := wasm.NewHostModule(
   271  		hostModuleName,
   272  		[]string{hostFnName},
   273  		map[string]*wasm.HostFunc{hostFnName: {ExportName: hostFnName, Code: wasm.Code{GoFunc: hostFn}}},
   274  		enabledFeatures,
   275  	)
   276  	require.NoError(t, err)
   277  
   278  	err = s.Engine.CompileModule(testCtx, hm, nil, false)
   279  	require.NoError(t, err)
   280  
   281  	typeIDs, err := s.GetFunctionTypeIDs(hm.TypeSection)
   282  	require.NoError(t, err)
   283  
   284  	_, err = s.Instantiate(testCtx, hm, hostModuleName, nil, typeIDs)
   285  	require.NoError(t, err)
   286  
   287  	const stackCorruption = "value_stack_corruption"
   288  	const callStackCorruption = "call_stack_corruption"
   289  	const expectedReturnValue = 0x1
   290  	m := &wasm.Module{
   291  		ImportFunctionCount: 1,
   292  		TypeSection: []wasm.FunctionType{
   293  			{Params: []wasm.ValueType{}, Results: []wasm.ValueType{wasm.ValueTypeI32}, ResultNumInUint64: 1},
   294  			{Params: []wasm.ValueType{}, Results: []wasm.ValueType{}},
   295  		},
   296  		FunctionSection: []wasm.Index{
   297  			wasm.Index(0),
   298  			wasm.Index(0),
   299  			wasm.Index(0),
   300  		},
   301  		CodeSection: []wasm.Code{
   302  			{
   303  				// value_stack_corruption
   304  				Body: []byte{
   305  					wasm.OpcodeCall, 0, // Call host function to shrink Goroutine stack
   306  					// We expect this value is returned, but if the stack is allocated on
   307  					// goroutine stack, we write this expected value into the old-location of
   308  					// stack.
   309  					wasm.OpcodeI32Const, expectedReturnValue,
   310  					wasm.OpcodeEnd,
   311  				},
   312  			},
   313  			{
   314  				// call_stack_corruption
   315  				Body: []byte{
   316  					wasm.OpcodeCall, 3, // Call the wasm function below.
   317  					// At this point, call stack's memory looks like [call_stack_corruption, index3]
   318  					// With this function call it should end up [call_stack_corruption, host func]
   319  					// but if the call-frame stack is allocated on goroutine stack, we exit the native code
   320  					// with  [call_stack_corruption, index3] (old call frame stack) with HostCall status code,
   321  					// and end up trying to call index3 as a host function which results in nil pointer exception.
   322  					wasm.OpcodeCall, 0,
   323  					wasm.OpcodeI32Const, expectedReturnValue,
   324  					wasm.OpcodeEnd,
   325  				},
   326  			},
   327  			{Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}},
   328  		},
   329  		ImportSection: []wasm.Import{{Module: hostModuleName, Name: hostFnName, DescFunc: 1}},
   330  		ImportPerModule: map[string][]*wasm.Import{
   331  			hostModuleName: {{Module: hostModuleName, Name: hostFnName, DescFunc: 1}},
   332  		},
   333  		ExportSection: []wasm.Export{
   334  			{Type: wasm.ExternTypeFunc, Index: 1, Name: stackCorruption},
   335  			{Type: wasm.ExternTypeFunc, Index: 2, Name: callStackCorruption},
   336  		},
   337  		Exports: map[string]*wasm.Export{
   338  			stackCorruption:     {Type: wasm.ExternTypeFunc, Index: 1, Name: stackCorruption},
   339  			callStackCorruption: {Type: wasm.ExternTypeFunc, Index: 2, Name: callStackCorruption},
   340  		},
   341  		ID: wasm.ModuleID{1},
   342  	}
   343  
   344  	err = s.Engine.CompileModule(testCtx, m, nil, false)
   345  	require.NoError(t, err)
   346  
   347  	typeIDs, err = s.GetFunctionTypeIDs(m.TypeSection)
   348  	require.NoError(t, err)
   349  
   350  	mi, err := s.Instantiate(testCtx, m, t.Name(), nil, typeIDs)
   351  	require.NoError(t, err)
   352  
   353  	for _, fnName := range []string{stackCorruption, callStackCorruption} {
   354  		fnName := fnName
   355  		t.Run(fnName, func(t *testing.T) {
   356  			ret, err := mi.ExportedFunction(fnName).Call(testCtx)
   357  			require.NoError(t, err)
   358  
   359  			require.Equal(t, uint32(expectedReturnValue), uint32(ret[0]))
   360  		})
   361  	}
   362  }
   363  
   364  func TestCallEngine_builtinFunctionTableGrow(t *testing.T) {
   365  	ce := &callEngine{
   366  		stack: []uint64{
   367  			0xff, // pseudo-ref
   368  			1,    // num
   369  			// Table Index = 0 (lower 32-bits), but the higher bits (32-63) are all sets,
   370  			// which happens if the previous value on that stack location was 64-bit wide.
   371  			0xffffffff << 32,
   372  		},
   373  		stackContext: stackContext{stackPointer: 3},
   374  	}
   375  
   376  	table := &wasm.TableInstance{References: []wasm.Reference{}, Min: 10}
   377  	ce.builtinFunctionTableGrow([]*wasm.TableInstance{table})
   378  
   379  	require.Equal(t, 1, len(table.References))
   380  	require.Equal(t, uintptr(0xff), table.References[0])
   381  }
   382  
   383  func ptrAsUint64(f *function) uint64 {
   384  	return uint64(uintptr(unsafe.Pointer(f)))
   385  }
   386  
   387  func TestCallEngine_deferredOnCall(t *testing.T) {
   388  	s := &wasm.Module{
   389  		FunctionSection: []wasm.Index{0, 1, 2},
   390  		CodeSection:     []wasm.Code{{}, {}, {}},
   391  		TypeSection:     []wasm.FunctionType{{}, {}, {}},
   392  	}
   393  	f1 := &function{
   394  		funcType: &wasm.FunctionType{ParamNumInUint64: 2},
   395  		parent:   &compiledFunction{parent: &compiledModule{source: s}, index: 0},
   396  	}
   397  	f2 := &function{
   398  		funcType: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3},
   399  		parent:   &compiledFunction{parent: &compiledModule{source: s}, index: 1},
   400  	}
   401  	f3 := &function{
   402  		funcType: &wasm.FunctionType{ResultNumInUint64: 1},
   403  		parent:   &compiledFunction{parent: &compiledModule{source: s}, index: 2},
   404  	}
   405  
   406  	ce := &callEngine{
   407  		stack: []uint64{
   408  			0xff, 0xff, // dummy argument for f1
   409  			0, 0, 0, 0,
   410  			0xcc, 0xcc, // local variable for f1.
   411  			// <----- stack base point of f2 (top) == index 8.
   412  			0xaa, 0xaa, 0xdeadbeaf, // dummy argument for f2 (0xaa, 0xaa) and the reserved slot for result 0xdeadbeaf)
   413  			0, 0, ptrAsUint64(f1), 0, // callFrame
   414  			0xcc, 0xcc, 0xcc, // local variable for f2.
   415  			// <----- stack base point of f3 (top) == index 18
   416  			0xdeadbeaf,                    // the reserved slot for result 0xdeadbeaf) from f3.
   417  			0, 8 << 3, ptrAsUint64(f2), 0, // callFrame
   418  		},
   419  		stackContext: stackContext{
   420  			stackBasePointerInBytes: 18 << 3, // currently executed function (f3)'s base pointer.
   421  			stackPointer:            0xff,    // dummy supposed to be reset to zero.
   422  		},
   423  		moduleContext: moduleContext{
   424  			fn:             f3, // currently executed function (f3)!
   425  			moduleInstance: nil,
   426  		},
   427  	}
   428  
   429  	beforeRecoverStack := ce.stack
   430  
   431  	err := ce.deferredOnCall(context.Background(), &wasm.ModuleInstance{}, errors.New("some error"))
   432  	require.EqualError(t, err, `some error (recovered by wazero)
   433  wasm stack trace:
   434  	.$2()
   435  	.$1()
   436  	.$0()`)
   437  
   438  	// After recover, the state of callEngine must be reset except that the underlying slices must be intact
   439  	// for the subsequent calls to avoid additional allocations on each call.
   440  	require.Equal(t, uint64(0), ce.stackBasePointerInBytes)
   441  	require.Equal(t, uint64(0), ce.stackPointer)
   442  	require.Equal(t, nil, ce.moduleInstance)
   443  	require.Equal(t, beforeRecoverStack, ce.stack)
   444  
   445  	// Keep f1, f2, and f3 alive until we reach here, as we access these functions from the uint64 raw pointers in the stack.
   446  	// In practice, they are guaranteed to be alive as they are held by moduleContext.
   447  	runtime.KeepAlive(f1)
   448  	runtime.KeepAlive(f2)
   449  	runtime.KeepAlive(f3)
   450  }
   451  
   452  func TestCallEngine_initializeStack(t *testing.T) {
   453  	const i32 = wasm.ValueTypeI32
   454  	const stackSize = 10
   455  	const initialVal = ^uint64(0)
   456  	tests := []struct {
   457  		name            string
   458  		funcType        *wasm.FunctionType
   459  		args            []uint64
   460  		expStackPointer uint64
   461  		expStack        [stackSize]uint64
   462  	}{
   463  		{
   464  			name:            "no param/result",
   465  			funcType:        &wasm.FunctionType{},
   466  			expStackPointer: callFrameDataSizeInUint64,
   467  			expStack: [stackSize]uint64{
   468  				0, 0, 0, // zeroed call frame
   469  				initialVal, initialVal, initialVal, initialVal, initialVal, initialVal, initialVal,
   470  			},
   471  		},
   472  		{
   473  			name: "no result",
   474  			funcType: &wasm.FunctionType{
   475  				Params:           []wasm.ValueType{i32, i32},
   476  				ParamNumInUint64: 2,
   477  			},
   478  			args:            []uint64{0xdeadbeaf, 0xdeadbeaf},
   479  			expStackPointer: callFrameDataSizeInUint64 + 2,
   480  			expStack: [stackSize]uint64{
   481  				0xdeadbeaf, 0xdeadbeaf, // arguments
   482  				0, 0, 0, // zeroed call frame
   483  				initialVal, initialVal, initialVal, initialVal, initialVal,
   484  			},
   485  		},
   486  		{
   487  			name: "no param",
   488  			funcType: &wasm.FunctionType{
   489  				Results:           []wasm.ValueType{i32, i32, i32},
   490  				ResultNumInUint64: 3,
   491  			},
   492  			expStackPointer: callFrameDataSizeInUint64 + 3,
   493  			expStack: [stackSize]uint64{
   494  				initialVal, initialVal, initialVal, // reserved slots for results
   495  				0, 0, 0, // zeroed call frame
   496  				initialVal, initialVal, initialVal, initialVal,
   497  			},
   498  		},
   499  		{
   500  			name: "params > results",
   501  			funcType: &wasm.FunctionType{
   502  				Params:            []wasm.ValueType{i32, i32, i32, i32, i32},
   503  				ParamNumInUint64:  5,
   504  				Results:           []wasm.ValueType{i32, i32, i32},
   505  				ResultNumInUint64: 3,
   506  			},
   507  			args:            []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf},
   508  			expStackPointer: callFrameDataSizeInUint64 + 5,
   509  			expStack: [stackSize]uint64{
   510  				0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf,
   511  				0, 0, 0, // zeroed call frame
   512  				initialVal, initialVal,
   513  			},
   514  		},
   515  		{
   516  			name: "params == results",
   517  			funcType: &wasm.FunctionType{
   518  				Params:            []wasm.ValueType{i32, i32, i32, i32, i32},
   519  				ParamNumInUint64:  5,
   520  				Results:           []wasm.ValueType{i32, i32, i32, i32, i32},
   521  				ResultNumInUint64: 5,
   522  			},
   523  			args:            []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf},
   524  			expStackPointer: callFrameDataSizeInUint64 + 5,
   525  			expStack: [stackSize]uint64{
   526  				0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf,
   527  				0, 0, 0, // zeroed call frame
   528  				initialVal, initialVal,
   529  			},
   530  		},
   531  		{
   532  			name: "params < results",
   533  			funcType: &wasm.FunctionType{
   534  				Params:            []wasm.ValueType{i32, i32, i32},
   535  				ParamNumInUint64:  3,
   536  				Results:           []wasm.ValueType{i32, i32, i32, i32, i32},
   537  				ResultNumInUint64: 5,
   538  			},
   539  			args:            []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf},
   540  			expStackPointer: callFrameDataSizeInUint64 + 5,
   541  			expStack: [stackSize]uint64{
   542  				0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf,
   543  				initialVal, initialVal, // reserved for results
   544  				0, 0, 0, // zeroed call frame
   545  				initialVal, initialVal,
   546  			},
   547  		},
   548  	}
   549  
   550  	for _, tc := range tests {
   551  		tc := tc
   552  		t.Run(tc.name, func(t *testing.T) {
   553  			initialStack := make([]uint64, stackSize)
   554  			for i := range initialStack {
   555  				initialStack[i] = initialVal
   556  			}
   557  			ce := &callEngine{stack: initialStack}
   558  			ce.initializeStack(tc.funcType, tc.args)
   559  			require.Equal(t, tc.expStackPointer, ce.stackPointer)
   560  			require.Equal(t, tc.expStack[:], ce.stack)
   561  		})
   562  	}
   563  }
   564  
   565  func Test_callFrameOffset(t *testing.T) {
   566  	require.Equal(t, 1, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 0, ResultNumInUint64: 1}))
   567  	require.Equal(t, 10, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 5, ResultNumInUint64: 10}))
   568  	require.Equal(t, 100, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 50, ResultNumInUint64: 100}))
   569  	require.Equal(t, 1, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 1, ResultNumInUint64: 0}))
   570  	require.Equal(t, 10, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 10, ResultNumInUint64: 5}))
   571  	require.Equal(t, 100, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 100, ResultNumInUint64: 50}))
   572  }
   573  
   574  type stackEntry struct {
   575  	def  api.FunctionDefinition
   576  	args []uint64
   577  }
   578  
   579  func assertStackIterator(t *testing.T, it experimental.StackIterator, expected []stackEntry) {
   580  	var actual []stackEntry
   581  	for it.Next() {
   582  		actual = append(actual, stackEntry{def: it.Function().Definition(), args: it.Parameters()})
   583  	}
   584  	require.Equal(t, expected, actual)
   585  }
   586  
   587  func TestCallEngine_builtinFunctionFunctionListenerBefore(t *testing.T) {
   588  	currentContext := context.Background()
   589  
   590  	f := &function{
   591  		funcType: &wasm.FunctionType{ParamNumInUint64: 3},
   592  		parent: &compiledFunction{
   593  			listener: mockListener{
   594  				before: func(ctx context.Context, _ api.Module, def api.FunctionDefinition, params []uint64, stackIterator experimental.StackIterator) {
   595  					require.Equal(t, currentContext, ctx)
   596  					require.Equal(t, []uint64{2, 3, 4}, params)
   597  					assertStackIterator(t, stackIterator, []stackEntry{{def: def, args: []uint64{2, 3, 4}}})
   598  				},
   599  			},
   600  			index: 0,
   601  			parent: &compiledModule{source: &wasm.Module{
   602  				FunctionSection: []wasm.Index{0},
   603  				CodeSection:     []wasm.Code{{}},
   604  				TypeSection:     []wasm.FunctionType{{}},
   605  			}},
   606  		},
   607  	}
   608  	ce := &callEngine{
   609  		stack:        []uint64{0, 1, 2, 3, 4, 0, 0, 0},
   610  		stackContext: stackContext{stackBasePointerInBytes: 16},
   611  	}
   612  	ce.builtinFunctionFunctionListenerBefore(currentContext, &wasm.ModuleInstance{}, f)
   613  }
   614  
   615  func TestCallEngine_builtinFunctionFunctionListenerAfter(t *testing.T) {
   616  	currentContext := context.Background()
   617  	f := &function{
   618  		funcType: &wasm.FunctionType{ResultNumInUint64: 1},
   619  		parent: &compiledFunction{
   620  			listener: mockListener{
   621  				after: func(ctx context.Context, mod api.Module, def api.FunctionDefinition, results []uint64) {
   622  					require.Equal(t, currentContext, ctx)
   623  					require.Equal(t, []uint64{5}, results)
   624  				},
   625  			},
   626  			index: 0,
   627  			parent: &compiledModule{source: &wasm.Module{
   628  				FunctionSection: []wasm.Index{0},
   629  				CodeSection:     []wasm.Code{{}},
   630  				TypeSection:     []wasm.FunctionType{{}},
   631  			}},
   632  		},
   633  	}
   634  
   635  	ce := &callEngine{
   636  		stack:        []uint64{0, 1, 2, 3, 4, 5},
   637  		stackContext: stackContext{stackBasePointerInBytes: 40},
   638  	}
   639  	ce.builtinFunctionFunctionListenerAfter(currentContext, &wasm.ModuleInstance{}, f)
   640  }
   641  
   642  type mockListener struct {
   643  	before func(context.Context, api.Module, api.FunctionDefinition, []uint64, experimental.StackIterator)
   644  	after  func(context.Context, api.Module, api.FunctionDefinition, []uint64)
   645  	abort  func(context.Context, api.Module, api.FunctionDefinition, error)
   646  }
   647  
   648  func (m mockListener) Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, params []uint64, stackIterator experimental.StackIterator) {
   649  	if m.before != nil {
   650  		m.before(ctx, mod, def, params, stackIterator)
   651  	}
   652  }
   653  
   654  func (m mockListener) After(ctx context.Context, mod api.Module, def api.FunctionDefinition, results []uint64) {
   655  	if m.after != nil {
   656  		m.after(ctx, mod, def, results)
   657  	}
   658  }
   659  
   660  func (m mockListener) Abort(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error) {
   661  	if m.abort != nil {
   662  		m.abort(ctx, mod, def, err)
   663  	}
   664  }
   665  
   666  func TestFunction_getSourceOffsetInWasmBinary(t *testing.T) {
   667  	tests := []struct {
   668  		name               string
   669  		pc, exp            uint64
   670  		codeInitialAddress uintptr
   671  		srcMap             sourceOffsetMap
   672  	}{
   673  		{name: "not found", srcMap: sourceOffsetMap{}},
   674  		{
   675  			name:               "first IR",
   676  			pc:                 4000,
   677  			codeInitialAddress: 3999,
   678  			srcMap: sourceOffsetMap{
   679  				irOperationOffsetsInNativeBinary: bitpack.NewOffsetArray([]uint64{
   680  					0 /*4000-3999=1 exists here*/, 5, 8, 15,
   681  				}),
   682  				irOperationSourceOffsetsInWasmBinary: bitpack.NewOffsetArray([]uint64{
   683  					10, 100, 800, 12344,
   684  				}),
   685  			},
   686  			exp: 10,
   687  		},
   688  		{
   689  			name:               "middle",
   690  			pc:                 100,
   691  			codeInitialAddress: 90,
   692  			srcMap: sourceOffsetMap{
   693  				irOperationOffsetsInNativeBinary: bitpack.NewOffsetArray([]uint64{
   694  					0, 5, 8 /*100-90=10 exists here*/, 15,
   695  				}),
   696  				irOperationSourceOffsetsInWasmBinary: bitpack.NewOffsetArray([]uint64{
   697  					10, 100, 800, 12344,
   698  				}),
   699  			},
   700  			exp: 800,
   701  		},
   702  		{
   703  			name:               "last",
   704  			pc:                 9999,
   705  			codeInitialAddress: 8999,
   706  			srcMap: sourceOffsetMap{
   707  				irOperationOffsetsInNativeBinary: bitpack.NewOffsetArray([]uint64{
   708  					0, 5, 8, 15, /*9999-8999=1000 exists here*/
   709  				}),
   710  				irOperationSourceOffsetsInWasmBinary: bitpack.NewOffsetArray([]uint64{
   711  					10, 100, 800, 12344,
   712  				}),
   713  			},
   714  			exp: 12344,
   715  		},
   716  	}
   717  
   718  	for _, tc := range tests {
   719  		tc := tc
   720  		t.Run(tc.name, func(t *testing.T) {
   721  			f := function{
   722  				parent:             &compiledFunction{sourceOffsetMap: tc.srcMap},
   723  				codeInitialAddress: tc.codeInitialAddress,
   724  			}
   725  
   726  			actual := f.getSourceOffsetInWasmBinary(tc.pc)
   727  			require.Equal(t, tc.exp, actual)
   728  		})
   729  	}
   730  }