Repository: tetratelabs/wazero
Pull request #345 (2022-03-08 00:09:37)

Introduce ModuleEngine interface
This commit adds ModuleEngine interface which is used to 
make calls and created per-module instance. Notably, this 
enables us to remove the necessity for store to holds 
FunctionInstances and FunctionIndex. 

This is a follow-up from #342 


mathetake: ok the left is call_indirect ..
mathetake: ok the left is linking.wast and imports.wast 
mathetake: ok started passing all tests on amd64 and riscv.. the left is to make changes to amd64 jits
mathetake: cool passed 
mathetake: 🤞 
mathetake: hmm..
mathetake: windows might become flaky with this but in any case I will take care of the subsequent prs and in the worse case I will add t.Skip in ad-hoc for windows and raise issue 
mathetake: this PR itself is too big to debug the potential flake which might have existed in  the first place
codefromthecrypt: I'll rebase and add any minor commits merging unless they become not minor :D
mathetake: haha they are still flaky clearly not ready 😄 
mathetake: `TestAmd64Compiler_compileCall` is the culprit for sure.. let me kill it tomorrow

Patch:
diff --git a/RATIONALE.md b/RATIONALE.md
index cf170262..07a167fe 100644
--- a/RATIONALE.md
+++ b/RATIONALE.md
@@ -94,9 +94,9 @@ WebAssembly 1.0 (20191205) specification allows runtimes to [limit certain aspec
 
 wazero limitations are imposed pragmatically and described below.
 
-### Number of functions in a store
+### Number of functions in a module
 
-The possible number of function instances in [a store](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#store%E2%91%A0) is not specified in the WebAssembly specifications since [`funcaddr`](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-funcaddr) corresponding to a function instance can be arbitrary number.
+The possible number of function instances in [a module](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#module-instances%E2%91%A0) is not specified in the WebAssembly specifications since [`funcaddr`](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-funcaddr) corresponding to a function instance in a store can be arbitrary number.
 wazero limits the maximum function instances to 2^27 as even that number would occupy 1GB in function pointers.
 
 That is because not only we _believe_ that all use cases are fine with the limitation, but also we have no way to test wazero runtimes under these unusual circumstances.
diff --git a/internal/wasm/engine.go b/internal/wasm/engine.go
index c1729383..0430623d 100644
--- a/internal/wasm/engine.go
+++ b/internal/wasm/engine.go
@@ -1,18 +1,25 @@
 package internalwasm
 
-// Engine is the interface implemented by interpreters.
+// Engine is a Store-scoped mechanism to compile functions declared or imported by a module.
+// This is a top-level type implemented by an interpreter or JIT compiler.
 type Engine interface {
+	// Compile compiles down the function instances in a module, and returns ModuleEngine for the module.
+	Compile(importedFunctions, moduleFunctions []*FunctionInstance) (ModuleEngine, error)
+}
+
+// ModuleEngine implements function calls for a given module.
+type ModuleEngine interface {
+	// FunctionAddress returns the absolute address of the compiled function for index.
+	// The returned address is stored and used as a TableInstance.Table element.
+	//
+	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-funcaddr
+	FunctionAddress(index Index) uintptr
+
 	// Call invokes a function instance f with given parameters.
 	// Returns the results from the function.
 	// The ctx's context.Context will be the outer-most ancestor of the argument to wasm.FunctionVoidReturn, etc.
 	Call(ctx *ModuleContext, f *FunctionInstance, params ...uint64) (results []uint64, err error)
 
-	// Compile compiles down the function instance.
-	Compile(f *FunctionInstance) error
-
-	// Release releases the resources allocated by a function instance.
-	// Note: this is only called after ensuring that no existing function will call the release target.
-	// Therefore, it is safe to reuse the resource, for example reallocate the new compiled function
-	// for the same FunctionIndex.
-	Release(f *FunctionInstance) error
+	// Release releases the resources allocated by functions in this ModuleEngine.
+	Release() error
 }
diff --git a/internal/wasm/host.go b/internal/wasm/host.go
index eef5fa5f..717d2c4b 100644
--- a/internal/wasm/host.go
+++ b/internal/wasm/host.go
@@ -97,6 +97,7 @@ func (m *Module) buildHostFunctionInstances() (functions []*FunctionInstance) {
 			Kind:   kind(fn.Type()),
 			Type:   m.TypeSection[typeIndex],
 			GoFunc: fn,
+			Index:  Index(idx),
 		}
 		functions = append(functions, f)
 	}
diff --git a/internal/wasm/interpreter/interpreter.go b/internal/wasm/interpreter/interpreter.go
index 9751c33f..f7bd036b 100644
--- a/internal/wasm/interpreter/interpreter.go
+++ b/internal/wasm/interpreter/interpreter.go
@@ -9,6 +9,7 @@ import (
 	"runtime/debug"
 	"strings"
 	"sync"
+	"unsafe"
 
 	"github.com/tetratelabs/wazero/internal/moremath"
 	wasm "github.com/tetratelabs/wazero/internal/wasm"
@@ -20,44 +21,57 @@ var callStackCeiling = buildoptions.CallStackCeiling
 
 // engine is an interpreter implementation of internalwasm.Engine
 type engine struct {
-	// compiledFunctions stores compiled functions where the index means wasm.FunctionIndex.
-	compiledFunctions []*compiledFunction
-
-	// mux is used for read/write access to functions slice.
-	// This is necessary as each compiled function will access the slice at runtime
-	// when they make function calls while engine might be modifying the underlying slice when
-	// adding a new compiled function. We take read lock when creating new callEngine
-	// for each function invocation while take write lock in engine.addCompiledFunction.
-	mux sync.RWMutex
+	compiledFunctions map[*wasm.FunctionInstance]*compiledFunction // gurded by mutex.
+	mux               sync.RWMutex
 }
 
-const initialCompiledFunctionsSliceSize = 128
-
 func NewEngine() wasm.Engine {
 	return &engine{
-		compiledFunctions: make([]*compiledFunction, initialCompiledFunctionsSliceSize),
+		compiledFunctions: make(map[*wasm.FunctionInstance]*compiledFunction),
 	}
 }
 
-// callEngine holds context per engine.Call, and shared across all the
-// function calls originating from the same engine.Call execution.
+func (e *engine) deleteCompiledFunction(f *wasm.FunctionInstance) {
+	e.mux.Lock()
+	defer e.mux.Lock()
+	delete(e.compiledFunctions, f)
+}
+
+func (e *engine) getCompiledFunction(f *wasm.FunctionInstance) (cf *compiledFunction, ok bool) {
+	e.mux.RLock()
+	defer e.mux.RUnlock()
+	cf, ok = e.compiledFunctions[f]
+	return
+}
+
+func (e *engine) addCompiledFunction(f *wasm.FunctionInstance, cf *compiledFunction) {
+	e.mux.Lock()
+	defer e.mux.Unlock()
+	e.compiledFunctions[f] = cf
+}
+
+// moduleEngine implements internalwasm.ModuleEngine
+type moduleEngine struct {
+	// compiledFunctions are the compiled functions in a module instances.
+	// The index is module instance-scoped.
+	compiledFunctions []*compiledFunction
+
+	// parentEngine holds *engine from which this module engine is created from.
+	parentEngine *engine
+}
+
+// callEngine holds context per moduleEngine.Call, and shared across all the
+// function calls originating from the same moduleEngine.Call execution.
 type callEngine struct {
 	// stack contains the operands.
 	// Note that all the values are represented as uint64.
 	stack []uint64
 	// frames are the function call stack.
 	frames []*callFrame
-
-	// compiledFunctions is engine.functions at the time when this virtual machine was created.
-	// engine.functions can be modified whenever engine compiles a new function, so
-	// we take a pointer to the current functions array so that it is safe even if engine.functions changes.
-	compiledFunctions []*compiledFunction
 }
 
-func (e *engine) newCallEngine() *callEngine {
-	e.mux.RLock()
-	defer e.mux.RUnlock()
-	return &callEngine{compiledFunctions: e.compiledFunctions}
+func (me *moduleEngine) newCallEngine() *callEngine {
+	return &callEngine{}
 }
 
 func (ce *callEngine) push(v uint64) {
@@ -119,6 +133,7 @@ type callFrame struct {
 }
 
 type compiledFunction struct {
+	moduleEngine *moduleEngine
 	funcInstance *wasm.FunctionInstance
 	body         []*interpreterOp
 	hostFn       *reflect.Value
@@ -133,44 +148,46 @@ type interpreterOp struct {
 }
 
 // Release implements wasm.Engine Release
-func (e *engine) Release(f *wasm.FunctionInstance) error {
-	e.mux.Lock()
-	defer e.mux.Unlock()
-	e.compiledFunctions[f.Index] = nil
+func (me *moduleEngine) Release() error {
+	for _, cf := range me.compiledFunctions {
+		me.parentEngine.deleteCompiledFunction(cf.funcInstance)
+	}
 	return nil
 }
 
-// Compile Implements wasm.Engine for engine.
-func (e *engine) Compile(f *wasm.FunctionInstance) error {
-	funcIndex := f.Index
-	var compiled *compiledFunction
-	if f.Kind == wasm.FunctionKindWasm {
-		ir, err := wazeroir.Compile(f)
-		if err != nil {
-			return fmt.Errorf("failed to compile Wasm to wazeroir: %w", err)
+// Compile implements internalwasm.Engine Compile
+func (e *engine) Compile(importedFunctions, moduleFunctions []*wasm.FunctionInstance) (me wasm.ModuleEngine, err error) {
+	compiledFunctions := make([]*compiledFunction, len(importedFunctions)+len(moduleFunctions))
+	for i, f := range importedFunctions {
+		cf, ok := e.getCompiledFunction(f)
+		if !ok {
+			return nil, fmt.Errorf("uncompiled imported function: %s", f.Name)
 		}
+		compiledFunctions[i] = cf
+	}
 
-		compiled, err = e.lowerIROps(f, ir.Operations)
-		if err != nil {
-			return fmt.Errorf("failed to convert wazeroir operations to engine ones: %w", err)
-		}
-	} else {
-		compiled = &compiledFunction{
-			hostFn: f.GoFunc, funcInstance: f,
+	ret := &moduleEngine{compiledFunctions: compiledFunctions, parentEngine: e}
+	for _, f := range moduleFunctions {
+		var compiled *compiledFunction
+		if f.Kind == wasm.FunctionKindWasm {
+			ir, err := wazeroir.Compile(f)
+			if err != nil {
+				return nil, fmt.Errorf("failed to compile Wasm to wazeroir: %w", err)
+			}
+
+			compiled, err = e.lowerIROps(f, ir.Operations)
+			if err != nil {
+				return nil, fmt.Errorf("failed to convert wazeroir operations to engine ones: %w", err)
+			}
+		} else {
+			compiled = &compiledFunction{hostFn: f.GoFunc, funcInstance: f}
 		}
-	}
 
-	if l := len(e.compiledFunctions); l <= int(funcIndex) {
-		// This case compiledFunctions slice needs to grow to store a new compiledFunction.
-		// However, it is read in newCallEngine, so we have to take write lock (via .Unlock)
-		// rather than read lock (via .RLock).
-		e.mux.Lock() // Write lock.
-		defer e.mux.Unlock()
-		// Double the size of compiled functions.
-		e.compiledFunctions = append(e.compiledFunctions, make([]*compiledFunction, l)...)
+		compiled.moduleEngine = ret
+		compiledFunctions[f.Index] = compiled
+		e.addCompiledFunction(f, compiled)
 	}
-	e.compiledFunctions[funcIndex] = compiled
-	return nil
+	return ret, nil
 }
 
 // Lowers the wazeroir operations to engine friendly struct.
@@ -266,9 +283,8 @@ func (e *engine) lowerIROps(f *wasm.FunctionInstance,
 				}
 			}
 		case *wazeroir.OperationCall:
-			target := f.Module.Functions[o.FunctionIndex]
 			op.us = make([]uint64, 1)
-			op.us[0] = uint64(target.Index)
+			op.us = []uint64{uint64(o.FunctionIndex)}
 		case *wazeroir.OperationCallIndirect:
 			op.us = make([]uint64, 2)
 			op.us[0] = uint64(o.TableIndex)
@@ -448,15 +464,26 @@ func (e *engine) lowerIROps(f *wasm.FunctionInstance,
 	return ret, nil
 }
 
-// Call implements an interpreted wasm.Engine.
-func (e *engine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
+// FunctionAddress implements internalwasm.ModuleEngine FunctionAddress.
+func (me *moduleEngine) FunctionAddress(index wasm.Index) uintptr {
+	return uintptr(unsafe.Pointer(me.compiledFunctions[index]))
+}
+
+// Call implements internalwasm.ModuleEngine Call.
+func (me *moduleEngine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
+	compiled := me.compiledFunctions[f.Index]
+	if compiled == nil {
+		err = fmt.Errorf("function not compiled")
+		return
+	}
+
 	paramSignature := f.Type.Params
 	paramCount := len(params)
 	if len(paramSignature) != paramCount {
 		return nil, fmt.Errorf("expected %d params, but passed %d", len(paramSignature), paramCount)
 	}
 
-	ce := e.newCallEngine()
+	ce := me.newCallEngine()
 	defer func() {
 		if v := recover(); v != nil {
 			if buildoptions.IsDebugMode {
@@ -487,12 +514,6 @@ func (e *engine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params
 		}
 	}()
 
-	compiled := e.compiledFunctions[f.Index]
-	if compiled == nil {
-		err = fmt.Errorf("function not compiled")
-		return
-	}
-
 	for _, param := range params {
 		ce.push(param)
 	}
@@ -568,6 +589,7 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *compiledFunctio
 	memoryInst := moduleInst.Memory
 	globals := moduleInst.Globals
 	table := moduleInst.Table
+	compiledFunctions := f.moduleEngine.compiledFunctions
 	ce.pushFrame(frame)
 	bodyLen := uint64(len(frame.f.body))
 	for frame.pc < bodyLen {
@@ -605,7 +627,7 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *compiledFunctio
 			}
 		case wazeroir.OperationKindCall:
 			{
-				f := ce.compiledFunctions[op.us[0]]
+				f := compiledFunctions[op.us[0]]
 				if f.hostFn != nil {
 					ce.callHostFunc(ctx, f)
 				} else {
@@ -619,20 +641,18 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *compiledFunctio
 				if offset >= uint64(len(table.Table)) {
 					panic(wasm.ErrRuntimeInvalidTableAccess)
 				}
-				tableElement := table.Table[offset]
-				// Type check.
-				if uint64(tableElement.FunctionTypeID) != op.us[1] {
-					if tableElement.FunctionTypeID == wasm.UninitializedTableElementTypeID {
-						panic(wasm.ErrRuntimeInvalidTableAccess)
-					}
+				targetCompiledFunction := (*compiledFunction)(unsafe.Pointer(table.Table[offset]))
+				if targetCompiledFunction == nil {
+					panic(wasm.ErrRuntimeInvalidTableAccess)
+				} else if uint64(targetCompiledFunction.funcInstance.TypeID) != op.us[1] {
 					panic(wasm.ErrRuntimeIndirectCallTypeMismatch)
 				}
-				target := ce.compiledFunctions[table.Table[offset].FunctionIndex]
+
 				// Call in.
-				if target.hostFn != nil {
-					ce.callHostFunc(ctx, target)
+				if targetCompiledFunction.hostFn != nil {
+					ce.callHostFunc(ctx, targetCompiledFunction)
 				} else {
-					ce.callNativeFunc(ctx, target)
+					ce.callNativeFunc(ctx, targetCompiledFunction)
 				}
 				frame.pc++
 			}
diff --git a/internal/wasm/jit/compiler.go b/internal/wasm/jit/compiler.go
index 9e0e0fe5..c0b56a22 100644
--- a/internal/wasm/jit/compiler.go
+++ b/internal/wasm/jit/compiler.go
@@ -1,7 +1,6 @@
 package jit
 
 import (
-	wasm "github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wazeroir"
 )
 
@@ -19,7 +18,7 @@ type compiler interface {
 	compile() (code []byte, staticData compiledFunctionStaticData, stackPointerCeil uint64, err error)
 	// compileHostFunction emits the trampoline code from which native code can jump into the host function.
 	// TODO: maybe we wouldn't need to have trampoline for host functions.
-	compileHostFunction(address wasm.FunctionIndex) error
+	compileHostFunction() error
 	// compileLabel notify compilers of the beginning of a label.
 	// Return true if the compiler decided to skip the entire label.
 	// See wazeroir.OperationLabel
diff --git a/internal/wasm/jit/engine.go b/internal/wasm/jit/engine.go
index e6398e35..106c7222 100644
--- a/internal/wasm/jit/engine.go
+++ b/internal/wasm/jit/engine.go
@@ -18,22 +18,24 @@ import (
 type (
 	// engine is an JIT implementation of internalwasm.Engine
 	engine struct {
-		// compiledFunctions are the currently compiled functions.
-		// The index means wasm.FunctionIndex, but we intentionally avoid using map
-		// as the underlying memory region is accessed by assembly directly by
-		// using compiledFunctionsElement0Address.
+		compiledFunctions map[*wasm.FunctionInstance]*compiledFunction // guarded by mutex.
+		mux               sync.RWMutex
+	}
+
+	// moduleEngine implements internalwasm.ModuleEngine
+	moduleEngine struct {
+		// compiledFunctions are the compiled functions in a module instances.
+		// The index is module instance-scoped. We intentionally avoid using map
+		// as the underlying memory region is accessed by assembly directly by using
+		// compiledFunctionsElement0Address.
 		compiledFunctions []*compiledFunction
 
-		// mux is used for read/write access to compiledFunctions slice.
-		// This is necessary as each compiled function will access the slice from the native code
-		// when they make function calls while engine might be modifying the underlying slice when
-		// adding a new compiled function. We take read lock when creating new callEngine
-		// for each function invocation while take write lock in engine.addCompiledFunction.
-		mux sync.RWMutex
+		// parentEngine holds *engine from which this module engine is created from.
+		parentEngine *engine
 	}
 
-	// callEngine holds context per engine.Call, and shared across all the
-	// function calls originating from the same engine.Call execution.
+	// callEngine holds context per moduleEngine.Call, and shared across all the
+	// function calls originating from the same moduleEngine.Call execution.
 	callEngine struct {
 		// These contexts are read and written by JITed code.
 		// Note: we embed these structs so we can reduce the costs to access fields inside of them.
@@ -47,20 +49,13 @@ type (
 		// The following fields are not accessed by JITed code directly.
 
 		// valueStack is the go-allocated stack for holding Wasm values.
-		// Note: We NEVER edit len or cap in JITed code so we won't get screwed when GC comes in.
+		// Note: We never edit len or cap in JITed code so we won't get screwed when GC comes in.
 		valueStack []uint64
 
 		// callFrameStack is initially callFrameStack[callFrameStackPointer].
 		// The currently executed function call frame lives at callFrameStack[callFrameStackPointer-1]
 		// and that is equivalent to  engine.callFrameTop().
 		callFrameStack []callFrame
-
-		// compiledFunctions is engine.compiledFunctions at the time when this callEngine was created.
-		// engine.compiledFunction's underlying array can change whenever it compiles a new function while
-		// we have to access it when we make function calls. By copying slice (= copying a pointer to the array)
-		// into this field, we can safely access the compiled function array from the native code without caring
-		// about the change made by engine.
-		compiledFunctions []*compiledFunction
 	}
 
 	// globalContext holds the data which is constant across multiple function calls.
@@ -83,9 +78,6 @@ type (
 		// callFrameStack[callFrameStackPointer]. This value is incremented/decremented in assembly
 		// when making function calls or returning from them.
 		callFrameStackPointer uint64
-
-		// compiledFunctionsElement0Address is &engine.compiledFunctions[0] as uintptr.
-		compiledFunctionsElement0Address uintptr
 	}
 
 	// moduleContext holds the per-function call specific module information.
@@ -109,6 +101,9 @@ type (
 		tableElement0Address uintptr
 		// tableSliceLen is the length of the memory buffer, i.e. len(ModuleInstance.Tables[0].Table).
 		tableSliceLen uint64
+
+		// compiledFunctionsElement0Address is &moduleContext.engine.compiledFunctions[0] as uintptr.
+		compiledFunctionsElement0Address uintptr
 	}
 
 	// valueStackContext stores the data to access engine.valueStack.
@@ -139,9 +134,9 @@ type (
 		// Where we store the status code of JIT execution.
 		statusCode jitCallStatusCode
 
-		// Set when statusCode == jitStatusCall{HostFunction,BuiltInFunction}
+		// Set when statusCode == jitStatusCallBuiltInFunction}
 		// Indicating the function call index.
-		functionCallAddress wasm.FunctionIndex
+		builtinFunctionCallIndex wasm.Index
 	}
 
 	// callFrame holds the information to which the caller function can return.
@@ -191,29 +186,32 @@ type (
 // Native code reads/writes Go's structs with the following constants.
 // See TestVerifyOffsetValue for how to derive these values.
 const (
+	// Offsets for moduleEngine.compiledFunctions
+	moduleEngineCompiledFunctionsOffset = 0
+
 	// Offsets for callEngine.globalContext.
-	callEngineGlobalContextValueStackElement0AddressOffset        = 0
-	callEngineGlobalContextValueStackLenOffset                    = 8
-	callEngineGlobalContextCallFrameStackElement0AddressOffset    = 16
-	callEngineGlobalContextCallFrameStackLenOffset                = 24
-	callEngineGlobalContextCallFrameStackPointerOffset            = 32
-	callEngineGlobalContextCompiledFunctionsElement0AddressOffset = 40
+	callEngineGlobalContextValueStackElement0AddressOffset     = 0
+	callEngineGlobalContextValueStackLenOffset                 = 8
+	callEngineGlobalContextCallFrameStackElement0AddressOffset = 16
+	callEngineGlobalContextCallFrameStackLenOffset             = 24
+	callEngineGlobalContextCallFrameStackPointerOffset         = 32
 
 	// Offsets for callEngine.moduleContext.
-	callEngineModuleContextModuleInstanceAddressOffset = 48
-	callEngineModuleContextGlobalElement0AddressOffset = 56
-	callEngineModuleContextMemoryElement0AddressOffset = 64
-	callEngineModuleContextMemorySliceLenOffset        = 72
-	callEngineModuleContextTableElement0AddressOffset  = 80
-	callEngineModuleContextTableSliceLenOffset         = 88
+	callEngineModuleContextModuleInstanceAddressOffset            = 40
+	callEngineModuleContextGlobalElement0AddressOffset            = 48
+	callEngineModuleContextMemoryElement0AddressOffset            = 56
+	callEngineModuleContextMemorySliceLenOffset                   = 64
+	callEngineModuleContextTableElement0AddressOffset             = 72
+	callEngineModuleContextTableSliceLenOffset                    = 80
+	callEngineModuleContextCompiledFunctionsElement0AddressOffset = 88
 
 	// Offsets for callEngine.valueStackContext.
 	callEngineValueStackContextStackPointerOffset     = 96
 	callEngineValueStackContextStackBasePointerOffset = 104
 
 	// Offsets for callEngine.exitContext.
-	callEngineExitContextJITCallStatusCodeOffset   = 112
-	callEngineExitContextFunctionCallAddressOffset = 120
+	callEngineExitContextJITCallStatusCodeOffset          = 112
+	callEngineExitContextBuiltinFunctionCallAddressOffset = 116
 
 	// Offsets for callFrame.
 	callFrameDataSize                      = 32
@@ -225,26 +223,32 @@ const (
 	// Offsets for compiledFunction.
 	compiledFunctionCodeInitialAddressOffset = 0
 	compiledFunctionStackPointerCeilOffset   = 8
-
-	// Offsets for wasm.TableElement
-	tableElementFunctionIndexOffset  = 0
-	tableElementFunctionTypeIDOffset = 8
+	compiledFunctionSourceOffset             = 16
 
 	// Offsets for wasm.ModuleInstance
 	moduleInstanceGlobalsOffset = 48
 	moduleInstanceMemoryOffset  = 72
 	moduleInstanceTableOffset   = 80
+	moduleInstanceEngineOffset  = 120
 
 	// Offsets for wasm.Table.
 	tableInstanceTableOffset    = 0
 	tableInstanceTableLenOffset = 8
 
+	// Offsets for wasm.FunctionInstance
+	functionInstanceTypeIDOffset = 96
+
 	// Offsets for wasm.Memory.
 	memoryInstanceBufferOffset    = 0
 	memoryInstanceBufferLenOffset = 8
 
 	// Offsets for wasm.GlobalInstance.
 	globalInstanceValueOffset = 8
+
+	// Offsets for Go's interface.
+	// https://research.swtch.com/interfaces
+	// https://github.com/golang/go/blob/release-branch.go1.17/src/runtime/runtime2.go#L207-L210
+	interfaceDataOffset = 8
 )
 
 // jitCallStatusCode represents the result of `jitcall`.
@@ -309,6 +313,7 @@ func (s jitCallStatusCode) String() (ret string) {
 	return
 }
 
+// String implements fmt.Stringer
 func (c *callFrame) String() string {
 	return fmt.Sprintf(
 		"[%s: return address=0x%x, return stack base pointer=%d]",
@@ -316,56 +321,85 @@ func (c *callFrame) String() string {
 	)
 }
 
-// Release implements wasm.Engine Release
-func (e *engine) Release(f *wasm.FunctionInstance) error {
-	e.mux.Lock()
-	defer e.mux.Unlock()
+// Compile implements internalwasm.Engine Compile
+func (e *engine) Compile(importedFunctions, moduleFunctions []*wasm.FunctionInstance) (me wasm.ModuleEngine, err error) {
+	imported := len(importedFunctions)
+	compiledFunctions := make([]*compiledFunction, imported+len(moduleFunctions))
+	for i, f := range importedFunctions {
+		cf, ok := e.getCompiledFunction(f)
+		if !ok {
+			return nil, fmt.Errorf("uncompiled imported function: %s", f.Name)
+		}
+		compiledFunctions[i] = cf
+	}
 
-	codeSegment := e.compiledFunctions[f.Index].codeSegment
-	if err := munmapCodeSegment(codeSegment); err != nil {
-		return err
+	for i, f := range moduleFunctions {
+		var compiled *compiledFunction
+		if f.Kind == wasm.FunctionKindWasm {
+			compiled, err = compileWasmFunction(f)
+		} else {
+			compiled, err = compileHostFunction(f)
+		}
+		if err != nil {
+			// TODO: Unmamp already compiled functions.
+			return nil, fmt.Errorf("function[%d/%d] %w", i, len(moduleFunctions)-1, err)
+		}
+		compiledFunctions[imported+i] = compiled
+		e.addCompiledFunction(f, compiled)
 	}
+	return &moduleEngine{compiledFunctions: compiledFunctions, parentEngine: e}, nil
+}
 
-	e.compiledFunctions[f.Index] = nil
-	return nil
+func (e *engine) deleteCompiledFunction(f *wasm.FunctionInstance) {
+	e.mux.Lock()
+	defer e.mux.Unlock()
+	delete(e.compiledFunctions, f)
 }
 
-func (e *engine) Compile(f *wasm.FunctionInstance) (err error) {
-	var compiled *compiledFunction
-	if f.Kind == wasm.FunctionKindWasm {
-		compiled, err = compileWasmFunction(f)
-	} else {
-		compiled, err = compileHostFunction(f)
-	}
-	if err != nil {
-		return fmt.Errorf("failed to compile function: %w", err)
-	}
+func (e *engine) addCompiledFunction(f *wasm.FunctionInstance, cf *compiledFunction) {
+	e.mux.Lock()
+	defer e.mux.Unlock()
+	e.compiledFunctions[f] = cf
+}
 
-	e.addCompiledFunction(f.Index, compiled)
+func (e *engine) getCompiledFunction(f *wasm.FunctionInstance) (cf *compiledFunction, ok bool) {
+	e.mux.RLock()
+	defer e.mux.RUnlock()
+	cf, ok = e.compiledFunctions[f]
 	return
 }
 
-func (e *engine) addCompiledFunction(index wasm.FunctionIndex, compiled *compiledFunction) {
-	if len(e.compiledFunctions) <= int(index) {
-		// This case compiledFunctions slice needs to grow to store a new compiledFunction.
-		// However, it is read in newCallEngine, so we have to take write lock (via .Unlock)
-		// rather than read lock (via .RLock).
-		e.mux.Lock()
-		defer e.mux.Unlock()
-		// Double the size of compiled functions.
-		e.compiledFunctions = append(e.compiledFunctions, make([]*compiledFunction, len(e.compiledFunctions))...)
-	}
-	e.compiledFunctions[index] = compiled
+// FunctionAddress implements internalwasm.ModuleEngine FunctionAddress
+func (me *moduleEngine) FunctionAddress(index wasm.Index) uintptr {
+	return uintptr(unsafe.Pointer(me.compiledFunctions[index]))
+}
+
+// Release implements internalwasm.ModuleEngine Release
+func (me *moduleEngine) Release() error {
+	for _, cf := range me.compiledFunctions {
+		if err := munmapCodeSegment(cf.codeSegment); err != nil {
+			return err
+		}
+		me.parentEngine.deleteCompiledFunction(cf.source)
+	}
+	return nil
 }
 
-func (e *engine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
+// Call implements internalwasm.ModuleEngine Call
+func (me *moduleEngine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
+	compiled := me.compiledFunctions[f.Index]
+	if compiled == nil {
+		err = fmt.Errorf("function not compiled")
+		return
+	}
+
 	paramSignature := f.Type.Params
 	paramCount := len(params)
 	if len(paramSignature) != paramCount {
 		return nil, fmt.Errorf("expected %d params, but passed %d", len(paramSignature), paramCount)
 	}
 
-	ce := e.newCallEngine()
+	ce := me.newCallEngine()
 
 	// We ensure that this Call method never panics as
 	// this Call method is indirectly invoked by embedders via store.CallFunction,
@@ -397,14 +431,8 @@ func (e *engine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params
 		}
 	}()
 
-	for _, param := range params {
-		ce.pushValue(param)
-	}
-
-	compiled := ce.compiledFunctions[f.Index]
-	if compiled == nil {
-		err = fmt.Errorf("function not compiled")
-		return
+	for _, v := range params {
+		ce.pushValue(v)
 	}
 
 	if f.Kind == wasm.FunctionKindWasm {
@@ -427,40 +455,30 @@ func NewEngine() wasm.Engine {
 }
 
 func newEngine() *engine {
-	return &engine{compiledFunctions: make([]*compiledFunction, initialCompiledFunctionsSliceSize)}
+	return &engine{compiledFunctions: map[*wasm.FunctionInstance]*compiledFunction{}}
 }
 
 // TODO: better make them configurable?
 const (
-	initialValueStackSize             = 64
-	initialCallFrameStackSize         = 16
-	initialCompiledFunctionsSliceSize = 128
+	initialValueStackSize     = 64
+	initialCallFrameStackSize = 16
 )
 
-func (e *engine) newCallEngine() *callEngine {
-	// We have to save the current engine.compiledFunctions into callEngine.compiledFunctions,
-	// therefore we have to take the read lock on it because it can change whenever engine compiles
-	// a new function.
-	e.mux.RLock()
-	defer e.mux.RUnlock()
-
+func (me *moduleEngine) newCallEngine() *callEngine {
 	ce := &callEngine{
-		valueStack:        make([]uint64, initialValueStackSize),
-		callFrameStack:    make([]callFrame, initialCallFrameStackSize),
-		archContext:       newArchContext(),
-		compiledFunctions: e.compiledFunctions,
+		valueStack:     make([]uint64, initialValueStackSize),
+		callFrameStack: make([]callFrame, initialCallFrameStackSize),
+		archContext:    newArchContext(),
 	}
 
 	valueStackHeader := (*reflect.SliceHeader)(unsafe.Pointer(&ce.valueStack))
 	callFrameStackHeader := (*reflect.SliceHeader)(unsafe.Pointer(&ce.callFrameStack))
-	compiledFunctionsHeader := (*reflect.SliceHeader)(unsafe.Pointer(&ce.compiledFunctions))
 	ce.globalContext = globalContext{
 		valueStackElement0Address:        valueStackHeader.Data,
 		valueStackLen:                    uint64(valueStackHeader.Len),
 		callFrameStackElementZeroAddress: callFrameStackHeader.Data,
 		callFrameStackLen:                uint64(callFrameStackHeader.Len),
 		callFrameStackPointer:            0,
-		compiledFunctionsElement0Address: compiledFunctionsHeader.Data,
 	}
 	return ce
 }
@@ -471,6 +489,12 @@ func (ce *callEngine) popValue() (ret uint64) {
 	return
 }
 
+func (ce *callEngine) resetValueStackElement0Address() {
+	valueStackHeader := (*reflect.SliceHeader)(unsafe.Pointer(&ce.valueStack))
+	ce.globalContext.valueStackElement0Address = valueStackHeader.Data
+	ce.globalContext.valueStackLen = uint64(valueStackHeader.Len)
+}
+
 func (ce *callEngine) pushValue(v uint64) {
 	ce.valueStack[ce.valueStackTopIndex()] = v
 	ce.valueStackContext.stackPointer++
@@ -489,7 +513,7 @@ func (ce *callEngine) valueStackTopIndex() uint64 {
 }
 
 const (
-	builtinFunctionIndexMemoryGrow wasm.FunctionIndex = iota
+	builtinFunctionIndexMemoryGrow wasm.Index = iota
 	builtinFunctionIndexGrowValueStack
 	builtinFunctionIndexGrowCallFrameStack
 	// builtinFunctionIndexBreakPoint is internal (only for wazero developers). Disabled by default.
@@ -569,6 +593,11 @@ jitentry:
 				frame.String(), ce.valueStackContext.stackBasePointer, ce.valueStackContext.stackPointer)
 		}
 
+		// Seems like even not using "append", accessing the slice might lead to relocation by Go runtime on amd64.
+		// For safety, we reset the cached addresses and length here.
+		// TODO: investigate the root cause.
+		ce.resetValueStackElement0Address()
+
 		// Call into the JIT code.
 		jitcall(frame.returnAddress, uintptr(unsafe.Pointer(ce)))
 
@@ -577,17 +606,17 @@ jitentry:
 		case jitCallStatusCodeReturned:
 			// Meaning that all the function frames above the previous call frame stack pointer are executed.
 		case jitCallStatusCodeCallHostFunction:
+			calleeHostFunction := ce.callFrameTop().compiledFunction.source
 			// Not "callFrameTop" but take the below of peek with "callFrameAt(1)" as the top frame is for host function,
 			// but when making host function calls, we need to pass the memory instance of host function caller.
-			fn := ce.compiledFunctions[ce.exitContext.functionCallAddress]
 			callerCompiledFunction := ce.callFrameAt(1).compiledFunction
 			// A host function is invoked with the calling frame's memory, which may be different if in another module.
-			ce.execHostFunction(fn.source.Kind, fn.source.GoFunc,
+			ce.execHostFunction(calleeHostFunction.Kind, calleeHostFunction.GoFunc,
 				ctx.WithMemory(callerCompiledFunction.source.Module.Memory),
 			)
 			goto jitentry
 		case jitCallStatusCodeCallBuiltInFunction:
-			switch ce.exitContext.functionCallAddress {
+			switch ce.exitContext.builtinFunctionCallIndex {
 			case builtinFunctionIndexMemoryGrow:
 				callerCompiledFunction := ce.callFrameTop().compiledFunction
 				ce.builtinFunctionMemoryGrow(callerCompiledFunction.source.Module.Memory)
@@ -598,7 +627,7 @@ jitentry:
 				ce.builtinFunctionGrowCallFrameStack()
 			}
 			if buildoptions.IsDebugMode {
-				if ce.exitContext.functionCallAddress == builtinFunctionIndexBreakPoint {
+				if ce.exitContext.builtinFunctionCallIndex == builtinFunctionIndexBreakPoint {
 					runtime.Breakpoint()
 				}
 			}
@@ -639,11 +668,6 @@ func (ce *callEngine) builtinFunctionGrowValueStack(stackPointerCeil uint64) {
 	top := ce.valueStackContext.stackBasePointer + ce.valueStackContext.stackPointer
 	copy(newStack[:top], ce.valueStack[:top])
 	ce.valueStack = newStack
-
-	// Update the globalContext's fields as they become stale after the update ^^.
-	stackSliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&newStack))
-	ce.globalContext.valueStackElement0Address = stackSliceHeader.Data
-	ce.globalContext.valueStackLen = uint64(stackSliceHeader.Len)
 }
 
 var callStackCeiling = uint64(buildoptions.CallStackCeiling)
@@ -671,7 +695,7 @@ func (ce *callEngine) builtinFunctionMemoryGrow(mem *wasm.MemoryInstance) {
 	res := mem.Grow(uint32(newPages))
 	ce.pushValue(uint64(res))
 
-	// Update the moduleContext's fields as they become stale after the update ^^.
+	// Update the moduleContext's ields as they become stale after the update ^^.
 	bufSliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&mem.Buffer))
 	ce.moduleContext.memorySliceLen = uint64(bufSliceHeader.Len)
 	ce.moduleContext.memoryElement0Address = bufSliceHeader.Data
@@ -685,7 +709,7 @@ func compileHostFunction(f *wasm.FunctionInstance) (*compiledFunction, error) {
 		return nil, err
 	}
 
-	if err = compiler.compileHostFunction(f.Index); err != nil {
+	if err = compiler.compileHostFunction(); err != nil {
 		return nil, err
 	}
 
@@ -713,10 +737,6 @@ func compileWasmFunction(f *wasm.FunctionInstance) (*compiledFunction, error) {
 		return nil, fmt.Errorf("failed to lower to wazeroir: %w", err)
 	}
 
-	if buildoptions.IsDebugMode {
-		fmt.Printf("compilation target wazeroir:\n%s\n", wazeroir.Format(ir.Operations))
-	}
-
 	compiler, done, err := newCompiler(f, ir)
 	defer done()
 	if err != nil {
@@ -892,7 +912,7 @@ func compileWasmFunction(f *wasm.FunctionInstance) (*compiledFunction, error) {
 			err = compiler.compileSignExtend64From32()
 		}
 		if err != nil {
-			return nil, fmt.Errorf("failed to compile operation %s: %w", op.Kind().String(), err)
+			return nil, fmt.Errorf("operation %s: %w", op.Kind().String(), err)
 		}
 	}
 
diff --git a/internal/wasm/jit/jit_amd64.go b/internal/wasm/jit/jit_amd64.go
index f60026d5..5123b6da 100644
--- a/internal/wasm/jit/jit_amd64.go
+++ b/internal/wasm/jit/jit_amd64.go
@@ -13,6 +13,7 @@ import (
 	"fmt"
 	"math"
 	"runtime"
+	"sync"
 	"unsafe"
 
 	asm "github.com/twitchyliquid64/golang-asm"
@@ -87,15 +88,27 @@ type archContext struct{}
 
 func newArchContext() (ret archContext) { return }
 
+// golang-asm is not goroutine-safe so we take lock until we complete the compilation.
+// TODO: delete after https://github.com/tetratelabs/wazero/issues/233
+var assemblerMutex = &sync.Mutex{}
+
+func unlockAssembler() {
+	assemblerMutex.Unlock()
+}
+
 // newCompiler returns a new compiler interface which can be used to compile the given function instance.
 // The function returned must be invoked when finished compiling, so use `defer` to ensure this.
 // Note: ir param can be nil for host functions.
 func newCompiler(f *wasm.FunctionInstance, ir *wazeroir.CompilationResult) (compiler, func(), error) {
+	// golang-asm is not goroutine-safe so we take lock until we complete the compilation.
+	// TODO: delete after https://github.com/tetratelabs/wazero/issues/233
+	assemblerMutex.Lock()
+
 	// We can choose arbitrary number instead of 1024 which indicates the cache size in the compiler.
 	// TODO: optimize the number.
 	b, err := asm.NewBuilder("amd64", 1024)
 	if err != nil {
-		return nil, func() {}, fmt.Errorf("failed to create a new assembly builder: %w", err)
+		return nil, unlockAssembler, fmt.Errorf("failed to create a new assembly builder: %w", err)
 	}
 
 	compiler := &amd64Compiler{
@@ -106,7 +119,7 @@ func newCompiler(f *wasm.FunctionInstance, ir *wazeroir.CompilationResult) (comp
 		ir:            ir,
 		labels:        map[string]*labelInfo{},
 	}
-	return compiler, func() {}, nil
+	return compiler, unlockAssembler, nil
 }
 
 func (c *amd64Compiler) String() string {
@@ -170,11 +183,11 @@ func (c *amd64Compiler) label(labelKey string) *labelInfo {
 
 // compileHostFunction constructs the entire code to enter the host function implementation,
 // and return back to the caller.
-func (c *amd64Compiler) compileHostFunction(address wasm.FunctionIndex) error {
+func (c *amd64Compiler) compileHostFunction() error {
 	// First we must update the location stack to reflect the number of host function inputs.
 	c.pushFunctionParams()
 
-	if err := c.callGoFunction(jitCallStatusCodeCallHostFunction, address); err != nil {
+	if err := c.callGoFunction(jitCallStatusCodeCallHostFunction, c.f.Index); err != nil {
 		return err
 	}
 
@@ -930,7 +943,7 @@ func (c *amd64Compiler) compileCall(o *wazeroir.OperationCall) error {
 	}
 
 	target := c.f.Module.Functions[o.FunctionIndex]
-	if err := c.callFunctionFromConst(target.Index, target.Type); err != nil {
+	if err := c.callFunctionFromConst(o.FunctionIndex, target.Type); err != nil {
 		return err
 	}
 
@@ -959,6 +972,11 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 		return nil
 	}
 
+	tmp, err := c.allocateRegister(generalPurposeRegisterTypeInt)
+	if err != nil {
+		return err
+	}
+
 	// First, we need to check if the offset doesn't exceed the length of table.
 	cmpLength := c.newProg()
 	cmpLength.As = x86.ACMPQ
@@ -979,18 +997,18 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 
 	// Next we check if the target's type matches the operation's one.
 	// In order to get the type instance's address, we have to multiply the offset
-	// by 16 as the offset is the "length" of table in Go's "[]wasm.TableElement",
-	// and size of wasm.Table equals 128 bit (64-bit wasm.FunctionIndex and 64-bit wasm.TypeID).
+	// by 8 as the offset is the "length" of table in Go's "[]uintptr",
+	// and size of uintptr equals 64 bit.
 	getTypeInstanceAddress := c.newProg()
 	notLengthExceedJump.To.SetTarget(getTypeInstanceAddress)
 	getTypeInstanceAddress.As = x86.ASHLQ
 	getTypeInstanceAddress.To.Type = obj.TYPE_REG
 	getTypeInstanceAddress.To.Reg = offset.register
 	getTypeInstanceAddress.From.Type = obj.TYPE_CONST
-	getTypeInstanceAddress.From.Offset = 4
+	getTypeInstanceAddress.From.Offset = 3
 	c.addInstruction(getTypeInstanceAddress)
 
-	// Adds the address of wasm.Table[0] stored as callEngine.tableSliceAddress to the offset.
+	// Adds the address of wasm.Table[0] stored as callEngine.tableElement0Address to the offset.
 	movTableSliceAddress := c.newProg()
 	movTableSliceAddress.As = x86.AADDQ
 	movTableSliceAddress.To.Type = obj.TYPE_REG
@@ -1000,31 +1018,24 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 	movTableSliceAddress.From.Offset = callEngineModuleContextTableElement0AddressOffset
 	c.addInstruction(movTableSliceAddress)
 
-	// At this point offset.register holds the address of wasm.TableElement at wasm.Table[offset].
-	ti := c.f.Module.Types[o.TypeIndex]
-	targetFunctionType := ti.Type
-	checkIfTypeMatch := c.newProg()
-	checkIfTypeMatch.As = x86.ACMPL // 32-bit as FunctionTypeID is in 32-bit unsigned integer.
-	checkIfTypeMatch.From.Type = obj.TYPE_MEM
-	checkIfTypeMatch.From.Reg = offset.register
-	checkIfTypeMatch.From.Offset = tableElementFunctionTypeIDOffset
-	checkIfTypeMatch.To.Type = obj.TYPE_CONST
-	checkIfTypeMatch.To.Offset = int64(ti.TypeID)
-	c.addInstruction(checkIfTypeMatch)
-
-	// Jump if the type matches.
-	jumpIfTypeMatch := c.newProg()
-	jumpIfTypeMatch.To.Type = obj.TYPE_BRANCH
-	jumpIfTypeMatch.As = x86.AJEQ
-	c.addInstruction(jumpIfTypeMatch)
+	// "offset = *offset (== table[offset] == *compiledFunction type)"
+	derefCompiledFunctionPointer := c.newProg()
+	derefCompiledFunctionPointer.As = x86.AMOVQ
+	derefCompiledFunctionPointer.To.Type = obj.TYPE_REG
+	derefCompiledFunctionPointer.To.Reg = offset.register
+	derefCompiledFunctionPointer.From.Type = obj.TYPE_MEM
+	derefCompiledFunctionPointer.From.Reg = offset.register
+	c.addInstruction(derefCompiledFunctionPointer)
 
+	// At this point offset.register holds the address of *compiledFunction (as uintptr) at wasm.Table[offset].
+	//
+	// Check if the value of table[offset] equals zero, meaning that the target is uninitialized.
 	checkIfInitialized := c.newProg()
-	checkIfInitialized.As = x86.ACMPL // 32-bit as FunctionTypeID is in 32-bit unsigned integer.
-	checkIfInitialized.From.Type = obj.TYPE_MEM
+	checkIfInitialized.As = x86.ACMPQ
+	checkIfInitialized.From.Type = obj.TYPE_REG
 	checkIfInitialized.From.Reg = offset.register
-	checkIfInitialized.From.Offset = tableElementFunctionTypeIDOffset
 	checkIfInitialized.To.Type = obj.TYPE_CONST
-	checkIfInitialized.To.Offset = int64(wasm.UninitializedTableElementTypeID)
+	checkIfInitialized.To.Offset = 0
 	c.addInstruction(checkIfInitialized)
 
 	// Jump if the target is initialized element.
@@ -1036,27 +1047,47 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 	// If not initialized, we return the function with jitCallStatusCodeInvalidTableAccess.
 	c.exit(jitCallStatusCodeInvalidTableAccess)
 
-	// Otherwise, we return the function with jitCallStatusCodeTypeMismatchOnIndirectCall.
 	c.addSetJmpOrigins(jumpIfInitialized)
-	c.exit(jitCallStatusCodeTypeMismatchOnIndirectCall)
 
-	// Now all checks passeed, so we start making function call.
-	readValue := c.newProg()
-	jumpIfTypeMatch.To.SetTarget(readValue)
-	readValue.As = x86.AMOVQ
-	readValue.To.Type = obj.TYPE_REG
-	readValue.To.Reg = offset.register
-	readValue.From.Offset = tableElementFunctionIndexOffset
-	readValue.From.Type = obj.TYPE_MEM
-	readValue.From.Reg = offset.register
-	c.addInstruction(readValue)
+	// Next we need to check the type matches, i.e. table[offset].source.TypeID == targetFunctionType.
+	//
+	// "tmp = table[offset].source ( == *FunctionInstance type)"
+	getFunctionInstancePointer := c.newProg()
+	getFunctionInstancePointer.As = x86.AMOVQ
+	getFunctionInstancePointer.To.Type = obj.TYPE_REG
+	getFunctionInstancePointer.To.Reg = tmp
+	getFunctionInstancePointer.From.Type = obj.TYPE_MEM
+	getFunctionInstancePointer.From.Reg = offset.register
+	getFunctionInstancePointer.From.Offset = compiledFunctionSourceOffset
+	c.addInstruction(getFunctionInstancePointer)
+
+	ti := c.f.Module.Types[o.TypeIndex]
+	targetFunctionType := ti.Type
+	checkIfTypeMatch := c.newProg()
+	checkIfTypeMatch.As = x86.ACMPL // 32-bit as FunctionTypeID is in 32-bit unsigned integer.
+	checkIfTypeMatch.From.Type = obj.TYPE_MEM
+	checkIfTypeMatch.From.Reg = tmp
+	checkIfTypeMatch.From.Offset = functionInstanceTypeIDOffset
+	checkIfTypeMatch.To.Type = obj.TYPE_CONST
+	checkIfTypeMatch.To.Offset = int64(ti.TypeID)
+	c.addInstruction(checkIfTypeMatch)
+
+	// Jump if the type matches.
+	jumpIfTypeMatch := c.newProg()
+	jumpIfTypeMatch.To.Type = obj.TYPE_BRANCH
+	jumpIfTypeMatch.As = x86.AJEQ
+	c.addInstruction(jumpIfTypeMatch)
 
+	// Otherwise, exit with type mismatch status.
+	c.exit(jitCallStatusCodeTypeMismatchOnIndirectCall)
+
+	c.addSetJmpOrigins(jumpIfTypeMatch)
 	if err := c.callFunctionFromRegister(offset.register, targetFunctionType); err != nil {
 		return nil
 	}
 
 	// The offset register should be marked as un-used as we consumed in the function call.
-	c.locationStack.markRegisterUnused(offset.register)
+	c.locationStack.markRegisterUnused(offset.register, tmp)
 
 	// We consumed the function parameters from the stack after call.
 	for i := 0; i < len(targetFunctionType.Params); i++ {
@@ -4222,6 +4253,7 @@ func (c *amd64Compiler) compileMemoryGrow() error {
 	// After the function call, we have to initialize the stack base pointer and memory reserved registers.
 	c.initializeReservedStackBasePointer()
 	c.initializeReservedMemoryPointer()
+
 	return nil
 }
 
@@ -4560,14 +4592,14 @@ func (c *amd64Compiler) getCallFrameStackPointer(destinationRegister int16) {
 }
 
 // callFunctionFromRegister adds instructions to call a function whose address equals the value on register.
-func (c *amd64Compiler) callFunctionFromRegister(register int16, functype *wasm.FunctionType) error {
-	return c.callFunction(0, register, functype)
+func (c *amd64Compiler) callFunctionFromRegister(compiledFunctionAddressRegister int16, functype *wasm.FunctionType) error {
+	return c.callFunction(0, compiledFunctionAddressRegister, functype)
 
 }
 
 // callFunctionFromConst adds instructions to call a function whose address equals addr constant.
-func (c *amd64Compiler) callFunctionFromConst(addr wasm.FunctionIndex, functype *wasm.FunctionType) error {
-	return c.callFunction(addr, nilRegister, functype)
+func (c *amd64Compiler) callFunctionFromConst(index wasm.Index, functype *wasm.FunctionType) error {
+	return c.callFunction(index, nilRegister, functype)
 }
 
 // callFunction adds instructions to call a function whose address equals either addr parameter or the value on indexReg.
@@ -4576,28 +4608,28 @@ func (c *amd64Compiler) callFunctionFromConst(addr wasm.FunctionIndex, functype
 //
 // Note: this is the counter part for returnFunction, and see the comments there as well
 // to understand how the function calls are achieved.
-func (c *amd64Compiler) callFunction(index wasm.FunctionIndex, indexReg int16, functype *wasm.FunctionType) error {
+func (c *amd64Compiler) callFunction(index wasm.Index, compiledFunctionAddressRegister int16, functype *wasm.FunctionType) error {
 	// Release all the registers as our calling convention requires the caller-save.
 	if err := c.releaseAllRegistersToStack(); err != nil {
 		return err
 	}
 
 	// First, we have to make sure that
-	if !isNilRegister(indexReg) {
-		c.locationStack.markRegisterUsed(indexReg)
+	if !isNilRegister(compiledFunctionAddressRegister) {
+		c.locationStack.markRegisterUsed(compiledFunctionAddressRegister)
 	}
 
 	// Obtain the temporary registers to be used in the followings.
-	regs, found := c.locationStack.takeFreeRegisters(generalPurposeRegisterTypeInt, 5)
+	freeRegs, found := c.locationStack.takeFreeRegisters(generalPurposeRegisterTypeInt, 4)
 	if !found {
-		// This in theory never happen as all the registers must be free except indexReg.
+		// This in theory never happen as all the registers must be free except compiledFunctionAddressRegister.
 		return fmt.Errorf("could not find enough free registers")
 	}
-	c.locationStack.markRegisterUsed(regs...)
+	c.locationStack.markRegisterUsed(freeRegs...)
 
 	// Alias these free tmp registers for readability.
 	callFrameStackPointerRegister, tmpRegister, targetAddressRegister,
-		callFrameStackTopAddressRegister, compiledFunctionIndexRegister := regs[0], regs[1], regs[2], regs[3], regs[4]
+		callFrameStackTopAddressRegister := freeRegs[0], freeRegs[1], freeRegs[2], freeRegs[3]
 
 	// First, we read the current call frame stack pointer.
 	c.getCallFrameStackPointer(callFrameStackPointerRegister)
@@ -4619,10 +4651,10 @@ func (c *amd64Compiler) callFunction(index wasm.FunctionIndex, indexReg int16, f
 	c.addInstruction(jmpIfNotCallFrameStackNeedsGrow)
 
 	// Otherwise, we have to make the builtin function call to grow the call frame stack.
-	if !isNilRegister(indexReg) {
+	if !isNilRegister(compiledFunctionAddressRegister) {
 		// If we need to get the target funcaddr from register (call_indirect case), we must save it before growing callframe stack,
 		// as the register is not saved across function calls.
-		savedOffsetLocation := c.locationStack.pushValueLocationOnRegister(indexReg)
+		savedOffsetLocation := c.locationStack.pushValueLocationOnRegister(compiledFunctionAddressRegister)
 		c.releaseRegisterToStack(savedOffsetLocation)
 	}
 
@@ -4632,13 +4664,13 @@ func (c *amd64Compiler) callFunction(index wasm.FunctionIndex, indexReg int16, f
 	}
 
 	// For call_indirect, we need to push the value back to the register.
-	if !isNilRegister(indexReg) {
+	if !isNilRegister(compiledFunctionAddressRegister) {
 		// Since this is right after callGoFunction, we have to initialize the stack base pointer
 		// to properly load the value on memory stack.
 		c.initializeReservedStackBasePointer()
 
 		savedOffsetLocation := c.locationStack.pop()
-		savedOffsetLocation.setRegister(indexReg)
+		savedOffsetLocation.setRegister(compiledFunctionAddressRegister)
 		c.moveStackToRegister(savedOffsetLocation)
 	}
 
@@ -4756,37 +4788,36 @@ func (c *amd64Compiler) callFunction(index wasm.FunctionIndex, indexReg int16, f
 
 	// 3) Set rc.next to specify which function is executed on the current call frame (needs to make builtin function calls).
 	{
-		// We must set the target function's address(pointer) of *compiledFunction into the next callframe stack.
-		// In the example, this is equivalent to writing the value into "rc.next".
-		//
-		// First, we read the address of the first item of callEngine.compiledFunctions slice (= &callEngine.compiledFunctions[0])
-		// into tmpRegister.
-		readCopmiledFunctionsElement0Address := c.newProg()
-		readCopmiledFunctionsElement0Address.As = x86.AMOVQ
-		readCopmiledFunctionsElement0Address.To.Type = obj.TYPE_REG
-		readCopmiledFunctionsElement0Address.To.Reg = tmpRegister
-		readCopmiledFunctionsElement0Address.From.Type = obj.TYPE_MEM
-		readCopmiledFunctionsElement0Address.From.Reg = reservedRegisterForCallEngine
-		readCopmiledFunctionsElement0Address.From.Offset = callEngineGlobalContextCompiledFunctionsElement0AddressOffset
-		c.addInstruction(readCopmiledFunctionsElement0Address)
-
-		// Next, read the address of the target function (= &callEngine.compiledFunctions[offset])
-		// into compiledFunctionIndexRegister.
-		readCompiledFunctionIndexAddress := c.newProg()
-		readCompiledFunctionIndexAddress.As = x86.AMOVQ
-		readCompiledFunctionIndexAddress.To.Type = obj.TYPE_REG
-		readCompiledFunctionIndexAddress.To.Reg = compiledFunctionIndexRegister
-		readCompiledFunctionIndexAddress.From.Type = obj.TYPE_MEM
-		readCompiledFunctionIndexAddress.From.Reg = tmpRegister
-		if !isNilRegister(indexReg) {
-			readCompiledFunctionIndexAddress.From.Index = indexReg
-			readCompiledFunctionIndexAddress.From.Scale = 8 // because the size of *compiledFunction equals 8 bytes.
-		} else {
+		if isNilRegister(compiledFunctionAddressRegister) {
+			// We must set the target function's address(pointer) of *compiledFunction into the next callframe stack.
+			// In the example, this is equivalent to writing the value into "rc.next".
+			//
+			// First, we read the address of the first item of callEngine.compiledFunctions slice (= &callEngine.compiledFunctions[0])
+			// into tmpRegister.
+			readCopmiledFunctionsElement0Address := c.newProg()
+			readCopmiledFunctionsElement0Address.As = x86.AMOVQ
+			readCopmiledFunctionsElement0Address.To.Type = obj.TYPE_REG
+			readCopmiledFunctionsElement0Address.To.Reg = tmpRegister
+			readCopmiledFunctionsElement0Address.From.Type = obj.TYPE_MEM
+			readCopmiledFunctionsElement0Address.From.Reg = reservedRegisterForCallEngine
+			readCopmiledFunctionsElement0Address.From.Offset = callEngineModuleContextCompiledFunctionsElement0AddressOffset
+			c.addInstruction(readCopmiledFunctionsElement0Address)
+
+			// Next, read the address of the target function (= &callEngine.compiledFunctions[offset])
+			// into targetAddressRegister.
+			readCompiledFunctionIndexAddress := c.newProg()
+			readCompiledFunctionIndexAddress.As = x86.AMOVQ
+			readCompiledFunctionIndexAddress.To.Type = obj.TYPE_REG
+			readCompiledFunctionIndexAddress.To.Reg = targetAddressRegister
+			readCompiledFunctionIndexAddress.From.Type = obj.TYPE_MEM
+			readCompiledFunctionIndexAddress.From.Reg = tmpRegister
 			// Note: FunctionIndex is limited up to 2^27 so this offset never exceeds 32-bit integer.
 			readCompiledFunctionIndexAddress.From.Offset = int64(index) * 8 // because the size of *compiledFunction equals 8 bytes.
-		}
-		c.addInstruction(readCompiledFunctionIndexAddress)
+			c.addInstruction(readCompiledFunctionIndexAddress)
 
+		} else {
+			targetAddressRegister = compiledFunctionAddressRegister
+		}
 		// Finally, we are ready to place the address of the target function's *compiledFunction into the new callframe.
 		// In the example, this is equivalent to set "rc.next".
 		putCompiledFunctionFunctionIndexOnNewCallFrame := c.newProg()
@@ -4795,7 +4826,7 @@ func (c *amd64Compiler) callFunction(index wasm.FunctionIndex, indexReg int16, f
 		putCompiledFunctionFunctionIndexOnNewCallFrame.To.Reg = callFrameStackTopAddressRegister
 		putCompiledFunctionFunctionIndexOnNewCallFrame.To.Offset = callFrameCompiledFunctionOffset
 		putCompiledFunctionFunctionIndexOnNewCallFrame.From.Type = obj.TYPE_REG
-		putCompiledFunctionFunctionIndexOnNewCallFrame.From.Reg = compiledFunctionIndexRegister
+		putCompiledFunctionFunctionIndexOnNewCallFrame.From.Reg = targetAddressRegister
 		c.addInstruction(putCompiledFunctionFunctionIndexOnNewCallFrame)
 	}
 
@@ -4830,15 +4861,12 @@ func (c *amd64Compiler) callFunction(index wasm.FunctionIndex, indexReg int16, f
 	jmpToTargetFunction := c.newProg()
 	jmpToTargetFunction.As = obj.AJMP
 	jmpToTargetFunction.To.Type = obj.TYPE_MEM
-	jmpToTargetFunction.To.Reg = compiledFunctionIndexRegister
+	jmpToTargetFunction.To.Reg = targetAddressRegister
 	jmpToTargetFunction.To.Offset = compiledFunctionCodeInitialAddressOffset
 	c.addInstruction(jmpToTargetFunction)
 
 	// All the registers used are temporary so we mark them unused.
-	c.locationStack.markRegisterUnused(
-		tmpRegister, targetAddressRegister, callFrameStackTopAddressRegister,
-		callFrameStackPointerRegister, compiledFunctionIndexRegister, indexReg,
-	)
+	c.locationStack.markRegisterUnused(freeRegs...)
 
 	// On the function return, we have to initialize the state.
 	// This could be reached after returnFunction(), so callEngine.valueStackContext.stackBasePointer
@@ -5021,16 +5049,18 @@ func (c *amd64Compiler) returnFunction() error {
 // callGoFunction adds instructions to call a Go function whose address equals the addr parameter.
 // jitStatus is set before making call, and it should be either jitCallStatusCodeCallBuiltInFunction or
 // jitCallStatusCodeCallHostFunction.
-func (c *amd64Compiler) callGoFunction(jitStatus jitCallStatusCode, addr wasm.FunctionIndex) error {
-	// Set the functionAddress to the callEngine.exitContext functionCallAddress.
-	setFunctionCallAddress := c.newProg()
-	setFunctionCallAddress.As = x86.AMOVQ
-	setFunctionCallAddress.From.Type = obj.TYPE_CONST
-	setFunctionCallAddress.From.Offset = int64(addr)
-	setFunctionCallAddress.To.Type = obj.TYPE_MEM
-	setFunctionCallAddress.To.Reg = reservedRegisterForCallEngine
-	setFunctionCallAddress.To.Offset = callEngineExitContextFunctionCallAddressOffset
-	c.addInstruction(setFunctionCallAddress)
+func (c *amd64Compiler) callGoFunction(jitStatus jitCallStatusCode, index wasm.Index) error {
+	if jitStatus == jitCallStatusCodeCallBuiltInFunction {
+		// Set the functionAddress to the callEngine.exitContext functionCallAddress.
+		setFunctionCallAddress := c.newProg()
+		setFunctionCallAddress.As = x86.AMOVL
+		setFunctionCallAddress.From.Type = obj.TYPE_CONST
+		setFunctionCallAddress.From.Offset = int64(index)
+		setFunctionCallAddress.To.Type = obj.TYPE_MEM
+		setFunctionCallAddress.To.Reg = reservedRegisterForCallEngine
+		setFunctionCallAddress.To.Offset = callEngineExitContextBuiltinFunctionCallAddressOffset
+		c.addInstruction(setFunctionCallAddress)
+	}
 
 	// Release all the registers as our calling convention requires the caller-save.
 	if err := c.releaseAllRegistersToStack(); err != nil {
@@ -5222,6 +5252,10 @@ func (c *amd64Compiler) exit(status jitCallStatusCode) {
 }
 
 func (c *amd64Compiler) compilePreamble() (err error) {
+	nop := c.newProg()
+	nop.As = obj.ANOP
+	c.addInstruction(nop)
+
 	// We assume all function parameters are already pushed onto the stack by
 	// the caller.
 	c.pushFunctionParams()
@@ -5411,6 +5445,7 @@ func (c *amd64Compiler) initializeModuleContext() error {
 	// * callEngine.moduleContext.tableSliceLen
 	// * callEngine.moduleContext.memoryElement0Address
 	// * callEngine.moduleContext.memorySliceLen
+	// * callEngine.moduleContext.compiledFunctionsElement0Address
 
 	// Update globalElement0Address.
 	//
@@ -5550,6 +5585,46 @@ func (c *amd64Compiler) initializeModuleContext() error {
 		c.addInstruction(putMemoryElement0Address)
 	}
 
+	// Update moduleContext.compiledFunctionsElement0Address
+	{
+		// "tmpRegister = [moduleInstanceAddressRegister + moduleInstanceEngineOffset + interfaceDataOffset] (== *moduleEngine)"
+		//
+		// Go's interface is layed out on memory as two quad words as struct {tab, data uintptr}
+		// where tab points to the interface table, and the latter points to the actual
+		// implementation of interface. This case, we extract "data" pointer as *moduleEngine.
+		// See the following references for detail:
+		// * https://research.swtch.com/interfaces
+		// * https://github.com/golang/go/blob/release-branch.go1.17/src/runtime/runtime2.go#L207-L210
+		readModuleEngineAddress := c.newProg()
+		readModuleEngineAddress.As = x86.AMOVQ
+		readModuleEngineAddress.To.Type = obj.TYPE_REG
+		readModuleEngineAddress.To.Reg = tmpRegister
+		readModuleEngineAddress.From.Type = obj.TYPE_MEM
+		readModuleEngineAddress.From.Reg = moduleInstanceAddressRegister
+		readModuleEngineAddress.From.Offset = moduleInstanceEngineOffset + interfaceDataOffset
+		c.addInstruction(readModuleEngineAddress)
+
+		// "tmpRegister = [tmpRegister + moduleEngineCompiledFunctionsOffset] (== &moduleEngine.compiledFunctions[0])"
+		readCompiledFunctionsElement0Address := c.newProg()
+		readCompiledFunctionsElement0Address.As = x86.AMOVQ
+		readCompiledFunctionsElement0Address.To.Type = obj.TYPE_REG
+		readCompiledFunctionsElement0Address.To.Reg = tmpRegister
+		readCompiledFunctionsElement0Address.From.Type = obj.TYPE_MEM
+		readCompiledFunctionsElement0Address.From.Reg = tmpRegister
+		readCompiledFunctionsElement0Address.From.Offset = moduleEngineCompiledFunctionsOffset
+		c.addInstruction(readCompiledFunctionsElement0Address)
+
+		// "callEngine.moduleContext.compiledFunctionsElement0Address = tmpRegister".
+		writeCompiledFunctionsElement0AddresssIntoCallEngine := c.newProg()
+		writeCompiledFunctionsElement0AddresssIntoCallEngine.As = x86.AMOVQ
+		writeCompiledFunctionsElement0AddresssIntoCallEngine.To.Type = obj.TYPE_MEM
+		writeCompiledFunctionsElement0AddresssIntoCallEngine.To.Reg = reservedRegisterForCallEngine
+		writeCompiledFunctionsElement0AddresssIntoCallEngine.To.Offset = callEngineModuleContextCompiledFunctionsElement0AddressOffset
+		writeCompiledFunctionsElement0AddresssIntoCallEngine.From.Type = obj.TYPE_REG
+		writeCompiledFunctionsElement0AddresssIntoCallEngine.From.Reg = tmpRegister
+		c.addInstruction(writeCompiledFunctionsElement0AddresssIntoCallEngine)
+	}
+
 	c.locationStack.markRegisterUnused(regs...)
 
 	// Set the jump target towards the next instruction for the case where module instance address hasn't changed.
diff --git a/internal/wasm/jit/jit_arm64.go b/internal/wasm/jit/jit_arm64.go
index 6aa23f09..a27160b3 100644
--- a/internal/wasm/jit/jit_arm64.go
+++ b/internal/wasm/jit/jit_arm64.go
@@ -57,11 +57,11 @@ type archContext struct {
 
 const (
 	// callEngineArchContextJITCallReturnAddressOffset is the offset of archContext.jitCallReturnAddress in callEngine.
-	callEngineArchContextJITCallReturnAddressOffset = 128
+	callEngineArchContextJITCallReturnAddressOffset = 120
 	// callEngineArchContextMinimum32BitSignedIntOffset is the offset of archContext.minimum32BitSignedIntAddress in callEngine.
-	callEngineArchContextMinimum32BitSignedIntOffset = 136
+	callEngineArchContextMinimum32BitSignedIntOffset = 128
 	// callEngineArchContextMinimum64BitSignedIntOffset is the offset of archContext.minimum64BitSignedIntAddress in callEngine.
-	callEngineArchContextMinimum64BitSignedIntOffset = 144
+	callEngineArchContextMinimum64BitSignedIntOffset = 136
 )
 
 // jitcall is implemented in jit_arm64.s as a Go Assembler function.
@@ -219,11 +219,17 @@ func (c *arm64Compiler) markRegisterUnused(regs ...int16) {
 func (c *arm64Compiler) compileConstToRegisterInstruction(instruction obj.As, constValue int64, destinationRegister int16) (inst *obj.Prog) {
 	inst = c.newProg()
 	inst.As = instruction
-	inst.From.Type = obj.TYPE_CONST
-	// Note: in raw arm64 assembly, immediates larger than 16-bits
-	// are not supported, but the assembler takes care of this and
-	// emits corresponding (at most) 4-instructions to load such large constants.
-	inst.From.Offset = constValue
+	if constValue == 0 {
+		inst.From.Type = obj.TYPE_REG
+		inst.From.Reg = zeroRegister
+	} else {
+		inst.From.Type = obj.TYPE_CONST
+		// Note: in raw arm64 assembly, immediates larger than 16-bits
+		// are not supported, but the assembler takes care of this and
+		// emits corresponding (at most) 4-instructions to load such large constants.
+		inst.From.Offset = constValue
+	}
+
 	inst.To.Type = obj.TYPE_REG
 	inst.To.Reg = destinationRegister
 	c.addInstruction(inst)
@@ -607,7 +613,7 @@ func (c *arm64Compiler) compileExitFromNativeCode(status jitCallStatusCode) erro
 }
 
 // compileHostFunction implements compiler.compileHostFunction for the arm64 architecture.
-func (c *arm64Compiler) compileHostFunction(address wasm.FunctionIndex) error {
+func (c *arm64Compiler) compileHostFunction() error {
 	// The assembler skips the first instruction so we intentionally add NOP here.
 	// TODO: delete after #233
 	c.compileNOP()
@@ -615,7 +621,7 @@ func (c *arm64Compiler) compileHostFunction(address wasm.FunctionIndex) error {
 	// First we must update the location stack to reflect the number of host function inputs.
 	c.pushFunctionParams()
 
-	if err := c.compileCallGoFunction(jitCallStatusCodeCallHostFunction, address); err != nil {
+	if err := c.compileCallGoFunction(jitCallStatusCodeCallHostFunction, c.f.Index); err != nil {
 		return err
 	}
 	return c.compileReturnFunction()
@@ -1088,12 +1094,12 @@ func (c *arm64Compiler) compileBrTable(o *wazeroir.OperationBrTable) error {
 
 // compileCall implements compiler.compileCall for the arm64 architecture.
 func (c *arm64Compiler) compileCall(o *wazeroir.OperationCall) error {
-	target := c.f.Module.Functions[o.FunctionIndex]
-	return c.compileCallImpl(target.Index, nilRegister, target.Type)
+	tp := c.f.Module.Functions[o.FunctionIndex].Type
+	return c.compileCallImpl(o.FunctionIndex, nilRegister, tp)
 }
 
 // compileCallImpl implements compiler.compileCall and compiler.compileCallIndirect for the arm64 architecture.
-func (c *arm64Compiler) compileCallImpl(addr wasm.FunctionIndex, indexRegister int16, functype *wasm.FunctionType) error {
+func (c *arm64Compiler) compileCallImpl(index wasm.Index, compiledFunctionAddressRegister int16, functype *wasm.FunctionType) error {
 	// Release all the registers as our calling convention requires the caller-save.
 	if err := c.compileReleaseAllRegistersToStack(); err != nil {
 		return err
@@ -1106,7 +1112,7 @@ func (c *arm64Compiler) compileCallImpl(addr wasm.FunctionIndex, indexRegister i
 	c.locationStack.markRegisterUsed(freeRegisters...)
 
 	// Alias for readability.
-	callFrameStackPointerRegister, callFrameStackTopAddressRegister, compiledFunctionIndexRegister, oldStackBasePointer,
+	callFrameStackPointerRegister, callFrameStackTopAddressRegister, compiledFunctionRegister, oldStackBasePointer,
 		tmp := freeRegisters[0], freeRegisters[1], freeRegisters[2], freeRegisters[3], freeRegisters[4]
 
 	// First, we have to check if we need to grow the callFrame stack.
@@ -1127,10 +1133,10 @@ func (c *arm64Compiler) compileCallImpl(addr wasm.FunctionIndex, indexRegister i
 
 	// If these values equal, we need to grow the callFrame stack.
 	// For call_indirect, we need to push the value back to the register.
-	if !isNilRegister(indexRegister) {
+	if !isNilRegister(compiledFunctionAddressRegister) {
 		// If we need to get the target funcaddr from register (call_indirect case), we must save it before growing callframe stack,
 		// as the register is not saved across function calls.
-		savedOffsetLocation := c.locationStack.pushValueLocationOnRegister(indexRegister)
+		savedOffsetLocation := c.locationStack.pushValueLocationOnRegister(compiledFunctionAddressRegister)
 		if err := c.compileReleaseRegisterToStack(savedOffsetLocation); err != nil {
 			return err
 		}
@@ -1141,13 +1147,13 @@ func (c *arm64Compiler) compileCallImpl(addr wasm.FunctionIndex, indexRegister i
 	}
 
 	// For call_indirect, we need to push the value back to the register.
-	if !isNilRegister(indexRegister) {
+	if !isNilRegister(compiledFunctionAddressRegister) {
 		// Since this is right after callGoFunction, we have to initialize the stack base pointer
 		// to properly load the value on memory stack.
 		c.compileReservedStackBasePointerRegisterInitialization()
 
 		savedOffsetLocation := c.locationStack.pop()
-		savedOffsetLocation.setRegister(indexRegister)
+		savedOffsetLocation.setRegister(compiledFunctionAddressRegister)
 		if err := c.compileLoadValueOnStackToRegister(savedOffsetLocation); err != nil {
 			return err
 		}
@@ -1210,29 +1216,23 @@ func (c *arm64Compiler) compileCallImpl(addr wasm.FunctionIndex, indexRegister i
 	// First, we read the address of the first item of ce.compiledFunctions slice (= &ce.compiledFunctions[0])
 	// into tmp.
 	c.compileMemoryToRegisterInstruction(arm64.AMOVD,
-		reservedRegisterForCallEngine, callEngineGlobalContextCompiledFunctionsElement0AddressOffset,
+		reservedRegisterForCallEngine, callEngineModuleContextCompiledFunctionsElement0AddressOffset,
 		tmp)
 
 	// Next, read the index of the target function (= &ce.compiledFunctions[offset])
 	// into compiledFunctionIndexRegister.
-	if isNilRegister(indexRegister) {
+	if isNilRegister(compiledFunctionAddressRegister) {
 		c.compileMemoryToRegisterInstruction(
 			arm64.AMOVD,
-			tmp, int64(addr)*8, // * 8 because the size of *compiledFunction equals 8 bytes.
-			compiledFunctionIndexRegister)
+			tmp, int64(index)*8, // * 8 because the size of *compiledFunction equals 8 bytes.
+			compiledFunctionRegister)
 	} else {
-		// Shift indexRegister by 3 because the size of *compiledFunction equals 8 bytes.
-		c.compileConstToRegisterInstruction(arm64.ALSLW, 3, indexRegister)
-		c.compileMemoryWithRegisterOffsetToRegisterInstruction(
-			arm64.AMOVD,
-			tmp, indexRegister,
-			compiledFunctionIndexRegister,
-		)
+		compiledFunctionRegister = compiledFunctionAddressRegister
 	}
 
 	// Finally, we are ready to write the address of the target function's *compiledFunction into the new callframe.
 	c.compileRegisterToMemoryInstruction(arm64.AMOVD,
-		compiledFunctionIndexRegister,
+		compiledFunctionRegister,
 		callFrameStackTopAddressRegister, callFrameCompiledFunctionOffset)
 
 	// 4) Set ra.current so that we can return back to this function properly.
@@ -1258,7 +1258,7 @@ func (c *arm64Compiler) compileCallImpl(addr wasm.FunctionIndex, indexRegister i
 
 	// Then, br into the target function's initial address.
 	c.compileMemoryToRegisterInstruction(arm64.AMOVD,
-		compiledFunctionIndexRegister, compiledFunctionCodeInitialAddressOffset,
+		compiledFunctionRegister, compiledFunctionCodeInitialAddressOffset,
 		tmp)
 
 	c.compileUnconditionalBranchToAddressOnRegister(tmp)
@@ -1421,54 +1421,52 @@ func (c *arm64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 		reservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset,
 		tmp,
 	)
-	// "offset = tmp + (offset << 4) (== &table[offset])"
+	// "offset = tmp + (offset << 3) (== &table[offset])"
+	// Here we left shiting by 3 in order to get the offset in bytes,
+	// and the table element type is uintptr which is 8 bytes.
 	c.compileAddInstructionWithLeftShiftedRegister(
-		offset.register, 4,
+		offset.register, 3,
 		tmp,
 		offset.register,
 	)
 
-	// Check if table[offset].TypeID == targetFunctionType.
-	ti := c.f.Module.Types[o.TypeIndex]
-	// "tmp = table[offset].TypeID"
+	// "offset = *offset (== table[offset] == *compiledFunction type)"
+	c.compileMemoryToRegisterInstruction(arm64.AMOVD, offset.register, 0, offset.register)
+
+	// Check if the value of table[offset] equals zero, meaning that the target element is uninitialized.
+	c.compileTwoRegistersToNoneInstruction(arm64.ACMP, zeroRegister, offset.register)
+	brIfInitizlied := c.compilelBranchInstruction(arm64.ABNE)
+	if err := c.compileExitFromNativeCode(jitCallStatusCodeInvalidTableAccess); err != nil {
+		return err
+	}
+	c.setBranchTargetOnNext(brIfInitizlied)
+
+	targetFunctionType := c.f.Module.Types[o.TypeIndex]
+	// Next we check the type matches, i.e. table[offset].source.TypeID == targetFunctionType.
+	// "tmp = table[offset].source ( == *FunctionInstance type)"
+	c.compileMemoryToRegisterInstruction(
+		arm64.AMOVD,
+		offset.register, compiledFunctionSourceOffset,
+		tmp,
+	)
+	// "tmp = [tmp + functionInstanceTypeIDOffset] (== table[offset].source.TypeID)"
 	c.compileMemoryToRegisterInstruction(
-		arm64.AMOVD, offset.register, tableElementFunctionTypeIDOffset,
+		arm64.AMOVW, tmp, functionInstanceTypeIDOffset,
 		tmp,
 	)
 	// "reservedRegisterForTemporary = targetFunctionType.TypeID"
-	c.compileConstToRegisterInstruction(arm64.AMOVD, int64(ti.TypeID), reservedRegisterForTemporary)
+	c.compileConstToRegisterInstruction(arm64.AMOVW, int64(targetFunctionType.TypeID), reservedRegisterForTemporary)
+
 	// Compare these two values, and if they equal, we are ready to make function call.
-	c.compileTwoRegistersToNoneInstruction(arm64.ACMP, tmp, reservedRegisterForTemporary)
+	c.compileTwoRegistersToNoneInstruction(arm64.ACMPW, tmp, reservedRegisterForTemporary)
 	brIfTypeMatched := c.compilelBranchInstruction(arm64.ABEQ)
-
-	// Otherwise, we have to exit the execution with either jitCallStatusCodeTypeMismatchOnIndirectCall or jitCallStatusCodeInvalidTableAccess.
-	{
-		// We exit with jitCallStatusCodeInvalidTableAccess if the targetFunctionType.TypeID equals the uninitialized one (wasm.UninitializedTableElementTypeID).
-		c.compileConstToRegisterInstruction(arm64.AMOVD, int64(wasm.UninitializedTableElementTypeID), reservedRegisterForTemporary)
-		c.compileTwoRegistersToNoneInstruction(arm64.ACMPW, tmp, reservedRegisterForTemporary)
-
-		brIfInitizlied := c.compilelBranchInstruction(arm64.ABNE)
-		if err := c.compileExitFromNativeCode(jitCallStatusCodeInvalidTableAccess); err != nil {
-			return err
-		}
-
-		// Otherwise exit with jitCallStatusCodeTypeMismatchOnIndirectCall.
-		c.setBranchTargetOnNext(brIfInitizlied)
-		if err := c.compileExitFromNativeCode(jitCallStatusCodeTypeMismatchOnIndirectCall); err != nil {
-			return err
-		}
+	if err := c.compileExitFromNativeCode(jitCallStatusCodeTypeMismatchOnIndirectCall); err != nil {
+		return err
 	}
 
 	c.setBranchTargetOnNext(brIfTypeMatched)
 
-	// Now all checks passed, so read the target's function address, and make call.
-	c.compileMemoryToRegisterInstruction(
-		arm64.AMOVW,
-		offset.register, tableElementFunctionIndexOffset,
-		offset.register,
-	)
-
-	if err := c.compileCallImpl(0, offset.register, ti.Type); err != nil {
+	if err := c.compileCallImpl(0, offset.register, targetFunctionType.Type); err != nil {
 		return err
 	}
 
@@ -3049,7 +3047,7 @@ func (c *arm64Compiler) compileMemorySize() error {
 // compileCallGoFunction adds instructions to call a Go function whose address equals the addr parameter.
 // jitStatus is set before making call, and it should be either jitCallStatusCodeCallBuiltInFunction or
 // jitCallStatusCodeCallHostFunction.
-func (c *arm64Compiler) compileCallGoFunction(jitStatus jitCallStatusCode, addr wasm.FunctionIndex) error {
+func (c *arm64Compiler) compileCallGoFunction(jitStatus jitCallStatusCode, builtinFunction wasm.Index) error {
 	// Release all the registers as our calling convention requires the caller-save.
 	if err := c.compileReleaseAllRegistersToStack(); err != nil {
 		return err
@@ -3065,20 +3063,22 @@ func (c *arm64Compiler) compileCallGoFunction(jitStatus jitCallStatusCode, addr
 	tmp, currentCallFrameStackPointerRegister, currentCallFrameTopAddressRegister, returnAddressRegister :=
 		freeRegs[0], freeRegs[1], freeRegs[2], freeRegs[3]
 
-	// Set the target function address to ce.functionCallAddress
-	// "tmp = $addr"
-	c.compileConstToRegisterInstruction(
-		arm64.AMOVD,
-		int64(addr),
-		tmp,
-	)
-	// "[reservedRegisterForCallEngine + callEngineExitContextFunctionCallAddressOffset] = tmp"
-	// In other words, "ce.functionCallAddress = tmp (== $addr)"
-	c.compileRegisterToMemoryInstruction(
-		arm64.AMOVD,
-		tmp,
-		reservedRegisterForCallEngine, callEngineExitContextFunctionCallAddressOffset,
-	)
+	if jitStatus == jitCallStatusCodeCallBuiltInFunction {
+		// Set the target function address to ce.functionCallAddress
+		// "tmp = $index"
+		c.compileConstToRegisterInstruction(
+			arm64.AMOVD,
+			int64(builtinFunction),
+			tmp,
+		)
+		// "[reservedRegisterForCallEngine + callEngineExitContextFunctionCallAddressOffset] = tmp"
+		// In other words, "ce.functionCallAddress = tmp (== $addr)"
+		c.compileRegisterToMemoryInstruction(
+			arm64.AMOVW,
+			tmp,
+			reservedRegisterForCallEngine, callEngineExitContextBuiltinFunctionCallAddressOffset,
+		)
+	}
 
 	// Next, we have to set the return address into callFrameStack[ce.callFrameStackPointer-1].returnAddress.
 	//
@@ -3416,11 +3416,12 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 	c.addInstruction(brIfModuleUnchanged)
 
 	// Otherwise, we have to update the following fields:
-	// * ce.moduleContext.globalElement0Address
-	// * ce.moduleContext.memoryElement0Address
-	// * ce.moduleContext.memorySliceLen
-	// * ce.moduleContext.tableElement0Address
-	// * ce.moduleContext.tableSliceLen
+	// * callEngine.moduleContext.globalElement0Address
+	// * callEngine.moduleContext.memoryElement0Address
+	// * callEngine.moduleContext.memorySliceLen
+	// * callEngine.moduleContext.tableElement0Address
+	// * callEngine.moduleContext.tableSliceLen
+	// * callEngine.moduleContext.compiledFunctionsElement0Address
 
 	// Update globalElement0Address.
 	//
@@ -3527,6 +3528,37 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 		)
 	}
 
+	// Update callEngine.moduleContext.compiledFunctionsElement0Address
+	{
+		// "tmpX = [moduleInstanceAddressRegister + moduleInstanceEngineOffset + interfaceDataOffset] (== *moduleEngine)"
+		//
+		// Go's interface is layed out on memory as two quad words as struct {tab, data uintptr}
+		// where tab points to the interface table, and the latter points to the actual
+		// implementation of interface. This case, we extract "data" pointer as *moduleEngine.
+		// See the following references for detail:
+		// * https://research.swtch.com/interfaces
+		// * https://github.com/golang/go/blob/release-branch.go1.17/src/runtime/runtime2.go#L207-L210
+		c.compileMemoryToRegisterInstruction(
+			arm64.AMOVD,
+			moduleInstanceAddressRegister, moduleInstanceEngineOffset+interfaceDataOffset,
+			tmpX,
+		)
+
+		// "tmpY = [tmpX + moduleEngineCompiledFunctionsOffset] (== &moduleEngine.compiledFunctions[0])"
+		c.compileMemoryToRegisterInstruction(
+			arm64.AMOVD,
+			tmpX, moduleEngineCompiledFunctionsOffset,
+			tmpY,
+		)
+
+		// "callEngine.moduleContext.compiledFunctionsElement0Address = tmpY".
+		c.compileRegisterToMemoryInstruction(
+			arm64.AMOVD,
+			tmpY,
+			reservedRegisterForCallEngine, callEngineModuleContextCompiledFunctionsElement0AddressOffset,
+		)
+	}
+
 	c.setBranchTargetOnNext(brIfModuleUnchanged)
 	c.locationStack.markRegisterUnused(regs...)
 	return nil
diff --git a/internal/wasm/jit/jit_arm64.s b/internal/wasm/jit/jit_arm64.s
index d49915a0..b59294fa 100644
--- a/internal/wasm/jit/jit_arm64.s
+++ b/internal/wasm/jit/jit_arm64.s
@@ -11,7 +11,7 @@ TEXT ·jitcall(SB),NOSPLIT|NOFRAME,$0-16
         MOVD ce+8(FP),R0
         // In arm64, return address is stored in R30 after jumping into the code.
         // We save the return address value into archContext.jitReturnAddress in Engine.
-        // Note that the const 128 drifts after editting Engine or archContext struct. See TestArchContextOffsetInEngine.
-        MOVD R30,128(R0)
+        // Note that the const 120 drifts after editting Engine or archContext struct. See TestArchContextOffsetInEngine.
+        MOVD R30,120(R0)
         // Jump to native code.
         JMP (R1)
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index 061987a1..dd98cd5a 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -155,6 +155,12 @@ type Module struct {
 	HostFunctionSection []*reflect.Value
 }
 
+// The wazero specific limitation described at RATIONALE.md.
+const (
+	maximumGlobals       = 1 << 27
+	maximumFunctionIndex = 1 << 27
+)
+
 // TypeOfFunction returns the internalwasm.SectionIDType index for the given function namespace index or nil.
 // Note: The function index namespace is preceded by imported functions.
 // TODO: Returning nil should be impossible when decode results are validated. Validate decode before backfilling tests.
@@ -197,8 +203,6 @@ func (m *Module) Validate(enabledFeatures Features) error {
 
 	functions, globals, memories, tables := m.allDeclarations()
 
-	// The wazero specific limitation described at RATIONALE.md.
-	const maximumGlobals = 1 << 27
 	if err := m.validateGlobals(globals, maximumGlobals); err != nil {
 		return err
 	}
@@ -212,7 +216,7 @@ func (m *Module) Validate(enabledFeatures Features) error {
 	}
 
 	if m.CodeSection != nil {
-		if err := m.validateFunctions(enabledFeatures, functions, globals, memories, tables); err != nil {
+		if err := m.validateFunctions(enabledFeatures, functions, globals, memories, tables, maximumFunctionIndex); err != nil {
 			return err
 		}
 	} else {
@@ -264,7 +268,11 @@ func (m *Module) validateGlobals(globals []*GlobalType, maxGlobals int) error {
 	return nil
 }
 
-func (m *Module) validateFunctions(enabledFeatures Features, functions []Index, globals []*GlobalType, memories []*MemoryType, tables []*TableType) error {
+func (m *Module) validateFunctions(enabledFeatures Features, functions []Index, globals []*GlobalType, memories []*MemoryType, tables []*TableType, maximumFunctionIndex int) error {
+	if len(functions) > maximumFunctionIndex {
+		return fmt.Errorf("too many functions in a store")
+	}
+
 	functionCount := m.SectionElementCount(SectionIDFunction)
 	codeCount := m.SectionElementCount(SectionIDCode)
 	if functionCount == 0 && codeCount == 0 {
@@ -492,6 +500,7 @@ func (m *Module) buildFunctions() (functions []*FunctionInstance) {
 			Type:       m.TypeSection[typeIndex],
 			Body:       m.CodeSection[codeIndex].Body,
 			LocalTypes: m.CodeSection[codeIndex].LocalTypes,
+			Index:      Index(importCount + uint32(codeIndex)),
 		}
 		functions = append(functions, f)
 	}
@@ -511,7 +520,12 @@ func (m *Module) buildMemory() (mem *MemoryInstance) {
 
 func (m *Module) buildTable() (table *TableInstance) {
 	for _, tableSeg := range m.TableSection {
-		table = newTableInstance(tableSeg.Limit.Min, tableSeg.Limit.Max)
+		table = &TableInstance{
+			Table:    make([]uintptr, tableSeg.Limit.Min),
+			Min:      tableSeg.Limit.Min,
+			Max:      tableSeg.Limit.Max,
+			ElemType: 0x70, // funcref
+		}
 	}
 	return
 }
@@ -631,10 +645,14 @@ type Export struct {
 	Index Index
 }
 
+// ElementSegment are initialization instructions for a TableInstance
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-elem
 type ElementSegment struct {
 	TableIndex Index
 	OffsetExpr *ConstantExpression
-	Init       []uint32
+	// Init are positions in the function index namespace.
+	Init []Index
 }
 
 // Code is an entry in the Module.CodeSection containing the locals and body of the function.
@@ -643,6 +661,7 @@ type Code struct {
 	// LocalTypes are any function-scoped variables in insertion order.
 	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-local
 	LocalTypes []ValueType
+
 	// Body is a sequence of expressions ending in OpcodeEnd
 	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-expr
 	Body []byte
diff --git a/internal/wasm/module_context.go b/internal/wasm/module_context.go
index 723a6662..9adec56d 100644
--- a/internal/wasm/module_context.go
+++ b/internal/wasm/module_context.go
@@ -13,20 +13,16 @@ var _ publicwasm.Module = &ModuleContext{}
 func NewModuleContext(ctx context.Context, engine Engine, instance *ModuleInstance) *ModuleContext {
 	return &ModuleContext{
 		ctx:    ctx,
-		engine: engine,
 		memory: instance.Memory,
-		Module: instance,
+		module: instance,
 	}
 }
 
 // ModuleContext implements wasm.Module
 type ModuleContext struct {
 	// ctx is returned by Context and overridden WithContext
-	ctx context.Context
-	// engine is used to implement function.Call
-	engine Engine
-	// Module is exported for spectests
-	Module *ModuleInstance
+	ctx    context.Context
+	module *ModuleInstance
 	// memory is returned by Memory and overridden WithMemory
 	memory publicwasm.Memory
 }
@@ -35,14 +31,14 @@ type ModuleContext struct {
 func (m *ModuleContext) WithMemory(memory *MemoryInstance) *ModuleContext {
 	// only re-allocate if it will change the effective memory
 	if m.memory == nil || (memory != nil && memory.Max != nil && *memory.Max > 0 && memory != m.memory) {
-		return &ModuleContext{engine: m.engine, Module: m.Module, memory: memory, ctx: m.ctx}
+		return &ModuleContext{module: m.module, memory: memory, ctx: m.ctx}
 	}
 	return m
 }
 
 // String implements fmt.Stringer
 func (m *ModuleContext) String() string {
-	return fmt.Sprintf("Module[%s]", m.Module.Name)
+	return fmt.Sprintf("Module[%s]", m.module.Name)
 }
 
 // Context implements wasm.Module Context
@@ -54,19 +50,19 @@ func (m *ModuleContext) Context() context.Context {
 func (m *ModuleContext) WithContext(ctx context.Context) publicwasm.Module {
 	// only re-allocate if it will change the effective context
 	if ctx != nil && ctx != m.ctx {
-		return &ModuleContext{engine: m.engine, Module: m.Module, memory: m.memory, ctx: ctx}
+		return &ModuleContext{module: m.module, memory: m.memory, ctx: ctx}
 	}
 	return m
 }
 
 // Memory implements wasm.Module Memory
 func (m *ModuleContext) Memory() publicwasm.Memory {
-	return m.Module.Memory
+	return m.module.Memory
 }
 
 // ExportedMemory implements wasm.Module ExportedMemory
 func (m *ModuleContext) ExportedMemory(name string) publicwasm.Memory {
-	exp, err := m.Module.getExport(name, ExternTypeMemory)
+	exp, err := m.module.getExport(name, ExternTypeMemory)
 	if err != nil {
 		return nil
 	}
@@ -75,7 +71,7 @@ func (m *ModuleContext) ExportedMemory(name string) publicwasm.Memory {
 
 // ExportedFunction implements wasm.Module ExportedFunction
 func (m *ModuleContext) ExportedFunction(name string) publicwasm.Function {
-	exp, err := m.Module.getExport(name, ExternTypeFunc)
+	exp, err := m.module.getExport(name, ExternTypeFunc)
 	if err != nil {
 		return nil
 	}
@@ -95,15 +91,15 @@ func (f *FunctionInstance) ResultTypes() []publicwasm.ValueType {
 // Call implements wasm.Function Call
 func (f *FunctionInstance) Call(ctx publicwasm.Module, params ...uint64) ([]uint64, error) {
 	if modCtx, ok := ctx.(*ModuleContext); !ok { // allow nil to substitute for the defining module
-		return f.Module.Ctx.engine.Call(f.Module.Ctx, f, params...)
+		return f.Module.Engine.Call(f.Module.Ctx, f, params...)
 	} else { // TODO: check if the importing context is correct
-		return modCtx.engine.Call(modCtx, f, params...)
+		return f.Module.Engine.Call(modCtx, f, params...)
 	}
 }
 
 // ExportedGlobal implements wasm.Module ExportedGlobal
 func (m *ModuleContext) ExportedGlobal(name string) publicwasm.Global {
-	exp, err := m.Module.getExport(name, ExternTypeGlobal)
+	exp, err := m.module.getExport(name, ExternTypeGlobal)
 	if err != nil {
 		return nil
 	}
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index 210f62de..db9d3a1c 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"math"
 	"reflect"
 	"sync"
 
@@ -47,29 +46,10 @@ type (
 		// do type-checks on indirect function calls.
 		typeIDs map[string]FunctionTypeID
 
-		// maximumFunctionIndex represents the limit on the number of function addresses (= function instances) in a store.
-		// Note: this is fixed to 2^27 but have this a field for testability.
-		maximumFunctionIndex FunctionIndex
-
 		//  maximumFunctionTypes represents the limit on the number of function types in a store.
 		// Note: this is fixed to 2^27 but have this a field for testability.
 		maximumFunctionTypes int
 
-		// releasedFunctionIndex holds reusable FunctionIndexes. An index is added when
-		// a function instance is released in releaseFunctions, and is popped when
-		// a new instance is added in addFunctions.
-		releasedFunctionIndex map[FunctionIndex]struct{}
-
-		// functions holds function instances (https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-instances%E2%91%A0),
-		// in this store.
-		// The slice index is to be interpreted as funcaddr (https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-funcaddr).
-		//
-		// Note: Functions are held by store as well as ModuleInstances, in contrast to other instances (memory, table, and globals)
-		// which are only owned by ModuleInstance. This is not only because the function call implementation in engines depend on
-		// store-scope funcaddr (FunctionIndex in wazero), but also call_indirect is specified to make function calls via funcaddr,
-		// which might end up calling a function outside of module-scope index space.
-		functions []*FunctionInstance
-
 		// mux is used to guard the fields from concurrent access.
 		mux sync.RWMutex
 	}
@@ -92,6 +72,9 @@ type (
 		// Ctx holds default function call context from this function instance.
 		Ctx *ModuleContext
 
+		// Engine implements function calls for this module.
+		Engine ModuleEngine
+
 		// mux is used to guard the fields from concurrent access.
 		mux sync.Mutex
 
@@ -149,13 +132,12 @@ type (
 
 		// ModuleInstance holds the pointer to the module instance to which this function belongs.
 		Module *ModuleInstance
+
 		// TypeID is assigned by a store for FunctionType.
 		TypeID FunctionTypeID
-		// Index is the index of this function instance in Store.functions, and is exported because
-		// all function calls are made via funcaddr at runtime, not the index (scoped to a module).
-		//
-		// This is used by both host and non-host functions.
-		Index FunctionIndex
+
+		// Index holds the index of this function instance in Module.
+		Index Index
 	}
 
 	// TypeInstance is a store-specific representation of FunctionType where the function type
@@ -180,39 +162,13 @@ type (
 	// Note this is fixed to function type until post 20191205 reference type is implemented.
 	TableInstance struct {
 		// Table holds the table elements managed by this table instance.
-		//
-		// Note: we intentionally use "[]TableElement", not "[]*TableElement",
-		// because the JIT Engine accesses this slice directly from assembly.
-		// If pointer type is used, the access becomes two level indirection (two hops of pointer jumps)
-		// which is a bit costly. TableElement is 96 bit (32 and 64 bit fields) so the cost of using value type
-		// would be ignorable.
-		Table []TableElement
+		Table []uintptr
 		Min   uint32
 		Max   *uint32
 		// Currently fixed to 0x70 (funcref type).
 		ElemType byte
 	}
 
-	// TableElement represents an item in a table instance.
-	//
-	// Note: this is fixed to function type as it is the only supported type in WebAssembly 1.0 (20191205)
-	TableElement struct {
-		// FunctionIndex is funcaddr (https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-funcaddr)
-		// of the target function instance. More precisely, this equals the index of
-		// the target function instance in Store.FunctionInstances.
-		FunctionIndex FunctionIndex
-		// FunctionTypeID is the type ID of the target function's type, which
-		// equals store.Functions[FunctionIndex].TypeID.
-		FunctionTypeID FunctionTypeID
-	}
-
-	// FunctionIndex is funcaddr (https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-funcaddr),
-	// and the index to Store.Functions.
-	FunctionIndex storeIndex
-
-	// storeIndex represents the offset in of an instance in a store.
-	storeIndex uint64
-
 	// FunctionTypeID is a uniquely assigned integer for a function type.
 	// This is wazero specific runtime object and specific to a store,
 	// and used at runtime to do type-checks on indirect function calls.
@@ -221,7 +177,6 @@ type (
 
 // The wazero specific limitations described at RATIONALE.md.
 const (
-	maximumFunctionIndex = 1 << 27
 	maximumFunctionTypes = 1 << 27
 )
 
@@ -269,9 +224,13 @@ func (m *ModuleInstance) buildExports(exports map[string]*Export) {
 			ei = &ExportInstance{Type: exp.Type, Function: m.Functions[index]}
 			// The module instance of the host function is a fake that only includes the function and its types.
 			// We need to assign the ModuleInstance when re-exporting so that any memory defined in the target is
-			// available to the wasm.Module Memory.
+			// available to the wasm.ModuleContext Memory.
+			//
+			// TODO(adrian): shouldn't this be race if multiple modules re-exporting?
+			// maybe we should limit this to only tests.
 			if ei.Function.GoFunc != nil {
 				ei.Function.Module = m
+				ei.Function.Index = index
 			}
 		case ExternTypeGlobal:
 			ei = &ExportInstance{Type: exp.Type, Global: m.Globals[index]}
@@ -328,11 +287,7 @@ func (m *ModuleInstance) applyElements(elements []*ElementSegment) {
 		table := m.Table.Table
 		for i, elm := range elem.Init {
 			pos := i + offset
-			targetFunc := m.Functions[elm]
-			table[pos] = TableElement{
-				FunctionIndex:  targetFunc.Index,
-				FunctionTypeID: targetFunc.TypeID,
-			}
+			table[pos] = m.Engine.FunctionAddress(elm)
 		}
 	}
 }
@@ -351,28 +306,16 @@ func (m *ModuleInstance) getExport(name string, et ExternType) (*ExportInstance,
 
 func NewStore(ctx context.Context, engine Engine, enabledFeatures Features) *Store {
 	return &Store{
-		ctx:                   ctx,
-		engine:                engine,
-		EnabledFeatures:       enabledFeatures,
-		moduleNames:           map[string]struct{}{},
-		modules:               map[string]*ModuleInstance{},
-		typeIDs:               map[string]FunctionTypeID{},
-		maximumFunctionIndex:  maximumFunctionIndex,
-		maximumFunctionTypes:  maximumFunctionTypes,
-		releasedFunctionIndex: map[FunctionIndex]struct{}{},
+		ctx:                  ctx,
+		engine:               engine,
+		EnabledFeatures:      enabledFeatures,
+		moduleNames:          map[string]struct{}{},
+		modules:              map[string]*ModuleInstance{},
+		typeIDs:              map[string]FunctionTypeID{},
+		maximumFunctionTypes: maximumFunctionTypes,
 	}
 }
 
-// checkFunctionIndexOverflow checks if there would be too many function instances in a store.
-func (s *Store) checkFunctionIndexOverflow(newInstanceNum int) error {
-	s.mux.RLock()
-	defer s.mux.RUnlock()
-	if len(s.functions) > int(s.maximumFunctionIndex)-newInstanceNum {
-		return fmt.Errorf("too many functions in a store")
-	}
-	return nil
-}
-
 // Instantiate uses name instead of the Module.NameSection ModuleName as it allows instantiating the same module under
 // different names safely and concurrently.
 func (s *Store) Instantiate(module *Module, name string) (*ModuleContext, error) {
@@ -380,13 +323,6 @@ func (s *Store) Instantiate(module *Module, name string) (*ModuleContext, error)
 		return nil, err
 	}
 
-	// Note: we do not take lock here in order to enable concurrent instantiation and compilation
-	// of multiple modules. When necessary, we take read or write locks in each method of store used here.
-	if err := s.checkFunctionIndexOverflow(len(module.FunctionSection)); err != nil {
-		s.deleteModule(name)
-		return nil, err
-	}
-
 	types, err := s.getTypes(module.TypeSection)
 	if err != nil {
 		s.deleteModule(name)
@@ -425,6 +361,12 @@ func (s *Store) Instantiate(module *Module, name string) (*ModuleContext, error)
 	m.addSections(module, importedFunctions, functions, importedGlobals,
 		globals, importedTable, table, importedMemory, memory, types, moduleImports)
 
+	// Plus we are ready to compile functions.
+	m.Engine, err = s.engine.Compile(importedFunctions, functions)
+	if err != nil {
+		return nil, fmt.Errorf("compilation failed: %w", err)
+	}
+
 	if err = m.validateElements(module.ElementSection); err != nil {
 		s.deleteModule(name)
 		return nil, err
@@ -435,18 +377,6 @@ func (s *Store) Instantiate(module *Module, name string) (*ModuleContext, error)
 		return nil, err
 	}
 
-	// Now we are ready to compile functions.
-	s.addFunctions(functions...) // Need to assign funcaddr to each instance before compilation.
-	for i, f := range functions {
-		// TODO: maybe better consider spawning multiple goroutines for compilations to accelerate.
-		if err = s.engine.Compile(f); err != nil {
-			s.deleteModule(name)
-			// On the failure, release the assigned funcaddr and already compiled functions.
-			_ = s.releaseFunctions(functions[:i]...) // ignore any release error, so we can report the original one.
-			return nil, fmt.Errorf("%s compilation failed: %w", module.funcDesc(funcSection, Index(i)), err)
-		}
-	}
-
 	// Now all the validation passes, we are safe to mutate memory/table instances (possibly imported ones).
 	m.applyElements(module.ElementSection)
 	m.applyData(module.DataSection)
@@ -460,7 +390,8 @@ func (s *Store) Instantiate(module *Module, name string) (*ModuleContext, error)
 	// Execute the start function.
 	if module.StartSection != nil {
 		funcIdx := *module.StartSection
-		if _, err = s.engine.Call(m.Ctx, m.Functions[funcIdx]); err != nil {
+		f := m.Functions[funcIdx]
+		if _, err := f.Module.Engine.Call(m.Ctx, f); err != nil {
 			s.deleteModule(name)
 			return nil, fmt.Errorf("start %s failed: %w", module.funcDesc(funcSection, funcIdx), err)
 		}
@@ -492,7 +423,7 @@ func (s *Store) ReleaseModule(moduleName string) error {
 		mod.decDependentCount()
 	}
 
-	if err := s.releaseFunctions(m.Functions...); err != nil {
+	if err := m.Engine.Release(); err != nil {
 		return fmt.Errorf("unable to release function instance: %w", err)
 	}
 
@@ -512,44 +443,6 @@ func (m *ModuleInstance) incDependentCount() {
 	m.dependentCount++
 }
 
-func (s *Store) releaseFunctions(fs ...*FunctionInstance) error {
-	s.mux.Lock()
-	defer s.mux.Unlock()
-
-	for _, f := range fs {
-		if err := s.engine.Release(f); err != nil {
-			return err
-		}
-
-		// Release reference to the function instance.
-		s.functions[f.Index] = nil
-
-		// Append the address so that we can reuse it in order to avoid index space explosion.
-		s.releasedFunctionIndex[f.Index] = struct{}{}
-	}
-	return nil
-}
-
-func (s *Store) addFunctions(fs ...*FunctionInstance) {
-	s.mux.Lock()
-	defer s.mux.Unlock()
-	for _, f := range fs {
-		var index FunctionIndex
-		if len(s.releasedFunctionIndex) > 0 {
-			for popped := range s.releasedFunctionIndex {
-				index = popped
-				break
-			}
-			s.functions[index] = f
-			delete(s.releasedFunctionIndex, index)
-		} else {
-			index = FunctionIndex(len(s.functions))
-			s.functions = append(s.functions, f)
-		}
-		f.Index = index
-	}
-}
-
 // deleteModule makes the moduleName available for instantiation again.
 func (s *Store) deleteModule(moduleName string) {
 	s.mux.Lock()
@@ -773,21 +666,3 @@ func (s *Store) getTypeInstance(t *FunctionType) (*TypeInstance, error) {
 	}
 	return &TypeInstance{Type: t, TypeID: id}, nil
 }
-
-func newTableInstance(min uint32, max *uint32) *TableInstance {
-	tableInst := &TableInstance{
-		Table:    make([]TableElement, min),
-		Min:      min,
-		Max:      max,
-		ElemType: 0x70, // funcref
-	}
-	for i := range tableInst.Table {
-		tableInst.Table[i] = TableElement{
-			FunctionTypeID: UninitializedTableElementTypeID,
-		}
-	}
-	return tableInst
-}
-
-// UninitializedTableElementTypeID math.MaxUint32 to represent the uninitialized elements.
-const UninitializedTableElementTypeID FunctionTypeID = math.MaxUint32


Test patch:
diff --git a/internal/wasm/func_validation_test.go b/internal/wasm/func_validation_test.go
index 8b945c7c..ec9b6e9d 100644
--- a/internal/wasm/func_validation_test.go
+++ b/internal/wasm/func_validation_test.go
@@ -81,7 +81,7 @@ func TestValidateFunction_SignExtensionOps(t *testing.T) {
 					body = append(body, OpcodeI64Const)
 				}
 				body = append(body, tc.input, 123, OpcodeDrop, OpcodeEnd)
-				err := validateFunction(Features20191205, &FunctionType{}, body, nil, nil, nil, nil, nil, nil, maxStackHeight)
+				err := validateFunction(FeatureSignExtensionOps, &FunctionType{}, body, nil, nil, nil, nil, nil, nil, maxStackHeight)
 				require.NoError(t, err)
 			})
 		})
diff --git a/internal/wasm/interpreter/interpreter_test.go b/internal/wasm/interpreter/interpreter_test.go
index 64c59ba9..f279ee29 100644
--- a/internal/wasm/interpreter/interpreter_test.go
+++ b/internal/wasm/interpreter/interpreter_test.go
@@ -100,23 +100,25 @@ func TestEngine_Call_HostFn(t *testing.T) {
 		},
 		Module: module,
 	}
-	require.NoError(t, e.Compile(f))
+
+	modEngine, err := e.Compile(nil, []*wasm.FunctionInstance{f})
+	require.NoError(t, err)
 
 	t.Run("defaults to module memory when call stack empty", func(t *testing.T) {
 		// When calling a host func directly, there may be no stack. This ensures the module's memory is used.
-		results, err := e.Call(modCtx, f, 3)
+		results, err := modEngine.Call(modCtx, f, 3)
 		require.NoError(t, err)
 		require.Equal(t, uint64(3), results[0])
 		require.Same(t, memory, ctxMemory)
 	})
 
 	t.Run("errs when not enough parameters", func(t *testing.T) {
-		_, err := e.Call(modCtx, f)
+		_, err := modEngine.Call(modCtx, f)
 		require.EqualError(t, err, "expected 1 params, but passed 0")
 	})
 
 	t.Run("errs when too many parameters", func(t *testing.T) {
-		_, err := e.Call(modCtx, f, 1, 2)
+		_, err := modEngine.Call(modCtx, f, 1, 2)
 		require.EqualError(t, err, "expected 1 params, but passed 2")
 	})
 }
@@ -165,6 +167,7 @@ func TestCallEngine_callNativeFunc_signExtend(t *testing.T) {
 			t.Run(fmt.Sprintf("%s(i32.const(0x%x))", wasm.InstructionName(tc.opcode), tc.in), func(t *testing.T) {
 				ce := &callEngine{}
 				f := &compiledFunction{
+					moduleEngine: &moduleEngine{},
 					funcInstance: &wasm.FunctionInstance{Module: &wasm.ModuleInstance{}},
 					body: []*interpreterOp{
 						{kind: wazeroir.OperationKindConstI32, us: []uint64{uint64(uint32(tc.in))}},
@@ -217,6 +220,7 @@ func TestCallEngine_callNativeFunc_signExtend(t *testing.T) {
 			t.Run(fmt.Sprintf("%s(i64.const(0x%x))", wasm.InstructionName(tc.opcode), tc.in), func(t *testing.T) {
 				ce := &callEngine{}
 				f := &compiledFunction{
+					moduleEngine: &moduleEngine{},
 					funcInstance: &wasm.FunctionInstance{Module: &wasm.ModuleInstance{}},
 					body: []*interpreterOp{
 						{kind: wazeroir.OperationKindConstI64, us: []uint64{uint64(tc.in)}},
diff --git a/internal/wasm/jit/engine_test.go b/internal/wasm/jit/engine_test.go
index fd4eee0e..eb806e21 100644
--- a/internal/wasm/jit/engine_test.go
+++ b/internal/wasm/jit/engine_test.go
@@ -16,6 +16,9 @@ import (
 
 // Ensures that the offset consts do not drift when we manipulate the target structs.
 func TestVerifyOffsetValue(t *testing.T) {
+	var me moduleEngine
+	require.Equal(t, int(unsafe.Offsetof(me.compiledFunctions)), moduleEngineCompiledFunctionsOffset)
+
 	var ce callEngine
 	// Offsets for callEngine.globalContext.
 	require.Equal(t, int(unsafe.Offsetof(ce.valueStackElement0Address)), callEngineGlobalContextValueStackElement0AddressOffset)
@@ -23,7 +26,6 @@ func TestVerifyOffsetValue(t *testing.T) {
 	require.Equal(t, int(unsafe.Offsetof(ce.callFrameStackElementZeroAddress)), callEngineGlobalContextCallFrameStackElement0AddressOffset)
 	require.Equal(t, int(unsafe.Offsetof(ce.callFrameStackLen)), callEngineGlobalContextCallFrameStackLenOffset)
 	require.Equal(t, int(unsafe.Offsetof(ce.callFrameStackPointer)), callEngineGlobalContextCallFrameStackPointerOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.compiledFunctionsElement0Address)), callEngineGlobalContextCompiledFunctionsElement0AddressOffset)
 
 	// Offsets for callEngine.moduleContext.
 	require.Equal(t, int(unsafe.Offsetof(ce.moduleInstanceAddress)), callEngineModuleContextModuleInstanceAddressOffset)
@@ -32,6 +34,7 @@ func TestVerifyOffsetValue(t *testing.T) {
 	require.Equal(t, int(unsafe.Offsetof(ce.memorySliceLen)), callEngineModuleContextMemorySliceLenOffset)
 	require.Equal(t, int(unsafe.Offsetof(ce.tableElement0Address)), callEngineModuleContextTableElement0AddressOffset)
 	require.Equal(t, int(unsafe.Offsetof(ce.tableSliceLen)), callEngineModuleContextTableSliceLenOffset)
+	require.Equal(t, int(unsafe.Offsetof(ce.compiledFunctionsElement0Address)), callEngineModuleContextCompiledFunctionsElement0AddressOffset)
 
 	// Offsets for callEngine.valueStackContext
 	require.Equal(t, int(unsafe.Offsetof(ce.stackPointer)), callEngineValueStackContextStackPointerOffset)
@@ -39,7 +42,7 @@ func TestVerifyOffsetValue(t *testing.T) {
 
 	// Offsets for callEngine.exitContext.
 	require.Equal(t, int(unsafe.Offsetof(ce.statusCode)), callEngineExitContextJITCallStatusCodeOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.functionCallAddress)), callEngineExitContextFunctionCallAddressOffset)
+	require.Equal(t, int(unsafe.Offsetof(ce.builtinFunctionCallIndex)), callEngineExitContextBuiltinFunctionCallAddressOffset)
 
 	// Size and offsets for callFrame.
 	var frame callFrame
@@ -55,17 +58,17 @@ func TestVerifyOffsetValue(t *testing.T) {
 	var compiledFunc compiledFunction
 	require.Equal(t, int(unsafe.Offsetof(compiledFunc.codeInitialAddress)), compiledFunctionCodeInitialAddressOffset)
 	require.Equal(t, int(unsafe.Offsetof(compiledFunc.stackPointerCeil)), compiledFunctionStackPointerCeilOffset)
-
-	// Offsets for wasm.TableElement.
-	var tableElement wasm.TableElement
-	require.Equal(t, int(unsafe.Offsetof(tableElement.FunctionIndex)), tableElementFunctionIndexOffset)
-	require.Equal(t, int(unsafe.Offsetof(tableElement.FunctionTypeID)), tableElementFunctionTypeIDOffset)
+	require.Equal(t, int(unsafe.Offsetof(compiledFunc.source)), compiledFunctionSourceOffset)
 
 	// Offsets for wasm.ModuleInstance.
 	var moduleInstance wasm.ModuleInstance
 	require.Equal(t, int(unsafe.Offsetof(moduleInstance.Globals)), moduleInstanceGlobalsOffset)
 	require.Equal(t, int(unsafe.Offsetof(moduleInstance.Memory)), moduleInstanceMemoryOffset)
 	require.Equal(t, int(unsafe.Offsetof(moduleInstance.Table)), moduleInstanceTableOffset)
+	require.Equal(t, int(unsafe.Offsetof(moduleInstance.Engine)), moduleInstanceEngineOffset)
+
+	var functionInstance wasm.FunctionInstance
+	require.Equal(t, int(unsafe.Offsetof(functionInstance.TypeID)), functionInstanceTypeIDOffset)
 
 	// Offsets for wasm.Table.
 	var tableInstance wasm.TableInstance
@@ -83,6 +86,15 @@ func TestVerifyOffsetValue(t *testing.T) {
 	// Offsets for wasm.GlobalInstance
 	var globalInstance wasm.GlobalInstance
 	require.Equal(t, int(unsafe.Offsetof(globalInstance.Val)), globalInstanceValueOffset)
+
+	// Offsets for Go's interface.
+	// The underlying struct is not exposed in the public API, so we simulate it here.
+	// https://github.com/golang/go/blob/release-branch.go1.17/src/runtime/runtime2.go#L207-L210
+	var eface struct {
+		_type *struct{}
+		data  unsafe.Pointer
+	}
+	require.Equal(t, int(unsafe.Offsetof(eface.data)), interfaceDataOffset)
 }
 
 func TestEngine_Call(t *testing.T) {
@@ -143,23 +155,25 @@ func TestEngine_Call_HostFn(t *testing.T) {
 		},
 		Module: module,
 	}
-	require.NoError(t, e.Compile(f))
+
+	modEngine, err := e.Compile(nil, []*wasm.FunctionInstance{f})
+	require.NoError(t, err)
 
 	t.Run("defaults to module memory when call stack empty", func(t *testing.T) {
 		// When calling a host func directly, there may be no stack. This ensures the module's memory is used.
-		results, err := e.Call(modCtx, f, 3)
+		results, err := modEngine.Call(modCtx, f, 3)
 		require.NoError(t, err)
 		require.Equal(t, uint64(3), results[0])
 		require.Same(t, memory, ctxMemory)
 	})
 
 	t.Run("errs when not enough parameters", func(t *testing.T) {
-		_, err := e.Call(modCtx, f)
+		_, err := modEngine.Call(modCtx, f)
 		require.EqualError(t, err, "expected 1 params, but passed 0")
 	})
 
 	t.Run("errs when too many parameters", func(t *testing.T) {
-		_, err := e.Call(modCtx, f, 1, 2)
+		_, err := modEngine.Call(modCtx, f, 1, 2)
 		require.EqualError(t, err, "expected 1 params, but passed 2")
 	})
 }
diff --git a/internal/wasm/jit/jit_amd64_test.go b/internal/wasm/jit/jit_amd64_test.go
index 35a27d07..b138d679 100644
--- a/internal/wasm/jit/jit_amd64_test.go
+++ b/internal/wasm/jit/jit_amd64_test.go
@@ -13,7 +13,6 @@ import (
 	"unsafe"
 
 	"github.com/stretchr/testify/require"
-	asm "github.com/twitchyliquid64/golang-asm"
 	"github.com/twitchyliquid64/golang-asm/obj"
 	"github.com/twitchyliquid64/golang-asm/obj/x86"
 
@@ -32,14 +31,10 @@ func (j *jitEnv) stackTopAsUint16() uint16 {
 
 func (j *jitEnv) requireNewCompiler(t *testing.T) *amd64Compiler {
 	requireSupportedOSArch(t)
-
-	b, err := asm.NewBuilder("amd64", 128)
+	ret, release, err := newCompiler(&wasm.FunctionInstance{Module: j.moduleInstance, Kind: wasm.FunctionKindWasm}, nil)
+	t.Cleanup(release)
 	require.NoError(t, err)
-	return &amd64Compiler{builder: b,
-		locationStack: newValueLocationStack(),
-		labels:        map[string]*labelInfo{},
-		f:             &wasm.FunctionInstance{Module: j.moduleInstance, Kind: wasm.FunctionKindWasm},
-	}
+	return ret.(*amd64Compiler)
 }
 
 func (c *amd64Compiler) movIntConstToRegister(val int64, targetRegister int16) *obj.Prog {
@@ -56,33 +51,37 @@ func (c *amd64Compiler) movIntConstToRegister(val int64, targetRegister int16) *
 func TestAmd64Compiler_maybeGrowValueStack(t *testing.T) {
 	t.Run("not grow", func(t *testing.T) {
 		for _, baseOffset := range []uint64{5, 10, 20} {
+			baseOffset := baseOffset
+			t.Run(fmt.Sprintf("%d", baseOffset), func(t *testing.T) {
 
-			env := newJITEnvironment()
-			compiler := env.requireNewCompiler(t)
+				env := newJITEnvironment()
+				compiler := env.requireNewCompiler(t)
 
-			compiler.initializeReservedStackBasePointer()
-			err := compiler.maybeGrowValueStack()
-			require.NoError(t, err)
-			require.NotNil(t, compiler.onStackPointerCeilDeterminedCallBack)
+				compiler.initializeReservedStackBasePointer()
+				err := compiler.maybeGrowValueStack()
+				require.NoError(t, err)
+				require.NotNil(t, compiler.onStackPointerCeilDeterminedCallBack)
 
-			valueStackLen := uint64(len(env.stack()))
-			stackPointerCeil := uint64(5)
-			stackBasePointer := valueStackLen - baseOffset // Base + Max <= valueStackLen = no need to grow!
-			compiler.onStackPointerCeilDeterminedCallBack(stackPointerCeil)
-			compiler.onStackPointerCeilDeterminedCallBack = nil
-			env.setValueStackBasePointer(stackBasePointer)
+				valueStackLen := uint64(len(env.stack()))
+				stackPointerCeil := uint64(5)
+				stackBasePointer := valueStackLen - baseOffset // Base + Max <= valueStackLen = no need to grow!
+				compiler.onStackPointerCeilDeterminedCallBack(stackPointerCeil)
+				compiler.onStackPointerCeilDeterminedCallBack = nil
+				env.setValueStackBasePointer(stackBasePointer)
 
-			compiler.exit(jitCallStatusCodeReturned)
+				compiler.exit(jitCallStatusCodeReturned)
 
-			// Generate the code under test.
-			code, _, _, err := compiler.compile()
-			require.NoError(t, err)
+				// Generate the code under test.
+				code, _, _, err := compiler.compile()
+				require.NoError(t, err)
 
-			// Run codes
-			env.exec(code)
+				// Run codes
+				env.exec(code)
+
+				// The status code must be "Returned", not "BuiltinFunctionCall".
+				require.Equal(t, jitCallStatusCodeReturned, env.jitStatus())
+			})
 
-			// The status code must be "Returned", not "BuiltinFunctionCall".
-			require.Equal(t, jitCallStatusCodeReturned, env.jitStatus())
 		}
 	})
 	t.Run("grow", func(t *testing.T) {
@@ -157,46 +156,52 @@ func TestAmd64Compiler_returnFunction(t *testing.T) {
 	})
 	t.Run("deep call stack", func(t *testing.T) {
 		env := newJITEnvironment()
-		engine := env.engine()
+		me := env.moduleEngine()
 		ce := env.callEngine()
 
 		// Push the call frames.
 		const callFrameNums = 10
 		stackPointerToExpectedValue := map[uint64]uint32{}
-		for funcaddr := wasm.FunctionIndex(0); funcaddr < callFrameNums; funcaddr++ {
-			//	Each function pushes its funcaddr and soon returns.
-			compiler := env.requireNewCompiler(t)
-			err := compiler.compilePreamble()
-			require.NoError(t, err)
+		for funcindex := wasm.Index(0); funcindex < callFrameNums; funcindex++ {
+			// We have to do compilation in a separate subtest since each compilation takes
+			// the mutext lock and must release on the cleanup of each subtest.
+			// TODO: delete after https://github.com/tetratelabs/wazero/issues/233
+			t.Run(fmt.Sprintf("compiling existing callframe %d", funcindex), func(t *testing.T) {
 
-			// Push its funcaddr.
-			expValue := uint32(funcaddr)
-			err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: expValue})
-			require.NoError(t, err)
+				//	Each function pushes its funcindex and soon returns.
+				compiler := env.requireNewCompiler(t)
+				err := compiler.compilePreamble()
+				require.NoError(t, err)
 
-			// And then return.
-			err = compiler.returnFunction()
-			require.NoError(t, err)
+				// Push its funcindex.
+				expValue := uint32(funcindex)
+				err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: expValue})
+				require.NoError(t, err)
 
-			code, _, _, err := compiler.compile()
-			require.NoError(t, err)
+				// And then return.
+				err = compiler.returnFunction()
+				require.NoError(t, err)
 
-			// Compiles and adds to the engine.
-			codeInitialAddress := uintptr(unsafe.Pointer(&code[0]))
-			compiledFunction := &compiledFunction{codeSegment: code, codeInitialAddress: codeInitialAddress}
-			engine.addCompiledFunction(funcaddr, compiledFunction)
-
-			// Pushes the frame whose return address equals the beginning of the function just compiled ^.
-			frame := callFrame{
-				returnAddress: codeInitialAddress,
-				// Note that return stack base pointer is set to funcaddr*5 and this is where the const should be pushed.
-				returnStackBasePointer: uint64(funcaddr) * 5,
-				compiledFunction:       compiledFunction,
-			}
-			ce.callFrameStack[ce.globalContext.callFrameStackPointer] = frame
-			ce.globalContext.callFrameStackPointer++
+				code, _, _, err := compiler.compile()
+				require.NoError(t, err)
+
+				// Compiles and adds to the engine.
+				codeInitialAddress := uintptr(unsafe.Pointer(&code[0]))
+				compiledFunction := &compiledFunction{codeSegment: code, codeInitialAddress: codeInitialAddress}
+				me.compiledFunctions = append(me.compiledFunctions, compiledFunction)
+
+				// Pushes the frame whose return address equals the beginning of the function just compiled ^.
+				frame := callFrame{
+					returnAddress: codeInitialAddress,
+					// Note that return stack base pointer is set to funcindex*5 and this is where the const should be pushed.
+					returnStackBasePointer: uint64(funcindex) * 5,
+					compiledFunction:       compiledFunction,
+				}
+				ce.callFrameStack[ce.globalContext.callFrameStackPointer] = frame
+				ce.globalContext.callFrameStackPointer++
 
-			stackPointerToExpectedValue[frame.returnStackBasePointer] = expValue
+				stackPointerToExpectedValue[frame.returnStackBasePointer] = expValue
+			})
 		}
 
 		require.Equal(t, uint64(callFrameNums), env.callFrameStackPointer())
@@ -215,6 +220,7 @@ func TestAmd64Compiler_returnFunction(t *testing.T) {
 }
 
 func TestAmd64Compiler_initializeModuleContext(t *testing.T) {
+	modEngine := &moduleEngine{compiledFunctions: make([]*compiledFunction, 10)}
 	for _, tc := range []struct {
 		name           string
 		moduleInstance *wasm.ModuleInstance
@@ -222,22 +228,25 @@ func TestAmd64Compiler_initializeModuleContext(t *testing.T) {
 		{
 			name: "no nil",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine:  modEngine,
 				Memory:  &wasm.MemoryInstance{Buffer: make([]byte, 10)},
-				Table:   &wasm.TableInstance{Table: make([]wasm.TableElement, 20)},
+				Table:   &wasm.TableInstance{Table: make([]uintptr, 20)},
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
 			},
 		},
 		{
 			name: "memory nil",
 			moduleInstance: &wasm.ModuleInstance{
-				Table:   &wasm.TableInstance{Table: make([]wasm.TableElement, 20)},
+				Engine:  modEngine,
+				Table:   &wasm.TableInstance{Table: make([]uintptr, 20)},
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
 			},
 		},
 		{
 			name: "memory zero length",
 			moduleInstance: &wasm.ModuleInstance{
-				Table:   &wasm.TableInstance{Table: make([]wasm.TableElement, 20)},
+				Engine:  modEngine,
+				Table:   &wasm.TableInstance{Table: make([]uintptr, 20)},
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
 				Memory:  &wasm.MemoryInstance{Buffer: make([]byte, 0)},
 			},
@@ -245,6 +254,7 @@ func TestAmd64Compiler_initializeModuleContext(t *testing.T) {
 		{
 			name: "table length zero",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine:  modEngine,
 				Memory:  &wasm.MemoryInstance{Buffer: make([]byte, 10)},
 				Table:   &wasm.TableInstance{Table: nil},
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
@@ -253,14 +263,16 @@ func TestAmd64Compiler_initializeModuleContext(t *testing.T) {
 		{
 			name: "table length zero part2",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine:  modEngine,
 				Memory:  &wasm.MemoryInstance{Buffer: make([]byte, 10)},
-				Table:   &wasm.TableInstance{Table: make([]wasm.TableElement, 0)},
+				Table:   &wasm.TableInstance{Table: make([]uintptr, 0)},
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
 			},
 		},
 		{
 			name: "table nil",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine:  modEngine,
 				Memory:  &wasm.MemoryInstance{Buffer: make([]byte, 10)},
 				Table:   &wasm.TableInstance{},
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
@@ -269,6 +281,7 @@ func TestAmd64Compiler_initializeModuleContext(t *testing.T) {
 		{
 			name: "table nil part2",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine:  modEngine,
 				Memory:  &wasm.MemoryInstance{Buffer: make([]byte, 10)},
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
 			},
@@ -276,8 +289,9 @@ func TestAmd64Compiler_initializeModuleContext(t *testing.T) {
 		{
 			name: "globals nil",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine: modEngine,
 				Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)},
-				Table:  &wasm.TableInstance{Table: make([]wasm.TableElement, 20)},
+				Table:  &wasm.TableInstance{Table: make([]uintptr, 20)},
 			},
 		},
 	} {
@@ -323,6 +337,8 @@ func TestAmd64Compiler_initializeModuleContext(t *testing.T) {
 				require.Equal(t, uint64(tableHeader.Len), ce.moduleContext.tableSliceLen)
 				require.Equal(t, tableHeader.Data, ce.moduleContext.tableElement0Address)
 			}
+
+			require.Equal(t, uintptr(unsafe.Pointer(&modEngine.compiledFunctions[0])), ce.moduleContext.compiledFunctionsElement0Address)
 		})
 	}
 }
@@ -5419,7 +5435,7 @@ func TestAmd64Compiler_compileMemoryGrow(t *testing.T) {
 	env.exec(code)
 
 	require.Equal(t, jitCallStatusCodeCallBuiltInFunction, env.jitStatus())
-	require.Equal(t, builtinFunctionIndexMemoryGrow, env.functionCallAddress())
+	require.Equal(t, builtinFunctionIndexMemoryGrow, env.builtinFunctionCallAddress())
 
 	returnAddress := env.callFrameStackPeek().returnAddress
 	require.NotZero(t, returnAddress)
@@ -5939,7 +5955,7 @@ func TestAmd64Compiler_compileGlobalGet(t *testing.T) {
 			env.addGlobals(globals...)
 			// Compiler needs global type information at compilation time.
 			compiler.f = &wasm.FunctionInstance{
-				Module: &wasm.ModuleInstance{Globals: globals},
+				Module: &wasm.ModuleInstance{Globals: globals, Engine: &moduleEngine{}},
 				Kind:   wasm.FunctionKindWasm,
 			}
 
@@ -6019,237 +6035,112 @@ func TestAmd64Compiler_compileGlobalSet(t *testing.T) {
 	}
 }
 
-func TestAmd64Compiler_callFunction(t *testing.T) {
-	for _, isAddressFromRegister := range []bool{false, true} {
-		isAddressFromRegister := isAddressFromRegister
-		t.Run(fmt.Sprintf("is_address_from_register=%v", isAddressFromRegister), func(t *testing.T) {
-			t.Run("need to grow call frame stack", func(t *testing.T) {
-				env := newJITEnvironment()
+func TestAmd64Compiler_compileCall(t *testing.T) {
+	for _, growCallFrameStack := range []bool{false, true} {
+		growCallFrameStack := growCallFrameStack
+		t.Run(fmt.Sprintf("grow=%v", growCallFrameStack), func(t *testing.T) {
+			env := newJITEnvironment()
+			me := env.moduleEngine()
+			expectedValue := uint32(0)
 
+			if growCallFrameStack {
 				env.setCallFrameStackPointerLen(1)
-				compiler := env.requireNewCompiler(t)
-				err := compiler.compilePreamble()
-				require.NoError(t, err)
-
-				require.Empty(t, compiler.locationStack.usedRegisters)
-				if isAddressFromRegister {
-					err = compiler.callFunctionFromRegister(x86.REG_AX, &wasm.FunctionType{})
-				} else {
-					err = compiler.callFunctionFromConst(11111 /* can be arbitrary*/, &wasm.FunctionType{})
-				}
-				require.NoError(t, err)
-				require.Empty(t, compiler.locationStack.usedRegisters)
-
-				// Because we must early return from the function this case,
-				// we emit the undefined instruction after the callFunctionFromConst.
-				compiler.undefined()
-
-				// Generate the code under test.
-				code, _, _, err := compiler.compile()
-				require.NoError(t, err)
-
-				// Run code.
-				env.exec(code)
-
-				// If the call frame stack pointer equals the length of call frame stack length,
-				// we have to call the builtin function to grow the slice.
-				require.Equal(t, jitCallStatusCodeCallBuiltInFunction, env.jitStatus())
-				require.Equal(t, builtinFunctionIndexGrowCallFrameStack, env.functionCallAddress())
-			})
-			t.Run("stack ok", func(t *testing.T) {
-				env := newJITEnvironment()
-				engine := env.engine()
-
-				// Emit the call target function.
-				const numCalls = 10
-				targetFunctionType := &wasm.FunctionType{
-					Params:  []wasm.ValueType{wasm.ValueTypeI32},
-					Results: []wasm.ValueType{wasm.ValueTypeI32},
-				}
-
-				expectedValue := uint32(0)
-				moduleInstanceToExpectedValueInMemory := map[*wasm.ModuleInstance]uint32{}
-				for i := 0; i < numCalls; i++ {
-					// Each function takes one argument, adds the value with 100 + i and returns the result.
-					addTargetValue := uint32(100 + i)
-					moduleInstance := &wasm.ModuleInstance{
-						Memory: &wasm.MemoryInstance{Buffer: make([]byte, 1024)},
-					}
-					moduleInstanceToExpectedValueInMemory[moduleInstance] = addTargetValue
+			}
 
+			// Emit the call target function.
+			const numCalls = 3
+			targetFunctionType := &wasm.FunctionType{
+				Params:  []wasm.ValueType{wasm.ValueTypeI32},
+				Results: []wasm.ValueType{wasm.ValueTypeI32},
+			}
+			for i := 0; i < numCalls; i++ {
+				// Each function takes one arguments, adds the value with 100 + i and returns the result.
+				addTargetValue := uint32(100 + i)
+				expectedValue += addTargetValue
+
+				// We have to do compilation in a separate subtest since each compilation takes
+				// the mutext lock and must release on the cleanup of each subtest.
+				// TODO: delete after https://github.com/tetratelabs/wazero/issues/233
+				t.Run(fmt.Sprintf("compiling call target %d", i), func(t *testing.T) {
 					compiler := env.requireNewCompiler(t)
-					compiler.f = &wasm.FunctionInstance{
-						Kind:   wasm.FunctionKindWasm,
-						Type:   targetFunctionType,
-						Module: moduleInstance,
-					}
+					compiler.f.Type = targetFunctionType
 
 					err := compiler.compilePreamble()
 					require.NoError(t, err)
 
-					expectedValue += addTargetValue
-					err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: addTargetValue})
+					err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: uint32(addTargetValue)})
 					require.NoError(t, err)
-
 					err = compiler.compileAdd(&wazeroir.OperationAdd{Type: wazeroir.UnsignedTypeI32})
 					require.NoError(t, err)
 
-					// Also, we modify the memory to ensure that context siwtch between module instances actually works.
-					const tmpReg = x86.REG_AX
-					moveValueToReg := compiler.newProg()
-					moveValueToReg.As = x86.AMOVL // 32bit!
-					moveValueToReg.From.Type = obj.TYPE_CONST
-					moveValueToReg.From.Offset = int64(addTargetValue)
-					moveValueToReg.To.Type = obj.TYPE_REG
-					moveValueToReg.To.Reg = tmpReg
-					compiler.addInstruction(moveValueToReg)
-
-					writeValueToMemory := compiler.newProg()
-					writeValueToMemory.As = x86.AMOVL // 32bit!
-					writeValueToMemory.From.Type = obj.TYPE_REG
-					writeValueToMemory.From.Reg = tmpReg
-					writeValueToMemory.To.Type = obj.TYPE_MEM
-					writeValueToMemory.To.Reg = reservedRegisterForMemory
-					compiler.addInstruction(writeValueToMemory)
-
 					err = compiler.returnFunction()
 					require.NoError(t, err)
 
 					code, _, _, err := compiler.compile()
 					require.NoError(t, err)
-
-					compiledFunction := &compiledFunction{
+					index := wasm.Index(i)
+					me.compiledFunctions = append(me.compiledFunctions, &compiledFunction{
 						codeSegment:        code,
 						codeInitialAddress: uintptr(unsafe.Pointer(&code[0])),
-					}
-					engine.addCompiledFunction(wasm.FunctionIndex(i), compiledFunction)
-				}
-
-				// Now we start building the caller's code.
-				compiler := env.requireNewCompiler(t)
-				err := compiler.compilePreamble()
-				require.NoError(t, err)
-
-				const initialValue = 100
-				expectedValue += initialValue
-				err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: 0}) // Dummy value so the base pointer would be non-trivial for callees.
-				require.NoError(t, err)
-				err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: initialValue})
-				require.NoError(t, err)
+					})
+					env.module().Functions = append(env.module().Functions,
+						&wasm.FunctionInstance{Type: targetFunctionType, Index: index})
+				})
+			}
 
-				// Call all the built functions.
-				for i := 0; i < numCalls; i++ {
-					if isAddressFromRegister {
-						const tmpReg = x86.REG_AX
-						compiler.movIntConstToRegister(int64(i), tmpReg)
-						err = compiler.callFunctionFromRegister(tmpReg, targetFunctionType)
-					} else {
-						err = compiler.callFunctionFromConst(wasm.FunctionIndex(i), targetFunctionType)
-					}
-					require.NoError(t, err)
-				}
+			// Now we start building the caller's code.
+			compiler := env.requireNewCompiler(t)
+			err := compiler.compilePreamble()
+			require.NoError(t, err)
 
-				err = compiler.returnFunction()
-				require.NoError(t, err)
+			const initialValue = 100
+			expectedValue += initialValue
+			err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: 0}) // Dummy value so the base pointer would be non-trivial for callees.
+			require.NoError(t, err)
+			err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: initialValue})
+			require.NoError(t, err)
 
-				// Generate the code under test.
-				code, _, _, err := compiler.compile()
+			// Call all the built functions.
+			for i := 0; i < numCalls; i++ {
+				err = compiler.compileCall(&wazeroir.OperationCall{FunctionIndex: uint32(i)})
 				require.NoError(t, err)
+			}
 
-				// Run code.
-				env.exec(code)
-
-				// Check status and returned values.
-				require.Equal(t, jitCallStatusCodeReturned, env.jitStatus())
-				require.Equal(t, uint64(2), env.stackPointer()) // Must be 2 (dummy value + the calculation results)
-				require.Equal(t, uint64(0), env.stackBasePointer())
-				require.Equal(t, expectedValue, env.stackTopAsUint32())
-
-				// Also, in the middle of function call, we write the added value into each memory instance.
-				for mod, expValue := range moduleInstanceToExpectedValueInMemory {
-					require.Equal(t, expValue, binary.LittleEndian.Uint32(mod.Memory.Buffer[0:]))
-				}
-			})
-		})
-	}
-}
-
-func TestAmd64Compiler_compileCall(t *testing.T) {
-	env := newJITEnvironment()
-	engine := env.engine()
-
-	const targetFunctionIndex wasm.FunctionIndex = 5 // arbitrary value for testing
-	targetFunctionType := &wasm.FunctionType{
-		Params:  []wasm.ValueType{wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32},
-		Results: []wasm.ValueType{wasm.ValueTypeI32},
-	}
-
-	{
-		// Call target function takes three i32 arguments and does ADD 2 times.
-		compiler := env.requireNewCompiler(t)
-		compiler.f = &wasm.FunctionInstance{
-			Module: &wasm.ModuleInstance{},
-			Kind:   wasm.FunctionKindWasm,
-			Type:   targetFunctionType,
-		}
-		err := compiler.compilePreamble()
-		require.NoError(t, err)
-		for i := 0; i < 2; i++ {
-			err = compiler.compileAdd(&wazeroir.OperationAdd{Type: wazeroir.UnsignedTypeI32})
+			err = compiler.returnFunction()
 			require.NoError(t, err)
-		}
-		err = compiler.returnFunction()
-		require.NoError(t, err)
 
-		code, _, _, err := compiler.compile()
-		require.NoError(t, err)
+			code, _, _, err := compiler.compile()
+			require.NoError(t, err)
+			env.exec(code)
 
-		engine.addCompiledFunction(targetFunctionIndex, &compiledFunction{
-			codeSegment:        code,
-			codeInitialAddress: uintptr(unsafe.Pointer(&code[0])),
-		})
-		env.module().Functions = append(env.module().Functions,
-			&wasm.FunctionInstance{Type: targetFunctionType, Index: targetFunctionIndex})
-	}
+			if growCallFrameStack {
+				// If the call frame stack pointer equals the length of call frame stack length,
+				// we have to call the builtin function to grow the slice.
+				require.Equal(t, jitCallStatusCodeCallBuiltInFunction, env.jitStatus())
+				require.Equal(t, builtinFunctionIndexGrowCallFrameStack, env.builtinFunctionCallAddress())
 
-	// Now we start building the caller's code.
-	compiler := env.requireNewCompiler(t)
-	err := compiler.compilePreamble()
-	require.NoError(t, err)
+				// Grow the callFrame stack, and exec again from the return address.
+				ce := env.callEngine()
+				ce.builtinFunctionGrowCallFrameStack()
+				jitcall(env.callFrameStackPeek().returnAddress, uintptr(unsafe.Pointer(ce)))
+			}
 
-	var expectedValue uint32
-	// Emit the const expressions for function arguments.
-	for i := 0; i < len(targetFunctionType.Params); i++ {
-		param := uint32(1 << (i + 1))
-		expectedValue += param
-		err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: param})
-		require.NoError(t, err)
+			// Check status and returned values.
+			require.Equal(t, jitCallStatusCodeReturned, env.jitStatus())
+			require.Equal(t, uint64(2), env.stackPointer()) // Must be 2 (dummy value + the calculation results)
+			require.Equal(t, uint64(0), env.stackBasePointer())
+			require.Equal(t, expectedValue, env.stackTopAsUint32())
+		})
 	}
-
-	err = compiler.compileCall(&wazeroir.OperationCall{FunctionIndex: 0})
-	require.NoError(t, err)
-
-	err = compiler.returnFunction()
-	require.NoError(t, err)
-
-	// Generate the code under test.
-	code, _, _, err := compiler.compile()
-	require.NoError(t, err)
-
-	// Run code.
-	env.exec(code)
-
-	// Check the status and returned value.
-	require.Equal(t, jitCallStatusCodeReturned, env.jitStatus())
-	require.Equal(t, uint64(1), env.stackPointer())
-	require.Equal(t, expectedValue, env.stackTopAsUint32())
 }
 
 func TestAmd64Compiler_compileCallIndirect(t *testing.T) {
 	t.Run("out of bounds", func(t *testing.T) {
 		env := newJITEnvironment()
-		env.setTable(make([]wasm.TableElement, 10))
+		env.setTable(make([]uintptr, 10))
 		compiler := env.requireNewCompiler(t)
+		err := compiler.compilePreamble()
+		require.NoError(t, err)
 
 		targetOperation := &wazeroir.OperationCallIndirect{}
 		// Ensure that the module instance has the type information for targetOperation.TypeIndex.
@@ -6259,20 +6150,18 @@ func TestAmd64Compiler_compileCallIndirect(t *testing.T) {
 		}
 
 		// Place the offfset value.
-		loc := compiler.locationStack.pushValueLocationOnStack()
-		env.stack()[loc.stackPointer] = 10
+		err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: 10})
+		require.NoError(t, err)
 
-		// Now emit the code.
-		err := compiler.compilePreamble()
+		err = compiler.compileCallIndirect(targetOperation)
 		require.NoError(t, err)
-		require.NoError(t, compiler.compileCallIndirect(targetOperation))
-		compiler.exit(jitCallStatusCodeReturned)
 
-		// Generate the code under test.
+		// We expect to exit from the code in callIndirect so the subsequet code must be unreachable.
+		compiler.exit(jitCallStatusCodeUnreachable)
+
+		// Generate the code under test and run.
 		code, _, _, err := compiler.compile()
 		require.NoError(t, err)
-
-		// Run code.
 		env.exec(code)
 
 		require.Equal(t, jitCallStatusCodeInvalidTableAccess, env.jitStatus())
@@ -6280,35 +6169,34 @@ func TestAmd64Compiler_compileCallIndirect(t *testing.T) {
 
 	t.Run("uninitialized", func(t *testing.T) {
 		env := newJITEnvironment()
-		table := make([]wasm.TableElement, 10)
-		env.setTable(table)
-
 		compiler := env.requireNewCompiler(t)
+		err := compiler.compilePreamble()
+		require.NoError(t, err)
+
 		targetOperation := &wazeroir.OperationCallIndirect{}
 		targetOffset := &wazeroir.OperationConstI32{Value: uint32(0)}
 		// Ensure that the module instance has the type information for targetOperation.TypeIndex,
 		compiler.f = &wasm.FunctionInstance{
-			Module: &wasm.ModuleInstance{Types: []*wasm.TypeInstance{{Type: &wasm.FunctionType{}, TypeID: 1000}}},
+			Module: &wasm.ModuleInstance{Types: []*wasm.TypeInstance{{Type: &wasm.FunctionType{}}}},
 			Kind:   wasm.FunctionKindWasm,
 		}
 		// and the typeID doesn't match the table[targetOffset]'s type ID.
-		table[0] = wasm.TableElement{FunctionTypeID: wasm.UninitializedTableElementTypeID}
+		table := make([]uintptr, 10)
+		env.setTable(table)
+		table[0] = 0
 
-		// Place the offfset value.
-		err := compiler.compileConstI32(targetOffset)
+		// Place the offset value.
+		err = compiler.compileConstI32(targetOffset)
 		require.NoError(t, err)
-
-		// Now emit the code.
-		err = compiler.compilePreamble()
+		err = compiler.compileCallIndirect(targetOperation)
 		require.NoError(t, err)
-		require.NoError(t, compiler.compileCallIndirect(targetOperation))
 
-		// Generate the code under test.
-		compiler.exit(jitCallStatusCodeReturned)
+		// We expect to exit from the code in callIndirect so the subsequet code must be unreachable.
+		compiler.exit(jitCallStatusCodeUnreachable)
+
+		// Generate the code under test and run.
 		code, _, _, err := compiler.compile()
 		require.NoError(t, err)
-
-		// Run code.
 		env.exec(code)
 
 		require.Equal(t, jitCallStatusCodeInvalidTableAccess, env.jitStatus())
@@ -6316,19 +6204,20 @@ func TestAmd64Compiler_compileCallIndirect(t *testing.T) {
 
 	t.Run("type not match", func(t *testing.T) {
 		env := newJITEnvironment()
-		table := make([]wasm.TableElement, 10)
-		env.setTable(table)
-
 		compiler := env.requireNewCompiler(t)
+		err := compiler.compilePreamble()
+		require.NoError(t, err)
+
 		targetOperation := &wazeroir.OperationCallIndirect{}
 		targetOffset := &wazeroir.OperationConstI32{Value: uint32(0)}
-		env.moduleInstance.Types = []*wasm.TypeInstance{{Type: &wasm.FunctionType{}, TypeID: 1000}}
+		env.module().Types = []*wasm.TypeInstance{{Type: &wasm.FunctionType{}, TypeID: 1000}}
 		// Ensure that the module instance has the type information for targetOperation.TypeIndex,
 		// and the typeID doesn't match the table[targetOffset]'s type ID.
-		table[0] = wasm.TableElement{FunctionTypeID: 50}
+		table := make([]uintptr, 10)
+		env.setTable(table)
 
-		err := compiler.compilePreamble()
-		require.NoError(t, err)
+		cf := &compiledFunction{source: &wasm.FunctionInstance{TypeID: 50}}
+		table[0] = uintptr(unsafe.Pointer(cf))
 
 		// Place the offfset value.
 		err = compiler.compileConstI32(targetOffset)
@@ -6337,94 +6226,120 @@ func TestAmd64Compiler_compileCallIndirect(t *testing.T) {
 		// Now emit the code.
 		require.NoError(t, compiler.compileCallIndirect(targetOperation))
 
-		// Generate the code under test.
-		compiler.exit(jitCallStatusCodeReturned)
+		// We expect to exit from the code in callIndirect so the subsequet code must be unreachable.
+		compiler.exit(jitCallStatusCodeUnreachable)
+
+		// Generate the code under test and run.
 		code, _, _, err := compiler.compile()
 		require.NoError(t, err)
-
-		// Run code.
 		env.exec(code)
 
 		require.Equal(t, jitCallStatusCodeTypeMismatchOnIndirectCall, env.jitStatus())
 	})
 
 	t.Run("ok", func(t *testing.T) {
-		targetType := &wasm.FunctionType{
-			Params:  []wasm.ValueType{},
-			Results: []wasm.ValueType{wasm.ValueTypeI32}}
-		targetTypeID := wasm.FunctionTypeID(10) // Arbitrary number is fine for testing.
-		operation := &wazeroir.OperationCallIndirect{TypeIndex: 0}
-		moduleInstance := &wasm.ModuleInstance{Types: make([]*wasm.TypeInstance, 100)}
-		moduleInstance.Types[operation.TableIndex] = &wasm.TypeInstance{Type: targetType, TypeID: targetTypeID}
-
-		table := make([]wasm.TableElement, 10)
-		for i := 0; i < len(table); i++ {
-			table[i] = wasm.TableElement{FunctionIndex: wasm.FunctionIndex(i), FunctionTypeID: targetTypeID}
-		}
+		for _, growCallFrameStack := range []bool{false} {
+			growCallFrameStack := growCallFrameStack
+			t.Run(fmt.Sprintf("grow=%v", growCallFrameStack), func(t *testing.T) {
+				targetType := &wasm.FunctionType{
+					Params:  []wasm.ValueType{},
+					Results: []wasm.ValueType{wasm.ValueTypeI32}}
+				targetTypeID := wasm.FunctionTypeID(10) // Arbitrary number is fine for testing.
+				operation := &wazeroir.OperationCallIndirect{TypeIndex: 0}
 
-		for i := 0; i < len(table); i++ {
-			t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+				// Ensure that the module instance has the type information for targetOperation.TypeIndex,
+				// and the typeID  matches the table[targetOffset]'s type ID.
+				moduleInstance := &wasm.ModuleInstance{Types: make([]*wasm.TypeInstance, 100), Engine: &moduleEngine{compiledFunctions: []*compiledFunction{}}}
+				moduleInstance.Types[operation.TableIndex] = &wasm.TypeInstance{Type: targetType, TypeID: targetTypeID}
+
+				table := make([]uintptr, 10)
 				env := newJITEnvironment()
 				env.setTable(table)
-				engine := env.engine()
+				me := env.moduleEngine()
+				for i := 0; i < len(table); i++ {
+					// First we create the call target function with function address = i,
+					// and it returns one value.
+					expectedReturnValue := uint32(i * 1000)
 
-				// First we creat the call target function with function address = i,
-				// and it returns one value.
-				expectedReturnValue := uint32(i * 1000)
-				{
-					compiler := env.requireNewCompiler(t)
-					err := compiler.compilePreamble()
-					require.NoError(t, err)
-					err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: expectedReturnValue})
-					require.NoError(t, err)
-					err = compiler.returnFunction()
-					require.NoError(t, err)
+					// We have to do compilation in a separate subtest since each compilation takes
+					// the mutext lock and must release on the cleanup of each subtest.
+					// TODO: delete after https://github.com/tetratelabs/wazero/issues/233
+					t.Run(fmt.Sprintf("compiling call target for %d", i), func(t *testing.T) {
 
-					code, _, _, err := compiler.compile()
-					require.NoError(t, err)
+						compiler := env.requireNewCompiler(t)
+						err := compiler.compilePreamble()
+						require.NoError(t, err)
+						err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: expectedReturnValue})
+						require.NoError(t, err)
+						err = compiler.returnFunction()
+						require.NoError(t, err)
 
-					engine.addCompiledFunction(table[i].FunctionIndex, &compiledFunction{
-						codeSegment:        code,
-						codeInitialAddress: uintptr(unsafe.Pointer(&code[0])),
+						code, _, _, err := compiler.compile()
+						require.NoError(t, err)
+
+						cf := &compiledFunction{
+							codeSegment:        code,
+							codeInitialAddress: uintptr(unsafe.Pointer(&code[0])),
+							source: &wasm.FunctionInstance{
+								TypeID: targetTypeID,
+							},
+						}
+						me.compiledFunctions = append(me.compiledFunctions, cf)
+						table[i] = uintptr(unsafe.Pointer(cf))
 					})
 				}
 
-				compiler := env.requireNewCompiler(t)
-				err := compiler.compilePreamble()
-				require.NoError(t, err)
+				for i := 1; i < len(table); i++ {
+					expectedReturnValue := uint32(i * 1000)
+					t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+						if growCallFrameStack {
+							env.setCallFrameStackPointerLen(1)
+						}
 
-				// Ensure that the module instance has the type information for targetOperation.TypeIndex,
-				compiler.f = &wasm.FunctionInstance{Module: moduleInstance, Kind: wasm.FunctionKindWasm}
-				// and the typeID  matches the table[targetOffset]'s type ID.
+						compiler := env.requireNewCompiler(t)
+						err := compiler.compilePreamble()
+						require.NoError(t, err)
 
-				// Place the offfset value. Here we try calling a function of functionaddr == table[i].FunctionIndex.
-				err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: uint32(i)})
-				require.NoError(t, err)
+						compiler.f = &wasm.FunctionInstance{Module: moduleInstance, Kind: wasm.FunctionKindWasm}
 
-				// At this point, we should have one item (offset value) on the stack.
-				require.Equal(t, uint64(1), compiler.locationStack.sp)
+						// Place the offfset value. Here we try calling a function of functionaddr == table[i].FunctionIndex.
+						err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: uint32(i)})
+						require.NoError(t, err)
 
-				// Now emit the code.
+						// At this point, we should have one item (offset value) on the stack.
+						require.Equal(t, uint64(1), compiler.locationStack.sp)
 
-				require.NoError(t, compiler.compileCallIndirect(operation))
+						require.NoError(t, compiler.compileCallIndirect(operation))
 
-				// At this point, we consumed the offset value, but the function returns one value,
-				// so the stack pointer results in the same.
-				require.Equal(t, uint64(1), compiler.locationStack.sp)
+						// At this point, we consumed the offset value, but the function returns one value,
+						// so the stack pointer results in the same.
+						require.Equal(t, uint64(1), compiler.locationStack.sp)
 
-				err = compiler.returnFunction()
-				require.NoError(t, err)
+						err = compiler.returnFunction()
+						require.NoError(t, err)
 
-				// Generate the code under test.
-				code, _, _, err := compiler.compile()
-				require.NoError(t, err)
+						// Generate the code under test and run.
+						code, _, _, err := compiler.compile()
+						require.NoError(t, err)
+						env.exec(code)
 
-				// Run code.
-				env.exec(code)
+						if growCallFrameStack {
+							// If the call frame stack pointer equals the length of call frame stack length,
+							// we have to call the builtin function to grow the slice.
+							require.Equal(t, jitCallStatusCodeCallBuiltInFunction, env.jitStatus())
+							require.Equal(t, builtinFunctionIndexGrowCallFrameStack, env.builtinFunctionCallAddress())
 
-				require.Equal(t, jitCallStatusCodeReturned, env.jitStatus())
-				require.Equal(t, uint64(1), env.stackPointer())
-				require.Equal(t, expectedReturnValue, env.stackTopAsUint32())
+							// Grow the callFrame stack, and exec again from the return address.
+							ce := env.callEngine()
+							ce.builtinFunctionGrowCallFrameStack()
+							jitcall(env.callFrameStackPeek().returnAddress, uintptr(unsafe.Pointer(ce)))
+						}
+
+						require.Equal(t, jitCallStatusCodeReturned, env.jitStatus())
+						require.Equal(t, uint64(1), env.stackPointer())
+						require.Equal(t, expectedReturnValue, uint32(env.ce.popValue()))
+					})
+				}
 			})
 		}
 	})
diff --git a/internal/wasm/jit/jit_arm64_test.go b/internal/wasm/jit/jit_arm64_test.go
index fff9bbbf..d970f4aa 100644
--- a/internal/wasm/jit/jit_arm64_test.go
+++ b/internal/wasm/jit/jit_arm64_test.go
@@ -86,24 +86,24 @@ func TestArm64Compiler_returnFunction(t *testing.T) {
 	})
 	t.Run("deep call stack", func(t *testing.T) {
 		env := newJITEnvironment()
-		engine := env.engine()
+		moduleEngine := env.moduleEngine()
 		ce := env.callEngine()
 
 		// Push the call frames.
 		const callFrameNums = 10
 		stackPointerToExpectedValue := map[uint64]uint32{}
-		for funcaddr := wasm.FunctionIndex(0); funcaddr < callFrameNums; funcaddr++ {
+		for funcIndex := wasm.Index(0); funcIndex < callFrameNums; funcIndex++ {
 			// We have to do compilation in a separate subtest since each compilation takes
 			// the mutext lock and must release on the cleanup of each subtest.
 			// TODO: delete after https://github.com/tetratelabs/wazero/issues/233
-			t.Run(fmt.Sprintf("compiling existing callframe %d", funcaddr), func(t *testing.T) {
+			t.Run(fmt.Sprintf("compiling existing callframe %d", funcIndex), func(t *testing.T) {
 				// Each function pushes its funcaddr and soon returns.
 				compiler := env.requireNewCompiler(t)
 				err := compiler.compilePreamble()
 				require.NoError(t, err)
 
-				// Push its funcaddr.
-				expValue := uint32(funcaddr)
+				// Push its functionIndex.
+				expValue := uint32(funcIndex)
 				err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: expValue})
 				require.NoError(t, err)
 
@@ -115,14 +115,14 @@ func TestArm64Compiler_returnFunction(t *testing.T) {
 
 				// Compiles and adds to the engine.
 				compiledFunction := &compiledFunction{codeSegment: code, codeInitialAddress: uintptr(unsafe.Pointer(&code[0]))}
-				engine.addCompiledFunction(funcaddr, compiledFunction)
+				moduleEngine.compiledFunctions = append(moduleEngine.compiledFunctions, compiledFunction)
 
 				// Pushes the frame whose return address equals the beginning of the function just compiled.
 				frame := callFrame{
 					// Set the return address to the beginning of the function so that we can execute the constI32 above.
 					returnAddress: compiledFunction.codeInitialAddress,
 					// Note: return stack base pointer is set to funcaddr*5 and this is where the const should be pushed.
-					returnStackBasePointer: uint64(funcaddr) * 5,
+					returnStackBasePointer: uint64(funcIndex) * 5,
 					compiledFunction:       compiledFunction,
 				}
 				ce.callFrameStack[ce.globalContext.callFrameStackPointer] = frame
@@ -300,6 +300,7 @@ func TestArm64Compiler_releaseRegisterToStack(t *testing.T) {
 
 			// Run native code after growing the value stack.
 			env.callEngine().builtinFunctionGrowValueStack(tc.stackPointer)
+			env.callEngine().resetValueStackElement0Address()
 			env.exec(code)
 
 			// JIT status must be returned and stack pointer must end up the specified one.
@@ -382,6 +383,7 @@ func TestArm64Compiler_compileLoadValueOnStackToRegister(t *testing.T) {
 
 			// Run native code after growing the value stack, and place the original value.
 			env.callEngine().builtinFunctionGrowValueStack(tc.stackPointer)
+			env.callEngine().resetValueStackElement0Address()
 			env.stack()[tc.stackPointer] = val
 			env.exec(code)
 
@@ -1722,7 +1724,7 @@ func TestArm64Compiler_compileCall(t *testing.T) {
 		growCallFrameStack := growCallFrameStack
 		t.Run(fmt.Sprintf("grow=%v", growCallFrameStack), func(t *testing.T) {
 			env := newJITEnvironment()
-			eng := env.engine()
+			me := env.moduleEngine()
 			expectedValue := uint32(0)
 
 			if growCallFrameStack {
@@ -1730,7 +1732,7 @@ func TestArm64Compiler_compileCall(t *testing.T) {
 			}
 
 			// Emit the call target function.
-			const numCalls = 1
+			const numCalls = 3
 			targetFunctionType := &wasm.FunctionType{
 				Params:  []wasm.ValueType{wasm.ValueTypeI32},
 				Results: []wasm.ValueType{wasm.ValueTypeI32},
@@ -1745,11 +1747,7 @@ func TestArm64Compiler_compileCall(t *testing.T) {
 				// TODO: delete after https://github.com/tetratelabs/wazero/issues/233
 				t.Run(fmt.Sprintf("compiling call target %d", i), func(t *testing.T) {
 					compiler := env.requireNewCompiler(t)
-					compiler.f = &wasm.FunctionInstance{
-						Kind:   wasm.FunctionKindWasm,
-						Type:   targetFunctionType,
-						Module: &wasm.ModuleInstance{},
-					}
+					compiler.f.Type = targetFunctionType
 
 					err := compiler.compilePreamble()
 					require.NoError(t, err)
@@ -1758,13 +1756,14 @@ func TestArm64Compiler_compileCall(t *testing.T) {
 					require.NoError(t, err)
 					err = compiler.compileAdd(&wazeroir.OperationAdd{Type: wazeroir.UnsignedTypeI32})
 					require.NoError(t, err)
+
 					err = compiler.compileReturnFunction()
 					require.NoError(t, err)
 
 					code, _, _, err := compiler.compile()
 					require.NoError(t, err)
-					index := wasm.FunctionIndex(i)
-					eng.addCompiledFunction(index, &compiledFunction{
+					index := wasm.Index(i)
+					me.compiledFunctions = append(me.compiledFunctions, &compiledFunction{
 						codeSegment:        code,
 						codeInitialAddress: uintptr(unsafe.Pointer(&code[0])),
 					})
@@ -1802,7 +1801,7 @@ func TestArm64Compiler_compileCall(t *testing.T) {
 				// If the call frame stack pointer equals the length of call frame stack length,
 				// we have to call the builtin function to grow the slice.
 				require.Equal(t, jitCallStatusCodeCallBuiltInFunction, env.jitStatus())
-				require.Equal(t, builtinFunctionIndexGrowCallFrameStack, env.functionCallAddress(), env.functionCallAddress())
+				require.Equal(t, builtinFunctionIndexGrowCallFrameStack, env.builtinFunctionCallAddress())
 
 				// Grow the callFrame stack, and exec again from the return address.
 				ce := env.callEngine()
@@ -1822,7 +1821,7 @@ func TestArm64Compiler_compileCall(t *testing.T) {
 func TestArm64Compiler_compileCallIndirect(t *testing.T) {
 	t.Run("out of bounds", func(t *testing.T) {
 		env := newJITEnvironment()
-		env.setTable(make([]wasm.TableElement, 10))
+		env.setTable(make([]uintptr, 10))
 		compiler := env.requireNewCompiler(t)
 		err := compiler.compilePreamble()
 		require.NoError(t, err)
@@ -1867,9 +1866,9 @@ func TestArm64Compiler_compileCallIndirect(t *testing.T) {
 			Kind:   wasm.FunctionKindWasm,
 		}
 		// and the typeID doesn't match the table[targetOffset]'s type ID.
-		table := make([]wasm.TableElement, 10)
+		table := make([]uintptr, 10)
 		env.setTable(table)
-		table[0] = wasm.TableElement{FunctionTypeID: wasm.UninitializedTableElementTypeID}
+		table[0] = 0
 
 		// Place the offset value.
 		err = compiler.compileConstI32(targetOffset)
@@ -1900,9 +1899,11 @@ func TestArm64Compiler_compileCallIndirect(t *testing.T) {
 		env.module().Types = []*wasm.TypeInstance{{Type: &wasm.FunctionType{}, TypeID: 1000}}
 		// Ensure that the module instance has the type information for targetOperation.TypeIndex,
 		// and the typeID doesn't match the table[targetOffset]'s type ID.
-		table := make([]wasm.TableElement, 10)
+		table := make([]uintptr, 10)
 		env.setTable(table)
-		table[0] = wasm.TableElement{FunctionTypeID: 50}
+
+		cf := &compiledFunction{source: &wasm.FunctionInstance{TypeID: 50}}
+		table[0] = uintptr(unsafe.Pointer(cf))
 
 		// Place the offfset value.
 		err = compiler.compileConstI32(targetOffset)
@@ -1924,8 +1925,7 @@ func TestArm64Compiler_compileCallIndirect(t *testing.T) {
 	})
 
 	t.Run("ok", func(t *testing.T) {
-		t.Skip()
-		for _, growCallFrameStack := range []bool{false, true} {
+		for _, growCallFrameStack := range []bool{false} {
 			growCallFrameStack := growCallFrameStack
 			t.Run(fmt.Sprintf("grow=%v", growCallFrameStack), func(t *testing.T) {
 				targetType := &wasm.FunctionType{
@@ -1936,19 +1936,14 @@ func TestArm64Compiler_compileCallIndirect(t *testing.T) {
 
 				// Ensure that the module instance has the type information for targetOperation.TypeIndex,
 				// and the typeID  matches the table[targetOffset]'s type ID.
-				moduleInstance := &wasm.ModuleInstance{Types: make([]*wasm.TypeInstance, 100)}
+				moduleInstance := &wasm.ModuleInstance{Types: make([]*wasm.TypeInstance, 100), Engine: &moduleEngine{compiledFunctions: []*compiledFunction{}}}
 				moduleInstance.Types[operation.TableIndex] = &wasm.TypeInstance{Type: targetType, TypeID: targetTypeID}
 
-				table := make([]wasm.TableElement, 10)
-				for i := 0; i < len(table); i++ {
-					table[i] = wasm.TableElement{FunctionIndex: wasm.FunctionIndex(i), FunctionTypeID: targetTypeID}
-				}
-
+				table := make([]uintptr, 10)
+				env := newJITEnvironment()
+				env.setTable(table)
+				me := env.moduleEngine()
 				for i := 0; i < len(table); i++ {
-					env := newJITEnvironment()
-					env.setTable(table)
-					eng := env.engine()
-
 					// First we create the call target function with function address = i,
 					// and it returns one value.
 					expectedReturnValue := uint32(i * 1000)
@@ -1971,10 +1966,17 @@ func TestArm64Compiler_compileCallIndirect(t *testing.T) {
 						cf := &compiledFunction{
 							codeSegment:        code,
 							codeInitialAddress: uintptr(unsafe.Pointer(&code[0])),
+							source: &wasm.FunctionInstance{
+								TypeID: targetTypeID,
+							},
 						}
-						eng.addCompiledFunction(table[i].FunctionIndex, cf)
+						me.compiledFunctions = append(me.compiledFunctions, cf)
+						table[i] = uintptr(unsafe.Pointer(cf))
 					})
+				}
 
+				for i := 1; i < len(table); i++ {
+					expectedReturnValue := uint32(i * 1000)
 					t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
 						if growCallFrameStack {
 							env.setCallFrameStackPointerLen(1)
@@ -2011,7 +2013,7 @@ func TestArm64Compiler_compileCallIndirect(t *testing.T) {
 							// If the call frame stack pointer equals the length of call frame stack length,
 							// we have to call the builtin function to grow the slice.
 							require.Equal(t, jitCallStatusCodeCallBuiltInFunction, env.jitStatus())
-							require.Equal(t, builtinFunctionIndexGrowCallFrameStack, env.functionCallAddress(), env.functionCallAddress())
+							require.Equal(t, builtinFunctionIndexGrowCallFrameStack, env.builtinFunctionCallAddress())
 
 							// Grow the callFrame stack, and exec again from the return address.
 							ce := env.callEngine()
@@ -2021,7 +2023,7 @@ func TestArm64Compiler_compileCallIndirect(t *testing.T) {
 
 						require.Equal(t, jitCallStatusCodeReturned, env.jitStatus())
 						require.Equal(t, uint64(1), env.stackPointer())
-						require.Equal(t, expectedReturnValue, env.stackTopAsUint32())
+						require.Equal(t, expectedReturnValue, uint32(env.ce.popValue()))
 					})
 				}
 			})
@@ -2135,6 +2137,7 @@ func TestArm64Compiler_compileSwap(t *testing.T) {
 }
 
 func TestArm64Compiler_compileModuleContextInitialization(t *testing.T) {
+	modEngine := &moduleEngine{compiledFunctions: make([]*compiledFunction, 100)}
 	for _, tc := range []struct {
 		name           string
 		moduleInstance *wasm.ModuleInstance
@@ -2142,28 +2145,32 @@ func TestArm64Compiler_compileModuleContextInitialization(t *testing.T) {
 		{
 			name: "no nil",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine:  modEngine,
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
 				Memory:  &wasm.MemoryInstance{Buffer: make([]byte, 10)},
-				Table:   &wasm.TableInstance{Table: make([]wasm.TableElement, 20)},
+				Table:   &wasm.TableInstance{Table: make([]uintptr, 20)},
 			},
 		},
 		{
 			name: "globals nil",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine: modEngine,
 				Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)},
-				Table:  &wasm.TableInstance{Table: make([]wasm.TableElement, 20)},
+				Table:  &wasm.TableInstance{Table: make([]uintptr, 20)},
 			},
 		},
 		{
 			name: "memory nil",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine:  modEngine,
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
-				Table:   &wasm.TableInstance{Table: make([]wasm.TableElement, 20)},
+				Table:   &wasm.TableInstance{Table: make([]uintptr, 20)},
 			},
 		},
 		{
 			name: "table nil",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine:  modEngine,
 				Memory:  &wasm.MemoryInstance{Buffer: make([]byte, 10)},
 				Table:   &wasm.TableInstance{Table: nil},
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
@@ -2172,22 +2179,26 @@ func TestArm64Compiler_compileModuleContextInitialization(t *testing.T) {
 		{
 			name: "table empty",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine:  modEngine,
 				Memory:  &wasm.MemoryInstance{Buffer: make([]byte, 10)},
-				Table:   &wasm.TableInstance{Table: make([]wasm.TableElement, 0)},
+				Table:   &wasm.TableInstance{Table: make([]uintptr, 0)},
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
 			},
 		},
 		{
 			name: "memory zero length",
 			moduleInstance: &wasm.ModuleInstance{
+				Engine:  modEngine,
 				Globals: []*wasm.GlobalInstance{{Val: 100}},
-				Table:   &wasm.TableInstance{Table: make([]wasm.TableElement, 0)},
+				Table:   &wasm.TableInstance{Table: make([]uintptr, 0)},
 				Memory:  &wasm.MemoryInstance{Buffer: make([]byte, 0)},
 			},
 		},
 		{
-			name:           "nil",
-			moduleInstance: &wasm.ModuleInstance{},
+			name: "all nil except mod engine",
+			moduleInstance: &wasm.ModuleInstance{
+				Engine: modEngine,
+			},
 		},
 	} {
 		tc := tc
@@ -2233,6 +2244,8 @@ func TestArm64Compiler_compileModuleContextInitialization(t *testing.T) {
 				require.Equal(t, uint64(tableHeader.Len), ce.moduleContext.tableSliceLen)
 				require.Equal(t, tableHeader.Data, ce.moduleContext.tableElement0Address)
 			}
+
+			require.Equal(t, uintptr(unsafe.Pointer(&modEngine.compiledFunctions[0])), ce.moduleContext.compiledFunctionsElement0Address)
 		})
 	}
 }
@@ -2754,7 +2767,7 @@ func TestArm64Compiler_compileMemoryGrow(t *testing.T) {
 
 	// After the initial exec, the code must exit with builtin function call status and funcaddress for memory grow.
 	require.Equal(t, jitCallStatusCodeCallBuiltInFunction, env.jitStatus())
-	require.Equal(t, builtinFunctionIndexMemoryGrow, env.functionCallAddress())
+	require.Equal(t, builtinFunctionIndexMemoryGrow, env.builtinFunctionCallAddress())
 
 	// Reenter from the return address.
 	jitcall(env.callFrameStackPeek().returnAddress, uintptr(unsafe.Pointer(env.callEngine())))
@@ -2872,8 +2885,7 @@ func TestArm64Compiler_compileHostFunction(t *testing.T) {
 	// TODO: delete after #233
 	compiler.compileNOP()
 
-	addr := wasm.FunctionIndex(100)
-	err := compiler.compileHostFunction(addr)
+	err := compiler.compileHostFunction()
 	require.NoError(t, err)
 
 	// Generate and run the code under test.
@@ -2881,9 +2893,8 @@ func TestArm64Compiler_compileHostFunction(t *testing.T) {
 	require.NoError(t, err)
 	env.exec(code)
 
-	// On the return, the code must exit with the host call status and the specified call address.
+	// On the return, the code must exit with the host call status.
 	require.Equal(t, jitCallStatusCodeCallHostFunction, env.jitStatus())
-	require.Equal(t, addr, env.functionCallAddress())
 
 	// Re-enter the return address.
 	jitcall(env.callFrameStackPeek().returnAddress, uintptr(unsafe.Pointer(env.callEngine())))
diff --git a/internal/wasm/jit/jit_test.go b/internal/wasm/jit/jit_test.go
index 9256209e..2a88ec89 100644
--- a/internal/wasm/jit/jit_test.go
+++ b/internal/wasm/jit/jit_test.go
@@ -11,7 +11,7 @@ import (
 )
 
 type jitEnv struct {
-	eng            *engine
+	me             *moduleEngine
 	ce             *callEngine
 	moduleInstance *wasm.ModuleInstance
 }
@@ -51,8 +51,8 @@ func (j *jitEnv) jitStatus() jitCallStatusCode {
 	return j.ce.exitContext.statusCode
 }
 
-func (j *jitEnv) functionCallAddress() wasm.FunctionIndex {
-	return j.ce.exitContext.functionCallAddress
+func (j *jitEnv) builtinFunctionCallAddress() wasm.Index {
+	return j.ce.exitContext.builtinFunctionCallIndex
 }
 
 func (j *jitEnv) stackPointer() uint64 {
@@ -75,7 +75,7 @@ func (j *jitEnv) getGlobal(index uint32) uint64 {
 	return j.moduleInstance.Globals[index].Val
 }
 
-func (j *jitEnv) setTable(table []wasm.TableElement) {
+func (j *jitEnv) setTable(table []uintptr) {
 	j.moduleInstance.Table = &wasm.TableInstance{Table: table}
 }
 
@@ -99,8 +99,8 @@ func (j *jitEnv) module() *wasm.ModuleInstance {
 	return j.moduleInstance
 }
 
-func (j *jitEnv) engine() *engine {
-	return j.eng
+func (j *jitEnv) moduleEngine() *moduleEngine {
+	return j.me
 }
 
 func (j *jitEnv) callEngine() *callEngine {
@@ -112,8 +112,9 @@ func (j *jitEnv) exec(code []byte) {
 		codeSegment:        code,
 		codeInitialAddress: uintptr(unsafe.Pointer(&code[0])),
 		source: &wasm.FunctionInstance{
-			Kind: wasm.FunctionKindWasm,
-			Type: &wasm.FunctionType{},
+			Kind:   wasm.FunctionKindWasm,
+			Type:   &wasm.FunctionType{},
+			Module: j.moduleInstance,
 		},
 	}
 
@@ -128,15 +129,16 @@ func (j *jitEnv) exec(code []byte) {
 const defaultMemoryPageNumInTest = 2
 
 func newJITEnvironment() *jitEnv {
-	eng := newEngine()
+	me := &moduleEngine{}
 	return &jitEnv{
-		eng: eng,
+		me: me,
 		moduleInstance: &wasm.ModuleInstance{
 			Memory:  &wasm.MemoryInstance{Buffer: make([]byte, wasm.MemoryPageSize*defaultMemoryPageNumInTest)},
 			Table:   &wasm.TableInstance{},
 			Globals: []*wasm.GlobalInstance{},
+			Engine:  me,
 		},
-		ce: eng.newCallEngine(),
+		ce: me.newCallEngine(),
 	}
 }
 
diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go
index 8b4f57e3..0e658571 100644
--- a/internal/wasm/module_test.go
+++ b/internal/wasm/module_test.go
@@ -414,16 +414,22 @@ func TestModule_validateFunctions(t *testing.T) {
 			FunctionSection: []uint32{0},
 			CodeSection:     []*Code{{Body: []byte{OpcodeI32Const, 0, OpcodeDrop, OpcodeEnd}}},
 		}
-		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil, maximumFunctionIndex)
 		require.NoError(t, err)
 	})
+	t.Run("too many functions", func(t *testing.T) {
+		m := Module{}
+		err := m.validateFunctions(Features20191205, []uint32{1, 2, 3, 4}, nil, nil, nil, 3)
+		require.Error(t, err)
+		require.EqualError(t, err, "too many functions in a store")
+	})
 	t.Run("function, but no code", func(t *testing.T) {
 		m := Module{
 			TypeSection:     []*FunctionType{{}},
 			FunctionSection: []Index{0},
 			CodeSection:     nil,
 		}
-		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil, maximumFunctionIndex)
 		require.Error(t, err)
 		require.EqualError(t, err, "code count (0) != function count (1)")
 	})
@@ -433,7 +439,7 @@ func TestModule_validateFunctions(t *testing.T) {
 			FunctionSection: []Index{1},
 			CodeSection:     []*Code{{Body: []byte{OpcodeEnd}}},
 		}
-		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil, maximumFunctionIndex)
 		require.Error(t, err)
 		require.EqualError(t, err, "invalid function[0]: type section index 1 out of range")
 	})
@@ -443,7 +449,7 @@ func TestModule_validateFunctions(t *testing.T) {
 			FunctionSection: []Index{0},
 			CodeSection:     []*Code{{Body: []byte{OpcodeF32Abs}}},
 		}
-		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil, maximumFunctionIndex)
 		require.Error(t, err)
 		require.Contains(t, err.Error(), "invalid function[0]: cannot pop the 1st f32 operand")
 	})
@@ -454,7 +460,7 @@ func TestModule_validateFunctions(t *testing.T) {
 			CodeSection:     []*Code{{Body: []byte{OpcodeF32Abs}}},
 			ExportSection:   map[string]*Export{"f1": {Name: "f1", Type: ExternTypeFunc, Index: 0}},
 		}
-		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil, maximumFunctionIndex)
 		require.Error(t, err)
 		require.Contains(t, err.Error(), `invalid function[0] export["f1"]: cannot pop the 1st f32`)
 	})
@@ -466,7 +472,7 @@ func TestModule_validateFunctions(t *testing.T) {
 			CodeSection:     []*Code{{Body: []byte{OpcodeF32Abs}}},
 			ExportSection:   map[string]*Export{"f1": {Name: "f1", Type: ExternTypeFunc, Index: 1}},
 		}
-		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil, maximumFunctionIndex)
 		require.Error(t, err)
 		require.Contains(t, err.Error(), `invalid function[0] export["f1"]: cannot pop the 1st f32`)
 	})
@@ -480,7 +486,7 @@ func TestModule_validateFunctions(t *testing.T) {
 				"f2": {Name: "f2", Type: ExternTypeFunc, Index: 0},
 			},
 		}
-		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil, maximumFunctionIndex)
 		require.Error(t, err)
 		require.Contains(t, err.Error(), `invalid function[0] export["f1","f2"]: cannot pop the 1st f32`)
 	})
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index ea0e0110..4427a5da 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -94,7 +94,7 @@ func TestPublicModule_String(t *testing.T) {
 	m, err := s.Instantiate(&Module{}, "module")
 	require.NoError(t, err)
 	require.Equal(t, "Module[module]", m.String())
-	require.Equal(t, "Module[module]", s.Module(m.Module.Name).String())
+	require.Equal(t, "Module[module]", s.Module(m.module.Name).String())
 }
 
 func TestStore_ReleaseModule(t *testing.T) {
@@ -156,16 +156,6 @@ func TestStore_ReleaseModule(t *testing.T) {
 			require.NoError(t, s.ReleaseModule(importedModuleName))
 			require.Nil(t, s.modules[importedModuleName])
 			require.NotContains(t, s.modules, importedModuleName)
-
-			// At this point, everything should be freed.
-			require.Len(t, s.modules, 0)
-			for _, f := range s.functions {
-				require.Nil(t, f)
-			}
-
-			// One function, globa, memory and table instance was created and freed,
-			// therefore, one released index must be captured by store.
-			require.Len(t, s.releasedFunctionIndex, 1)
 		})
 	}
 }
@@ -221,11 +211,6 @@ func TestStore_concurrent(t *testing.T) {
 	// At this point 1000 modules import host modules.
 	require.Equal(t, goroutines, hm.dependentCount)
 
-	require.Len(t, s.functions, goroutines+1) // Instantiated + imported one.
-	for _, f := range s.functions {
-		require.NotNil(t, f)
-	}
-
 	// Concurrent release.
 	wg.Add(goroutines)
 	for i := 0; i < goroutines; i++ {
@@ -287,8 +272,6 @@ func TestStore_Instantiate_Errors(t *testing.T) {
 
 	t.Run("compilation failed", func(t *testing.T) {
 		s := newStore()
-		catch := s.engine.(*catchContext)
-		catch.compilationFailIndex = 3
 
 		_, err = s.Instantiate(m, importedModuleName)
 		require.NoError(t, err)
@@ -296,16 +279,13 @@ func TestStore_Instantiate_Errors(t *testing.T) {
 		hm := s.modules[importedModuleName]
 		require.NotNil(t, hm)
 
+		engine := s.engine.(*mockEngine)
+		engine.shouldCompileFail = true
+
 		_, err = s.Instantiate(&Module{
 			TypeSection:     []*FunctionType{{}},
-			FunctionSection: []uint32{0, 0, 0 /* compilation failing function */},
+			FunctionSection: []uint32{0, 0},
 			CodeSection: []*Code{
-				{Body: []byte{OpcodeEnd}}, // FunctionIndex = 1
-				{Body: []byte{OpcodeEnd}}, // FunctionIndex = 2
-				{Body: []byte{OpcodeEnd}}, // FunctionIndex = 3 == compilation failing function.
-				// Functions after failure must not be passed to engine.Release.
-				{Body: []byte{OpcodeEnd}},
-				{Body: []byte{OpcodeEnd}},
 				{Body: []byte{OpcodeEnd}},
 				{Body: []byte{OpcodeEnd}},
 			},
@@ -313,18 +293,16 @@ func TestStore_Instantiate_Errors(t *testing.T) {
 				{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0},
 			},
 		}, importingModuleName)
-		require.EqualError(t, err, "function[2] compilation failed: compilation failed")
+		require.EqualError(t, err, "compilation failed: some compilation error")
 
 		// hm.dependentCount must be intact as the instantiation failed.
 		require.Zero(t, hm.dependentCount)
-
-		require.Equal(t, catch.releasedCalledFunctionIndex, []FunctionIndex{0x1, 0x2})
 	})
 
 	t.Run("start func failed", func(t *testing.T) {
 		s := newStore()
-		catch := s.engine.(*catchContext)
-		catch.callFailIndex = 1
+		engine := s.engine.(*mockEngine)
+		engine.callFailIndex = 1
 
 		_, err = s.Instantiate(m, importedModuleName)
 		require.NoError(t, err)
@@ -413,8 +391,7 @@ func TestFunctionInstance_Call(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			engine := &catchContext{callFailIndex: -1, compilationFailIndex: -1}
-			store := NewStore(storeCtx, engine, Features20191205)
+			store := NewStore(storeCtx, &mockEngine{shouldCompileFail: false, callFailIndex: -1}, Features20191205)
 
 			// Add the host module
 			hm, err := store.Instantiate(m, "host")
@@ -425,7 +402,7 @@ func TestFunctionInstance_Call(t *testing.T) {
 				TypeSection: []*FunctionType{{}},
 				ImportSection: []*Import{{
 					Type:     ExternTypeFunc,
-					Module:   hm.Module.Name,
+					Module:   hm.module.Name,
 					Name:     functionName,
 					DescFunc: 0,
 				}},
@@ -437,56 +414,52 @@ func TestFunctionInstance_Call(t *testing.T) {
 			// This fails if the function wasn't invoked, or had an unexpected context.
 			_, err = mod.ExportedFunction(functionName).Call(mod.WithContext(tc.ctx))
 			require.NoError(t, err)
+
+			modEngine := store.modules["test"].Engine.(*mockModuleEngine)
 			if tc.expected == nil {
-				require.Nil(t, engine.ctx)
+				require.Nil(t, modEngine.ctx)
 			} else {
-				require.Equal(t, tc.expected, engine.ctx.Context())
+				require.Equal(t, tc.expected, modEngine.ctx.Context())
 			}
 		})
 	}
 }
 
-type catchContext struct {
-	ctx                                 *ModuleContext
-	compilationFailIndex, callFailIndex int
-	releasedCalledFunctionIndex         []FunctionIndex
+type mockEngine struct {
+	shouldCompileFail bool
+	callFailIndex     int
 }
 
-func (e *catchContext) Call(ctx *ModuleContext, f *FunctionInstance, _ ...uint64) (results []uint64, err error) {
-	if e.callFailIndex >= 0 && f.Index == FunctionIndex(e.callFailIndex) {
-		return nil, fmt.Errorf("call failed")
-	}
-	e.ctx = ctx
-	return
+type mockModuleEngine struct {
+	ctx           *ModuleContext
+	callFailIndex int
+}
+
+func newStore() *Store {
+	return NewStore(context.Background(), &mockEngine{shouldCompileFail: false, callFailIndex: -1}, Features20191205)
 }
 
-func (e *catchContext) Compile(f *FunctionInstance) error {
-	if e.compilationFailIndex >= 0 && f.Index == FunctionIndex(e.compilationFailIndex) {
-		return fmt.Errorf("compilation failed")
+func (e *mockEngine) Compile(_, _ []*FunctionInstance) (ModuleEngine, error) {
+	if e.shouldCompileFail {
+		return nil, fmt.Errorf("some compilation error")
 	}
-	return nil
+	return &mockModuleEngine{callFailIndex: e.callFailIndex}, nil
 }
 
-func (e *catchContext) Release(f *FunctionInstance) error {
-	e.releasedCalledFunctionIndex = append(e.releasedCalledFunctionIndex, f.Index)
-	return nil
+func (e *mockModuleEngine) FunctionAddress(index Index) uintptr {
+	return uintptr(index)
 }
 
-func TestStore_checkFuncAddrOverflow(t *testing.T) {
-	t.Run("too many functions", func(t *testing.T) {
-		s := newStore()
-		const max = 10
-		s.maximumFunctionIndex = max
-		err := s.checkFunctionIndexOverflow(max + 1)
-		require.Error(t, err)
-	})
-	t.Run("ok", func(t *testing.T) {
-		s := newStore()
-		const max = 10
-		s.maximumFunctionIndex = max
-		err := s.checkFunctionIndexOverflow(max)
-		require.NoError(t, err)
-	})
+func (e *mockModuleEngine) Call(ctx *ModuleContext, f *FunctionInstance, _ ...uint64) (results []uint64, err error) {
+	if e.callFailIndex >= 0 && f.Index == Index(e.callFailIndex) {
+		return nil, fmt.Errorf("call failed")
+	}
+	e.ctx = ctx
+	return
+}
+
+func (e *mockModuleEngine) Release() error {
+	return nil
 }
 
 func TestStore_getTypeInstance(t *testing.T) {
@@ -609,72 +582,6 @@ func TestExecuteConstExpression(t *testing.T) {
 	})
 }
 
-func TestStore_releaseFunctionInstances(t *testing.T) {
-	s := newStore()
-	nonReleaseTargetAddr := FunctionIndex(0)
-	maxAddr := FunctionIndex(10)
-	s.functions = make([]*FunctionInstance, maxAddr+1)
-
-	s.functions[nonReleaseTargetAddr] = &FunctionInstance{} // Non-nil!
-
-	// Set up existing function instances.
-	fs := []*FunctionInstance{{Index: 1}, {Index: 2}, {Index: 3}, {Index: 4}, {Index: maxAddr}}
-	for _, f := range fs {
-		s.functions[f.Index] = &FunctionInstance{} // Non-nil!
-	}
-
-	err := s.releaseFunctions(fs...)
-	require.NoError(t, err)
-
-	// Ensure the release targets become nil.
-	for _, f := range fs {
-		require.Nil(t, s.functions[f.Index])
-		require.Contains(t, s.releasedFunctionIndex, f.Index)
-	}
-
-	// Plus non-target should remain intact.
-	require.NotNil(t, s.functions[nonReleaseTargetAddr])
-}
-
-func TestStore_addFunctionInstances(t *testing.T) {
-	t.Run("no released index", func(t *testing.T) {
-		s := newStore()
-		prevMaxAddr := FunctionIndex(10)
-		s.functions = make([]*FunctionInstance, prevMaxAddr+1)
-
-		for i := FunctionIndex(0); i < 10; i++ {
-			expectedIndex := prevMaxAddr + 1 + i
-			f := &FunctionInstance{}
-			s.addFunctions(f)
-
-			// After adding function intance to store, an funcaddr must be assigned.
-			require.Equal(t, expectedIndex, f.Index)
-		}
-	})
-	t.Run("reuse released index", func(t *testing.T) {
-		s := newStore()
-		expectedAddr := FunctionIndex(10)
-		s.releasedFunctionIndex[expectedAddr] = struct{}{}
-
-		maxAddr := expectedAddr * 10
-		tailInstance := &FunctionInstance{}
-		s.functions = make([]*FunctionInstance, maxAddr+1)
-		s.functions[maxAddr] = tailInstance
-
-		f := &FunctionInstance{}
-		s.addFunctions(f)
-
-		// Index must be reused.
-		require.Equal(t, expectedAddr, f.Index)
-		require.Equal(t, f, s.functions[expectedAddr])
-
-		// And the others must be intact.
-		require.Equal(t, tailInstance, s.functions[maxAddr])
-
-		require.Len(t, s.releasedFunctionIndex, 0)
-	})
-}
-
 func TestStore_resolveImports(t *testing.T) {
 	const moduleName = "test"
 	const name = "target"
@@ -891,7 +798,7 @@ func TestModuleInstance_applyData(t *testing.T) {
 func TestModuleInstance_validateElements(t *testing.T) {
 	functionCounts := uint32(0xa)
 	m := &ModuleInstance{
-		Table:     &TableInstance{Table: make([]TableElement, 10)},
+		Table:     &TableInstance{Table: make([]uintptr, 10)},
 		Functions: make([]*FunctionInstance, 10),
 	}
 	for _, tc := range []struct {
@@ -941,19 +848,20 @@ func TestModuleInstance_validateElements(t *testing.T) {
 func TestModuleInstance_applyElements(t *testing.T) {
 	functionCounts := uint32(0xa)
 	m := &ModuleInstance{
-		Table:     &TableInstance{Table: make([]TableElement, 10)},
+		Table:     &TableInstance{Table: make([]uintptr, 10)},
 		Functions: make([]*FunctionInstance, 10),
+		Engine:    &mockModuleEngine{},
 	}
-	targetAddr, targetOffset := uint32(1), byte(0)
-	targetAddr2, targetOffset2 := functionCounts-1, byte(0x8)
-	m.Functions[targetAddr] = &FunctionInstance{Type: &FunctionType{}, Index: FunctionIndex(targetAddr)}
-	m.Functions[targetAddr2] = &FunctionInstance{Type: &FunctionType{}, Index: FunctionIndex(targetAddr2)}
+	targetIndex, targetOffset := uint32(1), byte(0)
+	targetIndex2, targetOffset2 := functionCounts-1, byte(0x8)
+	m.Functions[targetIndex] = &FunctionInstance{Type: &FunctionType{}, Index: Index(targetIndex)}
+	m.Functions[targetIndex2] = &FunctionInstance{Type: &FunctionType{}, Index: Index(targetIndex2)}
 	m.applyElements([]*ElementSegment{
-		{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{targetOffset}}, Init: []uint32{uint32(targetAddr)}},
-		{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{targetOffset2}}, Init: []uint32{targetAddr2}},
+		{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{targetOffset}}, Init: []uint32{targetIndex}},
+		{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{targetOffset2}}, Init: []uint32{targetIndex2}},
 	})
-	require.Equal(t, FunctionIndex(targetAddr), m.Table.Table[targetOffset].FunctionIndex)
-	require.Equal(t, FunctionIndex(targetAddr2), m.Table.Table[targetOffset2].FunctionIndex)
+	require.Equal(t, uintptr(targetIndex), m.Table.Table[targetOffset])
+	require.Equal(t, uintptr(targetIndex2), m.Table.Table[targetOffset2])
 }
 
 func TestModuleInstance_decDependentCount(t *testing.T) {
@@ -986,7 +894,3 @@ func TestModuleInstance_incDependentCount(t *testing.T) {
 	wg.Wait()
 	require.Equal(t, count, m.dependentCount)
 }
-
-func newStore() *Store {
-	return NewStore(context.Background(), &catchContext{compilationFailIndex: -1, callFailIndex: -1}, Features20191205)
-}
diff --git a/tests/engine/adhoc_test.go b/tests/engine/adhoc_test.go
index d4c5e02a..45be0249 100644
--- a/tests/engine/adhoc_test.go
+++ b/tests/engine/adhoc_test.go
@@ -93,9 +93,6 @@ func testFibonacci(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig)
 }
 
 func testFac(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig) {
-	r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
-	module, err := r.InstantiateModuleFromSource(facWasm)
-	require.NoError(t, err)
 	for _, name := range []string{
 		"fac-rec",
 		"fac-iter",
@@ -104,18 +101,23 @@ func testFac(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig) {
 		"fac-opt",
 	} {
 		name := name
-
-		fac := module.ExportedFunction("fac")
-
 		t.Run(name, func(t *testing.T) {
-			results, err := fac.Call(nil, 25)
+			r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
+			module, err := r.InstantiateModuleFromSource(facWasm)
+			require.NoError(t, err)
+
+			results, err := module.ExportedFunction(name).Call(nil, 25)
 			require.NoError(t, err)
 			require.Equal(t, uint64(7034535277573963776), results[0])
 		})
 	}
 
 	t.Run("fac-rec - stack overflow", func(t *testing.T) {
-		_, err := module.ExportedFunction("fac-rec").Call(nil, 1073741824)
+		r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
+		module, err := r.InstantiateModuleFromSource(facWasm)
+		require.NoError(t, err)
+
+		_, err = module.ExportedFunction("fac-rec").Call(nil, 1073741824)
 		require.ErrorIs(t, err, wasm.ErrRuntimeCallStackOverflow)
 	})
 }