Repository: tetratelabs/wazero
Pull request #782 (2022-08-30 07:20:27)

compiler: simplify calling convention
This simplifies the calling convention, and consolidates the call frame stack
and value stack into a single stack.

As a result, the cost of function calls decreases because we now don't need
to check the boundary twice (value and call frame stacks) at each function call.


The following is the result of the bench mark for recursive fibonnaci
function in integration_test/bench/testdata/case.go, and it shows that
this actually improves the performance of function calls.


### amd64
```
$ benchstat fib_old.txt fib_new.txt
name                               old time/op  new time/op  delta
Invocation/compiler/fib_for_5-32    109ns ± 3%    81ns ± 1%  -25.86%  (p=0.008 n=5+5)
Invocation/compiler/fib_for_10-32   556ns ± 3%   473ns ± 3%  -14.99%  (p=0.008 n=5+5)
Invocation/compiler/fib_for_20-32  61.4µs ± 2%  55.9µs ± 5%   -8.98%  (p=0.008 n=5+5)
Invocation/compiler/fib_for_30-32  7.41ms ± 3%  6.83ms ± 3%   -7.90%  (p=0.008 n=5+5)
```


### arm64
```
$ benchstat old.txt new.txt                    
name                               old time/op    new time/op    delta
Invocation/compiler/fib_for_5-10     67.7ns ± 1%    60.2ns ± 1%  -11.12%  (p=0.000 n=9+9)
Invocation/compiler/fib_for_10-10     487ns ± 1%     460ns ± 0%   -5.56%  (p=0.000 n=10+9)
Invocation/compiler/fib_for_20-10    58.0µs ± 1%    54.3µs ± 1%   -6.38%  (p=0.000 n=10+10)
Invocation/compiler/fib_for_30-10    7.12ms ± 1%    6.67ms ± 1%   -6.31%  (p=0.000 n=10+9)
```

mathetake: ok progress 20% 😄 
mathetake: ok I would say 50% :D
srenatus: ![image](https://user-images.githubusercontent.com/870638/187889329-367156d7-447c-41af-a93d-0f62e9e01888.png)
😄 
mathetake: ok now amd64 has started passing all tests in engine/compiler pkg
mathetake: ok passed all tests on amd64! 
mathetake: cool clearly the function call gets faster! 

```
benchstat fib_old.txt fib_new.txt
name                               old time/op  new time/op  delta
Invocation/compiler/fib_for_5-32    109ns ± 3%    81ns ± 1%  -25.86%  (p=0.008 n=5+5)
Invocation/compiler/fib_for_10-32   556ns ± 3%   473ns ± 3%  -14.99%  (p=0.008 n=5+5)
Invocation/compiler/fib_for_20-32  61.4µs ± 2%  55.9µs ± 5%   -8.98%  (p=0.008 n=5+5)
Invocation/compiler/fib_for_30-32  7.41ms ± 3%  6.83ms ± 3%   -7.90%  (p=0.008 n=5+5)
```

^ is the bench result for
```go
func fibonacci(in uint32) uint32 {
	if in <= 1 {
		return in
	}
	return fibonacci(in-1) + fibonacci(in-2)
}

```

in integration_test/bench/testdata/case.go
mathetake: cool done with arm64... now fuzzing phase!

Patch:
diff --git a/internal/buildoptions/stackoverflow.go b/internal/buildoptions/stackoverflow.go
deleted file mode 100644
index c47e995d..00000000
--- a/internal/buildoptions/stackoverflow.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package buildoptions
-
-// CallStackCeiling is the maximum WebAssembly call stack height. This allows wazero to raise
-// wasm.ErrCallStackOverflow instead of overflowing the Go runtime.
-//
-// The default value should suffice for most use cases. Those wishing to change this can via `go build -ldflags`.
-var CallStackCeiling = 2000
diff --git a/internal/engine/compiler/compiler_value_location.go b/internal/engine/compiler/compiler_value_location.go
index 41a6a692..28f4535b 100644
--- a/internal/engine/compiler/compiler_value_location.go
+++ b/internal/engine/compiler/compiler_value_location.go
@@ -5,6 +5,7 @@ import (
 	"strings"
 
 	"github.com/tetratelabs/wazero/internal/asm"
+	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
 var (
@@ -15,10 +16,6 @@ var (
 	unreservedVectorRegisters []asm.Register
 )
 
-func isNilRegister(r asm.Register) bool {
-	return r == asm.NilRegister
-}
-
 func isGeneralPurposeRegister(r asm.Register) bool {
 	return unreservedGeneralPurposeRegisters[0] <= r && r <= unreservedGeneralPurposeRegisters[len(unreservedGeneralPurposeRegisters)-1]
 }
@@ -212,7 +209,7 @@ func (v *runtimeValueLocationStack) push(loc *runtimeValueLocation) {
 	}
 	v.sp++
 	// stackPointerCeil must be set after sp is incremented since
-	// we skip the stack grow if len(valueStack) >= basePointer+stackPointerCeil.
+	// we skip the stack grow if len(stack) >= basePointer+stackPointerCeil.
 	if v.sp > v.stackPointerCeil {
 		v.stackPointerCeil = v.sp
 	}
@@ -343,3 +340,78 @@ func (v *runtimeValueLocationStack) takeStealTargetFromUsedRegister(tp registerT
 	}
 	return nil, false
 }
+
+// init sets up the runtimeValueLocationStack which reflects the state of
+// the stack at the beginning of the function.
+//
+// See the diagram in callEngine.stack.
+func (v *runtimeValueLocationStack) init(sig *wasm.FunctionType) {
+	for _, t := range sig.Params {
+		loc := v.pushRuntimeValueLocationOnStack()
+		switch t {
+		case wasm.ValueTypeI32:
+			loc.valueType = runtimeValueTypeI32
+		case wasm.ValueTypeI64, wasm.ValueTypeFuncref, wasm.ValueTypeExternref:
+			loc.valueType = runtimeValueTypeI64
+		case wasm.ValueTypeF32:
+			loc.valueType = runtimeValueTypeF32
+		case wasm.ValueTypeF64:
+			loc.valueType = runtimeValueTypeF64
+		case wasm.ValueTypeV128:
+			loc.valueType = runtimeValueTypeV128Lo
+			hi := v.pushRuntimeValueLocationOnStack()
+			hi.valueType = runtimeValueTypeV128Hi
+		default:
+			panic("BUG")
+		}
+	}
+
+	// If the len(results) > len(args), the slots for all results are reserved after
+	// arguments, so we reflect that into the location stack.
+	for i := 0; i < sig.ResultNumInUint64-sig.ParamNumInUint64; i++ {
+		_ = v.pushRuntimeValueLocationOnStack()
+	}
+
+	// Then push the control frame fields.
+	for i := 0; i < callFrameDataSizeInUint64; i++ {
+		loc := v.pushRuntimeValueLocationOnStack()
+		loc.valueType = runtimeValueTypeI64
+	}
+}
+
+// getCallFrameLocations returns each field of callFrame's runtime location.
+//
+// See the diagram in callEngine.stack.
+func (v *runtimeValueLocationStack) getCallFrameLocations(sig *wasm.FunctionType) (
+	returnAddress, callerStackBasePointerInBytes, callerFunction *runtimeValueLocation) {
+	offset := callFrameOffset(sig)
+	return v.stack[offset], v.stack[offset+1], v.stack[offset+2]
+}
+
+// pushCallFrame pushes a call frame's runtime locations onto the stack assuming that
+// the function call parameters are already pushed there.
+//
+// See the diagram in callEngine.stack.
+func (v *runtimeValueLocationStack) pushCallFrame(callTargetFunctionType *wasm.FunctionType) (
+	returnAddress, callerStackBasePointerInBytes, callerFunction *runtimeValueLocation,
+) {
+	// If len(results) > len(args), we reserve the slots for the results below the call frame.
+	reservedSlotsBeforeCallFrame := callTargetFunctionType.ResultNumInUint64 - callTargetFunctionType.ParamNumInUint64
+	for i := 0; i < reservedSlotsBeforeCallFrame; i++ {
+		v.pushRuntimeValueLocationOnStack()
+	}
+
+	// Push the runtime location for each field of callFrame struct. Note that each of them has
+	// uint64 type, and therefore must be treated as runtimeValueTypeI64.
+
+	// callFrame.returnAddress
+	returnAddress = v.pushRuntimeValueLocationOnStack()
+	returnAddress.valueType = runtimeValueTypeI64
+	// callFrame.returnStackBasePointerInBytes
+	callerStackBasePointerInBytes = v.pushRuntimeValueLocationOnStack()
+	callerStackBasePointerInBytes.valueType = runtimeValueTypeI64
+	// callFrame.function
+	callerFunction = v.pushRuntimeValueLocationOnStack()
+	callerFunction.valueType = runtimeValueTypeI64
+	return
+}
diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go
index f6bad49c..e2ebe86a 100644
--- a/internal/engine/compiler/engine.go
+++ b/internal/engine/compiler/engine.go
@@ -51,54 +51,81 @@ type (
 		// These contexts are read and written by compiled code.
 		// Note: structs are embedded to reduce the costs to access fields inside them. Also, this eases field offset
 		// calculation.
-		globalContext
 		moduleContext
-		valueStackContext
+		stackContext
 		exitContext
 		archContext
 
 		// The following fields are not accessed by compiled code directly.
 
-		// valueStack is the go-allocated stack for holding Wasm values.
+		// stack is the go-allocated stack for holding values and call frames.
 		// Note: We never edit len or cap in compiled 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
-
-		// compiled is the initial function for this call engine.
-		compiled *function
-		// source is the FunctionInstance from which compiled is created from.
-		source *wasm.FunctionInstance
-	}
+		//
+		// At any point of execution, say currently executing function F2 which was called by F1, then
+		// the stack should look like like:
+		//
+		// 	[..., arg0, arg1, ..., argN, _, _, _, v1, v2, v3, ....
+		//	      ^                     {       }
+		//	      |                F1's callFrame
+		//	      |
+		//  stackBasePointer
+		//
+		// where
+		//  - callFrame is the F1's callFrame which called F2. It contains F1's return address, F1's base pointer, and F1's *function.
+		//  - stackBasePointer is the stack base pointer stored at (callEngine stackContext.stackBasePointerInBytes)
+		//  - arg0, ..., argN are the function parameters, and v1, v2, v3,... are the local variables
+		//    including the non-function param locals as well as the temporary variable produced by instructions (e.g i32.const).
+		//
+		// If the F2 makes a function call to F3 which takes two arguments, then the stack will become:
+		//
+		// 	[..., arg0, arg1, ..., argN, _, _, _, v1, v2, v3, _, _, _
+		//	                            {       }     ^      {       }
+		//	                       F1's callFrame     | F2's callFrame
+		//	                                          |
+		//                                     stackBasePointer
+		// where
+		// 	- F2's callFrame is pushed above the v2 and v3 (arguments for F3).
+		//  - The previous stackBasePointer (pointed at arg0) was saved inside the F2's callFrame.
+		//
+		// Then, if F3 returns one result, say w1, then the result will look like:
+		//
+		// 	[..., arg0, arg1, ..., argN, _, _, _, v1, w1, ...
+		//	      ^                     {       }
+		//	      |                F1's callFrame
+		//	      |
+		//  stackBasePointer
+		//
+		// where
+		// 	- stackBasePointer was reverted to the position at arg0
+		//  - The result from F3 was pushed above v1
+		//
+		// If the number of parameters is smaller than that of return values, then the empty slots are reserved
+		// below the callFrame to store the results on teh return.
+		// For example, if F3 takes no parameter but returns N(>0) results, then the stack
+		// after making a call against F3 will look like:
+		//
+		// 	[..., arg0, arg1, ..., argN, _, _, _, v1, v2, v3, res_1, _, res_N, _, _, _
+		//	                            {       }            ^                {       }
+		//	                       F1's callFrame            |           F2's callFrame
+		//	                                                 |
+		//                                            stackBasePointer
+		// where res_1, ..., res_N are the reserved slots below the call frame. In general,
+		// the number of reserved slots equals max(0, len(results)-len(params).
+		//
+		// This reserved slots are necessary to save the result values onto the stack while not destroying
+		// the callFrame value on function returns.
+		stack []uint64
 
-	// globalContext holds the data which is constant across multiple function calls.
-	globalContext struct {
-		// valueStackElement0Address is &engine.valueStack[0] as uintptr.
-		// Note: this is updated when growing the stack in builtinFunctionGrowValueStack.
-		valueStackElement0Address uintptr
-		// valueStackLenInBytes is len(engine.valueStack[0]) * 8 (bytes).
-		// Note: this is updated when growing the stack in builtinFunctionGrowValueStack.
-		valueStackLenInBytes uint64
-
-		// callFrameStackElementZeroAddress is &engine.callFrameStack[0] as uintptr.
-		// Note: this is updated when growing the stack in builtinFunctionGrowCallFrameStack.
-		callFrameStackElementZeroAddress uintptr
-		// callFrameStackLen is len(engine.callFrameStack).
-		// Note: this is updated when growing the stack in builtinFunctionGrowCallFrameStack.
-		callFrameStackLen uint64
-		// callFrameStackPointer points at the next empty slot on the call frame stack.
-		// For example, for the next function call, we push the new callFrame onto
-		// callFrameStack[callFrameStackPointer]. This value is incremented/decremented in assembly
-		// when making function calls or returning from them.
-		callFrameStackPointer uint64
+		// initialFn is the initial function for this call engine.
+		initialFn *function
 	}
 
 	// moduleContext holds the per-function call specific module information.
 	// This is subject to be manipulated from compiled native code whenever we make function calls.
 	moduleContext struct {
+		// fn holds the currently executed *function.
+		fn *function
+
 		// moduleInstanceAddress is the address of module instance from which we initialize
 		// the following fields. This is set whenever we enter a function or return from function calls.
 		//
@@ -114,6 +141,8 @@ type (
 		memoryElement0Address uintptr
 		// memorySliceLen is the length of the memory buffer, i.e. len(ModuleInstance.Memory.Buffer).
 		memorySliceLen uint64
+		// memoryInstance holds the memory instance for this module instance.
+		memoryInstance *wasm.MemoryInstance
 		// tableElement0Address is the address of the first item in the tables slice,
 		// i.e. &ModuleInstance.Tables[0] as uintptr.
 		tablesElement0Address uintptr
@@ -131,9 +160,9 @@ type (
 		elementInstancesElement0Address uintptr
 	}
 
-	// valueStackContext stores the data to access engine.valueStack.
-	valueStackContext struct {
-		// stackPointer on .valueStack field which is accessed by valueStack[stackBasePointer+stackBasePointerInBytes*8].
+	// stackContext stores the data to access engine.stack.
+	stackContext struct {
+		// stackPointer on .stack field which is accessed by stack[stackBasePointer+stackBasePointerInBytes*8].
 		//
 		// Note: stackPointer is not used in assembly since the native code knows exact position of
 		// each variable in the value stack from the info from compilation.
@@ -147,11 +176,15 @@ type (
 		// functions are compiled, so they access the stack via [stackBasePointer](fixed for entire function) + [stackPointer].
 		// More precisely, stackBasePointer is set to [callee's stack pointer] + [callee's stack base pointer] - [caller's params].
 		// This way, compiled functions can be independent of the timing of functions calls made against them.
-		//
-		// Note: This is saved on callFrameTop().returnStackBasePointer whenever making function call.
-		// Also, this is changed whenever we make function call or return from functions where we execute jump instruction.
-		// In either case, the caller of "jmp" instruction must set this field properly.
 		stackBasePointerInBytes uint64
+
+		// stackElement0Address is &engine.stack[0] as uintptr.
+		// Note: this is updated when growing the stack in builtinFunctionGrowStack.
+		stackElement0Address uintptr
+
+		// stackLenInBytes is len(engine.stack[0]) * 8 (bytes).
+		// Note: this is updated when growing the stack in builtinFunctionGrowStack.
+		stackLenInBytes uint64
 	}
 
 	// exitContext will be manipulated whenever compiled native code returns into the Go function.
@@ -162,23 +195,25 @@ type (
 		// Set when statusCode == compilerStatusCallBuiltInFunction
 		// Indicating the function call index.
 		builtinFunctionCallIndex wasm.Index
+
+		// returnAddress is the return address which the engine jumps into
+		// after executing a builtin function or host function.
+		returnAddress uintptr
 	}
 
 	// callFrame holds the information to which the caller function can return.
-	// callFrame is created for currently executed function frame as well,
-	// so some fields are not yet set when native code is currently executing it.
-	// That is, callFrameTop().returnAddress or returnStackBasePointerInBytes are not set
-	// until it makes a function call.
+	// This is mixed in callEngine.stack with other Wasm values just like any other
+	// native program (where the stack is the system stack though), and we retrieve the struct
+	// with unsafe pointer casts.
 	callFrame struct {
-		// Set when making function call from this function frame, or for the initial function frame to call from
-		// callEngine.execWasmFunction.
+		// returnAddress is the return address to which the engine jumps when the callee function returns.
 		returnAddress uintptr
-		// Set when making function call from this function frame.
+		// returnStackBasePointerInBytes is the stack base pointer to set on stackContext.stackBasePointerInBytes
+		// when the callee function returns.
 		returnStackBasePointerInBytes uint64
-		// Set when making function call to this function frame.
+		// function is the caller *function, and is used to retrieve the stack trace.
+		// Note: should be possible to revive *function from returnAddress, but might be costly.
 		function *function
-		// _ is a necessary padding to make the size of callFrame struct a power of 2.
-		_ [8]byte
 	}
 
 	// Function corresponds to function instance in Wasm, and is created from `code`.
@@ -188,7 +223,7 @@ type (
 		// and we cache the value (uintptr(unsafe.Pointer(&.codeSegment[0]))) to this field,
 		// so we don't need to repeat the calculation on each function call.
 		codeInitialAddress uintptr
-		// stackPointerCeil is the max of the stack pointer this function can reach. Lazily applied via maybeGrowValueStack.
+		// stackPointerCeil is the max of the stack pointer this function can reach. Lazily applied via maybeGrowStack.
 		stackPointerCeil uint64
 		// source is the source function instance from which this is compiled.
 		source *wasm.FunctionInstance
@@ -204,7 +239,7 @@ type (
 		// codeSegment is holding the compiled native code as a byte slice.
 		codeSegment []byte
 		// See the doc for codeStaticData type.
-		// stackPointerCeil is the max of the stack pointer this function can reach. Lazily applied via maybeGrowValueStack.
+		// stackPointerCeil is the max of the stack pointer this function can reach. Lazily applied via maybeGrowStack.
 		stackPointerCeil uint64
 
 		// indexInModule is the index of this function in the module. For logging purpose.
@@ -231,42 +266,32 @@ const (
 	// Offsets for moduleEngine.functions
 	moduleEngineFunctionsOffset = 16
 
-	// Offsets for callEngine globalContext.
-	callEngineGlobalContextValueStackElement0AddressOffset     = 0
-	callEngineGlobalContextValueStackLenInBytesOffset          = 8
-	callEngineGlobalContextCallFrameStackElement0AddressOffset = 16
-	callEngineGlobalContextCallFrameStackLenOffset             = 24
-	callEngineGlobalContextCallFrameStackPointerOffset         = 32
-
 	// Offsets for callEngine moduleContext.
-	callEngineModuleContextModuleInstanceAddressOffset           = 40
-	callEngineModuleContextGlobalElement0AddressOffset           = 48
-	callEngineModuleContextMemoryElement0AddressOffset           = 56
-	callEngineModuleContextMemorySliceLenOffset                  = 64
-	callEngineModuleContextTablesElement0AddressOffset           = 72
-	callEngineModuleContextFunctionsElement0AddressOffset        = 80
-	callEngineModuleContextTypeIDsElement0AddressOffset          = 88
-	callEngineModuleContextDataInstancesElement0AddressOffset    = 96
-	callEngineModuleContextElementInstancesElement0AddressOffset = 104
-
-	// Offsets for callEngine valueStackContext.
-	callEngineValueStackContextStackPointerOffset            = 112
-	callEngineValueStackContextStackBasePointerInBytesOffset = 120
+	callEngineModuleContextFnOffset                              = 0
+	callEngineModuleContextModuleInstanceAddressOffset           = 8
+	callEngineModuleContextGlobalElement0AddressOffset           = 16
+	callEngineModuleContextMemoryElement0AddressOffset           = 24
+	callEngineModuleContextMemorySliceLenOffset                  = 32
+	callEngineModuleContextMemoryInstanceOffset                  = 40
+	callEngineModuleContextTablesElement0AddressOffset           = 48
+	callEngineModuleContextFunctionsElement0AddressOffset        = 56
+	callEngineModuleContextTypeIDsElement0AddressOffset          = 64
+	callEngineModuleContextDataInstancesElement0AddressOffset    = 72
+	callEngineModuleContextElementInstancesElement0AddressOffset = 80
+
+	// Offsets for callEngine stackContext.
+	callEngineStackContextStackPointerOffset            = 88
+	callEngineStackContextStackBasePointerInBytesOffset = 96
+	callEngineStackContextStackElement0AddressOffset    = 104
+	callEngineStackContextStackLenInBytesOffset         = 112
 
 	// Offsets for callEngine exitContext.
-	callEngineExitContextNativeCallStatusCodeOffset       = 128
-	callEngineExitContextBuiltinFunctionCallAddressOffset = 132
-
-	// Offsets for callFrame.
-	callFrameDataSize                            = 32
-	callFrameDataSizeMostSignificantSetBit       = 5
-	callFrameReturnAddressOffset                 = 0
-	callFrameReturnStackBasePointerInBytesOffset = 8
-	callFrameFunctionOffset                      = 16
+	callEngineExitContextNativeCallStatusCodeOffset     = 120
+	callEngineExitContextBuiltinFunctionCallIndexOffset = 124
+	callEngineExitContextReturnAddressOffset            = 128
 
 	// Offsets for function.
 	functionCodeInitialAddressOffset    = 0
-	functionStackPointerCeilOffset      = 8
 	functionSourceOffset                = 16
 	functionModuleInstanceAddressOffset = 24
 
@@ -306,6 +331,9 @@ const (
 
 	// pointerSizeLog2 satisfies: 1 << pointerSizeLog2 = sizeOf(uintptr)
 	pointerSizeLog2 = 3
+
+	// callFrameDataSizeInUint64 is the size of callFrame struct per 8 bytes (= size of uint64).
+	callFrameDataSizeInUint64 = 24 / 8
 )
 
 // nativeCallStatusCode represents the result of `nativecall`.
@@ -315,8 +343,8 @@ type nativeCallStatusCode uint32
 const (
 	// nativeCallStatusCodeReturned means the nativecall reaches the end of function, and returns successfully.
 	nativeCallStatusCodeReturned nativeCallStatusCode = iota
-	// nativeCallStatusCodeCallHostFunction means the nativecall returns to make a host function call.
-	nativeCallStatusCodeCallHostFunction
+	// nativeCallStatusCodeCallGoHostFunction means the nativecall returns to make a host function call.
+	nativeCallStatusCodeCallGoHostFunction
 	// nativeCallStatusCodeCallBuiltInFunction means the nativecall returns to make a builtin function call.
 	nativeCallStatusCodeCallBuiltInFunction
 	// nativeCallStatusCodeUnreachable means the function invocation reaches "unreachable" instruction.
@@ -360,7 +388,7 @@ func (s nativeCallStatusCode) String() (ret string) {
 	switch s {
 	case nativeCallStatusCodeReturned:
 		ret = "returned"
-	case nativeCallStatusCodeCallHostFunction:
+	case nativeCallStatusCodeCallGoHostFunction:
 		ret = "call_host_function"
 	case nativeCallStatusCodeCallBuiltInFunction:
 		ret = "call_builtin_function"
@@ -384,14 +412,6 @@ func (s nativeCallStatusCode) 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]",
-		c.function.source.FunctionDefinition.DebugName(), c.returnAddress, c.returnStackBasePointerInBytes>>3,
-	)
-}
-
 // releaseCode is a runtime.SetFinalizer function that munmaps the code.codeSegment.
 func releaseCode(compiledFn *code) {
 	codeSegment := compiledFn.codeSegment
@@ -430,7 +450,7 @@ func (e *engine) CompileModule(ctx context.Context, module *wasm.Module) error {
 
 	funcs := make([]*code, 0, len(module.FunctionSection))
 
-	irs, err := wazeroir.CompileFunctions(ctx, e.enabledFeatures, module)
+	irs, err := wazeroir.CompileFunctions(ctx, e.enabledFeatures, callFrameDataSizeInUint64, module)
 	if err != nil {
 		return err
 	}
@@ -542,14 +562,21 @@ func (e *moduleEngine) NewCallEngine(callCtx *wasm.CallContext, f *wasm.Function
 		}
 		return
 	}
-	return e.newCallEngine(f, compiled), nil
+
+	initStackSize := initialStackSize
+	if initialStackSize < compiled.stackPointerCeil {
+		initStackSize = compiled.stackPointerCeil * 2
+	}
+	return e.newCallEngine(initStackSize, compiled), nil
 }
 
 // Call implements the same method as documented on wasm.ModuleEngine.
 func (ce *callEngine) Call(ctx context.Context, callCtx *wasm.CallContext, params ...uint64) (results []uint64, err error) {
+	tp := ce.initialFn.source.Type
+
 	paramCount := len(params)
-	if ce.source.Type.ParamNumInUint64 != paramCount {
-		return nil, fmt.Errorf("expected %d params, but passed %d", ce.source.Type.ParamNumInUint64, paramCount)
+	if tp.ParamNumInUint64 != paramCount {
+		return nil, fmt.Errorf("expected %d params, but passed %d", ce.initialFn.source.Type.ParamNumInUint64, paramCount)
 	}
 
 	// We ensure that this Call method never panics as
@@ -565,11 +592,58 @@ func (ce *callEngine) Call(ctx context.Context, callCtx *wasm.CallContext, param
 		}
 	}()
 
-	for _, v := range params {
+	ce.initializeStack(tp, params)
+	ce.execWasmFunction(ctx, callCtx)
+
+	results = ce.stack[:tp.ResultNumInUint64]
+	return
+}
+
+// initializeStack initializes callEngine.stack before entering native code.
+//
+// The stack must look like, if len(params) < len(results):
+//
+//	[arg0, arg1, ..., argN, 0, 0, 0, ...
+//	                       {       } ^
+//	                       callFrame |
+//	                                 |
+//	                            stackPointer
+//
+// else:
+//
+//	[arg0, arg1, ..., argN, _, _, _,  0, 0, 0, ...
+//	                      |        | {       }  ^
+//	                      |reserved| callFrame  |
+//	                      |        |            |
+//	                      |-------->       stackPointer
+//	                 len(results)-len(params)
+//
+//		 where we reserve the slots below the callframe with the length len(results)-len(params).
+//
+// Note: callFrame {  } is zeroed to indicate that the initial "caller" is this callEngine, not the Wasm function.
+//
+// See callEngine.stack as well.
+func (ce *callEngine) initializeStack(tp *wasm.FunctionType, args []uint64) {
+	for _, v := range args {
 		ce.pushValue(v)
 	}
-	ce.execWasmFunction(ctx, callCtx)
-	results = wasm.PopValues(ce.source.Type.ResultNumInUint64, ce.popValue)
+
+	ce.stackPointer = uint64(callFrameOffset(tp))
+
+	for i := 0; i < callFrameDataSizeInUint64; i++ {
+		ce.stack[ce.stackPointer] = 0
+		ce.stackPointer++
+	}
+}
+
+// callFrameOffset returns the offset of the call frame from the stack base pointer.
+//
+// See the diagram in callEngine.stack.
+func callFrameOffset(funcType *wasm.FunctionType) (ret int) {
+	ret = funcType.ResultNumInUint64
+	if ret < funcType.ParamNumInUint64 {
+		ret = funcType.ParamNumInUint64
+	}
 	return
 }
 
@@ -581,15 +655,30 @@ func (ce *callEngine) Call(ctx context.Context, callCtx *wasm.CallContext, param
 func (ce *callEngine) deferredOnCall(recovered interface{}) (err error) {
 	if recovered != nil {
 		builder := wasmdebug.NewErrorBuilder()
-		for i := uint64(0); i < ce.callFrameStackPointer; i++ {
-			def := ce.callFrameStack[ce.callFrameStackPointer-1-i].function.source.FunctionDefinition
+
+		// Unwinds call frames from the values stack, starting from the
+		// current function `ce.fn`, and the current stack base pointer `ce.stackBasePointerInBytes`.
+		fn := ce.fn
+		stackBasePointer := int(ce.stackBasePointerInBytes >> 3)
+		for {
+			def := fn.source.FunctionDefinition
 			builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes())
+
+			callFrameOffset := callFrameOffset(fn.source.Type)
+			if stackBasePointer != 0 {
+				frame := *(*callFrame)(unsafe.Pointer(&ce.stack[stackBasePointer+callFrameOffset]))
+				fn = frame.function
+				stackBasePointer = int(frame.returnStackBasePointerInBytes >> 3)
+			} else { // base == 0 means that this was the last call frame stacked.
+				break
+			}
 		}
 		err = builder.FromRecovered(recovered)
 	}
 
 	// Allows the reuse of CallEngine.
-	ce.stackBasePointerInBytes, ce.stackPointer, ce.callFrameStackPointer, ce.moduleInstanceAddress = 0, 0, 0, 0
+	ce.stackBasePointerInBytes, ce.stackPointer, ce.moduleInstanceAddress = 0, 0, 0
+	ce.moduleContext.fn = ce.initialFn
 	return
 }
 
@@ -611,7 +700,7 @@ func newEngine(ctx context.Context, enabledFeatures wasm.Features) *engine {
 	}
 }
 
-// Do not make these variables as constants, otherwise there would be
+// Do not make this variable as constant, otherwise there would be
 // dangerous memory access from native code.
 //
 // Background: Go has a mechanism called "goroutine stack-shrink" where Go
@@ -638,114 +727,80 @@ func newEngine(ctx context.Context, enabledFeatures wasm.Features) *engine {
 //	[2] https://github.com/golang/go/blob/68ecdc2c70544c303aa923139a5f16caf107d955/src/runtime/mgc.go#L9
 //	[3] https://mayurwadekar2.medium.com/escape-analysis-in-golang-ee40a1c064c1
 //	[4] https://medium.com/@yulang.chu/go-stack-or-heap-2-slices-which-keep-in-stack-have-limitation-of-size-b3f3adfd6190
-var (
-	initialValueStackSize     = 64
-	initialCallFrameStackSize = 16
-)
+var initialStackSize uint64 = 512
 
-func (e *moduleEngine) newCallEngine(source *wasm.FunctionInstance, compiled *function) *callEngine {
+func (e *moduleEngine) newCallEngine(stackSize uint64, fn *function) *callEngine {
 	ce := &callEngine{
-		valueStack:     make([]uint64, initialValueStackSize),
-		callFrameStack: make([]callFrame, initialCallFrameStackSize),
-		archContext:    newArchContext(),
-		source:         source,
-		compiled:       compiled,
+		stack:         make([]uint64, stackSize),
+		archContext:   newArchContext(),
+		initialFn:     fn,
+		moduleContext: moduleContext{fn: fn},
 	}
 
-	valueStackHeader := (*reflect.SliceHeader)(unsafe.Pointer(&ce.valueStack))
-	callFrameStackHeader := (*reflect.SliceHeader)(unsafe.Pointer(&ce.callFrameStack))
-	ce.globalContext = globalContext{
-		valueStackElement0Address:        valueStackHeader.Data,
-		valueStackLenInBytes:             uint64(valueStackHeader.Len) << 3,
-		callFrameStackElementZeroAddress: callFrameStackHeader.Data,
-		callFrameStackLen:                uint64(callFrameStackHeader.Len),
-		callFrameStackPointer:            0,
+	stackHeader := (*reflect.SliceHeader)(unsafe.Pointer(&ce.stack))
+	ce.stackContext = stackContext{
+		stackElement0Address: stackHeader.Data,
+		stackLenInBytes:      uint64(stackHeader.Len) << 3,
 	}
 	return ce
 }
 
 func (ce *callEngine) popValue() (ret uint64) {
-	ce.valueStackContext.stackPointer--
-	ret = ce.valueStack[ce.valueStackTopIndex()]
+	ce.stackContext.stackPointer--
+	ret = ce.stack[ce.stackTopIndex()]
 	return
 }
 
 func (ce *callEngine) pushValue(v uint64) {
-	ce.valueStack[ce.valueStackTopIndex()] = v
-	ce.valueStackContext.stackPointer++
-}
-
-func (ce *callEngine) callFrameTop() *callFrame {
-	return &ce.callFrameStack[ce.globalContext.callFrameStackPointer-1]
-}
-
-func (ce *callEngine) callFrameAt(depth uint64) *callFrame {
-	idx := ce.globalContext.callFrameStackPointer - 1 - depth
-	return &ce.callFrameStack[idx]
+	ce.stack[ce.stackTopIndex()] = v
+	ce.stackContext.stackPointer++
 }
 
-func (ce *callEngine) valueStackTopIndex() uint64 {
-	return ce.valueStackContext.stackPointer + (ce.valueStackContext.stackBasePointerInBytes >> 3)
+func (ce *callEngine) stackTopIndex() uint64 {
+	return ce.stackContext.stackPointer + (ce.stackContext.stackBasePointerInBytes >> 3)
 }
 
 const (
 	builtinFunctionIndexMemoryGrow wasm.Index = iota
-	builtinFunctionIndexGrowValueStack
-	builtinFunctionIndexGrowCallFrameStack
+	builtinFunctionIndexGrowStack
 	builtinFunctionIndexTableGrow
 	// builtinFunctionIndexBreakPoint is internal (only for wazero developers). Disabled by default.
 	builtinFunctionIndexBreakPoint
 )
 
 func (ce *callEngine) execWasmFunction(ctx context.Context, callCtx *wasm.CallContext) {
-	// Push the initial callframe.
-	ce.callFrameStack[0] = callFrame{returnAddress: ce.compiled.codeInitialAddress, function: ce.compiled}
-	ce.globalContext.callFrameStackPointer++
+	codeAddr := ce.initialFn.codeInitialAddress
+	modAddr := ce.initialFn.moduleInstanceAddress
 
 entry:
 	{
-		frame := ce.callFrameTop()
-		if buildoptions.IsDebugMode {
-			fmt.Printf("callframe=%s, stackBasePointer: %d, stackPointer: %d\n",
-				frame.String(), ce.valueStackContext.stackBasePointerInBytes>>3, ce.valueStackContext.stackPointer)
-		}
-
 		// Call into the native code.
-		nativecall(frame.returnAddress, uintptr(unsafe.Pointer(ce)), frame.function.moduleInstanceAddress)
+		nativecall(codeAddr, uintptr(unsafe.Pointer(ce)), modAddr)
 
 		// Check the status code from Compiler code.
 		switch status := ce.exitContext.statusCode; status {
 		case nativeCallStatusCodeReturned:
-			// Meaning that all the function frames above the previous call frame stack pointer are executed.
-		case nativeCallStatusCodeCallHostFunction:
-			calleeHostFunction := ce.callFrameTop().function
-			// 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.
-			callerFunction := ce.callFrameAt(1).function
-			params := wasm.PopGoFuncParams(calleeHostFunction.source, ce.popValue)
+		case nativeCallStatusCodeCallGoHostFunction:
+			calleeHostFunction := ce.moduleContext.fn
+			base := int(ce.stackBasePointerInBytes >> 3)
+			params := ce.stack[base : base+len(calleeHostFunction.source.Type.Params)]
 			results := wasm.CallGoFunc(
 				ctx,
-				// Use the caller's memory, which might be different from the defining module on an imported function.
-				callCtx.WithMemory(callerFunction.source.Module.Memory),
+				callCtx.WithMemory(ce.memoryInstance),
 				calleeHostFunction.source,
 				params,
 			)
-			for _, v := range results {
-				ce.pushValue(v)
-			}
+			copy(ce.stack[base:], results)
+			codeAddr, modAddr = ce.returnAddress, ce.moduleInstanceAddress
 			goto entry
 		case nativeCallStatusCodeCallBuiltInFunction:
+			caller := ce.moduleContext.fn
 			switch ce.exitContext.builtinFunctionCallIndex {
 			case builtinFunctionIndexMemoryGrow:
-				callerFunction := ce.callFrameTop().function
-				ce.builtinFunctionMemoryGrow(ctx, callerFunction.source.Module.Memory)
-			case builtinFunctionIndexGrowValueStack:
-				callerFunction := ce.callFrameTop().function
-				ce.builtinFunctionGrowValueStack(callerFunction.stackPointerCeil)
-			case builtinFunctionIndexGrowCallFrameStack:
-				ce.builtinFunctionGrowCallFrameStack()
+				ce.builtinFunctionMemoryGrow(ctx, caller.source.Module.Memory)
+			case builtinFunctionIndexGrowStack:
+				ce.builtinFunctionGrowStack(caller.stackPointerCeil)
 			case builtinFunctionIndexTableGrow:
-				caller := ce.callFrameTop().function
 				ce.builtinFunctionTableGrow(ctx, caller.source.Module.Tables)
 			}
 			if buildoptions.IsDebugMode {
@@ -753,6 +808,8 @@ entry:
 					runtime.Breakpoint()
 				}
 			}
+
+			codeAddr, modAddr = ce.returnAddress, ce.moduleInstanceAddress
 			goto entry
 		default:
 			status.causePanic()
@@ -760,36 +817,29 @@ entry:
 	}
 }
 
-func (ce *callEngine) builtinFunctionGrowValueStack(stackPointerCeil uint64) {
-	// Extends the valueStack's length to currentLen*2+stackPointerCeil.
-	newLen := uint64(len(ce.valueStack))<<1 + (stackPointerCeil)
-	newStack := make([]uint64, newLen)
-	ce.valueStackTopIndex()
-	top := ce.valueStackTopIndex()
-	copy(newStack[:top], ce.valueStack[:top])
-	ce.valueStack = newStack
-	valueStackHeader := (*reflect.SliceHeader)(unsafe.Pointer(&ce.valueStack))
-	ce.globalContext.valueStackElement0Address = valueStackHeader.Data
-	ce.globalContext.valueStackLenInBytes = newLen << 3
-}
-
-var callStackCeiling = uint64(buildoptions.CallStackCeiling)
+// callStackCeiling is the maximum WebAssembly call frame stack height. This allows wazero to raise
+// wasm.ErrCallStackOverflow instead of overflowing the Go runtime.
+//
+// The default value should suffice for most use cases. Those wishing to change this can via `go build -ldflags`.
+//
+// TODO: allows to configure this via context?
+var callStackCeiling = uint64(5000000) // in uint64 (8 bytes) == 40000000 bytes in total == 40mb.
 
-func (ce *callEngine) builtinFunctionGrowCallFrameStack() {
-	if callStackCeiling < uint64(len(ce.callFrameStack)+1) {
-		panic(wasmruntime.ErrRuntimeCallStackOverflow)
+func (ce *callEngine) builtinFunctionGrowStack(stackPointerCeil uint64) {
+	oldLen := uint64(len(ce.stack))
+	if callStackCeiling < oldLen {
+		panic(wasmruntime.ErrRuntimeStackOverflow)
 	}
 
-	// Double the callstack slice length.
-	newLen := uint64(ce.globalContext.callFrameStackLen) * 2
-	newStack := make([]callFrame, newLen)
-	copy(newStack, ce.callFrameStack)
-	ce.callFrameStack = newStack
-
-	// Update the globalContext's fields as they become stale after the update ^^.
-	stackSliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&newStack))
-	ce.globalContext.callFrameStackLen = uint64(stackSliceHeader.Len)
-	ce.globalContext.callFrameStackElementZeroAddress = stackSliceHeader.Data
+	// Extends the stack's length to oldLen*2+stackPointerCeil.
+	newLen := oldLen<<1 + (stackPointerCeil)
+	newStack := make([]uint64, newLen)
+	top := ce.stackTopIndex()
+	copy(newStack[:top], ce.stack[:top])
+	ce.stack = newStack
+	stackHeader := (*reflect.SliceHeader)(unsafe.Pointer(&ce.stack))
+	ce.stackContext.stackElement0Address = stackHeader.Data
+	ce.stackContext.stackLenInBytes = newLen << 3
 }
 
 func (ce *callEngine) builtinFunctionMemoryGrow(ctx context.Context, mem *wasm.MemoryInstance) {
diff --git a/internal/engine/compiler/impl_amd64.go b/internal/engine/compiler/impl_amd64.go
index 9f77e1c5..bb442a0c 100644
--- a/internal/engine/compiler/impl_amd64.go
+++ b/internal/engine/compiler/impl_amd64.go
@@ -72,10 +72,10 @@ var (
 )
 
 var (
-	// amd64CallingConventionModuleInstanceAddressRegister holds *wasm.ModuleInstance of the
+	// amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister holds *wasm.ModuleInstance of the
 	// next executing function instance. The value is set and used when making function calls
 	// or function returns in the ModuleContextInitialization. See compileModuleContextInitialization.
-	amd64CallingConventionModuleInstanceAddressRegister = amd64.RegR12
+	amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister = amd64.RegR12
 )
 
 func (c *amd64Compiler) String() string {
@@ -161,12 +161,14 @@ func (c *amd64Compiler) label(labelKey string) *amd64LabelInfo {
 // and return to the caller.
 func (c *amd64Compiler) compileGoDefinedHostFunction() error {
 	// First we must update the location stack to reflect the number of host function inputs.
-	c.pushFunctionParams()
+	c.locationStack.init(c.ir.Signature)
 
-	if err := c.compileCallHostFunction(); err != nil {
+	if err := c.compileCallGoHostFunction(); err != nil {
 		return err
 	}
 
+	// Initializes the reserved stack base pointer which is used to retrieve the call frame stack.
+	c.compileReservedStackBasePointerInitialization()
 	return c.compileReturnFunction()
 }
 
@@ -196,28 +198,6 @@ func (c *amd64Compiler) compile() (code []byte, stackPointerCeil uint64, err err
 	return
 }
 
-func (c *amd64Compiler) pushFunctionParams() {
-	for _, t := range c.ir.Signature.Params {
-		loc := c.locationStack.pushRuntimeValueLocationOnStack()
-		switch t {
-		case wasm.ValueTypeI32:
-			loc.valueType = runtimeValueTypeI32
-		case wasm.ValueTypeI64, wasm.ValueTypeFuncref, wasm.ValueTypeExternref:
-			loc.valueType = runtimeValueTypeI64
-		case wasm.ValueTypeF32:
-			loc.valueType = runtimeValueTypeF32
-		case wasm.ValueTypeF64:
-			loc.valueType = runtimeValueTypeF64
-		case wasm.ValueTypeV128:
-			loc.valueType = runtimeValueTypeV128Lo
-			hi := c.locationStack.pushRuntimeValueLocationOnStack()
-			hi.valueType = runtimeValueTypeV128Hi
-		default:
-			panic("BUG")
-		}
-	}
-}
-
 // compileUnreachable implements compiler.compileUnreachable for the amd64 architecture.
 func (c *amd64Compiler) compileUnreachable() error {
 	c.compileExitFromNativeCode(nativeCallStatusCodeUnreachable)
@@ -696,34 +676,34 @@ func (c *amd64Compiler) compileLabel(o *wazeroir.OperationLabel) (skipLabel bool
 
 // compileCall implements compiler.compileCall for the amd64 architecture.
 func (c *amd64Compiler) compileCall(o *wazeroir.OperationCall) error {
+	if err := c.maybeCompileMoveTopConditionalToGeneralPurposeRegister(); err != nil {
+		return err
+	}
+
 	target := c.ir.Functions[o.FunctionIndex]
 	targetType := c.ir.Types[target]
-	if err := c.compileCallFunctionImpl(o.FunctionIndex, asm.NilRegister, targetType); err != nil {
+
+	targetAddressRegister, err := c.allocateRegister(registerTypeGeneralPurpose)
+	if err != nil {
 		return err
 	}
 
-	// We consumed the function parameters from the stack after call.
-	for i := 0; i < targetType.ParamNumInUint64; i++ {
-		c.locationStack.pop()
-	}
+	// First, we read the address of the first item of callEngine.functions slice (= &callEngine.functions[0])
+	// into tmpRegister.
+	c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine,
+		callEngineModuleContextFunctionsElement0AddressOffset, targetAddressRegister)
 
-	// Also, the function results were pushed by the call.
-	for _, t := range targetType.Results {
-		loc := c.locationStack.pushRuntimeValueLocationOnStack()
-		switch t {
-		case wasm.ValueTypeI32:
-			loc.valueType = runtimeValueTypeI32
-		case wasm.ValueTypeI64, wasm.ValueTypeFuncref, wasm.ValueTypeExternref:
-			loc.valueType = runtimeValueTypeI64
-		case wasm.ValueTypeF32:
-			loc.valueType = runtimeValueTypeF32
-		case wasm.ValueTypeF64:
-			loc.valueType = runtimeValueTypeF64
-		case wasm.ValueTypeV128:
-			loc.valueType = runtimeValueTypeV128Lo
-			hi := c.locationStack.pushRuntimeValueLocationOnStack()
-			hi.valueType = runtimeValueTypeV128Hi
-		}
+	// next, read the address of the target function (= &callEngine.codes[offset])
+	// into targetAddressRegister.
+	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
+		// Note: FunctionIndex is limited up to 2^27 so this offset never exceeds 32-bit integer.
+		// *8 because the size of *code equals 8 bytes.
+		targetAddressRegister, int64(o.FunctionIndex)*8,
+		targetAddressRegister,
+	)
+
+	if err := c.compileCallFunctionImpl(targetAddressRegister, targetType); err != nil {
+		return err
 	}
 	return nil
 }
@@ -806,36 +786,12 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 
 	c.assembler.SetJumpTargetOnNext(jumpIfTypeMatch)
 	targetFunctionType := c.ir.Types[o.TypeIndex]
-	if err = c.compileCallFunctionImpl(0, offset.register, targetFunctionType); err != nil {
+	if err = c.compileCallFunctionImpl(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, tmp, tmp2)
-
-	// We consumed the function parameters from the stack after call.
-	for i := 0; i < targetFunctionType.ParamNumInUint64; i++ {
-		c.locationStack.pop()
-	}
-
-	// Also, the function results were pushed by the call.
-	for _, t := range targetFunctionType.Results {
-		loc := c.locationStack.pushRuntimeValueLocationOnStack()
-		switch t {
-		case wasm.ValueTypeI32:
-			loc.valueType = runtimeValueTypeI32
-		case wasm.ValueTypeI64, wasm.ValueTypeFuncref, wasm.ValueTypeExternref:
-			loc.valueType = runtimeValueTypeI64
-		case wasm.ValueTypeF32:
-			loc.valueType = runtimeValueTypeF32
-		case wasm.ValueTypeF64:
-			loc.valueType = runtimeValueTypeF64
-		case wasm.ValueTypeV128:
-			loc.valueType = runtimeValueTypeV128Lo
-			hi := c.locationStack.pushRuntimeValueLocationOnStack()
-			hi.valueType = runtimeValueTypeV128Hi
-		}
-	}
 	return nil
 }
 
@@ -4487,220 +4443,124 @@ func (c *amd64Compiler) allocateRegister(t registerType) (reg asm.Register, err
 }
 
 // callFunction adds instructions to call a function whose address equals either addr parameter or the value on indexReg.
-// Pass indexReg == asm.NilRegister to indicate that use addr argument as the source of target function's address.
-// Otherwise, the added code tries to read the function address from the register for indexReg argument.
 //
 // Note: this is the counterpart for returnFunction, and see the comments there as well
 // to understand how the function calls are achieved.
-func (c *amd64Compiler) compileCallFunctionImpl(index wasm.Index, functionAddressRegister asm.Register, functype *wasm.FunctionType) error {
+func (c *amd64Compiler) compileCallFunctionImpl(functionAddressRegister asm.Register, functype *wasm.FunctionType) error {
 	// Release all the registers as our calling convention requires the caller-save.
 	if err := c.compileReleaseAllRegistersToStack(); err != nil {
 		return err
 	}
 
-	// First, we have to make sure that
-	if !isNilRegister(functionAddressRegister) {
-		c.locationStack.markRegisterUsed(functionAddressRegister)
-	}
+	c.locationStack.markRegisterUsed(functionAddressRegister)
 
-	// Obtain the temporary registers to be used in the followings.
-	freeRegs, found := c.locationStack.takeFreeRegisters(registerTypeGeneralPurpose, 4)
+	// Obtain a temporary register to be used in the followings.
+	tmpRegister, found := c.locationStack.takeFreeRegister(registerTypeGeneralPurpose)
 	if !found {
 		// This in theory never happen as all the registers must be free except codeAddressRegister.
 		return fmt.Errorf("could not find enough free registers")
 	}
-	c.locationStack.markRegisterUsed(freeRegs...)
-
-	// Alias these free tmp registers for readability.
-	callFrameStackPointerRegister, tmpRegister, targetFunctionAddressRegister,
-		callFrameStackTopAddressRegister := freeRegs[0], freeRegs[1], freeRegs[2], freeRegs[3]
-
-	// First, we read the current call frame stack pointer.
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		amd64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset,
-		callFrameStackPointerRegister)
-
-	// And compare it with the underlying slice length.
-	c.assembler.CompileMemoryToRegister(amd64.CMPQ,
-		amd64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackLenOffset, callFrameStackPointerRegister)
-
-	// If they do not equal, then we don't have to grow the call frame stack.
-	jmpIfNotCallFrameStackNeedsGrow := c.assembler.CompileJump(amd64.JNE)
-
-	// Otherwise, we have to make the builtin function call to grow the call frame stack.
-	if !isNilRegister(functionAddressRegister) {
-		// If we need to get the target funcaddr from register (call_indirect case), we must save it before growing the
-		// call-frame stack, as the register is not saved across function calls.
-		savedOffsetLocation := c.pushRuntimeValueLocationOnRegister(functionAddressRegister, runtimeValueTypeI64)
-		c.compileReleaseRegisterToStack(savedOffsetLocation)
-	}
-
-	// Grow the stack.
-	if err := c.compileCallBuiltinFunction(builtinFunctionIndexGrowCallFrameStack); err != nil {
-		return err
-	}
 
-	// For call_indirect, we need to push the value back to the register.
-	if !isNilRegister(functionAddressRegister) {
-		// Since this is right after callGoFunction, we have to initialize the stack base pointer
-		// to properly load the value on memory stack.
-		c.compileReservedStackBasePointerInitialization()
+	// The stack should look like:
+	//
+	//               reserved slots for results (if len(results) > len(args))
+	//                      |     |
+	//    ,arg0, ..., argN, ..., _, .returnAddress, .returnStackBasePointerInBytes, .function, ....
+	//      |                       |                                                        |
+	//      |             callFrame{^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^}
+	//      |
+	// nextStackBasePointerOffset
+	//
+	// where callFrame is used to return to this currently executed function.
 
-		savedOffsetLocation := c.locationStack.pop()
-		savedOffsetLocation.setRegister(functionAddressRegister)
-		c.compileLoadValueOnStackToRegister(savedOffsetLocation)
-	}
+	nextStackBasePointerOffset := int64(c.locationStack.sp) - int64(functype.ParamNumInUint64)
 
-	// Also we have to re-read the call frame stack pointer into callFrameStackPointerRegister.
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		amd64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset,
-		callFrameStackPointerRegister)
+	callFrameReturnAddressLoc, callFrameStackBasePointerInBytesLoc, callFrameFunctionLoc :=
+		c.locationStack.pushCallFrame(functype)
 
-	// Now that call-frame stack is enough length, we are ready to create a new call frame
-	// for the function call we are about to make.
-	c.assembler.SetJumpTargetOnNext(jmpIfNotCallFrameStackNeedsGrow)
+	// Save the current stack base pointer at callFrameStackBasePointerInBytesLoc.
 	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		amd64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackElement0AddressOffset,
+		amd64ReservedRegisterForCallEngine, callEngineStackContextStackBasePointerInBytesOffset,
 		tmpRegister)
+	callFrameStackBasePointerInBytesLoc.setRegister(tmpRegister)
+	c.compileReleaseRegisterToStack(callFrameStackBasePointerInBytesLoc)
 
-	// Since call frame stack pointer is the index for callEngine.callFrameStack slice,
-	// here we get the actual offset in bytes via shifting callFrameStackPointerRegister by callFrameDataSizeMostSignificantSetBit.
-	// That is valid because the size of callFrame struct is a power of 2 (see TestVerifyOffsetValue), which means
-	// multiplying with the size of struct equals shifting by its most significant bit.
-	c.assembler.CompileConstToRegister(amd64.SHLQ, int64(callFrameDataSizeMostSignificantSetBit), callFrameStackPointerRegister)
+	// Set callEngine.stackContext.stackBasePointer for the next function.
+	c.assembler.CompileConstToRegister(amd64.ADDQ, nextStackBasePointerOffset<<3, tmpRegister)
 
-	// At this point, callFrameStackPointerRegister holds the offset in call frame slice in bytes,
-	// and tmpRegister holds the absolute address of the first item of call frame slice.
-	// To illustrate the situation:
-	//
-	//  tmpRegister (holding the absolute address of &callFrame[0])
-	//      |
-	//      [ra.0, rb.0, rc.0, _, ra.1, rb.1, rc.1, _, ra.next, rb.next, rc.next, ...]  <--- call frame stack's data region (somewhere in the memory)
-	//      |                                        |
-	//      |---------------------------------------->
-	//          callFrameStackPointerRegister (holding the offset from &callFrame[0] in bytes.)
-	//
-	// where:
-	//      ra.* = callFrame.returnAddress
-	//      rb.* = callFrame.returnStackBasePointer
-	//      rc.* = callFrame.code
-	//      _  = callFrame's padding (see comment on callFrame._ field.)
-	//
-	// In the following comment, we use the notations in the above example.
-	//
-	// What we have to do in the following is that
-	//   1) Set rb.1 so that we can return back to this function properly.
-	//   2) Set callEngine.valueStackContext.stackBasePointer for the next function.
-	//   3) Set rc.next to specify which function is executed on the current call frame (needs to make builtin function calls).
-	//   4) Set ra.1 so that we can return back to this function properly.
-
-	// First, read the address corresponding to tmpRegister+callFrameStackPointerRegister
-	// by LEA instruction which equals the address of call frame stack top.
-	c.assembler.CompileMemoryWithIndexToRegister(amd64.LEAQ,
-		tmpRegister, 0, callFrameStackPointerRegister, 1,
-		callFrameStackTopAddressRegister)
-
-	// 1) Set rb.1 so that we can return back to this function properly.
-	{
-		// We must save the current stack base pointer (which lives on callEngine.valueStackContext.stackPointer)
-		// to the call frame stack. In the example, this is equivalent to writing the value into "rb.1".
-		c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine, callEngineValueStackContextStackBasePointerInBytesOffset, tmpRegister)
-
-		c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister,
-			// "rb.1" is BELOW the top address. See the above example for detail.
-			callFrameStackTopAddressRegister, -(callFrameDataSize - callFrameReturnStackBasePointerInBytesOffset),
-		)
-	}
+	// Write the calculated value to callEngine.stackContext.stackBasePointer.
+	c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister,
+		amd64ReservedRegisterForCallEngine, callEngineStackContextStackBasePointerInBytesOffset)
 
-	// 2) Set callEngine.valueStackContext.stackBasePointer for the next function.
-	if offset := int64(c.locationStack.sp) - int64(functype.ParamNumInUint64); offset > 0 {
-		// At this point, tmpRegister holds the old stack base pointer. We could get the new frame's
-		// stack base pointer by "old stack base pointer + old stack pointer - # of function params"
-		// See the comments in callEngine.pushCallFrame which does exactly the same calculation in Go.
-		c.assembler.CompileConstToRegister(amd64.ADDQ, offset<<3, tmpRegister)
+	// Save the currently executed *function (placed at callEngine.moduleContext.fn) into callFrameFunctionLoc.
+	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
+		amd64ReservedRegisterForCallEngine, callEngineModuleContextFnOffset,
+		tmpRegister)
+	callFrameFunctionLoc.setRegister(tmpRegister)
+	c.compileReleaseRegisterToStack(callFrameFunctionLoc)
 
-		// Write the calculated value to callEngine.valueStackContext.stackBasePointer.
-		c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister, amd64ReservedRegisterForCallEngine, callEngineValueStackContextStackBasePointerInBytesOffset)
-	}
+	// Set callEngine.moduleContext.fn to the next *function.
+	c.assembler.CompileRegisterToMemory(amd64.MOVQ, functionAddressRegister,
+		amd64ReservedRegisterForCallEngine, callEngineModuleContextFnOffset)
 
-	// 3) Set rc.next to specify which function is executed on the current call frame (needs to make builtin function calls).
-	{
-		if isNilRegister(functionAddressRegister) {
-			// We must set the target function's address(pointer) of *code into the next call-frame 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.functions slice (= &callEngine.functions[0])
-			// into tmpRegister.
-			c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine,
-				callEngineModuleContextFunctionsElement0AddressOffset, tmpRegister)
-
-			// next, read the address of the target function (= &callEngine.codes[offset])
-			// into targetAddressRegister.
-			c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-				// Note: FunctionIndex is limited up to 2^27 so this offset never exceeds 32-bit integer.
-				// *8 because the size of *code equals 8 bytes.
-				tmpRegister, int64(index)*8,
-				targetFunctionAddressRegister,
-			)
-		} else {
-			targetFunctionAddressRegister = functionAddressRegister
-		}
-		// Finally, we are ready to place the address of the target function's *code into the new call-frame.
-		// In the example, this is equivalent to set "rc.next".
-		c.assembler.CompileRegisterToMemory(amd64.MOVQ, targetFunctionAddressRegister,
-			callFrameStackTopAddressRegister, callFrameFunctionOffset)
-	}
-
-	// 4) Set ra.1 so that we can return to this function properly.
-	//
-	// We have to set the return address for the current call frame (which is "ra.1" in the example).
-	// First, Get the return address into the tmpRegister.
+	// Write the return address into callFrameReturnAddressLoc.
 	c.assembler.CompileReadInstructionAddress(tmpRegister, amd64.JMP)
+	callFrameReturnAddressLoc.setRegister(tmpRegister)
+	c.compileReleaseRegisterToStack(callFrameReturnAddressLoc)
 
-	// Now we are ready to set the return address to the current call frame.
-	// This is equivalent to set "ra.1" in the example.
-	c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister,
-		callFrameStackTopAddressRegister,
-		// "ra.1" is BELOW the top address. See the above example for detail.
-		-(callFrameDataSize - callFrameReturnAddressOffset),
-	)
-
-	// Every preparation (1 to 5 in the description above) is done to enter into the target function.
-	// So we increment the call frame stack pointer.
-	c.assembler.CompileNoneToMemory(amd64.INCQ, amd64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset)
-
-	if amd64CallingConventionModuleInstanceAddressRegister == targetFunctionAddressRegister {
+	if amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister == functionAddressRegister {
 		// This case we must move the value on targetFunctionAddressRegister to another register, otherwise
 		// the address (jump target below) will be modified and result in segfault.
 		// See #526.
-		c.assembler.CompileRegisterToRegister(amd64.MOVQ, targetFunctionAddressRegister, tmpRegister)
-		targetFunctionAddressRegister = tmpRegister
+		c.assembler.CompileRegisterToRegister(amd64.MOVQ, functionAddressRegister, tmpRegister)
+		functionAddressRegister = tmpRegister
 	}
 
-	// Also, we have to put the target function's *wasm.ModuleInstance into amd64CallingConventionModuleInstanceAddressRegister.
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ, targetFunctionAddressRegister, functionModuleInstanceAddressOffset,
-		amd64CallingConventionModuleInstanceAddressRegister)
+	// Also, we have to put the target function's *wasm.ModuleInstance into amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister.
+	c.assembler.CompileMemoryToRegister(amd64.MOVQ, functionAddressRegister, functionModuleInstanceAddressOffset,
+		amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister)
 
 	// And jump into the initial address of the target function.
-	c.assembler.CompileJumpToMemory(amd64.JMP, targetFunctionAddressRegister, functionCodeInitialAddressOffset)
+	c.assembler.CompileJumpToMemory(amd64.JMP, functionAddressRegister, functionCodeInitialAddressOffset)
 
 	// All the registers used are temporary, so we mark them unused.
-	c.locationStack.markRegisterUnused(freeRegs...)
+	c.locationStack.markRegisterUnused(tmpRegister, functionAddressRegister)
 
 	// On the function return, we have to initialize the state.
-	// This could be reached after returnFunction(), so callEngine.valueStackContext.stackBasePointer
-	// and callEngine.moduleContext.moduleInstanceAddress are changed (See comments in returnFunction()).
-	// Therefore we have to initialize the state according to these changes.
 	if err := c.compileModuleContextInitialization(); err != nil {
 		return err
 	}
 
-	// Due to the change to callEngine.valueStackContext.stackBasePointer.
+	// Due to the change to callEngine.stackContext.stackBasePointer.
 	c.compileReservedStackBasePointerInitialization()
 
 	// Due to the change to callEngine.moduleContext.moduleInstanceAddress as that might result in
 	// the memory instance manipulation.
 	c.compileReservedMemoryPointerInitialization()
+
+	// We consumed the function parameters, the call frame stack and reserved slots during the call.
+	c.locationStack.sp = uint64(nextStackBasePointerOffset)
+
+	// Now the function results are pushed by the call.
+	for _, t := range functype.Results {
+		loc := c.locationStack.pushRuntimeValueLocationOnStack()
+		switch t {
+		case wasm.ValueTypeI32:
+			loc.valueType = runtimeValueTypeI32
+		case wasm.ValueTypeI64, wasm.ValueTypeFuncref, wasm.ValueTypeExternref:
+			loc.valueType = runtimeValueTypeI64
+		case wasm.ValueTypeF32:
+			loc.valueType = runtimeValueTypeF32
+		case wasm.ValueTypeF64:
+			loc.valueType = runtimeValueTypeF64
+		case wasm.ValueTypeV128:
+			loc.valueType = runtimeValueTypeV128Lo
+			hi := c.locationStack.pushRuntimeValueLocationOnStack()
+			hi.valueType = runtimeValueTypeV128Hi
+		default:
+			panic("BUG: invalid type: " + wasm.ValueTypeName(t))
+		}
+	}
 	return nil
 }
 
@@ -4717,118 +4577,64 @@ func (c *amd64Compiler) compileReturnFunction() error {
 		return err
 	}
 
-	// amd64CallingConventionModuleInstanceAddressRegister holds the module intstance's address
+	// amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister holds the module instance's address
 	// so mark it used so that it won't be used as a free register.
-	c.locationStack.markRegisterUsed(amd64CallingConventionModuleInstanceAddressRegister)
-	defer c.locationStack.markRegisterUnused(amd64CallingConventionModuleInstanceAddressRegister)
+	c.locationStack.markRegisterUsed(amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister)
+	defer c.locationStack.markRegisterUnused(amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister)
 
-	// Obtain the temporary registers to be used in the followings.
-	regs, found := c.locationStack.takeFreeRegisters(registerTypeGeneralPurpose, 3)
+	// Obtain a temporary register to be used in the following.
+	returnAddressRegister, found := c.locationStack.takeFreeRegister(registerTypeGeneralPurpose)
 	if !found {
 		panic("BUG: all the registers should be free at this point: " + c.locationStack.String())
 	}
-	c.locationStack.markRegisterUsed(regs...)
-
-	// Alias these free tmp registers for readability.
-	decrementedCallFrameStackPointerRegister, callFrameStackTopAddressRegister, tmpRegister := regs[0], regs[1], regs[2]
 
-	// Since we return from the function, we need to decrement the callframe stack pointer.
-	c.assembler.CompileNoneToMemory(amd64.DECQ, amd64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset)
+	returnAddress, callerStackBasePointerInBytes, callerFunction := c.locationStack.getCallFrameLocations(c.ir.Signature)
 
-	// next, get the decremented callframe stack pointer into decrementedCallFrameStackPointerRegister.
+	// A zero return address means return from the execution.
 	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		amd64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset,
-		decrementedCallFrameStackPointerRegister)
-
-	// We have to exit if the decremented stack pointer equals zero.
-	c.assembler.CompileRegisterToRegister(amd64.TESTQ, decrementedCallFrameStackPointerRegister, decrementedCallFrameStackPointerRegister)
-
-	jmpIfNotCallStackPointerZero := c.assembler.CompileJump(amd64.JNE)
+		amd64ReservedRegisterForStackBasePointerAddress, int64(returnAddress.stackPointer)*8,
+		returnAddressRegister,
+	)
+	c.assembler.CompileRegisterToRegister(amd64.TESTQ, returnAddressRegister, returnAddressRegister)
 
-	// If the callframe stack pointer equals the previous one,
-	// we exit the Compiler call with returned status.
+	jmpIfNotReturn := c.assembler.CompileJump(amd64.JNE)
 	c.compileExitFromNativeCode(nativeCallStatusCodeReturned)
 
-	// Otherwise, we return back to the top call frame.
-	//
-	// Since call frame stack pointer is the index for callEngine.callFrameStack slice,
-	// here we get the actual offset in bytes via shifting decrementedCallFrameStackPointerRegister by callFrameDataSizeMostSignificantSetBit.
-	// That is valid because the size of callFrame struct is a power of 2 (see TestVerifyOffsetValue), which means
-	// multiplying withe the size of struct equals shifting by its most significant bit.
-	c.assembler.SetJumpTargetOnNext(jmpIfNotCallStackPointerZero)
-	c.assembler.CompileConstToRegister(amd64.SHLQ, int64(callFrameDataSizeMostSignificantSetBit), decrementedCallFrameStackPointerRegister)
-
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		amd64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackElement0AddressOffset, tmpRegister)
+	// Otherwise, we return to the caller.
+	c.assembler.SetJumpTargetOnNext(jmpIfNotReturn)
 
-	c.assembler.CompileMemoryWithIndexToRegister(amd64.LEAQ,
-		tmpRegister, 0, decrementedCallFrameStackPointerRegister, 1,
-		callFrameStackTopAddressRegister)
+	// Alias for readability.
+	tmpRegister := amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister
 
-	// At this point, decrementedCallFrameStackPointerRegister holds the offset in call frame slice in bytes,
-	// and tmpRegister holds the absolute address of the first item of call frame slice.
-	// To illustrate the situation:
-	//
-	//  tmpRegister (holding the absolute address of &callFrame[0])
-	//      |                              callFrameStackTopAddressRegister (absolute address of tmpRegister+decrementedCallFrameStackPointerRegister)
-	//      |                                           |
-	//      [......., ra.caller, rb.caller, rc.caller, _, ra.current, rb.current, rc.current, _, ...]  <--- call frame stack's data region (somewhere in the memory)
-	//      |                                           |
-	//      |------------------------------------------->
-	//           decrementedCallFrameStackPointerRegister (holding the offset from &callFrame[0] in bytes.)
-	//
-	// where:
-	//      ra.* = callFrame.returnAddress
-	//      rb.* = callFrame.returnStackBasePointer
-	//      rc.* = callFrame.code
-	//      _  = callFrame's padding (see comment on callFrame._ field.)
-	//
-	// What we have to do in the following is that
-	//   1) Set callEngine.valueStackContext.stackBasePointer to the value on "rb.caller".
-	//   2) Load rc.caller.moduleInstanceAddress into amd64CallingConventionModuleInstanceAddressRegister.
-	//   3) Jump into the address of "ra.caller".
-
-	// 1) Set callEngine.valueStackContext.stackBasePointer to the value on "rb.caller"
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		// "rb.caller" is BELOW the top address. See the above example for detail.
-		callFrameStackTopAddressRegister, -(callFrameDataSize - callFrameReturnStackBasePointerInBytesOffset),
-		tmpRegister,
-	)
+	// First, restore the stackContext.stackBasePointerInBytesOffset from callerStackBasePointerInBytes.
+	callerStackBasePointerInBytes.setRegister(tmpRegister)
+	c.compileLoadValueOnStackToRegister(callerStackBasePointerInBytes)
 	c.assembler.CompileRegisterToMemory(amd64.MOVQ,
-		tmpRegister, amd64ReservedRegisterForCallEngine, callEngineValueStackContextStackBasePointerInBytesOffset)
+		tmpRegister, amd64ReservedRegisterForCallEngine, callEngineStackContextStackBasePointerInBytesOffset)
 
-	// 2) Load rc.caller.moduleInstanceAddress into amd64CallingConventionModuleInstanceAddressRegister
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		// "rc.caller" is BELOW the top address. See the above example for detail.
-		callFrameStackTopAddressRegister, -(callFrameDataSize - callFrameFunctionOffset),
-		amd64CallingConventionModuleInstanceAddressRegister,
-	)
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		amd64CallingConventionModuleInstanceAddressRegister, functionModuleInstanceAddressOffset,
-		amd64CallingConventionModuleInstanceAddressRegister,
-	)
+	// Next, restore moduleContext.fn from callerFunction.
+	callerFunction.setRegister(tmpRegister)
+	c.compileLoadValueOnStackToRegister(callerFunction)
+	c.assembler.CompileRegisterToMemory(amd64.MOVQ,
+		tmpRegister, amd64ReservedRegisterForCallEngine, callEngineModuleContextFnOffset)
 
-	// 3) Jump into the address of "ra.caller".
+	// Also, we have to put the target function's *wasm.ModuleInstance into amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister.
 	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		// "ra.caller" is BELOW the top address. See the above example for detail.
-		callFrameStackTopAddressRegister, -(callFrameDataSize - callFrameReturnAddressOffset),
-		tmpRegister,
-	)
-
-	c.assembler.CompileJumpToRegister(amd64.JMP, tmpRegister)
+		tmpRegister, functionModuleInstanceAddressOffset,
+		amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister)
 
-	// They were temporarily used, so we mark them unused.
-	c.locationStack.markRegisterUnused(regs...)
+	// Then, jump into the return address!
+	c.assembler.CompileJumpToRegister(amd64.JMP, returnAddressRegister)
 	return nil
 }
 
-func (c *amd64Compiler) compileCallHostFunction() error {
-	return c.compileCallGoFunction(nativeCallStatusCodeCallHostFunction)
+func (c *amd64Compiler) compileCallGoHostFunction() error {
+	return c.compileCallGoFunction(nativeCallStatusCodeCallGoHostFunction)
 }
 
 func (c *amd64Compiler) compileCallBuiltinFunction(index wasm.Index) error {
 	// Set the functionAddress to the callEngine.exitContext functionCallAddress.
-	c.assembler.CompileConstToMemory(amd64.MOVL, int64(index), amd64ReservedRegisterForCallEngine, callEngineExitContextBuiltinFunctionCallAddressOffset)
+	c.assembler.CompileConstToMemory(amd64.MOVL, int64(index), amd64ReservedRegisterForCallEngine, callEngineExitContextBuiltinFunctionCallIndexOffset)
 	return c.compileCallGoFunction(nativeCallStatusCodeCallBuiltInFunction)
 }
 
@@ -4838,43 +4644,16 @@ func (c *amd64Compiler) compileCallGoFunction(compilerStatus nativeCallStatusCod
 		return err
 	}
 
-	// Obtain the temporary registers to be used in the followings.
-	regs, found := c.locationStack.takeFreeRegisters(registerTypeGeneralPurpose, 3)
-	if !found {
-		// This in theory never happen as all the registers must be free except indexReg.
-		return fmt.Errorf("could not find enough free registers")
+	// Read the return address, and write it to callEngine.exitContext.returnAddress.
+	returnAddressReg, ok := c.locationStack.takeFreeRegister(registerTypeGeneralPurpose)
+	if !ok {
+		panic("BUG: cannot take free register")
 	}
-	c.locationStack.markRegisterUsed(regs...)
-
-	// Alias these free tmp registers for readability.
-	instructionAddressRegister, currentCallFrameAddressRegister, tmpRegister := regs[0], regs[1], regs[2]
-
-	// We need to store the address of the current callFrame's return address.
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		amd64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset, currentCallFrameAddressRegister)
-
-	// next we shift the stack pointer so we get the actual offset from the address of stack's initial item.
-	c.assembler.CompileConstToRegister(amd64.SHLQ, int64(callFrameDataSizeMostSignificantSetBit), currentCallFrameAddressRegister)
-
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		amd64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackElement0AddressOffset, tmpRegister)
-
-	// Now we can get the current call frame's address, which is equivalent to get &callEngine.callFrameStack[callEngine.callStackFramePointer-1].returnAddress.
-	c.assembler.CompileMemoryWithIndexToRegister(
-		amd64.LEAQ,
-		tmpRegister, -(callFrameDataSize - callFrameReturnAddressOffset), currentCallFrameAddressRegister, 1,
-		currentCallFrameAddressRegister,
-	)
-
-	c.assembler.CompileReadInstructionAddress(instructionAddressRegister, amd64.RET)
-
-	// We are ready to store the return address (in instructionAddressRegister) to callEngine.callFrameStack[callEngine.callStackFramePointer-1].
-	c.assembler.CompileRegisterToMemory(amd64.MOVQ, instructionAddressRegister, currentCallFrameAddressRegister, callFrameReturnAddressOffset)
+	c.assembler.CompileReadInstructionAddress(returnAddressReg, amd64.RET)
+	c.assembler.CompileRegisterToMemory(amd64.MOVQ,
+		returnAddressReg, amd64ReservedRegisterForCallEngine, callEngineExitContextReturnAddressOffset)
 
 	c.compileExitFromNativeCode(compilerStatus)
-
-	// They were temporarily used, so we mark them unused.
-	c.locationStack.markRegisterUnused(regs...)
 	return nil
 }
 
@@ -4940,7 +4719,7 @@ func (c *amd64Compiler) compileExitFromNativeCode(status nativeCallStatusCode) {
 
 	// Write back the cached SP to the actual eng.stackPointer.
 	c.assembler.CompileConstToMemory(amd64.MOVQ, int64(c.locationStack.sp),
-		amd64ReservedRegisterForCallEngine, callEngineValueStackContextStackPointerOffset)
+		amd64ReservedRegisterForCallEngine, callEngineStackContextStackPointerOffset)
 
 	c.assembler.CompileStandAlone(amd64.RET)
 }
@@ -4948,14 +4727,14 @@ func (c *amd64Compiler) compileExitFromNativeCode(status nativeCallStatusCode) {
 func (c *amd64Compiler) compilePreamble() (err error) {
 	// We assume all function parameters are already pushed onto the stack by
 	// the caller.
-	c.pushFunctionParams()
+	c.locationStack.init(c.ir.Signature)
 
 	if err := c.compileModuleContextInitialization(); err != nil {
 		return err
 	}
 
 	// Check if it's necessary to grow the value stack by using max stack pointer.
-	if err = c.compileMaybeGrowValueStack(); err != nil {
+	if err = c.compileMaybeGrowStack(); err != nil {
 		return err
 	}
 
@@ -4969,12 +4748,12 @@ func (c *amd64Compiler) compilePreamble() (err error) {
 func (c *amd64Compiler) compileReservedStackBasePointerInitialization() {
 	// First, make reservedRegisterForStackBasePointer point to the beginning of the slice backing array.
 	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		amd64ReservedRegisterForCallEngine, callEngineGlobalContextValueStackElement0AddressOffset,
+		amd64ReservedRegisterForCallEngine, callEngineStackContextStackElement0AddressOffset,
 		amd64ReservedRegisterForStackBasePointerAddress)
 
 	// next we move the base pointer (callEngine.stackBasePointer) to the tmp register.
 	c.assembler.CompileMemoryToRegister(amd64.ADDQ,
-		amd64ReservedRegisterForCallEngine, callEngineValueStackContextStackBasePointerInBytesOffset,
+		amd64ReservedRegisterForCallEngine, callEngineStackContextStackBasePointerInBytesOffset,
 		amd64ReservedRegisterForStackBasePointerAddress,
 	)
 }
@@ -4988,16 +4767,21 @@ func (c *amd64Compiler) compileReservedMemoryPointerInitialization() {
 	}
 }
 
-// compileMaybeGrowValueStack adds instructions to check the necessity to grow the value stack,
+// compileMaybeGrowStack adds instructions to check the necessity to grow the value stack,
 // and if so, make the builtin function call to do so. These instructions are called in the function's
 // preamble.
-func (c *amd64Compiler) compileMaybeGrowValueStack() error {
-	tmpRegister, _ := c.allocateRegister(registerTypeGeneralPurpose)
+func (c *amd64Compiler) compileMaybeGrowStack() error {
+	tmpRegister, ok := c.locationStack.takeFreeRegister(registerTypeGeneralPurpose)
+	if !ok {
+		panic("BUG: cannot take free register")
+	}
 
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine, callEngineGlobalContextValueStackLenInBytesOffset, tmpRegister)
-	c.assembler.CompileMemoryToRegister(amd64.SUBQ, amd64ReservedRegisterForCallEngine, callEngineValueStackContextStackBasePointerInBytesOffset, tmpRegister)
+	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
+		amd64ReservedRegisterForCallEngine, callEngineStackContextStackLenInBytesOffset, tmpRegister)
+	c.assembler.CompileMemoryToRegister(amd64.SUBQ,
+		amd64ReservedRegisterForCallEngine, callEngineStackContextStackBasePointerInBytesOffset, tmpRegister)
 
-	// If stack base pointer + max stack pointer > valueStackLen, we need to grow the stack.
+	// If stack base pointer + max stack pointer > stackLen, we need to grow the stack.
 	cmpWithStackPointerCeil := c.assembler.CompileRegisterToConst(amd64.CMPQ, tmpRegister, 0)
 	c.onStackPointerCeilDeterminedCallBack = func(stackPointerCeil uint64) {
 		cmpWithStackPointerCeil.AssignDestinationConstant(int64(stackPointerCeil) << 3)
@@ -5007,7 +4791,7 @@ func (c *amd64Compiler) compileMaybeGrowValueStack() error {
 	jmpIfNoNeedToGrowStack := c.assembler.CompileJump(amd64.JCC)
 
 	// Otherwise, we have to make the builtin function call to grow the call stack.
-	if err := c.compileCallBuiltinFunction(builtinFunctionIndexGrowValueStack); err != nil {
+	if err := c.compileCallBuiltinFunction(builtinFunctionIndexGrowStack); err != nil {
 		return err
 	}
 
@@ -5019,10 +4803,10 @@ func (c *amd64Compiler) compileMaybeGrowValueStack() error {
 // callEngine.ModuleContext.ModuleInstanceAddress.
 // This is called in two cases: in function preamble, and on the return from (non-Go) function calls.
 func (c *amd64Compiler) compileModuleContextInitialization() error {
-	// amd64CallingConventionModuleInstanceAddressRegister holds the module instance's address
+	// amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister holds the module instance's address
 	// so mark it used so that it won't be used as a free register until the module context initialization finishes.
-	c.locationStack.markRegisterUsed(amd64CallingConventionModuleInstanceAddressRegister)
-	defer c.locationStack.markRegisterUnused(amd64CallingConventionModuleInstanceAddressRegister)
+	c.locationStack.markRegisterUsed(amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister)
+	defer c.locationStack.markRegisterUnused(amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister)
 
 	// Obtain the temporary registers to be used in the followings.
 	regs, found := c.locationStack.takeFreeRegisters(registerTypeGeneralPurpose, 2)
@@ -5041,17 +4825,18 @@ func (c *amd64Compiler) compileModuleContextInitialization() error {
 	// binaries. As a result, this cmp and jmp instruction sequence below must be easy for
 	// x64 CPU to do branch prediction since almost 100% jump happens across function calls.
 	c.assembler.CompileMemoryToRegister(amd64.CMPQ,
-		amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset, amd64CallingConventionModuleInstanceAddressRegister)
+		amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset, amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister)
 	jmpIfModuleNotChange := c.assembler.CompileJump(amd64.JEQ)
 
-	// If engine.CallContext.ModuleInstanceAddress is not euqal the value on amd64CallingConventionModuleInstanceAddressRegister,
+	// If engine.CallContext.ModuleInstanceAddress is not equal the value on amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister,
 	// we have to put the new value there.
-	c.assembler.CompileRegisterToMemory(amd64.MOVQ, amd64CallingConventionModuleInstanceAddressRegister,
+	c.assembler.CompileRegisterToMemory(amd64.MOVQ, amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister,
 		amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset)
 
 	// Also, we have to update the following fields:
 	// * callEngine.moduleContext.globalElement0Address
 	// * callEngine.moduleContext.tableElement0Address
+	// * callEngine.moduleContext.memoryInstance
 	// * callEngine.moduleContext.memoryElement0Address
 	// * callEngine.moduleContext.memorySliceLen
 	// * callEngine.moduleContext.codesElement0Address
@@ -5068,7 +4853,7 @@ func (c *amd64Compiler) compileModuleContextInitialization() error {
 		// Since ModuleInstance.Globals is []*globalInstance, internally
 		// the address of the first item in the underlying array lies exactly on the globals offset.
 		// See https://go.dev/blog/slices-intro if unfamiliar.
-		c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64CallingConventionModuleInstanceAddressRegister, moduleInstanceGlobalsOffset, tmpRegister)
+		c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister, moduleInstanceGlobalsOffset, tmpRegister)
 
 		c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister, amd64ReservedRegisterForCallEngine, callEngineModuleContextGlobalElement0AddressOffset)
 	}
@@ -5080,7 +4865,7 @@ func (c *amd64Compiler) compileModuleContextInitialization() error {
 	// why it is ok to skip the initialization if the module's table doesn't exist.
 	if c.ir.HasTable {
 		// First, we need to read the *wasm.Table.
-		c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64CallingConventionModuleInstanceAddressRegister, moduleInstanceTablesOffset, tmpRegister)
+		c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister, moduleInstanceTablesOffset, tmpRegister)
 
 		// At this point, tmpRegister holds the address of ModuleInstance.Table.
 		// So we are ready to read and put the first item's address stored in Table.Table.
@@ -5090,7 +4875,7 @@ func (c *amd64Compiler) compileModuleContextInitialization() error {
 
 		// Finally, we put &ModuleInstance.TypeIDs[0] into moduleContext.typeIDsElement0Address.
 		c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-			amd64CallingConventionModuleInstanceAddressRegister, moduleInstanceTypeIDsOffset, tmpRegister)
+			amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister, moduleInstanceTypeIDsOffset, tmpRegister)
 		c.assembler.CompileRegisterToMemory(amd64.MOVQ,
 			tmpRegister, amd64ReservedRegisterForCallEngine, callEngineModuleContextTypeIDsElement0AddressOffset)
 	}
@@ -5101,7 +4886,13 @@ func (c *amd64Compiler) compileModuleContextInitialization() error {
 	// That is ensured by function validation at module instantiation phase, and that's
 	// why it is ok to skip the initialization if the module's memory instance is nil.
 	if c.ir.HasMemory {
-		c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64CallingConventionModuleInstanceAddressRegister, moduleInstanceMemoryOffset, tmpRegister)
+		c.assembler.CompileMemoryToRegister(amd64.MOVQ,
+			amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister, moduleInstanceMemoryOffset,
+			tmpRegister)
+
+		// Set memory instance.
+		c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister,
+			amd64ReservedRegisterForCallEngine, callEngineModuleContextMemoryInstanceOffset)
 
 		// Set length.
 		c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmpRegister, memoryInstanceBufferLenOffset, tmpRegister2)
@@ -5124,7 +4915,7 @@ func (c *amd64Compiler) compileModuleContextInitialization() error {
 		// 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.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64CallingConventionModuleInstanceAddressRegister, moduleInstanceEngineOffset+interfaceDataOffset, tmpRegister)
+		c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister, moduleInstanceEngineOffset+interfaceDataOffset, tmpRegister)
 
 		// "tmpRegister = [tmpRegister + moduleEnginecodesOffset] (== &moduleEngine.codes[0])"
 		c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmpRegister, moduleEngineFunctionsOffset, tmpRegister)
@@ -5139,7 +4930,7 @@ func (c *amd64Compiler) compileModuleContextInitialization() error {
 		// "tmpRegister = &moduleInstance.DataInstances[0]"
 		c.assembler.CompileMemoryToRegister(
 			amd64.MOVQ,
-			amd64CallingConventionModuleInstanceAddressRegister, moduleInstanceDataInstancesOffset,
+			amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister, moduleInstanceDataInstancesOffset,
 			tmpRegister,
 		)
 		// "callEngine.moduleContext.dataInstancesElement0Address = tmpRegister".
@@ -5155,7 +4946,7 @@ func (c *amd64Compiler) compileModuleContextInitialization() error {
 		// "tmpRegister = &moduleInstance.ElementInstnaces[0]"
 		c.assembler.CompileMemoryToRegister(
 			amd64.MOVQ,
-			amd64CallingConventionModuleInstanceAddressRegister, moduleInstanceElementInstancesOffset,
+			amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister, moduleInstanceElementInstancesOffset,
 			tmpRegister,
 		)
 		// "callEngine.moduleContext.dataInstancesElement0Address = tmpRegister".
diff --git a/internal/engine/compiler/impl_arm64.go b/internal/engine/compiler/impl_arm64.go
index 7b16b9ff..5e64fa48 100644
--- a/internal/engine/compiler/impl_arm64.go
+++ b/internal/engine/compiler/impl_arm64.go
@@ -174,35 +174,15 @@ func (c *arm64Compiler) markRegisterUnused(regs ...asm.Register) {
 
 func (c *arm64Compiler) String() (ret string) { return c.locationStack.String() }
 
-// pushFunctionParams pushes any function parameters onto the stack, setting appropriate register types.
-func (c *arm64Compiler) pushFunctionParams() {
-	for _, t := range c.ir.Signature.Params {
-		loc := c.locationStack.pushRuntimeValueLocationOnStack()
-		switch t {
-		case wasm.ValueTypeI32:
-			loc.valueType = runtimeValueTypeI32
-		case wasm.ValueTypeI64, wasm.ValueTypeFuncref, wasm.ValueTypeExternref:
-			loc.valueType = runtimeValueTypeI64
-		case wasm.ValueTypeF32:
-			loc.valueType = runtimeValueTypeF32
-		case wasm.ValueTypeF64:
-			loc.valueType = runtimeValueTypeF64
-		case wasm.ValueTypeV128:
-			loc.valueType = runtimeValueTypeV128Lo
-			hi := c.locationStack.pushRuntimeValueLocationOnStack()
-			hi.valueType = runtimeValueTypeV128Hi
-		default:
-			panic("BUG")
-		}
-	}
-}
-
 // compilePreamble implements compiler.compilePreamble for the arm64 architecture.
 func (c *arm64Compiler) compilePreamble() error {
-	c.pushFunctionParams()
+	c.markRegisterUsed(arm64CallingConventionModuleInstanceAddressRegister)
+	defer c.markRegisterUnused(arm64CallingConventionModuleInstanceAddressRegister)
+
+	c.locationStack.init(c.ir.Signature)
 
 	// Check if it's necessary to grow the value stack before entering function body.
-	if err := c.compileMaybeGrowValueStack(); err != nil {
+	if err := c.compileMaybeGrowStack(); err != nil {
 		return err
 	}
 
@@ -217,31 +197,31 @@ func (c *arm64Compiler) compilePreamble() error {
 	return nil
 }
 
-// compileMaybeGrowValueStack adds instructions to check the necessity to grow the value stack,
+// compileMaybeGrowStack adds instructions to check the necessity to grow the value stack,
 // and if so, make the builtin function call to do so. These instructions are called in the function's
 // preamble.
-func (c *arm64Compiler) compileMaybeGrowValueStack() error {
+func (c *arm64Compiler) compileMaybeGrowStack() error {
 	tmpRegs, found := c.locationStack.takeFreeRegisters(registerTypeGeneralPurpose, 2)
 	if !found {
 		panic("BUG: all the registers should be free at this point")
 	}
 	tmpX, tmpY := tmpRegs[0], tmpRegs[1]
 
-	// "tmpX = len(ce.valueStack)"
+	// "tmpX = len(ce.stack)"
 	c.assembler.CompileMemoryToRegister(
 		arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineGlobalContextValueStackLenInBytesOffset,
+		arm64ReservedRegisterForCallEngine, callEngineStackContextStackLenInBytesOffset,
 		tmpX,
 	)
 
 	// "tmpY = ce.stackBasePointer"
 	c.assembler.CompileMemoryToRegister(
 		arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineValueStackContextStackBasePointerInBytesOffset,
+		arm64ReservedRegisterForCallEngine, callEngineStackContextStackBasePointerInBytesOffset,
 		tmpY,
 	)
 
-	// "tmpX = tmpX - tmpY", in other words "tmpX = len(ce.valueStack) - ce.stackBasePointer"
+	// "tmpX = tmpX - tmpY", in other words "tmpX = len(ce.stack) - ce.stackBasePointer"
 	c.assembler.CompileRegisterToRegister(
 		arm64.SUB,
 		tmpY,
@@ -260,17 +240,17 @@ func (c *arm64Compiler) compileMaybeGrowValueStack() error {
 		loadStackPointerCeil.AssignSourceConstant(int64(stackPointerCeil) << 3)
 	}
 
-	// Compare tmpX (len(ce.valueStack) - ce.stackBasePointer) and tmpY (ce.stackPointerCeil)
+	// Compare tmpX (len(ce.stack) - ce.stackBasePointer) and tmpY (ce.stackPointerCeil)
 	c.assembler.CompileTwoRegistersToNone(arm64.CMP, tmpX, tmpY)
 
-	// If ceil > valueStackLen - stack base pointer, we need to grow the stack by calling builtin Go function.
-	brIfValueStackOK := c.assembler.CompileJump(arm64.BCONDLS)
-	if err := c.compileCallGoFunction(nativeCallStatusCodeCallBuiltInFunction, builtinFunctionIndexGrowValueStack); err != nil {
+	// If ceil > stackLen - stack base pointer, we need to grow the stack by calling builtin Go function.
+	brIfStackOK := c.assembler.CompileJump(arm64.BCONDLS)
+	if err := c.compileCallGoFunction(nativeCallStatusCodeCallBuiltInFunction, builtinFunctionIndexGrowStack); err != nil {
 		return err
 	}
 
 	// Otherwise, skip calling it.
-	c.assembler.SetJumpTargetOnNext(brIfValueStackOK)
+	c.assembler.SetJumpTargetOnNext(brIfStackOK)
 
 	c.markRegisterUnused(tmpRegs...)
 	return nil
@@ -290,88 +270,42 @@ func (c *arm64Compiler) compileReturnFunction() error {
 	c.locationStack.markRegisterUsed(arm64CallingConventionModuleInstanceAddressRegister)
 	defer c.locationStack.markRegisterUnused(arm64CallingConventionModuleInstanceAddressRegister)
 
-	tmpRegs, found := c.locationStack.takeFreeRegisters(registerTypeGeneralPurpose, 3)
-	if !found {
-		panic("BUG: all the registers should be free at this point")
-	}
-
-	// Alias for readability.
-	callFramePointerReg, callFrameStackTopAddressRegister, tmpReg := tmpRegs[0], tmpRegs[1], tmpRegs[2]
+	returnAddress, callerStackBasePointerInBytes, callerFunction := c.locationStack.getCallFrameLocations(c.ir.Signature)
 
-	// First we decrement the call-frame stack pointer.
-	c.assembler.CompileMemoryToRegister(arm64.LDRD, arm64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset, callFramePointerReg)
-	c.assembler.CompileConstToRegister(arm64.SUBS, 1, callFramePointerReg)
-	c.assembler.CompileRegisterToMemory(arm64.STRD, callFramePointerReg, arm64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset)
+	// If the return address is zero, meaning that we return from the execution.
+	returnAddress.setRegister(arm64ReservedRegisterForTemporary)
+	c.compileLoadValueOnStackToRegister(returnAddress)
+	c.assembler.CompileTwoRegistersToNone(arm64.CMP, arm64ReservedRegisterForTemporary, arm64.RegRZR)
 
-	// next we compare the decremented call frame stack pointer with zero.
-	c.assembler.CompileTwoRegistersToNone(arm64.CMP, callFramePointerReg, arm64.RegRZR)
-
-	// If the values are identical, we return back to the Go code with returned status.
+	// Br if the address does not equal zero.
 	brIfNotEqual := c.assembler.CompileJump(arm64.BCONDNE)
+	// Otherwise, exit.
 	c.compileExitFromNativeCode(nativeCallStatusCodeReturned)
 
-	// Otherwise, we have to jump to the caller's return address.
+	// If the address doesn't equal zero, return br into returnAddressRegister (caller's return address).
 	c.assembler.SetJumpTargetOnNext(brIfNotEqual)
 
-	// First, we have to calculate the caller callFrame's absolute address to acquire the return address.
-	//
-	// "tmpReg = &ce.callFrameStack[0]"
-	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackElement0AddressOffset,
-		tmpReg,
-	)
-	// "callFrameStackTopAddressRegister = tmpReg + callFramePointerReg << ${callFrameDataSizeMostSignificantSetBit}"
-	c.assembler.CompileLeftShiftedRegisterToRegister(
-		arm64.ADD,
-		callFramePointerReg, callFrameDataSizeMostSignificantSetBit,
-		tmpReg,
-		callFrameStackTopAddressRegister,
-	)
+	// Alias for readability.
+	tmp := arm64CallingConventionModuleInstanceAddressRegister
 
-	// At this point, we have
-	//
-	//      [......., ra.caller, rb.caller, rc.caller, _, ra.current, rb.current, rc.current, _, ...]  <- call frame stack's data region (somewhere in the memory)
-	//                                                  ^
-	//                               callFrameStackTopAddressRegister
-	//                   (absolute address of &callFrameStack[ce.callFrameStackPointer])
-	//
-	// where:
-	//      ra.* = callFrame.returnAddress
-	//      rb.* = callFrame.returnStackBasePointer
-	//      rc.* = callFrame.code
-	//      _  = callFrame's padding (see comment on callFrame._ field.)
-	//
-	// What we have to do in the following is that
-	//   1) Set ce.valueStackContext.stackBasePointer to the value on "rb.caller".
-	//   2) Load rc.caller.moduleInstanceAddress into arm64CallingConventionModuleInstanceAddressRegister.
-	//   3) Jump into the address of "ra.caller".
+	// First, restore the stackContext.stackBasePointerInBytesOffset from callerStackBasePointerInBytes.
+	callerStackBasePointerInBytes.setRegister(tmp)
+	c.compileLoadValueOnStackToRegister(callerStackBasePointerInBytes)
+	c.assembler.CompileRegisterToMemory(arm64.STRD, tmp,
+		arm64ReservedRegisterForCallEngine, callEngineStackContextStackBasePointerInBytesOffset)
 
-	// 1) Set ce.valueStackContext.stackBasePointer to the value on "rb.caller".
-	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		// "rb.caller" is below the top address.
-		callFrameStackTopAddressRegister, -(callFrameDataSize - callFrameReturnStackBasePointerInBytesOffset),
-		tmpReg)
-	c.assembler.CompileRegisterToMemory(arm64.STRD,
-		tmpReg,
-		arm64ReservedRegisterForCallEngine, callEngineValueStackContextStackBasePointerInBytesOffset)
+	// Next, restore moduleContext.fn from callerFunction.
+	callerFunction.setRegister(tmp)
+	c.compileLoadValueOnStackToRegister(callerFunction)
+	c.assembler.CompileRegisterToMemory(arm64.STRD, tmp,
+		arm64ReservedRegisterForCallEngine, callEngineModuleContextFnOffset)
 
-	// 2) Load rc.caller.moduleInstanceAddress into arm64CallingConventionModuleInstanceAddressRegister.
+	// Also, we have to put the target function's *wasm.ModuleInstance into arm64CallingConventionModuleInstanceAddressRegister.
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		// "rb.caller" is below the top address.
-		callFrameStackTopAddressRegister, -(callFrameDataSize - callFrameFunctionOffset),
+		tmp, functionModuleInstanceAddressOffset,
 		arm64CallingConventionModuleInstanceAddressRegister)
-	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		arm64CallingConventionModuleInstanceAddressRegister, functionModuleInstanceAddressOffset,
-		arm64CallingConventionModuleInstanceAddressRegister)
-
-	// 3) Branch into the address of "ra.caller".
-	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		// "rb.caller" is below the top address.
-		callFrameStackTopAddressRegister, -(callFrameDataSize - callFrameReturnAddressOffset),
-		tmpReg)
-	c.assembler.CompileJumpToRegister(arm64.B, tmpReg)
 
-	c.markRegisterUnused(tmpRegs...)
+	c.assembler.CompileJumpToRegister(arm64.B, returnAddress.register)
 	return nil
 }
 
@@ -380,20 +314,24 @@ func (c *arm64Compiler) compileExitFromNativeCode(status nativeCallStatusCode) {
 	// Write the current stack pointer to the ce.stackPointer.
 	c.assembler.CompileConstToRegister(arm64.MOVD, int64(c.locationStack.sp), arm64ReservedRegisterForTemporary)
 	c.assembler.CompileRegisterToMemory(arm64.STRD, arm64ReservedRegisterForTemporary, arm64ReservedRegisterForCallEngine,
-		callEngineValueStackContextStackPointerOffset)
+		callEngineStackContextStackPointerOffset)
 
 	if status != 0 {
 		c.assembler.CompileConstToRegister(arm64.MOVW, int64(status), arm64ReservedRegisterForTemporary)
-		c.assembler.CompileRegisterToMemory(arm64.STRW, arm64ReservedRegisterForTemporary, arm64ReservedRegisterForCallEngine, callEngineExitContextNativeCallStatusCodeOffset)
+		c.assembler.CompileRegisterToMemory(arm64.STRW, arm64ReservedRegisterForTemporary,
+			arm64ReservedRegisterForCallEngine, callEngineExitContextNativeCallStatusCodeOffset)
 	} else {
 		// If the status == 0, we use zero register to store zero.
-		c.assembler.CompileRegisterToMemory(arm64.STRW, arm64.RegRZR, arm64ReservedRegisterForCallEngine, callEngineExitContextNativeCallStatusCodeOffset)
+		c.assembler.CompileRegisterToMemory(arm64.STRW, arm64.RegRZR,
+			arm64ReservedRegisterForCallEngine, callEngineExitContextNativeCallStatusCodeOffset)
 	}
 
 	// The return address to the Go code is stored in archContext.compilerReturnAddress which
 	// is embedded in ce. We load the value to the tmpRegister, and then
 	// invoke RET with that register.
-	c.assembler.CompileMemoryToRegister(arm64.LDRD, arm64ReservedRegisterForCallEngine, arm64CallEngineArchContextCompilerCallReturnAddressOffset, arm64ReservedRegisterForTemporary)
+	c.assembler.CompileMemoryToRegister(arm64.LDRD,
+		arm64ReservedRegisterForCallEngine, arm64CallEngineArchContextCompilerCallReturnAddressOffset,
+		arm64ReservedRegisterForTemporary)
 
 	c.assembler.CompileJumpToRegister(arm64.RET, arm64ReservedRegisterForTemporary)
 }
@@ -401,11 +339,14 @@ func (c *arm64Compiler) compileExitFromNativeCode(status nativeCallStatusCode) {
 // compileGoHostFunction implements compiler.compileHostFunction for the arm64 architecture.
 func (c *arm64Compiler) compileGoDefinedHostFunction() error {
 	// First we must update the location stack to reflect the number of host function inputs.
-	c.pushFunctionParams()
+	c.locationStack.init(c.ir.Signature)
 
-	if err := c.compileCallGoFunction(nativeCallStatusCodeCallHostFunction, 0); err != nil {
+	if err := c.compileCallGoFunction(nativeCallStatusCodeCallGoHostFunction, 0); err != nil {
 		return err
 	}
+
+	// Initializes the reserved stack base pointer which is used to retrieve the call frame stack.
+	c.compileReservedStackBasePointerRegisterInitialization()
 	return c.compileReturnFunction()
 }
 
@@ -890,191 +831,117 @@ func (c *arm64Compiler) compileBrTable(o *wazeroir.OperationBrTable) error {
 
 // compileCall implements compiler.compileCall for the arm64 architecture.
 func (c *arm64Compiler) compileCall(o *wazeroir.OperationCall) error {
-	tp := c.ir.Types[c.ir.Functions[o.FunctionIndex]]
-	return c.compileCallImpl(o.FunctionIndex, asm.NilRegister, tp)
-}
-
-// compileCallImpl implements compiler.compileCall and compiler.compileCallIndirect for the arm64 architecture.
-func (c *arm64Compiler) compileCallImpl(index wasm.Index, targetFunctionAddressRegister asm.Register, functype *wasm.FunctionType) error {
-	// Release all the registers as our calling convention requires the caller-save.
-	if err := c.compileReleaseAllRegistersToStack(); err != nil {
+	if err := c.maybeCompileMoveTopConditionalToGeneralPurposeRegister(); err != nil {
 		return err
 	}
 
-	freeRegisters, found := c.locationStack.takeFreeRegisters(registerTypeGeneralPurpose, 5)
-	if !found {
-		return fmt.Errorf("BUG: all registers except indexReg should be free at this point")
-	}
-	c.markRegisterUsed(freeRegisters...)
+	tp := c.ir.Types[c.ir.Functions[o.FunctionIndex]]
 
-	// Alias for readability.
-	callFrameStackPointerRegister, callFrameStackTopAddressRegister, targetFunctionRegister, oldStackBasePointer,
-		tmp := freeRegisters[0], freeRegisters[1], freeRegisters[2], freeRegisters[3], freeRegisters[4]
+	targetFunctionAddressReg, err := c.allocateRegister(registerTypeGeneralPurpose)
+	if err != nil {
+		return err
+	}
+	c.markRegisterUsed(targetFunctionAddressReg)
+	defer c.markRegisterUnused(targetFunctionAddressReg)
 
-	// First, we have to check if we need to grow the callFrame stack.
+	// 3) Set rc.next to specify which function is executed on the current call frame.
 	//
-	// "callFrameStackPointerRegister = ce.callFrameStackPointer"
+	// First, we read the address of the first item of ce.functions slice (= &ce.functions[0])
+	// into tmp.
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset,
-		callFrameStackPointerRegister)
-	// "tmp = len(ce.callFrameStack)"
+		arm64ReservedRegisterForCallEngine, callEngineModuleContextFunctionsElement0AddressOffset,
+		targetFunctionAddressReg)
+
 	c.assembler.CompileMemoryToRegister(
 		arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackLenOffset,
-		tmp,
-	)
-	// Compare tmp(len(ce.callFrameStack)) with callFrameStackPointerRegister(ce.callFrameStackPointer).
-	c.assembler.CompileTwoRegistersToNone(arm64.CMP, tmp, callFrameStackPointerRegister)
-	brIfCallFrameStackOK := c.assembler.CompileJump(arm64.BCONDNE)
+		targetFunctionAddressReg, int64(o.FunctionIndex)*8, // * 8 because the size of *function equals 8 bytes.
+		targetFunctionAddressReg)
 
-	// 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(targetFunctionAddressRegister) {
-		// If we need to get the target funcaddr from register (call_indirect case), we must save it before growing the
-		// call-frame stack, as the register is not saved across function calls.
-		savedOffsetLocation := c.pushRuntimeValueLocationOnRegister(targetFunctionAddressRegister, runtimeValueTypeI64)
-		c.compileReleaseRegisterToStack(savedOffsetLocation)
-	}
+	return c.compileCallImpl(targetFunctionAddressReg, tp)
+}
 
-	if err := c.compileCallGoFunction(nativeCallStatusCodeCallBuiltInFunction, builtinFunctionIndexGrowCallFrameStack); err != nil {
+// compileCallImpl implements compiler.compileCall and compiler.compileCallIndirect for the arm64 architecture.
+func (c *arm64Compiler) compileCallImpl(targetFunctionAddressRegister asm.Register, functype *wasm.FunctionType) error {
+	// Release all the registers as our calling convention requires the caller-save.
+	if err := c.compileReleaseAllRegistersToStack(); err != nil {
 		return err
 	}
 
-	// For call_indirect, we need to push the value back to the register.
-	if !isNilRegister(targetFunctionAddressRegister) {
-		// 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(targetFunctionAddressRegister)
-		c.compileLoadValueOnStackToRegister(savedOffsetLocation)
+	tmp, ok := c.locationStack.takeFreeRegister(registerTypeGeneralPurpose)
+	if !ok {
+		panic("BUG: cannot take a free register")
 	}
 
-	// On the function return, we again have to set ce.callFrameStackPointer into callFrameStackPointerRegister.
-	// "callFrameStackPointerRegister = ce.callFrameStackPointer"
-	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset,
-		callFrameStackPointerRegister)
-
-	// Now that we ensured callFrameStack length is enough.
-	c.assembler.SetJumpTargetOnNext(brIfCallFrameStackOK)
-	c.compileCalcCallFrameStackTopAddress(callFrameStackPointerRegister, callFrameStackTopAddressRegister)
-
-	// At this point, we have:
-	//
-	//    [..., ra.current, rb.current, rc.current, _, ra.next, rb.next, rc.next, ...]  <- call frame stack's data region (somewhere in the memory)
-	//                                               ^
-	//                              callFrameStackTopAddressRegister
-	//                (absolute address of &callFrame[ce.callFrameStackPointer]])
-	//
-	// where:
-	//      ra.* = callFrame.returnAddress
-	//      rb.* = callFrame.returnStackBasePointer
-	//      rc.* = callFrame.code
-	//      _  = callFrame's padding (see comment on callFrame._ field.)
+	// The stack should look like:
 	//
-	// In the following comment, we use the notations in the above example.
+	//               reserved slots for results (if len(results) > len(args))
+	//                      |     |
+	//    ,arg0, ..., argN, ..., _, .returnAddress, .returnStackBasePointerInBytes, .function, ....
+	//      |                       |                                                        |
+	//      |             callFrame{^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^}
+	//      |
+	// nextStackBasePointerOffset
 	//
-	// What we have to do in the following is that
-	//   1) Set rb.current so that we can return back to this function properly.
-	//   2) Set ce.valueStackContext.stackBasePointer for the next function.
-	//   3) Set rc.next to specify which function is executed on the current call frame (needs to make Go function calls).
-	//   4) Set ra.current so that we can return back to this function properly.
+	// where callFrame is used to return to this currently executed function.
 
-	// 1) Set rb.current so that we can return back to this function properly.
-	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineValueStackContextStackBasePointerInBytesOffset,
-		oldStackBasePointer)
-	c.assembler.CompileRegisterToMemory(arm64.STRD,
-		oldStackBasePointer,
-		// "rb.current" is BELOW the top address. See the above example for detail.
-		callFrameStackTopAddressRegister, -(callFrameDataSize - callFrameReturnStackBasePointerInBytesOffset))
+	nextStackBasePointerOffset := int64(c.locationStack.sp) - int64(functype.ParamNumInUint64)
 
-	// 2) Set ce.valueStackContext.stackBasePointer for the next function.
-	//
-	// At this point, oldStackBasePointer holds the old stack base pointer. We could get the new frame's
-	// stack base pointer by "old stack base pointer + old stack pointer - # of function params"
-	// See the comments in ce.pushCallFrame which does exactly the same calculation in Go.
-	if offset := int64(c.locationStack.sp) - int64(functype.ParamNumInUint64); offset > 0 {
-		c.assembler.CompileConstToRegister(arm64.ADD, offset<<3, oldStackBasePointer)
-		c.assembler.CompileRegisterToMemory(arm64.STRD,
-			oldStackBasePointer,
-			arm64ReservedRegisterForCallEngine, callEngineValueStackContextStackBasePointerInBytesOffset)
-	}
+	callFrameReturnAddressLoc, callFrameStackBasePointerInBytesLoc, callFrameFunctionLoc :=
+		c.locationStack.pushCallFrame(functype)
 
-	// 3) Set rc.next to specify which function is executed on the current call frame.
-	//
-	// First, we read the address of the first item of ce.functions slice (= &ce.functions[0])
-	// into tmp.
+	// Save the current stack base pointer at callFrameStackBasePointerInBytesLoc.
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineModuleContextFunctionsElement0AddressOffset,
+		arm64ReservedRegisterForCallEngine, callEngineStackContextStackBasePointerInBytesOffset,
 		tmp)
+	callFrameStackBasePointerInBytesLoc.setRegister(tmp)
+	c.compileReleaseRegisterToStack(callFrameStackBasePointerInBytesLoc)
 
-	// next, read the index of the target function (= &ce.functions[offset])
-	// into codeIndexRegister.
-	if isNilRegister(targetFunctionAddressRegister) {
-		c.assembler.CompileMemoryToRegister(
-			arm64.LDRD,
-			tmp, int64(index)*8, // * 8 because the size of *function equals 8 bytes.
-			targetFunctionRegister)
-	} else {
-		targetFunctionRegister = targetFunctionAddressRegister
-	}
-
-	// Finally, we are ready to write the address of the target function's *function into the new call-frame.
-	c.assembler.CompileRegisterToMemory(arm64.STRD,
-		targetFunctionRegister,
-		callFrameStackTopAddressRegister, callFrameFunctionOffset)
-
-	// 4) Set ra.current so that we can return back to this function properly.
-	//
-	// First, Get the return address into the tmp.
-	c.assembler.CompileReadInstructionAddress(tmp, arm64.B)
-	// Then write the address into the call-frame.
+	// Set callEngine.stackContext.stackBasePointer for the next function.
+	c.assembler.CompileConstToRegister(arm64.ADD, nextStackBasePointerOffset<<3, tmp)
 	c.assembler.CompileRegisterToMemory(arm64.STRD,
 		tmp,
-		// "ra.current" is BELOW the top address. See the above example for detail.
-		callFrameStackTopAddressRegister, -(callFrameDataSize - callFrameReturnAddressOffset),
-	)
+		arm64ReservedRegisterForCallEngine, callEngineStackContextStackBasePointerInBytesOffset)
 
-	// Everything is done to make function call now: increment the call-frame stack pointer.
+	// Save the currently executed *function (placed at callEngine.moduleContext.fn) into callFrameFunctionLoc.
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset,
+		arm64ReservedRegisterForCallEngine, callEngineModuleContextFnOffset,
 		tmp)
-	c.assembler.CompileConstToRegister(arm64.ADD, 1, tmp)
+	callFrameFunctionLoc.setRegister(tmp)
+	c.compileReleaseRegisterToStack(callFrameFunctionLoc)
+
+	// Set callEngine.moduleContext.fn to the next *function.
 	c.assembler.CompileRegisterToMemory(arm64.STRD,
-		tmp,
-		arm64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset)
+		targetFunctionAddressRegister,
+		arm64ReservedRegisterForCallEngine, callEngineModuleContextFnOffset)
+
+	// Write the return address into callFrameReturnAddressLoc.
+	c.assembler.CompileReadInstructionAddress(tmp, arm64.B)
+	callFrameReturnAddressLoc.setRegister(tmp)
+	c.compileReleaseRegisterToStack(callFrameReturnAddressLoc)
 
-	if targetFunctionRegister == arm64CallingConventionModuleInstanceAddressRegister {
+	if targetFunctionAddressRegister == arm64CallingConventionModuleInstanceAddressRegister {
 		// This case we must move the value on targetFunctionAddressRegister to another register, otherwise
 		// the address (jump target below) will be modified and result in segfault.
 		// See #526.
-		c.assembler.CompileRegisterToRegister(arm64.MOVD, targetFunctionRegister, tmp)
-		targetFunctionRegister = tmp
+		c.assembler.CompileRegisterToRegister(arm64.MOVD, targetFunctionAddressRegister, tmp)
+		targetFunctionAddressRegister = tmp
 	}
 
 	// Also, we have to put the code's moduleInstance address into arm64CallingConventionModuleInstanceAddressRegister.
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		targetFunctionRegister, functionModuleInstanceAddressOffset,
+		targetFunctionAddressRegister, functionModuleInstanceAddressOffset,
 		arm64CallingConventionModuleInstanceAddressRegister,
 	)
 
 	// Then, br into the target function's initial address.
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		targetFunctionRegister, functionCodeInitialAddressOffset,
-		tmp)
+		targetFunctionAddressRegister, functionCodeInitialAddressOffset,
+		targetFunctionAddressRegister)
 
-	c.assembler.CompileJumpToRegister(arm64.B, tmp)
+	c.assembler.CompileJumpToRegister(arm64.B, targetFunctionAddressRegister)
 
-	// All the registers used are temporary so we mark them unused.
-	c.markRegisterUnused(freeRegisters...)
-
-	// We consumed the function parameters from the stack after call.
-	for i := 0; i < functype.ParamNumInUint64; i++ {
-		c.locationStack.pop()
-	}
+	// We consumed the function parameters, the call frame stack and reserved slots during the call.
+	c.locationStack.sp = uint64(nextStackBasePointerOffset)
 
 	// Also, the function results were pushed by the call.
 	for _, t := range functype.Results {
@@ -1106,22 +973,6 @@ func (c *arm64Compiler) compileCallImpl(index wasm.Index, targetFunctionAddressR
 	return nil
 }
 
-// compileCalcCallFrameStackTopAddress adds instructions to set the absolute address of
-// ce.callFrameStack[callFrameStackPointerRegister] into destinationRegister.
-func (c *arm64Compiler) compileCalcCallFrameStackTopAddress(callFrameStackPointerRegister, destinationRegister asm.Register) {
-	// "destinationRegister = &ce.callFrameStack[0]"
-	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackElement0AddressOffset,
-		destinationRegister)
-	// "destinationRegister += callFrameStackPointerRegister << $callFrameDataSizeMostSignificantSetBit"
-	c.assembler.CompileLeftShiftedRegisterToRegister(
-		arm64.ADD,
-		callFrameStackPointerRegister, callFrameDataSizeMostSignificantSetBit,
-		destinationRegister,
-		destinationRegister,
-	)
-}
-
 // compileCallIndirect implements compiler.compileCallIndirect for the arm64 architecture.
 func (c *arm64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) error {
 	offset := c.locationStack.pop()
@@ -1229,7 +1080,7 @@ func (c *arm64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 	c.assembler.SetJumpTargetOnNext(brIfTypeMatched)
 
 	targetFunctionType := c.ir.Types[o.TypeIndex]
-	if err := c.compileCallImpl(0, offset.register, targetFunctionType); err != nil {
+	if err := c.compileCallImpl(offset.register, targetFunctionType); err != nil {
 		return err
 	}
 
@@ -2873,62 +2724,37 @@ func (c *arm64Compiler) compileMemorySize() error {
 
 // compileCallGoFunction adds instructions to call a Go function whose address equals the addr parameter.
 // compilerStatus is set before making call, and it should be either nativeCallStatusCodeCallBuiltInFunction or
-// nativeCallStatusCodeCallHostFunction.
+// nativeCallStatusCodeCallGoHostFunction.
 func (c *arm64Compiler) compileCallGoFunction(compilerStatus nativeCallStatusCode, builtinFunction wasm.Index) error {
 	// Release all the registers as our calling convention requires the caller-save.
 	if err := c.compileReleaseAllRegistersToStack(); err != nil {
 		return err
 	}
 
-	freeRegs, found := c.locationStack.takeFreeRegisters(registerTypeGeneralPurpose, 4)
-	if !found {
-		return fmt.Errorf("BUG: all registers except indexReg should be free at this point")
-	}
-	c.markRegisterUsed(freeRegs...)
-
-	// Alias these free tmp registers for readability.
-	tmp, currentCallFrameStackPointerRegister, currentCallFrameTopAddressRegister, returnAddressRegister :=
-		freeRegs[0], freeRegs[1], freeRegs[2], freeRegs[3]
-
 	if compilerStatus == nativeCallStatusCodeCallBuiltInFunction {
 		// Set the target function address to ce.functionCallAddress
 		// "tmp = $index"
 		c.assembler.CompileConstToRegister(
 			arm64.MOVD,
 			int64(builtinFunction),
-			tmp,
+			arm64ReservedRegisterForTemporary,
 		)
 		// "[arm64ReservedRegisterForCallEngine + callEngineExitContextFunctionCallAddressOffset] = tmp"
 		// In other words, "ce.functionCallAddress = tmp (== $addr)"
 		c.assembler.CompileRegisterToMemory(
 			arm64.STRW,
-			tmp,
-			arm64ReservedRegisterForCallEngine, callEngineExitContextBuiltinFunctionCallAddressOffset,
+			arm64ReservedRegisterForTemporary,
+			arm64ReservedRegisterForCallEngine, callEngineExitContextBuiltinFunctionCallIndexOffset,
 		)
 	}
 
-	// next, we have to set the return address into callFrameStack[ce.callFrameStackPointer-1].returnAddress.
-	//
-	// "currentCallFrameStackPointerRegister = ce.callFrameStackPointer"
-	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineGlobalContextCallFrameStackPointerOffset,
-		currentCallFrameStackPointerRegister)
-
-	// Set the address of callFrameStack[ce.callFrameStackPointer] into currentCallFrameTopAddressRegister.
-	c.compileCalcCallFrameStackTopAddress(currentCallFrameStackPointerRegister, currentCallFrameTopAddressRegister)
-
-	// Set the return address (after RET in c.compileExitFromNativeCode below) into returnAddressRegister.
-	c.assembler.CompileReadInstructionAddress(returnAddressRegister, arm64.RET)
-
-	// Write returnAddressRegister into callFrameStack[ce.callFrameStackPointer-1].returnAddress.
+	// Read the return address, and write it to callEngine.exitContext.returnAddress.
+	c.assembler.CompileReadInstructionAddress(arm64ReservedRegisterForTemporary, arm64.RET)
 	c.assembler.CompileRegisterToMemory(
-		arm64.STRD,
-		returnAddressRegister,
-		// callFrameStack[ce.callFrameStackPointer-1] is below of currentCallFrameTopAddressRegister.
-		currentCallFrameTopAddressRegister, -(callFrameDataSize - callFrameReturnAddressOffset),
+		arm64.STRD, arm64ReservedRegisterForTemporary,
+		arm64ReservedRegisterForCallEngine, callEngineExitContextReturnAddressOffset,
 	)
 
-	c.markRegisterUnused(freeRegs...)
 	c.compileExitFromNativeCode(compilerStatus)
 	return nil
 }
@@ -4095,15 +3921,15 @@ func (c *arm64Compiler) compileReleaseRegisterToStack(loc *runtimeValueLocation)
 func (c *arm64Compiler) compileReservedStackBasePointerRegisterInitialization() {
 	// First, load the address of the first element in the value stack into arm64ReservedRegisterForStackBasePointerAddress temporarily.
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineGlobalContextValueStackElement0AddressOffset,
+		arm64ReservedRegisterForCallEngine, callEngineStackContextStackElement0AddressOffset,
 		arm64ReservedRegisterForStackBasePointerAddress)
 
 	// next we move the base pointer (ce.stackBasePointer) to arm64ReservedRegisterForTemporary.
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		arm64ReservedRegisterForCallEngine, callEngineValueStackContextStackBasePointerInBytesOffset,
+		arm64ReservedRegisterForCallEngine, callEngineStackContextStackBasePointerInBytesOffset,
 		arm64ReservedRegisterForTemporary)
 
-	// Finally, we calculate "callEngineValueStackContextStackBasePointerInBytesOffset + arm64ReservedRegisterForTemporary"
+	// Finally, we calculate "callEngineStackContextStackBasePointerInBytesOffset + arm64ReservedRegisterForTemporary"
 	c.assembler.CompileRegisterToRegister(arm64.ADD, arm64ReservedRegisterForTemporary, arm64ReservedRegisterForStackBasePointerAddress)
 }
 
@@ -4123,9 +3949,6 @@ func (c *arm64Compiler) compileReservedMemoryRegisterInitialization() {
 // This is called in two cases: in function preamble, and on the return from (non-Go) function calls.
 func (c *arm64Compiler) compileModuleContextInitialization() error {
 
-	c.markRegisterUsed(arm64CallingConventionModuleInstanceAddressRegister)
-	defer c.markRegisterUnused(arm64CallingConventionModuleInstanceAddressRegister)
-
 	regs, found := c.locationStack.takeFreeRegisters(registerTypeGeneralPurpose, 2)
 	if !found {
 		panic("BUG: all the registers should be free at this point")
@@ -4152,6 +3975,7 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 	// * callEngine.moduleContext.globalElement0Address
 	// * callEngine.moduleContext.memoryElement0Address
 	// * callEngine.moduleContext.memorySliceLen
+	// * callEngine.moduleContext.memoryInstance
 	// * callEngine.moduleContext.tableElement0Address
 	// * callEngine.moduleContext.tableSliceLen
 	// * callEngine.moduleContext.functionsElement0Address
@@ -4191,7 +4015,14 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 			tmpX,
 		)
 
-		// First, we write the memory length into ce.MemorySliceLen.
+		// First, set ce.memoryInstance
+		c.assembler.CompileRegisterToMemory(
+			arm64.STRD,
+			tmpX,
+			arm64ReservedRegisterForCallEngine, callEngineModuleContextMemoryInstanceOffset,
+		)
+
+		// Next, we write the memory length into ce.MemorySliceLen.
 		//
 		// "tmpY = [tmpX + memoryInstanceBufferLenOffset] (== len(memory.Buffer))"
 		c.assembler.CompileMemoryToRegister(
@@ -4206,7 +4037,7 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 			arm64ReservedRegisterForCallEngine, callEngineModuleContextMemorySliceLenOffset,
 		)
 
-		// next write ce.memoryElement0Address.
+		// Finally, we write ce.memoryElement0Address.
 		//
 		// "tmpY = *tmpX (== &memory.Buffer[0])"
 		c.assembler.CompileMemoryToRegister(
diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go
index 328998a4..72d57c59 100644
--- a/internal/engine/interpreter/interpreter.go
+++ b/internal/engine/interpreter/interpreter.go
@@ -12,7 +12,6 @@ import (
 	"unsafe"
 
 	"github.com/tetratelabs/wazero/experimental"
-	"github.com/tetratelabs/wazero/internal/buildoptions"
 	"github.com/tetratelabs/wazero/internal/moremath"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wasmdebug"
@@ -20,7 +19,11 @@ import (
 	"github.com/tetratelabs/wazero/internal/wazeroir"
 )
 
-var callStackCeiling = buildoptions.CallStackCeiling
+// callStackCeiling is the maximum WebAssembly call frame stack height. This allows wazero to raise
+// wasm.ErrCallStackOverflow instead of overflowing the Go runtime.
+//
+// The default value should suffice for most use cases. Those wishing to change this can via `go build -ldflags`.
+var callStackCeiling = 2000
 
 // engine is an interpreter implementation of wasm.Engine
 type engine struct {
@@ -145,7 +148,7 @@ func (ce *callEngine) drop(r *wazeroir.InclusiveRange) {
 
 func (ce *callEngine) pushFrame(frame *callFrame) {
 	if callStackCeiling <= len(ce.frames) {
-		panic(wasmruntime.ErrRuntimeCallStackOverflow)
+		panic(wasmruntime.ErrRuntimeStackOverflow)
 	}
 	ce.frames = append(ce.frames, frame)
 }
@@ -213,6 +216,9 @@ type interpreterOp struct {
 	rs     []*wazeroir.InclusiveRange
 }
 
+// interpreter mode doesn't maintain call frames in the stack, so pass the zero size to the IR.
+const callFrameStackSize = 0
+
 // CompileModule implements the same method as documented on wasm.Engine.
 func (e *engine) CompileModule(ctx context.Context, module *wasm.Module) error {
 	if _, ok := e.getCodes(module); ok { // cache hit!
@@ -220,7 +226,7 @@ func (e *engine) CompileModule(ctx context.Context, module *wasm.Module) error {
 	}
 
 	funcs := make([]*code, 0, len(module.FunctionSection))
-	irs, err := wazeroir.CompileFunctions(ctx, e.enabledFeatures, module)
+	irs, err := wazeroir.CompileFunctions(ctx, e.enabledFeatures, callFrameStackSize, module)
 	if err != nil {
 		return err
 	}
diff --git a/internal/wasmruntime/errors.go b/internal/wasmruntime/errors.go
index 031777c4..43f951e0 100644
--- a/internal/wasmruntime/errors.go
+++ b/internal/wasmruntime/errors.go
@@ -4,9 +4,9 @@
 package wasmruntime
 
 var (
-	// ErrRuntimeCallStackOverflow indicates that there are too many function calls,
+	// ErrRuntimeStackOverflow indicates that there are too many function calls,
 	// and the Engine terminated the execution.
-	ErrRuntimeCallStackOverflow = New("callstack overflow")
+	ErrRuntimeStackOverflow = New("stack overflow")
 	// ErrRuntimeInvalidConversionToInteger indicates the Wasm function tries to
 	// convert NaN floating point value to integers during trunc variant instructions.
 	ErrRuntimeInvalidConversionToInteger = New("invalid conversion to integer")
diff --git a/internal/wazeroir/compiler.go b/internal/wazeroir/compiler.go
index 1737db12..2ce982eb 100644
--- a/internal/wazeroir/compiler.go
+++ b/internal/wazeroir/compiler.go
@@ -106,8 +106,7 @@ func (c *controlFrames) push(frame *controlFrame) {
 	c.frames = append(c.frames, frame)
 }
 
-// localIndexToStackHeight initializes localIndexToStackHeight field. See the comment on localIndexToStackHeight.
-func (c *compiler) calcLocalIndexToStackHeight() {
+func (c *compiler) initializeStack() {
 	c.localIndexToStackHeightInUint64 = make(map[uint32]int, len(c.sig.Params)+len(c.localTypes))
 	var current int
 	for index, lt := range c.sig.Params {
@@ -118,6 +117,16 @@ func (c *compiler) calcLocalIndexToStackHeight() {
 		current++
 	}
 
+	if c.callFrameStackSizeInUint64 > 0 {
+		// We reserve the stack slots for result values below the return call frame slots.
+		if diff := c.sig.ResultNumInUint64 - c.sig.ParamNumInUint64; diff > 0 {
+			current += diff
+		}
+	}
+
+	// Non-func param locals start after the return call frame.
+	current += c.callFrameStackSizeInUint64
+
 	for index, lt := range c.localTypes {
 		index += len(c.sig.Params)
 		c.localIndexToStackHeightInUint64[wasm.Index(index)] = current
@@ -126,14 +135,32 @@ func (c *compiler) calcLocalIndexToStackHeight() {
 		}
 		current++
 	}
+
+	// Push function arguments.
+	for _, t := range c.sig.Params {
+		c.stackPush(wasmValueTypeToUnsignedType(t)...)
+	}
+
+	if c.callFrameStackSizeInUint64 > 0 {
+		// Reserve the stack slots for results.
+		for i := 0; i < c.sig.ResultNumInUint64-c.sig.ParamNumInUint64; i++ {
+			c.stackPush(UnsignedTypeI64)
+		}
+
+		// Reserve the stack slots for call frame.
+		for i := 0; i < c.callFrameStackSizeInUint64; i++ {
+			c.stackPush(UnsignedTypeI64)
+		}
+	}
 }
 
 type compiler struct {
-	enabledFeatures  wasm.Features
-	stack            []UnsignedType
-	currentID        uint32
-	controlFrames    *controlFrames
-	unreachableState struct {
+	enabledFeatures            wasm.Features
+	callFrameStackSizeInUint64 int
+	stack                      []UnsignedType
+	currentID                  uint32
+	controlFrames              *controlFrames
+	unreachableState           struct {
 		on    bool
 		depth int
 	}
@@ -223,7 +250,7 @@ type CompilationResult struct {
 	HasElementInstances bool
 }
 
-func CompileFunctions(_ context.Context, enabledFeatures wasm.Features, module *wasm.Module) ([]*CompilationResult, error) {
+func CompileFunctions(_ context.Context, enabledFeatures wasm.Features, callFrameStackSizeInUint64 int, module *wasm.Module) ([]*CompilationResult, error) {
 	functions, globals, mem, tables, err := module.AllDeclarations()
 	if err != nil {
 		return nil, err
@@ -250,9 +277,15 @@ func CompileFunctions(_ context.Context, enabledFeatures wasm.Features, module *
 				GoFunc:     code.GoFunc,
 				Signature:  sig,
 			})
+
+			if len(sig.Params) > 0 && sig.ParamNumInUint64 == 0 {
+				panic("a")
+			} else if len(sig.Results) > 0 && sig.ResultNumInUint64 == 0 {
+				panic("if len(sig.Results) > 0 && sig.ResultNumInUint64 == 0")
+			}
 			continue
 		}
-		r, err := compile(enabledFeatures, sig, code.Body, code.LocalTypes, module.TypeSection, functions, globals)
+		r, err := compile(enabledFeatures, callFrameStackSizeInUint64, sig, code.Body, code.LocalTypes, module.TypeSection, functions, globals)
 		if err != nil {
 			def := module.FunctionDefinitionSection[uint32(funcIndex)+module.ImportFuncCount()]
 			return nil, fmt.Errorf("failed to lower func[%s] to wazeroir: %w", def.DebugName(), err)
@@ -276,6 +309,7 @@ func CompileFunctions(_ context.Context, enabledFeatures wasm.Features, module *
 // so that the resulting operations can be consumed by the interpreter
 // or the Compiler compilation engine.
 func compile(enabledFeatures wasm.Features,
+	callFrameStackSizeInUint64 int,
 	sig *wasm.FunctionType,
 	body []byte,
 	localTypes []wasm.ValueType,
@@ -283,23 +317,20 @@ func compile(enabledFeatures wasm.Features,
 	functions []uint32, globals []*wasm.GlobalType,
 ) (*CompilationResult, error) {
 	c := compiler{
-		enabledFeatures: enabledFeatures,
-		controlFrames:   &controlFrames{},
-		result:          CompilationResult{LabelCallers: map[string]uint32{}},
-		body:            body,
-		localTypes:      localTypes,
-		sig:             sig,
-		globals:         globals,
-		funcs:           functions,
-		types:           types,
+		enabledFeatures:            enabledFeatures,
+		controlFrames:              &controlFrames{},
+		callFrameStackSizeInUint64: callFrameStackSizeInUint64,
+		result:                     CompilationResult{LabelCallers: map[string]uint32{}},
+		body:                       body,
+		localTypes:                 localTypes,
+		sig:                        sig,
+		globals:                    globals,
+		funcs:                      functions,
+		types:                      types,
 	}
 
-	c.calcLocalIndexToStackHeight()
+	c.initializeStack()
 
-	// Push function arguments.
-	for _, t := range sig.Params {
-		c.stackPush(wasmValueTypeToUnsignedType(t)...)
-	}
 	// Emit const expressions for locals.
 	// Note that here we don't take function arguments
 	// into account, meaning that callers must push


Test patch:
diff --git a/internal/engine/compiler/compiler_bench_test.go b/internal/engine/compiler/compiler_bench_test.go
index b736adfc..0cc88135 100644
--- a/internal/engine/compiler/compiler_bench_test.go
+++ b/internal/engine/compiler/compiler_bench_test.go
@@ -110,12 +110,8 @@ func BenchmarkCompiler_compileMemoryFill(b *testing.B) {
 }
 
 func (j *compilerEnv) execBench(b *testing.B, codeSegment []byte) {
-	f := j.newFunction(codeSegment)
-
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
-		j.ce.callFrameStack[j.ce.globalContext.callFrameStackPointer] = callFrame{function: f}
-		j.ce.globalContext.callFrameStackPointer++
 		nativecall(
 			uintptr(unsafe.Pointer(&codeSegment[0])),
 			uintptr(unsafe.Pointer(j.ce)),
diff --git a/internal/engine/compiler/compiler_conditional_save_test.go b/internal/engine/compiler/compiler_conditional_save_test.go
index e01e0b3c..fa5daf65 100644
--- a/internal/engine/compiler/compiler_conditional_save_test.go
+++ b/internal/engine/compiler/compiler_conditional_save_test.go
@@ -26,9 +26,12 @@ func TestCompiler_conditional_value_saving(t *testing.T) {
 	}
 
 	// Pick the f32 floating point local (1.0) twice.
-	err = compiler.compilePick(&wazeroir.OperationPick{Depth: int(compiler.runtimeValueLocationStack().sp - 1)})
+	// Note that the f32 (function local variable in general) is placed above the call frame.
+	err = compiler.compilePick(&wazeroir.OperationPick{Depth: int(compiler.runtimeValueLocationStack().sp - 1 - callFrameDataSizeInUint64)})
+
 	require.NoError(t, err)
-	err = compiler.compilePick(&wazeroir.OperationPick{Depth: int(compiler.runtimeValueLocationStack().sp - 1)})
+	err = compiler.compilePick(&wazeroir.OperationPick{Depth: int(compiler.runtimeValueLocationStack().sp - 1 - callFrameDataSizeInUint64)})
+
 	require.NoError(t, err)
 	// Generate conditional flag via floating point comparisons.
 	err = compiler.compileLe(&wazeroir.OperationLe{Type: wazeroir.SignedTypeFloat32})
diff --git a/internal/engine/compiler/compiler_controlflow_test.go b/internal/engine/compiler/compiler_controlflow_test.go
index 5040582c..6871918f 100644
--- a/internal/engine/compiler/compiler_controlflow_test.go
+++ b/internal/engine/compiler/compiler_controlflow_test.go
@@ -23,10 +23,11 @@ func TestCompiler_compileHostFunction(t *testing.T) {
 	env.exec(code)
 
 	// On the return, the code must exit with the host call status.
-	require.Equal(t, nativeCallStatusCodeCallHostFunction, env.compilerStatus())
+	require.Equal(t, nativeCallStatusCodeCallGoHostFunction, env.compilerStatus())
 
 	// Re-enter the return address.
-	nativecall(env.callFrameStackPeek().returnAddress,
+	require.NotEqual(t, uintptr(0), uintptr(env.ce.returnAddress))
+	nativecall(env.ce.returnAddress,
 		uintptr(unsafe.Pointer(env.callEngine())),
 		uintptr(unsafe.Pointer(env.module())),
 	)
@@ -233,7 +234,7 @@ func TestCompiler_compileBrIf(t *testing.T) {
 					require.NoError(t, err)
 
 					tc.setupFunc(t, compiler, shouldGoToElse)
-					require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+					requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 
 					err = compiler.compileBrIf(&wazeroir.OperationBrIf{Then: thenBranchTarget, Else: elseBranchTarget})
 					require.NoError(t, err)
@@ -634,110 +635,100 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 	})
 
 	t.Run("ok", func(t *testing.T) {
-		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}
-
-				table := make([]wasm.Reference, 10)
-				env := newCompilerEnvironment()
-				env.addTable(&wasm.TableInstance{References: table})
-
-				// Ensure that the module instance has the type information for targetOperation.TypeIndex,
-				// and the typeID  matches the table[targetOffset]'s type ID.
-				env.module().TypeIDs = make([]wasm.FunctionTypeID, 100)
-				env.module().TypeIDs[operation.TypeIndex] = targetTypeID
-				env.module().Engine = &moduleEngine{functions: []*function{}}
-
-				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)
+		targetType := &wasm.FunctionType{
+			Results:           []wasm.ValueType{wasm.ValueTypeI32},
+			ResultNumInUint64: 1,
+		}
+		targetTypeID := wasm.FunctionTypeID(10)
+		operation := &wazeroir.OperationCallIndirect{TypeIndex: 0}
 
-					compiler := env.requireNewCompiler(t, newCompiler, nil)
-					err := compiler.compilePreamble()
-					require.NoError(t, err)
-					err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: expectedReturnValue})
-					require.NoError(t, err)
-					err = compiler.compileReturnFunction()
-					require.NoError(t, err)
+		table := make([]wasm.Reference, 10)
+		env := newCompilerEnvironment()
+		env.addTable(&wasm.TableInstance{References: table})
 
-					c, _, err := compiler.compile()
-					require.NoError(t, err)
+		// Ensure that the module instance has the type information for targetOperation.TypeIndex,
+		// and the typeID matches the table[targetOffset]'s type ID.
+		env.module().TypeIDs = make([]wasm.FunctionTypeID, 100)
+		env.module().TypeIDs[operation.TypeIndex] = targetTypeID
+		env.module().Engine = &moduleEngine{functions: []*function{}}
 
-					f := &function{
-						parent:                &code{codeSegment: c},
-						codeInitialAddress:    uintptr(unsafe.Pointer(&c[0])),
-						moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)),
-						source: &wasm.FunctionInstance{
-							TypeID: targetTypeID,
-						},
-					}
-					me.functions = append(me.functions, f)
-					table[i] = uintptr(unsafe.Pointer(f))
-				}
+		me := env.moduleEngine()
+		for i := 0; i < len(table); i++ {
+			// First, we create the call target function for the table element i.
+			// To match its function type, it must return one value.
+			expectedReturnValue := uint32(i * 1000)
 
-				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)
-						}
-
-						compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{
-							Signature: targetType,
-							Types:     []*wasm.FunctionType{targetType},
-							HasTable:  true,
-						})
-						err := compiler.compilePreamble()
-						require.NoError(t, err)
-
-						// Place the offset value. Here we try calling a function of functionaddr == table[i].FunctionIndex.
-						err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: uint32(i)})
-						require.NoError(t, err)
-
-						// At this point, we should have one item (offset value) on the stack.
-						require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
-
-						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.runtimeValueLocationStack().sp)
-
-						err = compiler.compileReturnFunction()
-						require.NoError(t, err)
-
-						// Generate the code under test and run.
-						code, _, err := compiler.compile()
-						require.NoError(t, err)
-						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, nativeCallStatusCodeCallBuiltInFunction, env.compilerStatus())
-							require.Equal(t, builtinFunctionIndexGrowCallFrameStack, env.builtinFunctionCallAddress())
-
-							// Grow the callFrame stack, and exec again from the return address.
-							ce := env.callEngine()
-							ce.builtinFunctionGrowCallFrameStack()
-							nativecall(
-								env.callFrameStackPeek().returnAddress, uintptr(unsafe.Pointer(ce)),
-								uintptr(unsafe.Pointer(env.module())),
-							)
-						}
-
-						require.Equal(t, nativeCallStatusCodeReturned.String(), env.compilerStatus().String())
-						require.Equal(t, uint64(1), env.stackPointer())
-						require.Equal(t, expectedReturnValue, uint32(env.ce.popValue()))
-					})
-				}
+			compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{
+				Signature: targetType,
+			})
+			err := compiler.compilePreamble()
+			require.NoError(t, err)
+			err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: expectedReturnValue})
+			require.NoError(t, err)
+
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
+			// The function result value must be set at the bottom of the stack.
+			err = compiler.compileSet(&wazeroir.OperationSet{Depth: int(compiler.runtimeValueLocationStack().sp - 1)})
+			require.NoError(t, err)
+			err = compiler.compileReturnFunction()
+			require.NoError(t, err)
+
+			c, _, err := compiler.compile()
+			require.NoError(t, err)
+
+			// Now that we've generated the code for this function,
+			// add it to the module engine and assign its pointer to the table index.
+			f := &function{
+				parent:                &code{codeSegment: c},
+				codeInitialAddress:    uintptr(unsafe.Pointer(&c[0])),
+				moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)),
+				source: &wasm.FunctionInstance{
+					TypeID: targetTypeID,
+				},
+			}
+			me.functions = append(me.functions, f)
+			table[i] = uintptr(unsafe.Pointer(f))
+		}
+
+		// Test to ensure that we can call all the functions stored in the table.
+		for i := 1; i < len(table); i++ {
+			expectedReturnValue := uint32(i * 1000)
+			t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+
+				compiler := env.requireNewCompiler(t, newCompiler,
+					&wazeroir.CompilationResult{
+						Signature: &wasm.FunctionType{},
+						Types:     []*wasm.FunctionType{targetType},
+						HasTable:  true,
+					},
+				)
+				err := compiler.compilePreamble()
+				require.NoError(t, err)
+
+				// Place the offset value. Here we try calling a function of functionaddr == table[i].FunctionIndex.
+				err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: uint32(i)})
+				require.NoError(t, err)
+
+				// At this point, we should have one item (offset value) on the stack.
+				requireRuntimeLocationStackPointerEqual(t, 1, compiler)
+
+				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.
+				requireRuntimeLocationStackPointerEqual(t, 1, compiler)
+
+				err = compiler.compileReturnFunction()
+				require.NoError(t, err)
+
+				// Generate the code under test and run.
+				code, _, err := compiler.compile()
+				require.NoError(t, err)
+				env.exec(code)
+
+				require.Equal(t, nativeCallStatusCodeReturned.String(), env.compilerStatus().String())
+				require.Equal(t, uint64(1), env.stackPointer())
+				require.Equal(t, expectedReturnValue, uint32(env.ce.popValue()))
 			})
 		}
 	})
@@ -804,184 +795,89 @@ func TestCompiler_callIndirect_largeTypeIndex(t *testing.T) {
 }
 
 func TestCompiler_compileCall(t *testing.T) {
-	for _, growCallFrameStack := range []bool{false, true} {
-		growCallFrameStack := growCallFrameStack
-		t.Run(fmt.Sprintf("grow=%v", growCallFrameStack), func(t *testing.T) {
-			env := newCompilerEnvironment()
-			me := env.moduleEngine()
-			expectedValue := uint32(0)
-
-			if growCallFrameStack {
-				env.setCallFrameStackPointerLen(1)
-			}
-
-			// Emit the call target function.
-			const numCalls = 3
-			targetFunctionType := &wasm.FunctionType{
-				Params:           []wasm.ValueType{wasm.ValueTypeI32},
-				Results:          []wasm.ValueType{wasm.ValueTypeI32},
-				ParamNumInUint64: 1, ResultNumInUint64: 1,
-			}
-			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)
-				expectedValue += addTargetValue
-
-				compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{Signature: targetFunctionType})
-
-				err := compiler.compilePreamble()
-				require.NoError(t, err)
-
-				err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: uint32(addTargetValue)})
-				require.NoError(t, err)
-				err = compiler.compileAdd(&wazeroir.OperationAdd{Type: wazeroir.UnsignedTypeI32})
-				require.NoError(t, err)
-
-				err = compiler.compileReturnFunction()
-				require.NoError(t, err)
-
-				c, _, err := compiler.compile()
-				require.NoError(t, err)
-				index := wasm.Index(i)
-				me.functions = append(me.functions, &function{
-					parent:                &code{codeSegment: c},
-					codeInitialAddress:    uintptr(unsafe.Pointer(&c[0])),
-					moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)),
-				})
-				env.module().Functions = append(env.module().Functions,
-					&wasm.FunctionInstance{Type: targetFunctionType, Idx: index})
-			}
-
-			// Now we start building the caller's code.
-			compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{
-				Signature: &wasm.FunctionType{},
-				Functions: make([]uint32, numCalls),
-				Types:     []*wasm.FunctionType{targetFunctionType},
-			})
-
-			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)
-
-			// Call all the built functions.
-			for i := 0; i < numCalls; i++ {
-				err = compiler.compileCall(&wazeroir.OperationCall{FunctionIndex: uint32(i)})
-				require.NoError(t, err)
-			}
-
-			err = compiler.compileReturnFunction()
-			require.NoError(t, err)
-
-			code, _, err := compiler.compile()
-			require.NoError(t, err)
-			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, nativeCallStatusCodeCallBuiltInFunction, env.compilerStatus())
-				require.Equal(t, builtinFunctionIndexGrowCallFrameStack, env.builtinFunctionCallAddress())
-
-				// Grow the callFrame stack, and exec again from the return address.
-				ce := env.callEngine()
-				ce.builtinFunctionGrowCallFrameStack()
-				nativecall(
-					env.callFrameStackPeek().returnAddress, uintptr(unsafe.Pointer(ce)),
-					uintptr(unsafe.Pointer(env.module())),
-				)
-			}
-
-			// Check status and returned values.
-			require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
-			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())
-		})
+	env := newCompilerEnvironment()
+	me := env.moduleEngine()
+	expectedValue := uint32(0)
+
+	// Emit the call target function.
+	const numCalls = 3
+	targetFunctionType := &wasm.FunctionType{
+		Params:           []wasm.ValueType{wasm.ValueTypeI32},
+		Results:          []wasm.ValueType{wasm.ValueTypeI32},
+		ParamNumInUint64: 1, ResultNumInUint64: 1,
 	}
-}
-
-func TestCompiler_returnFunction(t *testing.T) {
-	t.Run("exit", func(t *testing.T) {
-		env := newCompilerEnvironment()
+	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)
+		expectedValue += addTargetValue
+		compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{
+			Signature: targetFunctionType,
+		})
 
-		// Compile code.
-		compiler := env.requireNewCompiler(t, newCompiler, nil)
 		err := compiler.compilePreamble()
 		require.NoError(t, err)
-		err = compiler.compileReturnFunction()
+
+		err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: addTargetValue})
+		require.NoError(t, err)
+		// Picks the function argument placed at the bottom of the stack.
+		err = compiler.compilePick(&wazeroir.OperationPick{Depth: int(compiler.runtimeValueLocationStack().sp - 1)})
+		require.NoError(t, err)
+		// Adds the const to the picked value.
+		err = compiler.compileAdd(&wazeroir.OperationAdd{Type: wazeroir.UnsignedTypeI32})
+		require.NoError(t, err)
+		// Then store the added result into the bottom of the stack (which is treated as the result of the function).
+		err = compiler.compileSet(&wazeroir.OperationSet{Depth: int(compiler.runtimeValueLocationStack().sp - 1)})
 		require.NoError(t, err)
 
-		code, _, err := compiler.compile()
+		err = compiler.compileReturnFunction()
 		require.NoError(t, err)
 
-		env.exec(code)
+		c, _, err := compiler.compile()
+		require.NoError(t, err)
+		index := wasm.Index(i)
+		me.functions = append(me.functions, &function{
+			parent:                &code{codeSegment: c},
+			codeInitialAddress:    uintptr(unsafe.Pointer(&c[0])),
+			moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)),
+		})
+		env.module().Functions = append(env.module().Functions,
+			&wasm.FunctionInstance{Type: targetFunctionType, Idx: index})
+	}
 
-		// Compiler status must be returned.
-		require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
-		// Plus, the call frame stack pointer must be zero after return.
-		require.Equal(t, uint64(0), env.callFrameStackPointer())
+	// Now we start building the caller's code.
+	compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{
+		Signature: &wasm.FunctionType{},
+		Functions: make([]uint32, numCalls),
+		Types:     []*wasm.FunctionType{targetFunctionType},
 	})
-	t.Run("deep call stack", func(t *testing.T) {
-		env := newCompilerEnvironment()
-		moduleEngine := env.moduleEngine()
-		ce := env.callEngine()
-
-		// Push the call frames.
-		const callFrameNums = 10
-		stackPointerToExpectedValue := map[uint64]uint32{}
-		for funcIndex := wasm.Index(0); funcIndex < callFrameNums; funcIndex++ {
-			// Each function pushes its funcaddr and soon returns.
-			compiler := env.requireNewCompiler(t, newCompiler, nil)
-			err := compiler.compilePreamble()
-			require.NoError(t, err)
-
-			// Push its functionIndex.
-			expValue := uint32(funcIndex)
-			err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: expValue})
-			require.NoError(t, err)
 
-			err = compiler.compileReturnFunction()
-			require.NoError(t, err)
+	err := compiler.compilePreamble()
+	require.NoError(t, err)
 
-			c, _, err := compiler.compile()
-			require.NoError(t, err)
+	const initialValue = 100
+	expectedValue += initialValue
+	err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: 1234}) // 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)
 
-			// Compiles and adds to the engine.
-			f := &function{
-				parent:                &code{codeSegment: c},
-				codeInitialAddress:    uintptr(unsafe.Pointer(&c[0])),
-				moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)),
-			}
-			moduleEngine.functions = append(moduleEngine.functions, f)
-
-			// 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: f.codeInitialAddress,
-				// Note: return stack base pointer is set to funcaddr*5 and this is where the const should be pushed.
-				returnStackBasePointerInBytes: (uint64(funcIndex) * 5) << 3,
-				function:                      f,
-			}
-			ce.callFrameStack[ce.globalContext.callFrameStackPointer] = frame
-			ce.globalContext.callFrameStackPointer++
-			stackPointerToExpectedValue[frame.returnStackBasePointerInBytes>>3] = expValue
-		}
+	// Call all the built functions.
+	for i := 0; i < numCalls; i++ {
+		err = compiler.compileCall(&wazeroir.OperationCall{FunctionIndex: uint32(i)})
+		require.NoError(t, err)
+	}
 
-		require.Equal(t, uint64(callFrameNums), env.callFrameStackPointer())
+	// Set the result slot
+	err = compiler.compileReturnFunction()
+	require.NoError(t, err)
 
-		// Run code from the top frame.
-		env.exec(ce.callFrameTop().function.parent.codeSegment)
+	code, _, err := compiler.compile()
+	require.NoError(t, err)
+	env.exec(code)
 
-		// Check the exit status and the values on stack.
-		require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
-		for pos, exp := range stackPointerToExpectedValue {
-			require.Equal(t, exp, uint32(env.stack()[pos]))
-		}
-	})
+	// Check status and returned values.
+	require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
+	require.Equal(t, uint64(0), env.stackBasePointer())
+	require.Equal(t, uint64(2), env.stackPointer()) // Must be 2 (dummy value + the calculation results)
+	require.Equal(t, expectedValue, env.stackTopAsUint32())
 }
diff --git a/internal/engine/compiler/compiler_global_test.go b/internal/engine/compiler/compiler_global_test.go
index 4537ad89..e0acc30a 100644
--- a/internal/engine/compiler/compiler_global_test.go
+++ b/internal/engine/compiler/compiler_global_test.go
@@ -53,7 +53,7 @@ func TestCompiler_compileGlobalGet(t *testing.T) {
 			env.exec(code)
 
 			// Since we call global.get, the top of the stack must be the global value.
-			require.Equal(t, globalValue, env.stack()[0])
+			require.Equal(t, globalValue, env.stackTopAsUint64())
 			// Plus as we push the value, the stack pointer must be incremented.
 			require.Equal(t, uint64(1), env.stackPointer())
 		})
@@ -99,8 +99,10 @@ func TestCompiler_compileGlobalGet_v128(t *testing.T) {
 
 	// Since we call global.get, the top of the stack must be the global value.
 	actual := globals[1]
-	require.Equal(t, actual.Val, env.stack()[0])
-	require.Equal(t, actual.ValHi, env.stack()[1])
+	sp := env.ce.stackContext.stackPointer
+	stack := env.stack()
+	require.Equal(t, actual.Val, stack[sp-2])
+	require.Equal(t, actual.ValHi, stack[sp-1])
 }
 
 func TestCompiler_compileGlobalSet(t *testing.T) {
@@ -140,7 +142,8 @@ func TestCompiler_compileGlobalSet(t *testing.T) {
 
 			op := &wazeroir.OperationGlobalSet{Index: 1}
 			err = compiler.compileGlobalSet(op)
-			require.Equal(t, uint64(0), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, 0, compiler)
+
 			require.NoError(t, err)
 
 			err = compiler.compileReturnFunction()
@@ -186,7 +189,7 @@ func TestCompiler_compileGlobalSet_v128(t *testing.T) {
 
 	op := &wazeroir.OperationGlobalSet{Index: 1}
 	err = compiler.compileGlobalSet(op)
-	require.Equal(t, uint64(0), compiler.runtimeValueLocationStack().sp)
+	requireRuntimeLocationStackPointerEqual(t, 0, compiler)
 	require.NoError(t, err)
 
 	err = compiler.compileReturnFunction()
diff --git a/internal/engine/compiler/compiler_initialization_test.go b/internal/engine/compiler/compiler_initialization_test.go
index 63e6c21e..6c407cb1 100644
--- a/internal/engine/compiler/compiler_initialization_test.go
+++ b/internal/engine/compiler/compiler_initialization_test.go
@@ -146,6 +146,7 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) {
 				bufSliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&tc.moduleInstance.Memory.Buffer))
 				require.Equal(t, uint64(bufSliceHeader.Len), ce.moduleContext.memorySliceLen)
 				require.Equal(t, bufSliceHeader.Data, ce.moduleContext.memoryElement0Address)
+				require.Equal(t, tc.moduleInstance.Memory, ce.moduleContext.memoryInstance)
 			}
 
 			if len(tc.moduleInstance.Tables) > 0 {
@@ -172,7 +173,7 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) {
 	}
 }
 
-func TestCompiler_compileMaybeGrowValueStack(t *testing.T) {
+func TestCompiler_compileMaybeGrowStack(t *testing.T) {
 	t.Run("not grow", func(t *testing.T) {
 		const stackPointerCeil = 5
 		for _, baseOffset := range []uint64{5, 10, 20} {
@@ -180,14 +181,15 @@ func TestCompiler_compileMaybeGrowValueStack(t *testing.T) {
 				env := newCompilerEnvironment()
 				compiler := env.requireNewCompiler(t, newCompiler, nil)
 
-				err := compiler.compileMaybeGrowValueStack()
+				err := compiler.compilePreamble()
 				require.NoError(t, err)
+
 				require.NotNil(t, compiler.getOnStackPointerCeilDeterminedCallBack())
 
-				valueStackLen := uint64(len(env.stack()))
-				stackBasePointer := valueStackLen - baseOffset // Ceil <= valueStackLen - stackBasePointer = no need to grow!
+				stackLen := uint64(len(env.stack()))
+				stackBasePointer := stackLen - baseOffset // Ceil <= stackLen - stackBasePointer = no need to grow!
 				compiler.getOnStackPointerCeilDeterminedCallBack()(stackPointerCeil)
-				env.setValueStackBasePointer(stackBasePointer)
+				env.setStackBasePointer(stackBasePointer)
 
 				compiler.compileExitFromNativeCode(nativeCallStatusCodeReturned)
 
@@ -202,7 +204,7 @@ func TestCompiler_compileMaybeGrowValueStack(t *testing.T) {
 		}
 	})
 
-	var defaultValueStackLen = uint64(initialValueStackSize)
+	var defaultStackLen = uint64(initialStackSize)
 	t.Run("grow", func(t *testing.T) {
 		tests := []struct {
 			name             string
@@ -212,7 +214,7 @@ func TestCompiler_compileMaybeGrowValueStack(t *testing.T) {
 			{
 				name:             "ceil=6/sbp=len-5",
 				stackPointerCeil: 6,
-				stackBasePointer: defaultValueStackLen - 5,
+				stackBasePointer: defaultStackLen - 5,
 			},
 			{
 				name:             "ceil=10000/sbp=0",
@@ -227,7 +229,7 @@ func TestCompiler_compileMaybeGrowValueStack(t *testing.T) {
 				env := newCompilerEnvironment()
 				compiler := env.requireNewCompiler(t, newCompiler, nil)
 
-				err := compiler.compileMaybeGrowValueStack()
+				err := compiler.compilePreamble()
 				require.NoError(t, err)
 
 				// On the return from grow value stack, we simply return.
@@ -240,14 +242,14 @@ func TestCompiler_compileMaybeGrowValueStack(t *testing.T) {
 				require.NoError(t, err)
 
 				// And run the code with the specified stackBasePointer.
-				env.setValueStackBasePointer(tc.stackBasePointer)
+				env.setStackBasePointer(tc.stackBasePointer)
 				env.exec(code)
 
 				// Check if the call exits with builtin function call status.
 				require.Equal(t, nativeCallStatusCodeCallBuiltInFunction, env.compilerStatus())
 
 				// Reenter from the return address.
-				returnAddress := env.callFrameStackPeek().returnAddress
+				returnAddress := env.ce.returnAddress
 				require.True(t, returnAddress != 0, "returnAddress was zero %d", returnAddress)
 				nativecall(
 					returnAddress, uintptr(unsafe.Pointer(env.callEngine())),
diff --git a/internal/engine/compiler/compiler_memory_test.go b/internal/engine/compiler/compiler_memory_test.go
index 3b46d75d..9dbee05f 100644
--- a/internal/engine/compiler/compiler_memory_test.go
+++ b/internal/engine/compiler/compiler_memory_test.go
@@ -40,7 +40,8 @@ func TestCompiler_compileMemoryGrow(t *testing.T) {
 
 	// Reenter from the return address.
 	nativecall(
-		env.callFrameStackPeek().returnAddress, uintptr(unsafe.Pointer(env.callEngine())),
+		env.ce.returnAddress,
+		uintptr(unsafe.Pointer(env.callEngine())),
 		uintptr(unsafe.Pointer(env.module())),
 	)
 
@@ -60,7 +61,7 @@ func TestCompiler_compileMemorySize(t *testing.T) {
 	err = compiler.compileMemorySize()
 	require.NoError(t, err)
 	// At this point, the size of memory should be pushed onto the stack.
-	require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+	requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 
 	err = compiler.compileReturnFunction()
 	require.NoError(t, err)
@@ -249,7 +250,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 			tc.operationSetupFn(t, compiler)
 
 			// At this point, the loaded value must be on top of the stack, and placed on a register.
-			require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 			loadedLocation := compiler.runtimeValueLocationStack().peek()
 			require.True(t, loadedLocation.onRegister())
@@ -391,7 +392,7 @@ func TestCompiler_compileStore(t *testing.T) {
 
 			// At this point, no registers must be in use, and no values on the stack since we consumed two values.
 			require.Zero(t, len(compiler.runtimeValueLocationStack().usedRegisters))
-			require.Equal(t, uint64(0), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(0), compiler)
 
 			// Generate the code under test.
 			err = compiler.compileReturnFunction()
diff --git a/internal/engine/compiler/compiler_numeric_test.go b/internal/engine/compiler/compiler_numeric_test.go
index 5abef0fa..4a0f4676 100644
--- a/internal/engine/compiler/compiler_numeric_test.go
+++ b/internal/engine/compiler/compiler_numeric_test.go
@@ -155,7 +155,7 @@ func TestCompiler_compile_Add_Sub_Mul(t *testing.T) {
 							}
 
 							// At this point, two values exist.
-							require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+							requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 
 							// Emit the operation.
 							switch kind {
@@ -169,7 +169,7 @@ func TestCompiler_compile_Add_Sub_Mul(t *testing.T) {
 							require.NoError(t, err)
 
 							// We consumed two values, but push the result back.
-							require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+							requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 							resultLocation := compiler.runtimeValueLocationStack().peek()
 							// Plus the result must be located on a register.
 							require.True(t, resultLocation.onRegister())
@@ -325,7 +325,7 @@ func TestCompiler_compile_And_Or_Xor_Shl_Rotl_Rotr(t *testing.T) {
 								}
 
 								// At this point, two values exist.
-								require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+								requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 
 								// Emit the operation.
 								switch kind {
@@ -345,7 +345,7 @@ func TestCompiler_compile_And_Or_Xor_Shl_Rotl_Rotr(t *testing.T) {
 								require.NoError(t, err)
 
 								// We consumed two values, but push the result back.
-								require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+								requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 								resultLocation := compiler.runtimeValueLocationStack().peek()
 								// Also, the result must have an appropriate register type.
 								require.Equal(t, registerTypeGeneralPurpose, resultLocation.getRegisterType())
@@ -453,14 +453,14 @@ func TestCompiler_compileShr(t *testing.T) {
 						}
 
 						// At this point, two values exist.
-						require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+						requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 
 						// Emit the operation.
 						err = compiler.compileShr(&wazeroir.OperationShr{Type: signedInt})
 						require.NoError(t, err)
 
 						// We consumed two values, but push the result back.
-						require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+						requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 						resultLocation := compiler.runtimeValueLocationStack().peek()
 						// Plus the result must be located on a register.
 						require.True(t, resultLocation.onRegister())
@@ -547,7 +547,7 @@ func TestCompiler_compile_Le_Lt_Gt_Ge_Eq_Eqz_Ne(t *testing.T) {
 						isEqz := kind == wazeroir.OperationKindEqz
 						if isEqz && (signedType == wazeroir.SignedTypeFloat32 || signedType == wazeroir.SignedTypeFloat64) {
 							// Eqz isn't defined for float.
-							t.Skip()
+							return
 						}
 						t.Run(fmt.Sprintf("x1=0x%x,x2=0x%x", x1, x2), func(t *testing.T) {
 							env := newCompilerEnvironment()
@@ -575,10 +575,10 @@ func TestCompiler_compile_Le_Lt_Gt_Ge_Eq_Eqz_Ne(t *testing.T) {
 							if isEqz {
 								// Eqz only needs one value, so pop the top one (x2).
 								compiler.runtimeValueLocationStack().pop()
-								require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+								requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 							} else {
 								// At this point, two values exist for comparison.
-								require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+								requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 							}
 
 							// Emit the operation.
@@ -627,7 +627,7 @@ func TestCompiler_compile_Le_Lt_Gt_Ge_Eq_Eqz_Ne(t *testing.T) {
 							require.NoError(t, err)
 
 							// We consumed two values, but push the result back.
-							require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+							requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 
 							err = compiler.compileReturnFunction()
 							require.NoError(t, err)
@@ -978,13 +978,13 @@ func TestCompiler_compile_Min_Max_Copysign(t *testing.T) {
 					}
 
 					// At this point two values are pushed.
-					require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+					requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 					require.Equal(t, 2, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 					tc.setupFunc(t, compiler)
 
 					// We consumed two values, but push one value after operation.
-					require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+					requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 					require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 					err = compiler.compileReturnFunction()
@@ -1281,13 +1281,13 @@ func TestCompiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.T) {
 					}
 
 					// At this point two values are pushed.
-					require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+					requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 					require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 					tc.setupFunc(t, compiler)
 
 					// We consumed one value, but push the result after operation.
-					require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+					requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 					require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 					err = compiler.compileReturnFunction()
@@ -1394,7 +1394,7 @@ func TestCompiler_compile_Div_Rem(t *testing.T) {
 							}
 
 							// At this point, two values exist for comparison.
-							require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+							requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 
 							switch kind {
 							case wazeroir.OperationKindDiv:
@@ -1411,10 +1411,10 @@ func TestCompiler_compile_Div_Rem(t *testing.T) {
 									err = compiler.compileRem(&wazeroir.OperationRem{Type: wazeroir.SignedUint64})
 								case wazeroir.SignedTypeFloat32:
 									// Rem undefined for float32.
-									t.Skip()
+									return
 								case wazeroir.SignedTypeFloat64:
 									// Rem undefined for float64.
-									t.Skip()
+									return
 								}
 							}
 							require.NoError(t, err)
diff --git a/internal/engine/compiler/compiler_stack_test.go b/internal/engine/compiler/compiler_stack_test.go
index 6f9a856e..7d0f8cc1 100644
--- a/internal/engine/compiler/compiler_stack_test.go
+++ b/internal/engine/compiler/compiler_stack_test.go
@@ -60,12 +60,12 @@ func TestCompiler_releaseRegisterToStack(t *testing.T) {
 			require.NoError(t, err)
 
 			// Run native code after growing the value stack.
-			env.callEngine().builtinFunctionGrowValueStack(tc.stackPointer)
+			env.callEngine().builtinFunctionGrowStack(tc.stackPointer)
 			env.exec(code)
 
 			// Compiler status must be returned and stack pointer must end up the specified one.
 			require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
-			require.Equal(t, tc.stackPointer+1, env.stackPointer())
+			require.Equal(t, tc.stackPointer+1, env.ce.stackPointer)
 
 			if tc.isFloat {
 				require.Equal(t, math.Float64frombits(val), env.stackTopAsFloat64())
@@ -143,13 +143,13 @@ func TestCompiler_compileLoadValueOnStackToRegister(t *testing.T) {
 			require.NoError(t, err)
 
 			// Run native code after growing the value stack, and place the original value.
-			env.callEngine().builtinFunctionGrowValueStack(tc.stackPointer)
+			env.callEngine().builtinFunctionGrowStack(tc.stackPointer)
 			env.stack()[tc.stackPointer] = val
 			env.exec(code)
 
 			// Compiler status must be returned and stack pointer must end up the specified one.
 			require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
-			require.Equal(t, tc.stackPointer+1, env.stackPointer())
+			require.Equal(t, tc.stackPointer+1, env.ce.stackPointer)
 
 			if tc.isFloat {
 				require.Equal(t, math.Float64frombits(val)+1, env.stackTopAsFloat64())
@@ -197,12 +197,12 @@ func TestCompiler_compilePick_v128(t *testing.T) {
 
 			// Push the unused median value.
 			_ = compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
-			require.Equal(t, uint64(3), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(3), compiler)
 
 			// Now ready to compile Pick operation.
 			err = compiler.compilePick(op)
 			require.NoError(t, err)
-			require.Equal(t, uint64(5), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(5), compiler)
 
 			hiLoc := compiler.runtimeValueLocationStack().peek()
 			loLoc := compiler.runtimeValueLocationStack().stack[hiLoc.stackPointer-1]
@@ -261,7 +261,7 @@ func TestCompiler_compilePick(t *testing.T) {
 			pickTargetSetupFunc: func(compiler compilerImpl, ce *callEngine) error {
 				pickTargetLocation := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
 				pickTargetLocation.valueType = runtimeValueTypeF64
-				ce.valueStack[pickTargetLocation.stackPointer] = pickTargetValue
+				ce.stack[pickTargetLocation.stackPointer] = pickTargetValue
 				return nil
 			},
 			isPickTargetFloat:      true,
@@ -272,7 +272,7 @@ func TestCompiler_compilePick(t *testing.T) {
 			pickTargetSetupFunc: func(compiler compilerImpl, ce *callEngine) error {
 				pickTargetLocation := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
 				pickTargetLocation.valueType = runtimeValueTypeI64
-				ce.valueStack[pickTargetLocation.stackPointer] = pickTargetValue
+				ce.stack[pickTargetLocation.stackPointer] = pickTargetValue
 				return nil
 			},
 			isPickTargetFloat:      false,
@@ -295,12 +295,12 @@ func TestCompiler_compilePick(t *testing.T) {
 
 			// Push the unused median value.
 			_ = compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 
 			// Now ready to compile Pick operation.
 			err = compiler.compilePick(op)
 			require.NoError(t, err)
-			require.Equal(t, uint64(3), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(3), compiler)
 
 			pickedLocation := compiler.runtimeValueLocationStack().peek()
 			require.True(t, pickedLocation.onRegister())
@@ -343,13 +343,13 @@ func TestCompiler_compileDrop(t *testing.T) {
 		for i := 0; i < liveNum; i++ {
 			compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
 		}
-		require.Equal(t, uint64(liveNum), compiler.runtimeValueLocationStack().sp)
+		requireRuntimeLocationStackPointerEqual(t, uint64(liveNum), compiler)
 
 		err = compiler.compileDrop(&wazeroir.OperationDrop{Depth: nil})
 		require.NoError(t, err)
 
 		// After the nil range drop, the stack must remain the same.
-		require.Equal(t, uint64(liveNum), compiler.runtimeValueLocationStack().sp)
+		requireRuntimeLocationStackPointerEqual(t, uint64(liveNum), compiler)
 
 		err = compiler.compileReturnFunction()
 		require.NoError(t, err)
@@ -381,13 +381,13 @@ func TestCompiler_compileDrop(t *testing.T) {
 				compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
 			}
 		}
-		require.Equal(t, uint64(liveNum+dropTargetNum), compiler.runtimeValueLocationStack().sp)
+		requireRuntimeLocationStackPointerEqual(t, uint64(liveNum+dropTargetNum), compiler)
 
 		err = compiler.compileDrop(&wazeroir.OperationDrop{Depth: r})
 		require.NoError(t, err)
 
 		// After the drop operation, the stack contains only live contents.
-		require.Equal(t, uint64(liveNum), compiler.runtimeValueLocationStack().sp)
+		requireRuntimeLocationStackPointerEqual(t, uint64(liveNum), compiler)
 		// Plus, the top value must stay on a register.
 		top := compiler.runtimeValueLocationStack().peek()
 		require.True(t, top.onRegister())
@@ -419,10 +419,15 @@ func TestCompiler_compileDrop(t *testing.T) {
 		err := compiler.compilePreamble()
 		require.NoError(t, err)
 
+		// We don't need call frame in this test case, so simply pop them out!
+		for i := 0; i < callFrameDataSizeInUint64; i++ {
+			compiler.runtimeValueLocationStack().pop()
+		}
+
 		// Put existing contents except the top on stack
 		for i := 0; i < total-1; i++ {
 			loc := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
-			ce.valueStack[loc.stackPointer] = uint64(i) // Put the initial value.
+			ce.stack[loc.stackPointer] = uint64(i) // Put the initial value.
 		}
 
 		// Place the top value.
@@ -448,7 +453,7 @@ func TestCompiler_compileDrop(t *testing.T) {
 
 		env.exec(code)
 		require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
-		require.Equal(t, uint64(liveTotal), env.stackPointer())
+		require.Equal(t, uint64(liveTotal), env.ce.stackPointer)
 
 		stack := env.stack()[:env.stackPointer()]
 		for i, val := range stack {
@@ -663,9 +668,10 @@ func TestCompiler_compileSwap_v128(t *testing.T) {
 			require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
 			require.Equal(t, uint64(3), env.stackPointer())
 
+			// The first variable is above the call frame.
 			st := env.stack()
-			require.Equal(t, x2Lo, st[0])
-			require.Equal(t, x2Hi, st[1])
+			require.Equal(t, x2Lo, st[callFrameDataSizeInUint64])
+			require.Equal(t, x2Hi, st[callFrameDataSizeInUint64+1])
 		})
 	}
 }
@@ -718,7 +724,7 @@ func TestCompiler_compileSet(t *testing.T) {
 				x1Value = 1
 			}
 
-			// Swap x1 and x2.
+			// Set x2 into the x1.
 			err = compiler.compileSet(&wazeroir.OperationSet{Depth: 2})
 			require.NoError(t, err)
 
@@ -732,8 +738,8 @@ func TestCompiler_compileSet(t *testing.T) {
 			env.exec(code)
 
 			require.Equal(t, uint64(2), env.stackPointer())
-			// Check the value was set.
-			require.Equal(t, uint64(x1Value), env.stack()[0])
+			// Check the value was set. Note that it is placed above the call frame.
+			require.Equal(t, uint64(x1Value), env.stack()[callFrameDataSizeInUint64])
 		})
 	}
 }
diff --git a/internal/engine/compiler/compiler_test.go b/internal/engine/compiler/compiler_test.go
index eaef2e70..29643565 100644
--- a/internal/engine/compiler/compiler_test.go
+++ b/internal/engine/compiler/compiler_test.go
@@ -35,46 +35,37 @@ func init() {
 	requireEqual(int(unsafe.Offsetof(me.functions)), moduleEngineFunctionsOffset, "moduleEngineFunctionsOffset")
 
 	var ce callEngine
-	// Offsets for callEngine.globalContext.
-	requireEqual(int(unsafe.Offsetof(ce.valueStackElement0Address)), callEngineGlobalContextValueStackElement0AddressOffset, "callEngineGlobalContextValueStackElement0AddressOffset")
-	requireEqual(int(unsafe.Offsetof(ce.valueStackLenInBytes)), callEngineGlobalContextValueStackLenInBytesOffset, "callEngineGlobalContextValueStackLenInBytesOffset")
-	requireEqual(int(unsafe.Offsetof(ce.callFrameStackElementZeroAddress)), callEngineGlobalContextCallFrameStackElement0AddressOffset, "callEngineGlobalContextCallFrameStackElement0AddressOffset")
-	requireEqual(int(unsafe.Offsetof(ce.callFrameStackLen)), callEngineGlobalContextCallFrameStackLenOffset, "callEngineGlobalContextCallFrameStackLenOffset")
-	requireEqual(int(unsafe.Offsetof(ce.callFrameStackPointer)), callEngineGlobalContextCallFrameStackPointerOffset, "callEngineGlobalContextCallFrameStackPointerOffset")
-
 	// Offsets for callEngine.moduleContext.
+	requireEqual(int(unsafe.Offsetof(ce.fn)), callEngineModuleContextFnOffset, "callEngineModuleContextFnOffset")
 	requireEqual(int(unsafe.Offsetof(ce.moduleInstanceAddress)), callEngineModuleContextModuleInstanceAddressOffset, "callEngineModuleContextModuleInstanceAddressOffset")
 	requireEqual(int(unsafe.Offsetof(ce.globalElement0Address)), callEngineModuleContextGlobalElement0AddressOffset, "callEngineModuleContextGlobalElement0AddressOffset")
 	requireEqual(int(unsafe.Offsetof(ce.memoryElement0Address)), callEngineModuleContextMemoryElement0AddressOffset, "callEngineModuleContextMemoryElement0AddressOffset")
 	requireEqual(int(unsafe.Offsetof(ce.memorySliceLen)), callEngineModuleContextMemorySliceLenOffset, "callEngineModuleContextMemorySliceLenOffset")
+	requireEqual(int(unsafe.Offsetof(ce.memoryInstance)), callEngineModuleContextMemoryInstanceOffset, "callEngineModuleContextMemoryInstanceOffset")
 	requireEqual(int(unsafe.Offsetof(ce.tablesElement0Address)), callEngineModuleContextTablesElement0AddressOffset, "callEngineModuleContextTablesElement0AddressOffset")
 	requireEqual(int(unsafe.Offsetof(ce.functionsElement0Address)), callEngineModuleContextFunctionsElement0AddressOffset, "callEngineModuleContextFunctionsElement0AddressOffset")
 	requireEqual(int(unsafe.Offsetof(ce.typeIDsElement0Address)), callEngineModuleContextTypeIDsElement0AddressOffset, "callEngineModuleContextTypeIDsElement0AddressOffset")
 	requireEqual(int(unsafe.Offsetof(ce.dataInstancesElement0Address)), callEngineModuleContextDataInstancesElement0AddressOffset, "callEngineModuleContextDataInstancesElement0AddressOffset")
 	requireEqual(int(unsafe.Offsetof(ce.elementInstancesElement0Address)), callEngineModuleContextElementInstancesElement0AddressOffset, "callEngineModuleContextElementInstancesElement0AddressOffset")
 
-	// Offsets for callEngine.valueStackContext
-	requireEqual(int(unsafe.Offsetof(ce.stackPointer)), callEngineValueStackContextStackPointerOffset, "callEngineValueStackContextStackPointerOffset")
-	requireEqual(int(unsafe.Offsetof(ce.stackBasePointerInBytes)), callEngineValueStackContextStackBasePointerInBytesOffset, "callEngineValueStackContextStackBasePointerInBytesOffset")
+	// Offsets for callEngine.stackContext
+	requireEqual(int(unsafe.Offsetof(ce.stackPointer)), callEngineStackContextStackPointerOffset, "callEngineStackContextStackPointerOffset")
+	requireEqual(int(unsafe.Offsetof(ce.stackBasePointerInBytes)), callEngineStackContextStackBasePointerInBytesOffset, "callEngineStackContextStackBasePointerInBytesOffset")
+	requireEqual(int(unsafe.Offsetof(ce.stackElement0Address)), callEngineStackContextStackElement0AddressOffset, "callEngineStackContextStackElement0AddressOffset")
+	requireEqual(int(unsafe.Offsetof(ce.stackLenInBytes)), callEngineStackContextStackLenInBytesOffset, "callEngineStackContextStackLenInBytesOffset")
 
 	// Offsets for callEngine.exitContext.
 	requireEqual(int(unsafe.Offsetof(ce.statusCode)), callEngineExitContextNativeCallStatusCodeOffset, "callEngineExitContextNativeCallStatusCodeOffset")
-	requireEqual(int(unsafe.Offsetof(ce.builtinFunctionCallIndex)), callEngineExitContextBuiltinFunctionCallAddressOffset, "callEngineExitContextBuiltinFunctionCallAddressOffset")
+	requireEqual(int(unsafe.Offsetof(ce.builtinFunctionCallIndex)), callEngineExitContextBuiltinFunctionCallIndexOffset, "callEngineExitContextBuiltinFunctionCallIndexOffset")
+	requireEqual(int(unsafe.Offsetof(ce.returnAddress)), callEngineExitContextReturnAddressOffset, "callEngineExitContextReturnAddressOffset")
 
 	// Size and offsets for callFrame.
 	var frame callFrame
-	requireEqual(int(unsafe.Sizeof(frame)), callFrameDataSize, "callFrameDataSize")
-	// Sizeof call-frame must be a power of 2 as we do SHL on the index by "callFrameDataSizeMostSignificantSetBit" to obtain the offset address.
-	requireEqual(0, callFrameDataSize&(callFrameDataSize-1), "callFrameDataSize&(callFrameDataSize-1)")
-	requireEqual(math.Ilogb(float64(callFrameDataSize)), callFrameDataSizeMostSignificantSetBit, "callFrameDataSizeMostSignificantSetBit")
-	requireEqual(int(unsafe.Offsetof(frame.returnAddress)), callFrameReturnAddressOffset, "callFrameReturnAddressOffset")
-	requireEqual(int(unsafe.Offsetof(frame.returnStackBasePointerInBytes)), callFrameReturnStackBasePointerInBytesOffset, "callFrameReturnStackBasePointerInBytesOffset")
-	requireEqual(int(unsafe.Offsetof(frame.function)), callFrameFunctionOffset, "callFrameFunctionOffset")
+	requireEqual(int(unsafe.Sizeof(frame))/8, callFrameDataSizeInUint64, "callFrameDataSize")
 
 	// Offsets for code.
 	var compiledFunc function
 	requireEqual(int(unsafe.Offsetof(compiledFunc.codeInitialAddress)), functionCodeInitialAddressOffset, "functionCodeInitialAddressOffset")
-	requireEqual(int(unsafe.Offsetof(compiledFunc.stackPointerCeil)), functionStackPointerCeilOffset, "functionStackPointerCeilOffset")
 	requireEqual(int(unsafe.Offsetof(compiledFunc.source)), functionSourceOffset, "functionSourceOffset")
 	requireEqual(int(unsafe.Offsetof(compiledFunc.moduleInstanceAddress)), functionModuleInstanceAddressOffset, "functionModuleInstanceAddressOffset")
 
@@ -125,31 +116,31 @@ type compilerEnv struct {
 }
 
 func (j *compilerEnv) stackTopAsUint32() uint32 {
-	return uint32(j.stack()[j.stackPointer()-1])
+	return uint32(j.stack()[j.ce.stackContext.stackPointer-1])
 }
 
 func (j *compilerEnv) stackTopAsInt32() int32 {
-	return int32(j.stack()[j.stackPointer()-1])
+	return int32(j.stack()[j.ce.stackContext.stackPointer-1])
 }
 func (j *compilerEnv) stackTopAsUint64() uint64 {
-	return j.stack()[j.stackPointer()-1]
+	return j.stack()[j.ce.stackContext.stackPointer-1]
 }
 
 func (j *compilerEnv) stackTopAsInt64() int64 {
-	return int64(j.stack()[j.stackPointer()-1])
+	return int64(j.stack()[j.ce.stackContext.stackPointer-1])
 }
 
 func (j *compilerEnv) stackTopAsFloat32() float32 {
-	return math.Float32frombits(uint32(j.stack()[j.stackPointer()-1]))
+	return math.Float32frombits(uint32(j.stack()[j.ce.stackContext.stackPointer-1]))
 }
 
 func (j *compilerEnv) stackTopAsFloat64() float64 {
-	return math.Float64frombits(j.stack()[j.stackPointer()-1])
+	return math.Float64frombits(j.stack()[j.ce.stackContext.stackPointer-1])
 }
 
 func (j *compilerEnv) stackTopAsV128() (lo uint64, hi uint64) {
 	st := j.stack()
-	return st[j.stackPointer()-2], st[j.stackPointer()-1]
+	return st[j.ce.stackContext.stackPointer-2], st[j.ce.stackContext.stackPointer-1]
 }
 
 func (j *compilerEnv) memory() []byte {
@@ -157,7 +148,7 @@ func (j *compilerEnv) memory() []byte {
 }
 
 func (j *compilerEnv) stack() []uint64 {
-	return j.ce.valueStack
+	return j.ce.stack
 }
 
 func (j *compilerEnv) compilerStatus() nativeCallStatusCode {
@@ -168,16 +159,17 @@ func (j *compilerEnv) builtinFunctionCallAddress() wasm.Index {
 	return j.ce.exitContext.builtinFunctionCallIndex
 }
 
+// stackPointer returns the stack pointer minus the call frame.
 func (j *compilerEnv) stackPointer() uint64 {
-	return j.ce.valueStackContext.stackPointer
+	return j.ce.stackContext.stackPointer - callFrameDataSizeInUint64
 }
 
 func (j *compilerEnv) stackBasePointer() uint64 {
-	return j.ce.valueStackContext.stackBasePointerInBytes >> 3
+	return j.ce.stackContext.stackBasePointerInBytes >> 3
 }
 
 func (j *compilerEnv) setStackPointer(sp uint64) {
-	j.ce.valueStackContext.stackPointer = sp
+	j.ce.stackContext.stackPointer = sp
 }
 
 func (j *compilerEnv) addGlobals(g ...*wasm.GlobalInstance) {
@@ -192,20 +184,8 @@ func (j *compilerEnv) addTable(table *wasm.TableInstance) {
 	j.moduleInstance.Tables = append(j.moduleInstance.Tables, table)
 }
 
-func (j *compilerEnv) callFrameStackPeek() *callFrame {
-	return &j.ce.callFrameStack[j.ce.globalContext.callFrameStackPointer-1]
-}
-
-func (j *compilerEnv) callFrameStackPointer() uint64 {
-	return j.ce.globalContext.callFrameStackPointer
-}
-
-func (j *compilerEnv) setValueStackBasePointer(sp uint64) {
-	j.ce.valueStackContext.stackBasePointerInBytes = sp << 3
-}
-
-func (j *compilerEnv) setCallFrameStackPointerLen(l uint64) {
-	j.ce.callFrameStackLen = l
+func (j *compilerEnv) setStackBasePointer(sp uint64) {
+	j.ce.stackContext.stackBasePointerInBytes = sp << 3
 }
 
 func (j *compilerEnv) module() *wasm.ModuleInstance {
@@ -235,9 +215,8 @@ func (j *compilerEnv) newFunction(codeSegment []byte) *function {
 
 func (j *compilerEnv) exec(codeSegment []byte) {
 	f := j.newFunction(codeSegment)
-	j.ce.callFrameStack[j.ce.globalContext.callFrameStackPointer] = callFrame{function: f}
-	j.ce.globalContext.callFrameStackPointer++
-	j.ce.compiled = f
+	j.ce.initialFn = f
+	j.ce.fn = f
 
 	nativecall(
 		uintptr(unsafe.Pointer(&codeSegment[0])),
@@ -272,7 +251,7 @@ func (j *compilerEnv) requireNewCompiler(t *testing.T, fn newTestCompiler, ir *w
 type compilerImpl interface {
 	compiler
 	compileExitFromNativeCode(nativeCallStatusCode)
-	compileMaybeGrowValueStack() error
+	compileMaybeGrowStack() error
 	compileReturnFunction() error
 	getOnStackPointerCeilDeterminedCallBack() func(uint64)
 	setStackPointerCeil(uint64)
@@ -294,6 +273,12 @@ func newCompilerEnvironment() *compilerEnv {
 			Globals: []*wasm.GlobalInstance{},
 			Engine:  me,
 		},
-		ce: me.newCallEngine(nil, nil),
+		ce: me.newCallEngine(initialStackSize, nil),
 	}
 }
+
+// requireRuntimeLocationStackPointerEqual ensures that the compiler's runtimeValueLocationStack has
+// the expected stack pointer value relative to the call frame.
+func requireRuntimeLocationStackPointerEqual(t *testing.T, expSP uint64, c compiler) {
+	require.Equal(t, expSP, c.runtimeValueLocationStack().sp-callFrameDataSizeInUint64)
+}
diff --git a/internal/engine/compiler/compiler_value_location_test.go b/internal/engine/compiler/compiler_value_location_test.go
index f86a57d9..bf18aeb1 100644
--- a/internal/engine/compiler/compiler_value_location_test.go
+++ b/internal/engine/compiler/compiler_value_location_test.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/tetratelabs/wazero/internal/asm"
 	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
 func Test_isIntRegister(t *testing.T) {
@@ -119,3 +120,103 @@ func TestRuntimeValueLocationStack_takeStealTargetFromUsedRegister(t *testing.T)
 	require.False(t, ok)
 	require.Nil(t, target)
 }
+
+func TestRuntimeValueLocationStack_setupInitialStack(t *testing.T) {
+	const f32 = wasm.ValueTypeF32
+	tests := []struct {
+		name       string
+		sig        *wasm.FunctionType
+		expectedSP uint64
+	}{
+		{
+			name:       "no params / no results",
+			sig:        &wasm.FunctionType{},
+			expectedSP: callFrameDataSizeInUint64,
+		},
+		{
+			name: "no results",
+			sig: &wasm.FunctionType{
+				Params:           []wasm.ValueType{f32, f32},
+				ParamNumInUint64: 2,
+			},
+			expectedSP: callFrameDataSizeInUint64 + 2,
+		},
+		{
+			name: "no params",
+			sig: &wasm.FunctionType{
+				Results:           []wasm.ValueType{f32, f32},
+				ResultNumInUint64: 2,
+			},
+			expectedSP: callFrameDataSizeInUint64 + 2,
+		},
+		{
+			name: "params == results",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{f32, f32},
+				ParamNumInUint64:  2,
+				Results:           []wasm.ValueType{f32, f32},
+				ResultNumInUint64: 2,
+			},
+			expectedSP: callFrameDataSizeInUint64 + 2,
+		},
+		{
+			name: "params > results",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{f32, f32, f32},
+				ParamNumInUint64:  3,
+				Results:           []wasm.ValueType{f32, f32},
+				ResultNumInUint64: 2,
+			},
+			expectedSP: callFrameDataSizeInUint64 + 3,
+		},
+		{
+			name: "params <  results",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{f32},
+				ParamNumInUint64:  1,
+				Results:           []wasm.ValueType{f32, f32, f32},
+				ResultNumInUint64: 3,
+			},
+			expectedSP: callFrameDataSizeInUint64 + 3,
+		},
+	}
+
+	for _, tc := range tests {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			s := newRuntimeValueLocationStack()
+			s.init(tc.sig)
+			require.Equal(t, tc.expectedSP, s.sp)
+
+			callFrameLocations := s.stack[s.sp-callFrameDataSizeInUint64 : s.sp]
+			for _, loc := range callFrameLocations {
+				require.Equal(t, runtimeValueTypeI64, loc.valueType)
+			}
+		})
+	}
+}
+
+func TestRuntimeValueLocation_pushCallFrame(t *testing.T) {
+	for _, sig := range []*wasm.FunctionType{
+		{ParamNumInUint64: 0, ResultNumInUint64: 1},
+		{ParamNumInUint64: 1, ResultNumInUint64: 0},
+		{ParamNumInUint64: 1, ResultNumInUint64: 1},
+		{ParamNumInUint64: 0, ResultNumInUint64: 2},
+		{ParamNumInUint64: 2, ResultNumInUint64: 0},
+		{ParamNumInUint64: 2, ResultNumInUint64: 3},
+	} {
+		sig := sig
+		t.Run(sig.String(), func(t *testing.T) {
+			s := newRuntimeValueLocationStack()
+			// pushCallFrame assumes that the parameters are already pushed.
+			s.sp += uint64(sig.ParamNumInUint64)
+
+			retAddr, stackBasePointer, fn := s.pushCallFrame(sig)
+
+			expOffset := uint64(callFrameOffset(sig))
+			require.Equal(t, expOffset, retAddr.stackPointer)
+			require.Equal(t, expOffset+1, stackBasePointer.stackPointer)
+			require.Equal(t, expOffset+2, fn.stackPointer)
+		})
+	}
+}
diff --git a/internal/engine/compiler/compiler_vec_test.go b/internal/engine/compiler/compiler_vec_test.go
index a13aa4dc..4da9de38 100644
--- a/internal/engine/compiler/compiler_vec_test.go
+++ b/internal/engine/compiler/compiler_vec_test.go
@@ -86,7 +86,7 @@ func TestCompiler_compileV128Add(t *testing.T) {
 			err = compiler.compileV128Add(&wazeroir.OperationV128Add{Shape: tc.shape})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -184,7 +184,7 @@ func TestCompiler_compileV128Sub(t *testing.T) {
 			err = compiler.compileV128Sub(&wazeroir.OperationV128Sub{Shape: tc.shape})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -557,7 +557,7 @@ func TestCompiler_compileV128Load(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 			loadedLocation := compiler.runtimeValueLocationStack().peek()
 			require.True(t, loadedLocation.onRegister())
@@ -765,7 +765,7 @@ func TestCompiler_compileV128LoadLane(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 			loadedLocation := compiler.runtimeValueLocationStack().peek()
 			require.True(t, loadedLocation.onRegister())
@@ -819,7 +819,7 @@ func TestCompiler_compileV128Store(t *testing.T) {
 			err = compiler.compileV128Store(&wazeroir.OperationV128Store{Arg: &wazeroir.MemoryArg{}})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(0), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(0), compiler)
 			require.Equal(t, 0, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -961,7 +961,7 @@ func TestCompiler_compileV128StoreLane(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(0), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(0), compiler)
 			require.Equal(t, 0, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -1132,7 +1132,7 @@ func TestCompiler_compileV128ExtractLane(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			vt := compiler.runtimeValueLocationStack().peek().valueType
@@ -1381,7 +1381,7 @@ func TestCompiler_compileV128ReplaceLane(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -1479,7 +1479,7 @@ func TestCompiler_compileV128Splat(t *testing.T) {
 			err = compiler.compileV128Splat(&wazeroir.OperationV128Splat{Shape: tc.shape})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -1527,7 +1527,7 @@ func TestCompiler_compileV128AnyTrue(t *testing.T) {
 			err = compiler.compileV128AnyTrue(&wazeroir.OperationV128AnyTrue{})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 
 			err = compiler.compileReturnFunction()
 			require.NoError(t, err)
@@ -1790,7 +1790,7 @@ func TestCompiler_compileV128Swizzle(t *testing.T) {
 			err = compiler.compileV128Swizzle(&wazeroir.OperationV128Swizzle{})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -1900,7 +1900,7 @@ func TestCompiler_compileV128Shuffle(t *testing.T) {
 			err = compiler.compileV128Shuffle(&wazeroir.OperationV128Shuffle{Lanes: tc.lanes})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -2036,7 +2036,7 @@ func TestCompiler_compileV128Bitmask(t *testing.T) {
 			err = compiler.compileV128BitMask(&wazeroir.OperationV128BitMask{Shape: tc.shape})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -2073,7 +2073,7 @@ func TestCompiler_compileV128_Not(t *testing.T) {
 	err = compiler.compileV128Not(&wazeroir.OperationV128Not{})
 	require.NoError(t, err)
 
-	require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+	requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 	require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 	err = compiler.compileReturnFunction()
@@ -2301,7 +2301,7 @@ func TestCompiler_compileV128_And_Or_Xor_AndNot(t *testing.T) {
 			}
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -2395,7 +2395,7 @@ func TestCompiler_compileV128Bitselect(t *testing.T) {
 			err = compiler.compileV128Bitselect(nil)
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -2680,7 +2680,7 @@ func TestCompiler_compileV128Shl(t *testing.T) {
 			err = compiler.compileV128Shl(&wazeroir.OperationV128Shl{Shape: tc.shape})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -2956,7 +2956,7 @@ func TestCompiler_compileV128Shr(t *testing.T) {
 			err = compiler.compileV128Shr(&wazeroir.OperationV128Shr{Shape: tc.shape, Signed: tc.signed})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -3387,7 +3387,7 @@ func TestCompiler_compileV128Cmp(t *testing.T) {
 			err = compiler.compileV128Cmp(&wazeroir.OperationV128Cmp{Type: tc.cmpType})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -3468,7 +3468,7 @@ func TestCompiler_compileV128AvgrU(t *testing.T) {
 			err = compiler.compileV128AvgrU(&wazeroir.OperationV128AvgrU{Shape: tc.shape})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -3535,7 +3535,7 @@ func TestCompiler_compileV128Sqrt(t *testing.T) {
 			err = compiler.compileV128Sqrt(&wazeroir.OperationV128Sqrt{Shape: tc.shape})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -3625,7 +3625,7 @@ func TestCompiler_compileV128Mul(t *testing.T) {
 			err = compiler.compileV128Mul(&wazeroir.OperationV128Mul{Shape: tc.shape})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -3722,7 +3722,7 @@ func TestCompiler_compileV128Neg(t *testing.T) {
 			err = compiler.compileV128Neg(&wazeroir.OperationV128Neg{Shape: tc.shape})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -3819,7 +3819,7 @@ func TestCompiler_compileV128Abs(t *testing.T) {
 			err = compiler.compileV128Abs(&wazeroir.OperationV128Abs{Shape: tc.shape})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -3895,7 +3895,7 @@ func TestCompiler_compileV128Div(t *testing.T) {
 			err = compiler.compileV128Div(&wazeroir.OperationV128Div{Shape: tc.shape})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -4088,7 +4088,7 @@ func TestCompiler_compileV128Min(t *testing.T) {
 			err = compiler.compileV128Min(&wazeroir.OperationV128Min{Shape: tc.shape, Signed: tc.signed})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -4315,7 +4315,7 @@ func TestCompiler_compileV128Max(t *testing.T) {
 			err = compiler.compileV128Max(&wazeroir.OperationV128Max{Shape: tc.shape, Signed: tc.signed})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -4456,7 +4456,7 @@ func TestCompiler_compileV128AddSat(t *testing.T) {
 			err = compiler.compileV128AddSat(&wazeroir.OperationV128AddSat{Shape: tc.shape, Signed: tc.signed})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -4568,7 +4568,7 @@ func TestCompiler_compileV128SubSat(t *testing.T) {
 			err = compiler.compileV128SubSat(&wazeroir.OperationV128SubSat{Shape: tc.shape, Signed: tc.signed})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -4638,7 +4638,7 @@ func TestCompiler_compileV128Popcnt(t *testing.T) {
 			err = compiler.compileV128Popcnt(&wazeroir.OperationV128Popcnt{})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -4818,7 +4818,7 @@ func TestCompiler_compileV128Round(t *testing.T) {
 			}
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -5111,7 +5111,7 @@ func TestCompiler_compileV128_Pmax_Pmin(t *testing.T) {
 			}
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -5805,7 +5805,7 @@ func TestCompiler_compileV128ExtMul(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -6278,7 +6278,7 @@ func TestCompiler_compileV128Extend(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -6363,7 +6363,7 @@ func TestCompiler_compileV128Q15mulrSatS(t *testing.T) {
 			err = compiler.compileV128Q15mulrSatS(&wazeroir.OperationV128Q15mulrSatS{})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -6436,7 +6436,7 @@ func TestCompiler_compileFloatPromote(t *testing.T) {
 			err = compiler.compileV128FloatPromote(&wazeroir.OperationV128FloatPromote{})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -6520,7 +6520,7 @@ func TestCompiler_compileV128FloatDemote(t *testing.T) {
 			err = compiler.compileV128FloatDemote(&wazeroir.OperationV128FloatDemote{})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -6730,7 +6730,7 @@ func TestCompiler_compileV128ExtAddPairwise(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -6978,7 +6978,7 @@ func TestCompiler_compileV128Narrow(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -7115,7 +7115,7 @@ func TestCompiler_compileV128FConvertFromI(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -7187,7 +7187,7 @@ func TestCompiler_compileV128Dot(t *testing.T) {
 			err = compiler.compileV128Dot(&wazeroir.OperationV128Dot{})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -7339,7 +7339,7 @@ func TestCompiler_compileV128ITruncSatFromF(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
@@ -7392,7 +7392,7 @@ func TestCompiler_compileSelect_v128(t *testing.T) {
 		err = compiler.compileSelect(&wazeroir.OperationSelect{IsTargetVector: true})
 		require.NoError(t, err)
 
-		require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+		requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 		require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 		err = compiler.compileReturnFunction()
diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go
index 1db8e074..d10c8c22 100644
--- a/internal/engine/compiler/engine_test.go
+++ b/internal/engine/compiler/engine_test.go
@@ -186,7 +186,7 @@ func TestCompiler_Releasecode_Panic(t *testing.T) {
 
 // Ensures that value stack and call-frame stack are allocated on heap which
 // allows us to safely access to their data region from native code.
-// See comments on initialValueStackSize and initialCallFrameStackSize.
+// See comments on initialStackSize and initialCallFrameStackSize.
 func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
 	enabledFeatures := wasm.Features20191205
 	e := newEngine(context.Background(), enabledFeatures)
@@ -219,7 +219,7 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
 	_, err = s.Instantiate(testCtx, ns, hm, hostModuleName, nil, nil)
 	require.NoError(t, err)
 
-	const valueStackCorruption = "value_stack_corruption"
+	const stackCorruption = "value_stack_corruption"
 	const callStackCorruption = "call_stack_corruption"
 	const expectedReturnValue = 0x1
 	m := &wasm.Module{
@@ -262,7 +262,7 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
 		},
 		ImportSection: []*wasm.Import{{Module: hostModuleName, Name: hostFnName, DescFunc: 1}},
 		ExportSection: []*wasm.Export{
-			{Type: wasm.ExternTypeFunc, Index: 1, Name: valueStackCorruption},
+			{Type: wasm.ExternTypeFunc, Index: 1, Name: stackCorruption},
 			{Type: wasm.ExternTypeFunc, Index: 2, Name: callStackCorruption},
 		},
 		ID: wasm.ModuleID{1},
@@ -275,7 +275,7 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
 	mi, err := s.Instantiate(testCtx, ns, m, t.Name(), nil, nil)
 	require.NoError(t, err)
 
-	for _, fnName := range []string{valueStackCorruption, callStackCorruption} {
+	for _, fnName := range []string{stackCorruption, callStackCorruption} {
 		fnName := fnName
 		t.Run(fnName, func(t *testing.T) {
 			ret, err := mi.ExportedFunction(fnName).Call(testCtx)
@@ -288,14 +288,14 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
 
 func TestCallEngine_builtinFunctionTableGrow(t *testing.T) {
 	ce := &callEngine{
-		valueStack: []uint64{
+		stack: []uint64{
 			0xff, // pseudo-ref
 			1,    // num
 			// Table Index = 0 (lower 32-bits), but the higher bits (32-63) are all sets,
 			// which happens if the previous value on that stack location was 64-bit wide.
 			0xffffffff << 32,
 		},
-		valueStackContext: valueStackContext{stackPointer: 3},
+		stackContext: stackContext{stackPointer: 3},
 	}
 
 	table := &wasm.TableInstance{References: []wasm.Reference{}, Min: 10}
@@ -305,28 +305,52 @@ func TestCallEngine_builtinFunctionTableGrow(t *testing.T) {
 	require.Equal(t, uintptr(0xff), table.References[0])
 }
 
+func ptrAsUint64(f *function) uint64 {
+	return uint64(uintptr(unsafe.Pointer(f)))
+}
+
 func TestCallEngine_deferredOnCall(t *testing.T) {
+	f1 := &function{source: &wasm.FunctionInstance{
+		FunctionDefinition: newMockFunctionDefinition("1"),
+		Type:               &wasm.FunctionType{ParamNumInUint64: 2},
+	}}
+	f2 := &function{source: &wasm.FunctionInstance{
+		FunctionDefinition: newMockFunctionDefinition("2"),
+		Type:               &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3},
+	}}
+	f3 := &function{source: &wasm.FunctionInstance{
+		FunctionDefinition: newMockFunctionDefinition("3"),
+		Type:               &wasm.FunctionType{ResultNumInUint64: 1},
+	}}
+
 	ce := &callEngine{
-		valueStack:        make([]uint64, 100),
-		valueStackContext: valueStackContext{stackPointer: 3},
-		globalContext:     globalContext{callFrameStackPointer: 5},
-		callFrameStack: []callFrame{
-			{function: &function{source: &wasm.FunctionInstance{FunctionDefinition: newMockFunctionDefinition("1")}}},
-			{function: &function{source: &wasm.FunctionInstance{FunctionDefinition: newMockFunctionDefinition("2")}}},
-			{function: &function{source: &wasm.FunctionInstance{FunctionDefinition: newMockFunctionDefinition("3")}}},
-			{function: &function{source: &wasm.FunctionInstance{FunctionDefinition: newMockFunctionDefinition("4")}}},
-			{function: &function{source: &wasm.FunctionInstance{FunctionDefinition: newMockFunctionDefinition("5")}}},
+		stack: []uint64{
+			0xff, 0xff, // dummy argument for f1
+			0, 0, 0, 0,
+			0xcc, 0xcc, // local variable for f1.
+			// <----- stack base point of f2 (top) == index 8.
+			0xaa, 0xaa, 0xdeadbeaf, // dummy argument for f2 (0xaa, 0xaa) and the reserved slot for result 0xdeadbeaf)
+			0, 0, ptrAsUint64(f1), 0, // callFrame
+			0xcc, 0xcc, 0xcc, // local variable for f2.
+			// <----- stack base point of f3 (top) == index 18
+			0xdeadbeaf,                    // the reserved slot for result 0xdeadbeaf) from f3.
+			0, 8 << 3, ptrAsUint64(f2), 0, // callFrame
+		},
+		stackContext: stackContext{
+			stackBasePointerInBytes: 18 << 3, // currently executed function (f3)'s base pointer.
+			stackPointer:            0xff,    // dummy supposed to be reset to zero.
+		},
+		moduleContext: moduleContext{
+			fn:                    f3, // currently executed function (f3)!
+			moduleInstanceAddress: 0xdeafbeaf,
 		},
-		moduleContext: moduleContext{moduleInstanceAddress: 0xdeafbeaf},
 	}
 
-	beforeRecoverValueStack, beforeRecoverCallFrameStack := ce.valueStack, ce.callFrameStack
+	beforeRecoverStack := ce.stack
 
 	err := ce.deferredOnCall(errors.New("some error"))
 	require.EqualError(t, err, `some error (recovered by wazero)
 wasm stack trace:
-	5()
-	4()
 	3()
 	2()
 	1()`)
@@ -335,10 +359,14 @@ wasm stack trace:
 	// for the subsequent calls to avoid additional allocations on each call.
 	require.Equal(t, uint64(0), ce.stackBasePointerInBytes)
 	require.Equal(t, uint64(0), ce.stackPointer)
-	require.Equal(t, uint64(0), ce.callFrameStackPointer)
 	require.Equal(t, uintptr(0), ce.moduleInstanceAddress)
-	require.Equal(t, beforeRecoverValueStack, ce.valueStack)
-	require.Equal(t, beforeRecoverCallFrameStack, ce.callFrameStack)
+	require.Equal(t, beforeRecoverStack, ce.stack)
+
+	// Keep f1, f2, and f3 alive until we reach here, as we access these functions from the uint64 raw pointers in the stack.
+	// In practice, they are guaranteed to be alive as they are held by moduleContext.
+	runtime.KeepAlive(f1)
+	runtime.KeepAlive(f2)
+	runtime.KeepAlive(f3)
 }
 
 func newMockFunctionDefinition(name string) api.FunctionDefinition {
@@ -364,3 +392,125 @@ func (f *mockFunctionDefinition) ParamTypes() []wasm.ValueType {
 func (f *mockFunctionDefinition) ResultTypes() []wasm.ValueType {
 	return []wasm.ValueType{}
 }
+
+func TestCallEngine_initializeStack(t *testing.T) {
+	const i32 = wasm.ValueTypeI32
+	const stackSize = 10
+	const initialVal = ^uint64(0)
+	tests := []struct {
+		name            string
+		funcType        *wasm.FunctionType
+		args            []uint64
+		expStackPointer uint64
+		expStack        [stackSize]uint64
+	}{
+		{
+			name:            "no param/result",
+			funcType:        &wasm.FunctionType{},
+			expStackPointer: callFrameDataSizeInUint64,
+			expStack: [stackSize]uint64{
+				0, 0, 0, // zeroed call frame
+				initialVal, initialVal, initialVal, initialVal, initialVal, initialVal, initialVal,
+			},
+		},
+		{
+			name: "no result",
+			funcType: &wasm.FunctionType{
+				Params:           []wasm.ValueType{i32, i32},
+				ParamNumInUint64: 2,
+			},
+			args:            []uint64{0xdeadbeaf, 0xdeadbeaf},
+			expStackPointer: callFrameDataSizeInUint64 + 2,
+			expStack: [stackSize]uint64{
+				0xdeadbeaf, 0xdeadbeaf, // arguments
+				0, 0, 0, // zeroed call frame
+				initialVal, initialVal, initialVal, initialVal, initialVal,
+			},
+		},
+		{
+			name: "no param",
+			funcType: &wasm.FunctionType{
+				Results:           []wasm.ValueType{i32, i32, i32},
+				ResultNumInUint64: 3,
+			},
+			expStackPointer: callFrameDataSizeInUint64 + 3,
+			expStack: [stackSize]uint64{
+				initialVal, initialVal, initialVal, // reserved slots for results
+				0, 0, 0, // zeroed call frame
+				initialVal, initialVal, initialVal, initialVal,
+			},
+		},
+		{
+			name: "params > results",
+			funcType: &wasm.FunctionType{
+				Params:            []wasm.ValueType{i32, i32, i32, i32, i32},
+				ParamNumInUint64:  5,
+				Results:           []wasm.ValueType{i32, i32, i32},
+				ResultNumInUint64: 3,
+			},
+			args:            []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf},
+			expStackPointer: callFrameDataSizeInUint64 + 5,
+			expStack: [stackSize]uint64{
+				0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf,
+				0, 0, 0, // zeroed call frame
+				initialVal, initialVal,
+			},
+		},
+		{
+			name: "params == results",
+			funcType: &wasm.FunctionType{
+				Params:            []wasm.ValueType{i32, i32, i32, i32, i32},
+				ParamNumInUint64:  5,
+				Results:           []wasm.ValueType{i32, i32, i32, i32, i32},
+				ResultNumInUint64: 5,
+			},
+			args:            []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf},
+			expStackPointer: callFrameDataSizeInUint64 + 5,
+			expStack: [stackSize]uint64{
+				0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf,
+				0, 0, 0, // zeroed call frame
+				initialVal, initialVal,
+			},
+		},
+		{
+			name: "params < results",
+			funcType: &wasm.FunctionType{
+				Params:            []wasm.ValueType{i32, i32, i32},
+				ParamNumInUint64:  3,
+				Results:           []wasm.ValueType{i32, i32, i32, i32, i32},
+				ResultNumInUint64: 5,
+			},
+			args:            []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf},
+			expStackPointer: callFrameDataSizeInUint64 + 5,
+			expStack: [stackSize]uint64{
+				0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf,
+				initialVal, initialVal, // reserved for results
+				0, 0, 0, // zeroed call frame
+				initialVal, initialVal,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			initialStack := make([]uint64, stackSize)
+			for i := range initialStack {
+				initialStack[i] = initialVal
+			}
+			ce := &callEngine{stack: initialStack}
+			ce.initializeStack(tc.funcType, tc.args)
+			require.Equal(t, tc.expStackPointer, ce.stackPointer)
+			require.Equal(t, tc.expStack[:], ce.stack)
+		})
+	}
+}
+
+func Test_callFrameOffset(t *testing.T) {
+	require.Equal(t, 1, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 0, ResultNumInUint64: 1}))
+	require.Equal(t, 10, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 5, ResultNumInUint64: 10}))
+	require.Equal(t, 100, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 50, ResultNumInUint64: 100}))
+	require.Equal(t, 1, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 1, ResultNumInUint64: 0}))
+	require.Equal(t, 10, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 10, ResultNumInUint64: 5}))
+	require.Equal(t, 100, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 100, ResultNumInUint64: 50}))
+}
diff --git a/internal/engine/compiler/impl_amd64_test.go b/internal/engine/compiler/impl_amd64_test.go
index 75a93026..218022da 100644
--- a/internal/engine/compiler/impl_amd64_test.go
+++ b/internal/engine/compiler/impl_amd64_test.go
@@ -12,7 +12,7 @@ import (
 )
 
 // TestAmd64Compiler_indirectCallWithTargetOnCallingConvReg is the regression test for #526.
-// In short, the offset register for call_indirect might be the same as amd64CallingConventionModuleInstanceAddressRegister
+// In short, the offset register for call_indirect might be the same as amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister
 // and that must not be a failure.
 func TestAmd64Compiler_indirectCallWithTargetOnCallingConvReg(t *testing.T) {
 	env := newCompilerEnvironment()
@@ -54,7 +54,7 @@ func TestAmd64Compiler_indirectCallWithTargetOnCallingConvReg(t *testing.T) {
 	require.NoError(t, err)
 
 	// Place the offset into the calling-convention reserved register.
-	offsetLoc := compiler.pushRuntimeValueLocationOnRegister(amd64CallingConventionModuleInstanceAddressRegister,
+	offsetLoc := compiler.pushRuntimeValueLocationOnRegister(amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister,
 		runtimeValueTypeI32)
 	compiler.assembler.CompileConstToRegister(amd64.MOVQ, 0, offsetLoc.register)
 
@@ -170,7 +170,7 @@ func TestAmd64Compiler_compile_Mul_Div_Rem(t *testing.T) {
 						require.NoError(t, err)
 
 						require.Equal(t, registerTypeGeneralPurpose, compiler.runtimeValueLocationStack().peek().getRegisterType())
-						require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+						requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 						require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 						// At this point, the previous value on the DX register is saved to the stack.
 						require.True(t, prevOnDX.onStack())
@@ -296,7 +296,7 @@ func TestAmd64Compiler_compile_Mul_Div_Rem(t *testing.T) {
 						require.NoError(t, err)
 
 						require.Equal(t, registerTypeGeneralPurpose, compiler.runtimeValueLocationStack().peek().getRegisterType())
-						require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp)
+						requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 						require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 						// At this point, the previous value on the DX register is saved to the stack.
 						require.True(t, prevOnDX.onStack())
diff --git a/internal/engine/compiler/impl_vec_amd64_test.go b/internal/engine/compiler/impl_vec_amd64_test.go
index f842e283..6e8b0eec 100644
--- a/internal/engine/compiler/impl_vec_amd64_test.go
+++ b/internal/engine/compiler/impl_vec_amd64_test.go
@@ -142,7 +142,7 @@ func TestAmd64Compiler_compileV128ShrI64x2SignedImpl(t *testing.T) {
 			},
 			verifyFn: func(t *testing.T, env *compilerEnv) {
 				// at the bottom of stack, the previous value on the CX register must be saved.
-				actual := env.stack()[0]
+				actual := env.stack()[callFrameDataSizeInUint64]
 				require.Equal(t, uint64(100), actual)
 			},
 		},
@@ -175,7 +175,7 @@ func TestAmd64Compiler_compileV128ShrI64x2SignedImpl(t *testing.T) {
 			},
 			verifyFn: func(t *testing.T, env *compilerEnv) {
 				// at the bottom of stack, the previous value on the CX register must be saved.
-				actual := env.stack()[0]
+				actual := env.stack()[callFrameDataSizeInUint64]
 				require.Equal(t, uint64(100), actual)
 			},
 		},
diff --git a/internal/engine/compiler/impl_vec_arm64_test.go b/internal/engine/compiler/impl_vec_arm64_test.go
index 7a6b8c02..4ceee2a6 100644
--- a/internal/engine/compiler/impl_vec_arm64_test.go
+++ b/internal/engine/compiler/impl_vec_arm64_test.go
@@ -122,7 +122,7 @@ func TestArm64Compiler_V128Shuffle_combinations(t *testing.T) {
 			},
 			verifyFnc: func(t *testing.T, env *compilerEnv) {
 				// Previous value on the V3 register must be saved onto the stack.
-				lo, hi := env.stack()[0], env.stack()[1]
+				lo, hi := env.stack()[callFrameDataSizeInUint64], env.stack()[callFrameDataSizeInUint64+1]
 				require.Equal(t, uint64(1234), lo)
 				require.Equal(t, uint64(5678), hi)
 			},
@@ -143,7 +143,7 @@ func TestArm64Compiler_V128Shuffle_combinations(t *testing.T) {
 			},
 			verifyFnc: func(t *testing.T, env *compilerEnv) {
 				// Previous value on the V3 register must be saved onto the stack.
-				lo, hi := env.stack()[0], env.stack()[1]
+				lo, hi := env.stack()[callFrameDataSizeInUint64], env.stack()[callFrameDataSizeInUint64+1]
 				require.Equal(t, uint64(1234), lo)
 				require.Equal(t, uint64(5678), hi)
 			},
@@ -195,7 +195,7 @@ func TestArm64Compiler_V128Shuffle_combinations(t *testing.T) {
 			err = compiler.compileV128Shuffle(&wazeroir.OperationV128Shuffle{Lanes: lanes})
 			require.NoError(t, err)
 
-			require.Equal(t, tc.expStackPointerAfterShuffle, compiler.runtimeValueLocationStack().sp)
+			requireRuntimeLocationStackPointerEqual(t, tc.expStackPointerAfterShuffle, compiler)
 			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
 
 			err = compiler.compileReturnFunction()
diff --git a/internal/engine/interpreter/interpreter_test.go b/internal/engine/interpreter/interpreter_test.go
index 383d809f..0fc8e457 100644
--- a/internal/engine/interpreter/interpreter_test.go
+++ b/internal/engine/interpreter/interpreter_test.go
@@ -11,7 +11,6 @@ import (
 
 	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/experimental/logging"
-	"github.com/tetratelabs/wazero/internal/buildoptions"
 	"github.com/tetratelabs/wazero/internal/testing/enginetest"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
@@ -45,7 +44,8 @@ func TestInterpreter_CallEngine_PushFrame(t *testing.T) {
 }
 
 func TestInterpreter_CallEngine_PushFrame_StackOverflow(t *testing.T) {
-	defer func() { callStackCeiling = buildoptions.CallStackCeiling }()
+	saved := callStackCeiling
+	defer func() { callStackCeiling = saved }()
 
 	callStackCeiling = 3
 
@@ -60,7 +60,7 @@ func TestInterpreter_CallEngine_PushFrame_StackOverflow(t *testing.T) {
 	vm.pushFrame(f3)
 
 	captured := require.CapturePanic(func() { vm.pushFrame(f4) })
-	require.EqualError(t, captured, "callstack overflow")
+	require.EqualError(t, captured, "stack overflow")
 }
 
 // et is used for tests defined in the enginetest package.
diff --git a/internal/integration_test/filecache/filecache_test.go b/internal/integration_test/filecache/filecache_test.go
index 976123af..591cfb96 100644
--- a/internal/integration_test/filecache/filecache_test.go
+++ b/internal/integration_test/filecache/filecache_test.go
@@ -41,7 +41,7 @@ func TestSpecTestCompilerCache(t *testing.T) {
 		// the subsequent execution of this test will enter the following "else" block.
 		var exp []string
 		buf := bytes.NewBuffer(nil)
-		for i := 0; i < 5; i++ {
+		for i := 0; i < 2; i++ {
 			cmd := exec.Command(testExecutable)
 			cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", cachePathKey, cacheDir))
 			cmd.Stdout = buf
@@ -51,7 +51,7 @@ func TestSpecTestCompilerCache(t *testing.T) {
 			exp = append(exp, "PASS\n")
 		}
 
-		// Ensures that the tests actually run 5 times.
+		// Ensures that the tests actually run 2 times.
 		require.Equal(t, strings.Join(exp, ""), buf.String())
 
 		// Check the number of cache files is greater than zero.
diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go
index 6baa8ae4..7d6df55c 100644
--- a/internal/integration_test/spectest/spectest.go
+++ b/internal/integration_test/spectest/spectest.go
@@ -326,7 +326,7 @@ var spectestWasm []byte
 //
 // See https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/imports.wast
 // See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25
-func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace) {
+func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace, enabledFeatures wasm.Features) {
 	mod, err := binaryformat.DecodeModule(spectestWasm, wasm.Features20220419, wasm.MemorySizer)
 	require.NoError(t, err)
 
@@ -365,6 +365,10 @@ func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *was
 
 	maybeSetMemoryCap(mod)
 	mod.BuildFunctionDefinitions()
+
+	err = mod.Validate(enabledFeatures)
+	require.NoError(t, err)
+
 	err = s.Engine.CompileModule(ctx, mod)
 	require.NoError(t, err)
 
@@ -408,7 +412,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(
 
 		t.Run(wastName, func(t *testing.T) {
 			s, ns := wasm.NewStore(enabledFeatures, newEngine(ctx, enabledFeatures))
-			addSpectestModule(t, ctx, s, ns)
+			addSpectestModule(t, ctx, s, ns, enabledFeatures)
 
 			var lastInstantiatedModuleName string
 			for _, c := range base.Commands {
@@ -536,7 +540,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(
 								msg += " in module " + c.Action.Module
 							}
 							_, _, err := callFunction(ns, ctx, moduleName, c.Action.Field, args...)
-							require.ErrorIs(t, err, wasmruntime.ErrRuntimeCallStackOverflow, msg)
+							require.ErrorIs(t, err, wasmruntime.ErrRuntimeStackOverflow, msg)
 						default:
 							t.Fatalf("unsupported action type type: %v", c)
 						}
diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go
index de243850..7bfa942a 100644
--- a/internal/testing/enginetest/enginetest.go
+++ b/internal/testing/enginetest/enginetest.go
@@ -587,7 +587,7 @@ func RunTestModuleEngine_Memory(t *testing.T, et EngineTester) {
 		},
 	}
 	m.BuildFunctionDefinitions()
-	// Compile the Wasm into wazeroir
+
 	err := e.CompileModule(testCtx, m)
 	require.NoError(t, err)
 
diff --git a/internal/wasmdebug/debug_test.go b/internal/wasmdebug/debug_test.go
index f252f299..c543a27f 100644
--- a/internal/wasmdebug/debug_test.go
+++ b/internal/wasmdebug/debug_test.go
@@ -118,13 +118,13 @@ wasm stack trace:
 			build: func(builder ErrorBuilder) error {
 				builder.AddFrame("wasi_snapshot_preview1.fd_write", i32i32i32i32, []api.ValueType{i32})
 				builder.AddFrame("x.y", nil, nil)
-				return builder.FromRecovered(wasmruntime.ErrRuntimeCallStackOverflow)
+				return builder.FromRecovered(wasmruntime.ErrRuntimeStackOverflow)
 			},
-			expectedErr: `wasm error: callstack overflow
+			expectedErr: `wasm error: stack overflow
 wasm stack trace:
 	wasi_snapshot_preview1.fd_write(i32,i32,i32,i32) i32
 	x.y()`,
-			expectUnwrap: wasmruntime.ErrRuntimeCallStackOverflow,
+			expectUnwrap: wasmruntime.ErrRuntimeStackOverflow,
 		},
 	}
 
diff --git a/internal/wazeroir/compiler_test.go b/internal/wazeroir/compiler_test.go
index 0097c48e..5aa32f9a 100644
--- a/internal/wazeroir/compiler_test.go
+++ b/internal/wazeroir/compiler_test.go
@@ -232,7 +232,7 @@ func TestCompile(t *testing.T) {
 			for _, tp := range tc.module.TypeSection {
 				tp.CacheNumInUint64()
 			}
-			res, err := CompileFunctions(ctx, enabledFeatures, tc.module)
+			res, err := CompileFunctions(ctx, enabledFeatures, 0, tc.module)
 			require.NoError(t, err)
 
 			fn := res[0]
@@ -372,7 +372,7 @@ func TestCompile_BulkMemoryOperations(t *testing.T) {
 		TableTypes:       []wasm.RefType{},
 	}
 
-	res, err := CompileFunctions(ctx, wasm.FeatureBulkMemoryOperations, module)
+	res, err := CompileFunctions(ctx, wasm.FeatureBulkMemoryOperations, 0, module)
 	require.NoError(t, err)
 	require.Equal(t, expected, res[0])
 }
@@ -668,7 +668,7 @@ func TestCompile_MultiValue(t *testing.T) {
 			for _, tp := range tc.module.TypeSection {
 				tp.CacheNumInUint64()
 			}
-			res, err := CompileFunctions(ctx, enabledFeatures, tc.module)
+			res, err := CompileFunctions(ctx, enabledFeatures, 0, tc.module)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, res[0])
 		})
@@ -706,7 +706,7 @@ func TestCompile_NonTrappingFloatToIntConversion(t *testing.T) {
 	for _, tp := range module.TypeSection {
 		tp.CacheNumInUint64()
 	}
-	res, err := CompileFunctions(ctx, wasm.FeatureNonTrappingFloatToIntConversion, module)
+	res, err := CompileFunctions(ctx, wasm.FeatureNonTrappingFloatToIntConversion, 0, module)
 	require.NoError(t, err)
 	require.Equal(t, expected, res[0])
 }
@@ -737,7 +737,7 @@ func TestCompile_SignExtensionOps(t *testing.T) {
 	for _, tp := range module.TypeSection {
 		tp.CacheNumInUint64()
 	}
-	res, err := CompileFunctions(ctx, wasm.FeatureSignExtensionOps, module)
+	res, err := CompileFunctions(ctx, wasm.FeatureSignExtensionOps, 0, module)
 	require.NoError(t, err)
 	require.Equal(t, expected, res[0])
 }
@@ -746,7 +746,7 @@ func requireCompilationResult(t *testing.T, enabledFeatures wasm.Features, expec
 	if enabledFeatures == 0 {
 		enabledFeatures = wasm.Features20220419
 	}
-	res, err := CompileFunctions(ctx, enabledFeatures, module)
+	res, err := CompileFunctions(ctx, enabledFeatures, 0, module)
 	require.NoError(t, err)
 	require.Equal(t, expected, res[0])
 }
@@ -785,7 +785,7 @@ func TestCompile_CallIndirectNonZeroTableIndex(t *testing.T) {
 		Types: []*wasm.FunctionType{v_v, v_v, v_v},
 	}
 
-	res, err := CompileFunctions(ctx, wasm.FeatureBulkMemoryOperations, module)
+	res, err := CompileFunctions(ctx, wasm.FeatureBulkMemoryOperations, 0, module)
 	require.NoError(t, err)
 	require.Equal(t, expected, res[0])
 }
@@ -875,7 +875,7 @@ func TestCompile_Refs(t *testing.T) {
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{{Body: tc.body}},
 			}
-			res, err := CompileFunctions(ctx, wasm.Features20220419, module)
+			res, err := CompileFunctions(ctx, wasm.Features20220419, 0, module)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, res[0].Operations)
 		})
@@ -944,7 +944,7 @@ func TestCompile_TableGetOrSet(t *testing.T) {
 				CodeSection:     []*wasm.Code{{Body: tc.body}},
 				TableSection:    []*wasm.Table{{}},
 			}
-			res, err := CompileFunctions(ctx, wasm.Features20220419, module)
+			res, err := CompileFunctions(ctx, wasm.Features20220419, 0, module)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, res[0].Operations)
 		})
@@ -1013,7 +1013,7 @@ func TestCompile_TableGrowFillSize(t *testing.T) {
 				CodeSection:     []*wasm.Code{{Body: tc.body}},
 				TableSection:    []*wasm.Table{{}},
 			}
-			res, err := CompileFunctions(ctx, wasm.Features20220419, module)
+			res, err := CompileFunctions(ctx, wasm.Features20220419, 0, module)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, res[0].Operations)
 			require.True(t, res[0].HasTable)
@@ -1221,7 +1221,7 @@ func TestCompile_Locals(t *testing.T) {
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
-			res, err := CompileFunctions(ctx, wasm.Features20220419, tc.mod)
+			res, err := CompileFunctions(ctx, wasm.Features20220419, 0, tc.mod)
 			require.NoError(t, err)
 			msg := fmt.Sprintf("\nhave:\n\t%s\nwant:\n\t%s", Format(res[0].Operations), Format(tc.expected))
 			require.Equal(t, tc.expected, res[0].Operations, msg)
@@ -2567,7 +2567,7 @@ func TestCompile_Vec(t *testing.T) {
 				MemorySection:   &wasm.Memory{},
 				CodeSection:     []*wasm.Code{{Body: tc.body}},
 			}
-			res, err := CompileFunctions(ctx, wasm.Features20220419, module)
+			res, err := CompileFunctions(ctx, wasm.Features20220419, 0, module)
 			require.NoError(t, err)
 
 			var actual Operation
@@ -2644,7 +2644,7 @@ func TestCompile_unreachable_Br_BrIf_BrTable(t *testing.T) {
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
-			res, err := CompileFunctions(ctx, wasm.Features20220419, tc.mod)
+			res, err := CompileFunctions(ctx, wasm.Features20220419, 0, tc.mod)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, res[0].Operations)
 		})
@@ -2684,7 +2684,7 @@ func TestCompile_drop_vectors(t *testing.T) {
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
-			res, err := CompileFunctions(ctx, wasm.Features20220419, tc.mod)
+			res, err := CompileFunctions(ctx, wasm.Features20220419, 0, tc.mod)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, res[0].Operations)
 		})
@@ -2754,9 +2754,179 @@ func TestCompile_select_vectors(t *testing.T) {
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
-			res, err := CompileFunctions(ctx, wasm.Features20220419, tc.mod)
+			res, err := CompileFunctions(ctx, wasm.Features20220419, 0, tc.mod)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, res[0].Operations)
 		})
 	}
 }
+
+func TestCompiler_initializeStack(t *testing.T) {
+	const v128 = wasm.ValueTypeV128
+	tests := []struct {
+		name                               string
+		sig                                *wasm.FunctionType
+		functionLocalTypes                 []wasm.ValueType
+		callFrameStackSizeInUint64         int
+		expLocalIndexToStackHeightInUint64 map[uint32]int
+	}{
+		{
+			name: "no function local, args>results",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{i32, f32},
+				Results:           []wasm.ValueType{i32},
+				ParamNumInUint64:  2,
+				ResultNumInUint64: 1,
+			},
+			expLocalIndexToStackHeightInUint64: map[uint32]int{
+				0: 0,
+				1: 1,
+			},
+		},
+		{
+			name: "no function local, args=results",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{i32},
+				Results:           []wasm.ValueType{i32},
+				ParamNumInUint64:  1,
+				ResultNumInUint64: 1,
+			},
+			expLocalIndexToStackHeightInUint64: map[uint32]int{
+				0: 0,
+			},
+		},
+		{
+			name: "no function local, args>results, with vector",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{i32, v128, f32},
+				Results:           []wasm.ValueType{i32},
+				ParamNumInUint64:  4,
+				ResultNumInUint64: 1,
+			},
+			expLocalIndexToStackHeightInUint64: map[uint32]int{
+				0: 0,
+				1: 1,
+				2: 3,
+			},
+		},
+		{
+			name: "no function local, args<results",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{},
+				Results:           []wasm.ValueType{i32},
+				ParamNumInUint64:  0,
+				ResultNumInUint64: 1,
+			},
+			callFrameStackSizeInUint64:         4,
+			expLocalIndexToStackHeightInUint64: map[uint32]int{},
+		},
+		{
+			name: "no function local, args<results",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{i32},
+				Results:           []wasm.ValueType{i32, f32},
+				ParamNumInUint64:  1,
+				ResultNumInUint64: 2,
+			},
+			callFrameStackSizeInUint64:         4,
+			expLocalIndexToStackHeightInUint64: map[uint32]int{0: 0},
+		},
+		{
+			name: "no function local, args<results, with vector",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{i32},
+				Results:           []wasm.ValueType{i32, v128, f32},
+				ParamNumInUint64:  1,
+				ResultNumInUint64: 4,
+			},
+			callFrameStackSizeInUint64:         4,
+			expLocalIndexToStackHeightInUint64: map[uint32]int{0: 0},
+		},
+
+		// With function locals
+		{
+			name: "function locals, args>results",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{i32, f32},
+				Results:           []wasm.ValueType{i32},
+				ParamNumInUint64:  2,
+				ResultNumInUint64: 1,
+			},
+			functionLocalTypes:         []wasm.ValueType{f64},
+			callFrameStackSizeInUint64: 4,
+			// [i32, f32, callframe.0, callframe.1, callframe.2, callframe.3, f64]
+			expLocalIndexToStackHeightInUint64: map[uint32]int{
+				0: 0,
+				1: 1,
+				// Function local comes after call frame.
+				2: 6,
+			},
+		},
+		{
+			name: "function locals, args>results, with vector",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{i32, v128, f32},
+				Results:           []wasm.ValueType{i32},
+				ParamNumInUint64:  4,
+				ResultNumInUint64: 1,
+			},
+			functionLocalTypes:         []wasm.ValueType{v128, v128},
+			callFrameStackSizeInUint64: 4,
+			// [i32, v128.lo, v128.hi, f32, callframe.0, callframe.1, callframe.2, callframe.3, v128.lo, v128.hi, v128.lo, v128.hi]
+			expLocalIndexToStackHeightInUint64: map[uint32]int{
+				0: 0,
+				1: 1,
+				2: 3,
+				// Function local comes after call frame.
+				3: 8,
+				4: 10,
+			},
+		},
+		{
+			name: "function locals, args<results",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{i32},
+				Results:           []wasm.ValueType{i32, i32, i32, i32},
+				ParamNumInUint64:  1,
+				ResultNumInUint64: 4,
+			},
+			functionLocalTypes:         []wasm.ValueType{f64},
+			callFrameStackSizeInUint64: 4,
+			// [i32, _, _, _, callframe.0, callframe.1, callframe.2, callframe.3, f64]
+			expLocalIndexToStackHeightInUint64: map[uint32]int{
+				0: 0,
+				1: 8,
+			},
+		},
+		{
+			name: "function locals, args<results with vector",
+			sig: &wasm.FunctionType{
+				Params:            []wasm.ValueType{v128, f64},
+				Results:           []wasm.ValueType{v128, i32, i32, v128},
+				ParamNumInUint64:  3,
+				ResultNumInUint64: 6,
+			},
+			functionLocalTypes:         []wasm.ValueType{f64},
+			callFrameStackSizeInUint64: 4,
+			// [v128.lo, v128.hi, f64, _, _, _, callframe.0, callframe.1, callframe.2, callframe.3, f64]
+			expLocalIndexToStackHeightInUint64: map[uint32]int{
+				0: 0,
+				1: 2,
+				2: 10,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			c := &compiler{sig: tc.sig, localTypes: tc.functionLocalTypes,
+				callFrameStackSizeInUint64: tc.callFrameStackSizeInUint64,
+			}
+
+			c.initializeStack()
+			require.Equal(t, tc.expLocalIndexToStackHeightInUint64, c.localIndexToStackHeightInUint64)
+		})
+
+	}
+}