Repository: tetratelabs/wazero
Pull request #335 (2022-03-05 05:11:58)

Flattens FunctionInstance FunctionType field
This flattens `FunctionInstance.FunctionType` into `Type` and `TypeID`
fields, where the former is known prior to instantiation. This helps
pave a way for integration between Wasm declared and host defined
modules.

This also clarifies that `FunctionInstance.String` was used as a lookup
key, by renaming and caching its impl. While at it, I renamed "null" to
"v" in its output as I had been using v for void noticing others were
doing that also. Moreover, null is easy to misunderstand as a bug.


Patch:
diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml
index 038b777a..ac08f427 100644
--- a/.github/workflows/commit.yaml
+++ b/.github/workflows/commit.yaml
@@ -37,16 +37,10 @@ jobs:
             ~/go/bin
           key: check-${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum', 'Makefile') }}
 
-      - name: Lint (arm64)
-        run: make lint golangci_lint_goarch=arm64
-
-      - name: Lint (amd64)
-        run: make lint golangci_lint_goarch=amd64
+      - run: make check
 
       - run: make build.spectest
 
-      - run: make check
-
   test_amd64:
     name: amd64, ${{ matrix.os }}, Go-${{ matrix.go-version }}
     runs-on: ${{ matrix.os }}
diff --git a/Makefile b/Makefile
index bd777c5a..be81ea16 100644
--- a/Makefile
+++ b/Makefile
@@ -64,6 +64,8 @@ format:
 
 .PHONY: check
 check:
+	@$(MAKE) lint golangci_lint_goarch=arm64
+	@$(MAKE) lint golangci_lint_goarch=amd64
 	@$(MAKE) format
 	@go mod tidy
 	@if [ ! -z "`git status -s`" ]; then \
diff --git a/internal/wasi/wasi.go b/internal/wasi/wasi.go
index efbaf6b9..2e75635f 100644
--- a/internal/wasi/wasi.go
+++ b/internal/wasi/wasi.go
@@ -931,7 +931,7 @@ type wasiAPI struct {
 
 // SnapshotPreview1Functions returns all go functions that implement SnapshotPreview1.
 // These should be exported in the module named wasi.ModuleSnapshotPreview1.
-// See internalwasm.NewGoFunc
+// See internalwasm.newGoFunc
 func SnapshotPreview1Functions(opts ...Option) (nameToGoFunc map[string]interface{}) {
 	a := NewAPI(opts...)
 	// Note: these are ordered per spec for consistency even if the resulting map can't guarantee that.
diff --git a/internal/wasm/binary/types.go b/internal/wasm/binary/types.go
index 49523a9e..474bbcaa 100644
--- a/internal/wasm/binary/types.go
+++ b/internal/wasm/binary/types.go
@@ -58,7 +58,7 @@ func decodeGlobalType(r *bytes.Reader, features wasm.Features) (*wasm.GlobalType
 
 var nullary = []byte{0x60, 0, 0}
 
-// encodedOneParam is a cache of FunctionType.encode values for param length 1 and result length 0
+// encodedOneParam is a cache of internalwasm.FunctionType values for param length 1 and result length 0
 var encodedOneParam = map[wasm.ValueType][]byte{
 	wasm.ValueTypeI32: {0x60, 1, wasm.ValueTypeI32, 0},
 	wasm.ValueTypeI64: {0x60, 1, wasm.ValueTypeI64, 0},
@@ -66,7 +66,7 @@ var encodedOneParam = map[wasm.ValueType][]byte{
 	wasm.ValueTypeF64: {0x60, 1, wasm.ValueTypeF64, 0},
 }
 
-// encodedOneResult is a cache of FunctionType.encode values for param length 0 and result length 1
+// encodedOneResult is a cache of internalwasm.FunctionType values for param length 0 and result length 1
 var encodedOneResult = map[wasm.ValueType][]byte{
 	wasm.ValueTypeI32: {0x60, 0, 1, wasm.ValueTypeI32},
 	wasm.ValueTypeI64: {0x60, 0, 1, wasm.ValueTypeI64},
@@ -74,7 +74,7 @@ var encodedOneResult = map[wasm.ValueType][]byte{
 	wasm.ValueTypeF64: {0x60, 0, 1, wasm.ValueTypeF64},
 }
 
-// encodeFunctionType returns the wasm.FunctionType encoded in WebAssembly 1.0 (20191205) Binary Format.
+// encodeFunctionType returns the internalwasm.FunctionType encoded in WebAssembly 1.0 (20191205) Binary Format.
 //
 // Note: Function types are encoded by the byte 0x60 followed by the respective vectors of parameter and result types.
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-types%E2%91%A4
diff --git a/internal/wasm/gofunc.go b/internal/wasm/gofunc.go
index ca945ab7..4e9a517d 100644
--- a/internal/wasm/gofunc.go
+++ b/internal/wasm/gofunc.go
@@ -2,6 +2,7 @@ package internalwasm
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"reflect"
 
@@ -24,20 +25,18 @@ const (
 	FunctionKindGoModuleContext
 )
 
-// GoFunc binds a WebAssembly 1.0 (20191205) Type Use to a Go func signature.
-type GoFunc struct {
-	wasmFunctionName string
+// goFunc binds a WebAssembly 1.0 (20191205) Type Use to a Go func signature.
+type goFunc struct {
 	// functionKind is never FunctionKindWasm
 	functionKind FunctionKind
 	functionType *FunctionType
 	goFunc       *reflect.Value
 }
 
-func NewGoFunc(wasmFunctionName string, goFunc interface{}) (hf *GoFunc, err error) {
-	hf = &GoFunc{wasmFunctionName: wasmFunctionName}
-	fn := reflect.ValueOf(goFunc)
-	hf.goFunc = &fn
-	hf.functionKind, hf.functionType, _, err = GetFunctionType(hf.wasmFunctionName, hf.goFunc, false)
+func newGoFunc(i interface{}) (hf *goFunc, err error) {
+	fn := reflect.ValueOf(i)
+	hf = &goFunc{goFunc: &fn}
+	hf.functionKind, hf.functionType, _, err = getFunctionType(hf.goFunc, false)
 	return
 }
 
@@ -63,13 +62,14 @@ func GetHostFunctionCallContextValue(fk FunctionKind, ctx *ModuleContext) *refle
 	return nil
 }
 
-// GetFunctionType returns the function type corresponding to the function signature or errs if invalid.
-func GetFunctionType(name string, fn *reflect.Value, allowErrorResult bool) (fk FunctionKind, ft *FunctionType, hasErrorResult bool, err error) {
+// getFunctionType returns the function type corresponding to the function signature or errs if invalid.
+func getFunctionType(fn *reflect.Value, allowErrorResult bool) (fk FunctionKind, ft *FunctionType, hasErrorResult bool, err error) {
+	p := fn.Type()
+
 	if fn.Kind() != reflect.Func {
-		err = fmt.Errorf("%s is a %s, but should be a Func", name, fn.Kind().String())
+		err = fmt.Errorf("kind != func: %s", fn.Kind().String())
 		return
 	}
-	p := fn.Type()
 
 	pOffset := 0
 	pCount := p.NumIn()
@@ -89,7 +89,7 @@ func GetFunctionType(name string, fn *reflect.Value, allowErrorResult bool) (fk
 
 	rCount := p.NumOut()
 	if (allowErrorResult && rCount > 2) || (!allowErrorResult && rCount > 1) {
-		err = fmt.Errorf("%s has more than one result", name)
+		err = errors.New("multiple results are unsupported")
 		return
 	}
 
@@ -119,9 +119,9 @@ func GetFunctionType(name string, fn *reflect.Value, allowErrorResult bool) (fk
 		}
 
 		if arg0Type != nil {
-			err = fmt.Errorf("%s param[%d] is a %s, which may be defined only once as param[0]", name, i+pOffset, arg0Type)
+			err = fmt.Errorf("param[%d] is a %s, which may be defined only once as param[0]", i+pOffset, arg0Type)
 		} else {
-			err = fmt.Errorf("%s param[%d] is unsupported: %s", name, i+pOffset, pI.Kind())
+			err = fmt.Errorf("param[%d] is unsupported: %s", i+pOffset, pI.Kind())
 		}
 		return
 	}
@@ -137,9 +137,9 @@ func GetFunctionType(name string, fn *reflect.Value, allowErrorResult bool) (fk
 	}
 
 	if result.Implements(errorType) {
-		err = fmt.Errorf("%s result[0] is an error, which is unsupported", name)
+		err = errors.New("result[0] is an error, which is unsupported")
 	} else {
-		err = fmt.Errorf("%s result[0] is unsupported: %s", name, result.Kind())
+		err = fmt.Errorf("result[0] is unsupported: %s", result.Kind())
 	}
 	return
 }
diff --git a/internal/wasm/host.go b/internal/wasm/host.go
index c54b588f..082289ba 100644
--- a/internal/wasm/host.go
+++ b/internal/wasm/host.go
@@ -105,22 +105,23 @@ func (s *Store) NewHostModule(moduleName string, nameToGoFunc map[string]interfa
 	}
 
 	for name, goFunc := range nameToGoFunc {
-		hf, err := NewGoFunc(name, goFunc)
+		hf, err := newGoFunc(goFunc)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("func[%s] %w", name, err)
 		}
 
 		f := &FunctionInstance{
-			Name:           fmt.Sprintf("%s.%s", hostModule.Name, hf.wasmFunctionName),
-			HostFunction:   hf.goFunc,
-			FunctionKind:   hf.functionKind,
-			ModuleInstance: hostModule,
+			Module: hostModule,
+			Name:   fmt.Sprintf("%s.%s", hostModule.Name, name),
+			Kind:   hf.functionKind,
+			Type:   hf.functionType,
+			GoFunc: hf.goFunc,
 		}
-		hostModule.Exports[hf.wasmFunctionName] = &ExportInstance{Type: ExternTypeFunc, Function: f}
+		hostModule.Exports[name] = &ExportInstance{Type: ExternTypeFunc, Function: f}
 		hostModule.Functions = append(hostModule.Functions, f)
 		ret.NameToFunctionInstance[name] = f
 
-		if err = s.compileHostFunction(f, hf); err != nil {
+		if err = s.compileHostFunction(f); err != nil {
 			return nil, err
 		}
 	}
@@ -129,11 +130,12 @@ func (s *Store) NewHostModule(moduleName string, nameToGoFunc map[string]interfa
 	return ret, nil
 }
 
-func (s *Store) compileHostFunction(f *FunctionInstance, hf *GoFunc) (err error) {
-	f.FunctionType, err = s.getTypeInstance(hf.functionType)
+func (s *Store) compileHostFunction(f *FunctionInstance) (err error) {
+	ti, err := s.getTypeInstance(f.Type)
 	if err != nil {
 		return err
 	}
+	f.TypeID = ti.TypeID
 	s.addFunctionInstances(f)
 
 	if err = s.engine.Compile(f); err != nil {
@@ -167,12 +169,12 @@ func (m *HostModule) String() string {
 
 // ParamTypes implements wasm.HostFunction ParamTypes
 func (f *FunctionInstance) ParamTypes() []publicwasm.ValueType {
-	return f.FunctionType.Type.Params
+	return f.Type.Params
 }
 
 // ResultTypes implements wasm.HostFunction ResultTypes
 func (f *FunctionInstance) ResultTypes() []publicwasm.ValueType {
-	return f.FunctionType.Type.Results
+	return f.Type.Results
 }
 
 // Call implements wasm.HostFunction Call
diff --git a/internal/wasm/interpreter/interpreter.go b/internal/wasm/interpreter/interpreter.go
index 0178faba..c1ce332d 100644
--- a/internal/wasm/interpreter/interpreter.go
+++ b/internal/wasm/interpreter/interpreter.go
@@ -144,7 +144,7 @@ func (e *engine) Release(f *wasm.FunctionInstance) error {
 func (e *engine) Compile(f *wasm.FunctionInstance) error {
 	funcIndex := f.Index
 	var compiled *compiledFunction
-	if f.FunctionKind == wasm.FunctionKindWasm {
+	if f.Kind == wasm.FunctionKindWasm {
 		ir, err := wazeroir.Compile(f)
 		if err != nil {
 			return fmt.Errorf("failed to compile Wasm to wazeroir: %w", err)
@@ -156,7 +156,7 @@ func (e *engine) Compile(f *wasm.FunctionInstance) error {
 		}
 	} else {
 		compiled = &compiledFunction{
-			hostFn: f.HostFunction, funcInstance: f,
+			hostFn: f.GoFunc, funcInstance: f,
 		}
 	}
 
@@ -266,13 +266,13 @@ func (e *engine) lowerIROps(f *wasm.FunctionInstance,
 				}
 			}
 		case *wazeroir.OperationCall:
-			target := f.ModuleInstance.Functions[o.FunctionIndex]
+			target := f.Module.Functions[o.FunctionIndex]
 			op.us = make([]uint64, 1)
 			op.us[0] = uint64(target.Index)
 		case *wazeroir.OperationCallIndirect:
 			op.us = make([]uint64, 2)
 			op.us[0] = uint64(o.TableIndex)
-			op.us[1] = uint64(f.ModuleInstance.Types[o.TypeIndex].TypeID)
+			op.us[1] = uint64(f.Module.Types[o.TypeIndex].TypeID)
 		case *wazeroir.OperationDrop:
 			op.rs = make([]*wazeroir.InclusiveRange, 1)
 			op.rs[0] = o.Range
@@ -448,7 +448,7 @@ func (e *engine) lowerIROps(f *wasm.FunctionInstance,
 
 // Call implements an interpreted wasm.Engine.
 func (e *engine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
-	paramSignature := f.FunctionType.Type.Params
+	paramSignature := f.Type.Params
 	paramCount := len(params)
 	if len(paramSignature) != paramCount {
 		return nil, fmt.Errorf("expected %d params, but passed %d", len(paramSignature), paramCount)
@@ -494,12 +494,12 @@ func (e *engine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params
 	for _, param := range params {
 		vm.push(param)
 	}
-	if f.FunctionKind == wasm.FunctionKindWasm {
+	if f.Kind == wasm.FunctionKindWasm {
 		vm.callNativeFunc(ctx, compiled)
 	} else {
 		vm.callHostFunc(ctx, compiled)
 	}
-	results = make([]uint64, len(f.FunctionType.Type.Results))
+	results = make([]uint64, len(f.Type.Results))
 	for i := range results {
 		results[len(results)-1-i] = vm.pop()
 	}
@@ -511,7 +511,7 @@ func (vm *callEngine) callHostFunc(ctx *wasm.ModuleContext, f *compiledFunction)
 	in := make([]reflect.Value, tp.NumIn())
 
 	wasmParamOffset := 0
-	if f.funcInstance.FunctionKind != wasm.FunctionKindGoNoContext {
+	if f.funcInstance.Kind != wasm.FunctionKindGoNoContext {
 		wasmParamOffset = 1
 	}
 	for i := len(in) - 1; i >= wasmParamOffset; i-- {
@@ -533,11 +533,11 @@ func (vm *callEngine) callHostFunc(ctx *wasm.ModuleContext, f *compiledFunction)
 
 	// A host function is invoked with the calling frame's memory, which may be different if in another module.
 	if len(vm.frames) > 0 {
-		ctx = ctx.WithMemory(vm.frames[len(vm.frames)-1].f.funcInstance.ModuleInstance.MemoryInstance)
+		ctx = ctx.WithMemory(vm.frames[len(vm.frames)-1].f.funcInstance.Module.MemoryInstance)
 	}
 
 	// Handle any special parameter zero
-	if val := wasm.GetHostFunctionCallContextValue(f.funcInstance.FunctionKind, ctx); val != nil {
+	if val := wasm.GetHostFunctionCallContextValue(f.funcInstance.Kind, ctx); val != nil {
 		in[0] = *val
 	}
 
@@ -562,7 +562,7 @@ func (vm *callEngine) callHostFunc(ctx *wasm.ModuleContext, f *compiledFunction)
 
 func (vm *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *compiledFunction) {
 	frame := &callFrame{f: f}
-	moduleInst := f.funcInstance.ModuleInstance
+	moduleInst := f.funcInstance.Module
 	memoryInst := moduleInst.MemoryInstance
 	globals := moduleInst.Globals
 	table := moduleInst.TableInstance
diff --git a/internal/wasm/jit/engine.go b/internal/wasm/jit/engine.go
index d9b3705c..e3605e80 100644
--- a/internal/wasm/jit/engine.go
+++ b/internal/wasm/jit/engine.go
@@ -332,7 +332,7 @@ func (e *engine) Release(f *wasm.FunctionInstance) error {
 
 func (e *engine) Compile(f *wasm.FunctionInstance) (err error) {
 	var compiled *compiledFunction
-	if f.FunctionKind == wasm.FunctionKindWasm {
+	if f.Kind == wasm.FunctionKindWasm {
 		compiled, err = compileWasmFunction(f)
 	} else {
 		compiled, err = compileHostFunction(f)
@@ -359,7 +359,7 @@ func (e *engine) addCompiledFunction(index wasm.FunctionIndex, compiled *compile
 }
 
 func (e *engine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
-	paramSignature := f.FunctionType.Type.Params
+	paramSignature := f.Type.Params
 	paramCount := len(params)
 	if len(paramSignature) != paramCount {
 		return nil, fmt.Errorf("expected %d params, but passed %d", len(paramSignature), paramCount)
@@ -407,15 +407,15 @@ func (e *engine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params
 		return
 	}
 
-	if f.FunctionKind == wasm.FunctionKindWasm {
+	if f.Kind == wasm.FunctionKindWasm {
 		ce.execWasmFunction(ctx, compiled)
 	} else {
-		ce.execHostFunction(f.FunctionKind, compiled.source.HostFunction, ctx)
+		ce.execHostFunction(f.Kind, compiled.source.GoFunc, ctx)
 	}
 
 	// Note the top value is the tail of the results,
 	// so we assign them in reverse order.
-	results = make([]uint64, len(f.FunctionType.Type.Results))
+	results = make([]uint64, len(f.Type.Results))
 	for i := range results {
 		results[len(results)-1-i] = ce.popValue()
 	}
@@ -582,15 +582,15 @@ jitentry:
 			fn := ce.compiledFunctions[ce.exitContext.functionCallAddress]
 			callerCompiledFunction := ce.callFrameAt(1).compiledFunction
 			// A host function is invoked with the calling frame's memory, which may be different if in another module.
-			ce.execHostFunction(fn.source.FunctionKind, fn.source.HostFunction,
-				ctx.WithMemory(callerCompiledFunction.source.ModuleInstance.MemoryInstance),
+			ce.execHostFunction(fn.source.Kind, fn.source.GoFunc,
+				ctx.WithMemory(callerCompiledFunction.source.Module.MemoryInstance),
 			)
 			goto jitentry
 		case jitCallStatusCodeCallBuiltInFunction:
 			switch ce.exitContext.functionCallAddress {
 			case builtinFunctionIndexMemoryGrow:
 				callerCompiledFunction := ce.callFrameTop().compiledFunction
-				ce.builtinFunctionMemoryGrow(callerCompiledFunction.source.ModuleInstance.MemoryInstance)
+				ce.builtinFunctionMemoryGrow(callerCompiledFunction.source.Module.MemoryInstance)
 			case builtinFunctionIndexGrowValueStack:
 				callerCompiledFunction := ce.callFrameTop().compiledFunction
 				ce.builtinFunctionGrowValueStack(callerCompiledFunction.stackPointerCeil)
@@ -629,7 +629,7 @@ func (ce *callEngine) pushCallFrame(f *compiledFunction) {
 	//
 	// That maens the next stack base poitner is calculated as follows (note stack pointer is relative to base):
 	ce.valueStackContext.stackBasePointer =
-		ce.valueStackContext.stackBasePointer + ce.valueStackContext.stackPointer - uint64(len(f.source.FunctionType.Type.Params))
+		ce.valueStackContext.stackBasePointer + ce.valueStackContext.stackPointer - uint64(len(f.source.Type.Params))
 }
 
 func (ce *callEngine) builtinFunctionGrowValueStack(stackPointerCeil uint64) {
@@ -694,8 +694,8 @@ func compileHostFunction(f *wasm.FunctionInstance) (*compiledFunction, error) {
 		return nil, err
 	}
 
-	stackPointerCeil := uint64(len(f.FunctionType.Type.Params))
-	if res := uint64(len(f.FunctionType.Type.Results)); stackPointerCeil < res {
+	stackPointerCeil := uint64(len(f.Type.Params))
+	if res := uint64(len(f.Type.Results)); stackPointerCeil < res {
 		stackPointerCeil = res
 	}
 
diff --git a/internal/wasm/jit/jit_amd64.go b/internal/wasm/jit/jit_amd64.go
index 6025bce9..27dd8f98 100644
--- a/internal/wasm/jit/jit_amd64.go
+++ b/internal/wasm/jit/jit_amd64.go
@@ -232,8 +232,8 @@ func (c *amd64Compiler) compile() (code []byte, staticData compiledFunctionStati
 }
 
 func (c *amd64Compiler) pushFunctionParams() {
-	if c.f != nil && c.f.FunctionType != nil {
-		for _, t := range c.f.FunctionType.Type.Params {
+	if c.f != nil && c.f.Type != nil {
+		for _, t := range c.f.Type.Params {
 			loc := c.locationStack.pushValueLocationOnStack()
 			switch t {
 			case wasm.ValueTypeI32, wasm.ValueTypeI64:
@@ -403,7 +403,7 @@ func (c *amd64Compiler) compileGlobalGet(o *wazeroir.OperationGlobalGet) error {
 
 	// When an integer, reuse the pointer register for the value. Otherwise, allocate a float register for it.
 	valueReg := intReg
-	wasmType := c.f.ModuleInstance.Globals[o.Index].Type.ValType
+	wasmType := c.f.Module.Globals[o.Index].Type.ValType
 	switch wasmType {
 	case wasm.ValueTypeF32, wasm.ValueTypeF64:
 		valueReg, err = c.allocateRegister(generalPurposeRegisterTypeFloat)
@@ -929,18 +929,18 @@ func (c *amd64Compiler) compileCall(o *wazeroir.OperationCall) error {
 		return err
 	}
 
-	target := c.f.ModuleInstance.Functions[o.FunctionIndex]
-	if err := c.callFunctionFromConst(target.Index, target.FunctionType.Type); err != nil {
+	target := c.f.Module.Functions[o.FunctionIndex]
+	if err := c.callFunctionFromConst(target.Index, target.Type); err != nil {
 		return err
 	}
 
 	// We consumed the function parameters from the stack after call.
-	for i := 0; i < len(target.FunctionType.Type.Params); i++ {
+	for i := 0; i < len(target.Type.Params); i++ {
 		c.locationStack.pop()
 	}
 
 	// Also, the function results were pushed by the call.
-	for _, t := range target.FunctionType.Type.Results {
+	for _, t := range target.Type.Results {
 		loc := c.locationStack.pushValueLocationOnStack()
 		switch t {
 		case wasm.ValueTypeI32, wasm.ValueTypeI64:
@@ -1001,14 +1001,15 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 	c.addInstruction(movTableSliceAddress)
 
 	// At this point offset.register holds the address of wasm.TableElement at wasm.TableInstance[offset].
-	targetFunctionType := c.f.ModuleInstance.Types[o.TypeIndex]
+	ti := c.f.Module.Types[o.TypeIndex]
+	targetFunctionType := ti.Type
 	checkIfTypeMatch := c.newProg()
 	checkIfTypeMatch.As = x86.ACMPL // 32-bit as FunctionTypeID is in 32-bit unsigned integer.
 	checkIfTypeMatch.From.Type = obj.TYPE_MEM
 	checkIfTypeMatch.From.Reg = offset.register
 	checkIfTypeMatch.From.Offset = tableElementFunctionTypeIDOffset
 	checkIfTypeMatch.To.Type = obj.TYPE_CONST
-	checkIfTypeMatch.To.Offset = int64(targetFunctionType.TypeID)
+	checkIfTypeMatch.To.Offset = int64(ti.TypeID)
 	c.addInstruction(checkIfTypeMatch)
 
 	// Jump if the type matches.
@@ -1050,7 +1051,7 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 	readValue.From.Reg = offset.register
 	c.addInstruction(readValue)
 
-	if err := c.callFunctionFromRegister(offset.register, targetFunctionType.Type); err != nil {
+	if err := c.callFunctionFromRegister(offset.register, targetFunctionType); err != nil {
 		return nil
 	}
 
@@ -1058,12 +1059,12 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 	c.locationStack.markRegisterUnused(offset.register)
 
 	// We consumed the function parameters from the stack after call.
-	for i := 0; i < len(targetFunctionType.Type.Params); i++ {
+	for i := 0; i < len(targetFunctionType.Params); i++ {
 		c.locationStack.pop()
 	}
 
 	// Also, the function results were pushed by the call.
-	for _, t := range targetFunctionType.Type.Results {
+	for _, t := range targetFunctionType.Results {
 		loc := c.locationStack.pushValueLocationOnStack()
 		switch t {
 		case wasm.ValueTypeI32, wasm.ValueTypeI64:
@@ -5257,7 +5258,7 @@ func (c *amd64Compiler) initializeReservedStackBasePointer() {
 }
 
 func (c *amd64Compiler) initializeReservedMemoryPointer() {
-	if c.f.ModuleInstance.MemoryInstance != nil {
+	if c.f.Module.MemoryInstance != nil {
 		setupMemoryRegister := c.newProg()
 		setupMemoryRegister.As = x86.AMOVQ
 		setupMemoryRegister.To.Type = obj.TYPE_REG
@@ -5340,7 +5341,7 @@ func (c *amd64Compiler) initializeModuleContext() error {
 	readModuleInstanceAddress.To.Type = obj.TYPE_REG
 	readModuleInstanceAddress.To.Reg = moduleInstanceAddressRegister
 	readModuleInstanceAddress.From.Type = obj.TYPE_CONST
-	readModuleInstanceAddress.From.Offset = int64(uintptr(unsafe.Pointer(c.f.ModuleInstance)))
+	readModuleInstanceAddress.From.Offset = int64(uintptr(unsafe.Pointer(c.f.Module)))
 	c.addInstruction(readModuleInstanceAddress)
 
 	// If the module instance address stays the same, we could skip the entire code below.
@@ -5385,7 +5386,7 @@ func (c *amd64Compiler) initializeModuleContext() error {
 	// Note: if there's global.get or set instruction in the function, the existence of the globals
 	// is ensured by function validation at module instantiation phase, and that's why it is ok to
 	// skip the initialization if the module's globals slice is empty.
-	if len(c.f.ModuleInstance.Globals) > 0 {
+	if len(c.f.Module.Globals) > 0 {
 		// 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.
@@ -5413,7 +5414,7 @@ func (c *amd64Compiler) initializeModuleContext() error {
 	// Note: if there's table instruction in the function, the existence of the table
 	// is ensured by function validation at module instantiation phase, and that's
 	// why it is ok to skip the initialization if the module's table doesn't exist.
-	if c.f.ModuleInstance.TableInstance != nil {
+	if c.f.Module.TableInstance != nil {
 		// First, we need to read the *wasm.TableInstance.
 		readTableInstancePointer := c.newProg()
 		readTableInstancePointer.As = x86.AMOVQ
@@ -5471,7 +5472,7 @@ func (c *amd64Compiler) initializeModuleContext() error {
 	// Note: if there's memory instruction in the function, memory instance must be non-nil.
 	// 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.f.ModuleInstance.MemoryInstance != nil {
+	if c.f.Module.MemoryInstance != nil {
 		getMemoryInstanceAddress := c.newProg()
 		getMemoryInstanceAddress.As = x86.AMOVQ
 		getMemoryInstanceAddress.To.Type = obj.TYPE_REG
diff --git a/internal/wasm/jit/jit_arm64.go b/internal/wasm/jit/jit_arm64.go
index 84eb0f3e..1f0b18ba 100644
--- a/internal/wasm/jit/jit_arm64.go
+++ b/internal/wasm/jit/jit_arm64.go
@@ -398,10 +398,10 @@ func (c *arm64Compiler) String() (ret string) { return }
 
 // pushFunctionParams pushes any function parameters onto the stack, setting appropriate register types.
 func (c *arm64Compiler) pushFunctionParams() {
-	if c.f == nil || c.f.FunctionType == nil {
+	if c.f == nil || c.f.Type == nil {
 		return
 	}
-	for _, t := range c.f.FunctionType.Type.Params {
+	for _, t := range c.f.Type.Params {
 		loc := c.locationStack.pushValueLocationOnStack()
 		switch t {
 		case wasm.ValueTypeI32, wasm.ValueTypeI64:
@@ -702,7 +702,7 @@ func (c *arm64Compiler) compileGlobalGet(o *wazeroir.OperationGlobalGet) error {
 	}
 
 	var intMov, floatMov obj.As = obj.ANOP, obj.ANOP
-	switch c.f.ModuleInstance.Globals[o.Index].Type.ValType {
+	switch c.f.Module.Globals[o.Index].Type.ValType {
 	case wasm.ValueTypeI32:
 		intMov = arm64.AMOVWU
 	case wasm.ValueTypeI64:
@@ -750,7 +750,7 @@ func (c *arm64Compiler) compileGlobalSet(o *wazeroir.OperationGlobalSet) error {
 	}
 
 	var mov obj.As
-	switch c.f.ModuleInstance.Globals[o.Index].Type.ValType {
+	switch c.f.Module.Globals[o.Index].Type.ValType {
 	case wasm.ValueTypeI32:
 		mov = arm64.AMOVWU
 	case wasm.ValueTypeI64:
@@ -1088,8 +1088,8 @@ func (c *arm64Compiler) compileBrTable(o *wazeroir.OperationBrTable) error {
 
 // compileCall implements compiler.compileCall for the arm64 architecture.
 func (c *arm64Compiler) compileCall(o *wazeroir.OperationCall) error {
-	target := c.f.ModuleInstance.Functions[o.FunctionIndex]
-	return c.compileCallImpl(target.Index, nilRegister, target.FunctionType.Type)
+	target := c.f.Module.Functions[o.FunctionIndex]
+	return c.compileCallImpl(target.Index, nilRegister, target.Type)
 }
 
 // compileCallImpl implements compiler.compileCall and compiler.compileCallIndirect for the arm64 architecture.
@@ -1429,14 +1429,14 @@ func (c *arm64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 	)
 
 	// Check if table[offset].TypeID == targetFunctionType.
-	targetFunctionType := c.f.ModuleInstance.Types[o.TypeIndex]
+	ti := c.f.Module.Types[o.TypeIndex]
 	// "tmp = table[offset].TypeID"
 	c.compileMemoryToRegisterInstruction(
 		arm64.AMOVD, offset.register, tableElementFunctionTypeIDOffset,
 		tmp,
 	)
 	// "reservedRegisterForTemporary = targetFunctionType.TypeID"
-	c.compileConstToRegisterInstruction(arm64.AMOVD, int64(targetFunctionType.TypeID), reservedRegisterForTemporary)
+	c.compileConstToRegisterInstruction(arm64.AMOVD, int64(ti.TypeID), reservedRegisterForTemporary)
 	// Compare these two values, and if they equal, we are ready to make function call.
 	c.compileTwoRegistersToNoneInstruction(arm64.ACMP, tmp, reservedRegisterForTemporary)
 	brIfTypeMatched := c.compilelBranchInstruction(arm64.ABEQ)
@@ -1468,7 +1468,7 @@ func (c *arm64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 		offset.register,
 	)
 
-	if err := c.compileCallImpl(0, offset.register, targetFunctionType.Type); err != nil {
+	if err := c.compileCallImpl(0, offset.register, ti.Type); err != nil {
 		return err
 	}
 
@@ -3355,7 +3355,7 @@ func (c *arm64Compiler) compileReservedStackBasePointerRegisterInitialization()
 }
 
 func (c *arm64Compiler) compileReservedMemoryRegisterInitialization() {
-	if c.f.ModuleInstance.MemoryInstance != nil {
+	if c.f.Module.MemoryInstance != nil {
 		// "reservedRegisterForMemory = ce.MemoryElement0Address"
 		c.compileMemoryToRegisterInstruction(
 			arm64.AMOVD,
@@ -3380,7 +3380,7 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 
 	// Load the absolute address of the current function's module instance.
 	// Note: this should be modified to support Clone() functionality per #179.
-	c.compileConstToRegisterInstruction(arm64.AMOVD, int64(uintptr(unsafe.Pointer(c.f.ModuleInstance))), moduleInstanceAddressRegister)
+	c.compileConstToRegisterInstruction(arm64.AMOVD, int64(uintptr(unsafe.Pointer(c.f.Module))), moduleInstanceAddressRegister)
 
 	// "tmpX = ce.ModuleInstanceAddress"
 	c.compileMemoryToRegisterInstruction(arm64.AMOVD, reservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset, tmpX)
@@ -3402,7 +3402,7 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 	// Note: if there's global.get or set instruction in the function, the existence of the globals
 	// is ensured by function validation at module instantiation phase, and that's why it is ok to
 	// skip the initialization if the module's globals slice is empty.
-	if len(c.f.ModuleInstance.Globals) > 0 {
+	if len(c.f.Module.Globals) > 0 {
 		// "tmpX = &moduleInstance.Globals[0]"
 		c.compileMemoryToRegisterInstruction(arm64.AMOVD,
 			moduleInstanceAddressRegister, moduleInstanceGlobalsOffset,
@@ -3421,7 +3421,7 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 	// Note: if there's memory instruction in the function, memory instance must be non-nil.
 	// 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.f.ModuleInstance.MemoryInstance != nil {
+	if c.f.Module.MemoryInstance != nil {
 		// "tmpX = moduleInstance.Memory"
 		c.compileMemoryToRegisterInstruction(
 			arm64.AMOVD,
@@ -3465,7 +3465,7 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 	// Note: if there's table instruction in the function, the existence of the table
 	// is ensured by function validation at module instantiation phase, and that's
 	// why it is ok to skip the initialization if the module's table doesn't exist.
-	if c.f.ModuleInstance.TableInstance != nil {
+	if c.f.Module.TableInstance != nil {
 		// "tmpX = &tables[0] (type of **wasm.TableInstance)"
 		c.compileMemoryToRegisterInstruction(
 			arm64.AMOVD,
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index e9d72b16..4bcd19f5 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -398,8 +398,8 @@ func (m *Module) buildFunctionInstances() (functions []*FunctionInstance) {
 
 	importCount := m.ImportFuncCount()
 	n, nLen := 0, len(functionNames)
-	for codeIndex := range m.FunctionSection {
-		funcIdx := Index(importCount + uint32(len(functions)))
+	for codeIndex, typeIndex := range m.FunctionSection {
+		funcIdx := importCount + uint32(len(functions))
 		// Seek to see if there's a better name than "unknown"
 		name := "unknown"
 		for ; n < nLen; n++ {
@@ -413,10 +413,11 @@ func (m *Module) buildFunctionInstances() (functions []*FunctionInstance) {
 		}
 
 		f := &FunctionInstance{
-			Name:         name,
-			FunctionKind: FunctionKindWasm,
-			Body:         m.CodeSection[codeIndex].Body,
-			LocalTypes:   m.CodeSection[codeIndex].LocalTypes,
+			Name:       name,
+			Kind:       FunctionKindWasm,
+			Type:       m.TypeSection[typeIndex],
+			Body:       m.CodeSection[codeIndex].Body,
+			LocalTypes: m.CodeSection[codeIndex].LocalTypes,
 		}
 		functions = append(functions, f)
 	}
@@ -462,23 +463,37 @@ type FunctionType struct {
 	// Note: In WebAssembly 1.0 (20191205), there can be at most one result.
 	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#result-types%E2%91%A0
 	Results []ValueType
+
+	// string is cached as it is used both for String and key
+	string string
 }
 
-func (t *FunctionType) String() (ret string) {
+// key gets or generates the key for Store.typeIDs. Ex. "i32_v" for one i32 parameter and no (void) result.
+func (t *FunctionType) key() string {
+	if t.string != "" {
+		return t.string
+	}
+	var ret string
 	for _, b := range t.Params {
 		ret += ValueTypeName(b)
 	}
 	if len(t.Params) == 0 {
-		ret += "null"
+		ret += "v"
 	}
 	ret += "_"
 	for _, b := range t.Results {
 		ret += ValueTypeName(b)
 	}
 	if len(t.Results) == 0 {
-		ret += "null"
+		ret += "v"
 	}
-	return
+	t.string = ret
+	return ret
+}
+
+// String implements fmt.Stringer.
+func (t *FunctionType) String() string {
+	return t.key()
 }
 
 // Import is the binary representation of an import indicated by Type
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index 8e5d6c0b..60c607e7 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -134,32 +134,42 @@ type (
 	// FunctionInstance represents a function instance in a Store.
 	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-instances%E2%91%A0
 	FunctionInstance struct {
-		// ModuleInstance holds the pointer to the module instance to which this function belongs.
-		ModuleInstance *ModuleInstance
-		// Body is the function body in WebAssembly Binary Format
-		Body []byte
-		// FunctionType holds the pointer to TypeInstance whose functionType field equals that of this function.
-		FunctionType *TypeInstance
-		// LocalTypes holds types of locals.
-		LocalTypes []ValueType
-		// FunctionKind describes how this function should be called.
-		FunctionKind FunctionKind
-		// HostFunction holds the runtime representation of host functions.
-		// This is nil when FunctionKind == FunctionKindWasm. Otherwise, all the above fields are ignored as they are
-		// specific to Wasm functions.
-		HostFunction *reflect.Value
-		// Index is the index of this function instance in store.Functions, and is exported because
-		// all function calls are made via funcaddr at runtime, not the index (scoped to a module).
-		//
-		// This is used by both host and non-host functions.
-		Index FunctionIndex
 		// Name is for debugging purpose, and is used to argument the stack traces.
 		//
-		// When HostFunction is not nil, this returns dot-delimited parameters given to
+		// When GoFunc is not nil, this returns dot-delimited parameters given to
 		// Store.NewHostModule. Ex. something.realistic
 		//
 		// Otherwise, this is the corresponding value in NameSection.FunctionNames or "unknown" if unavailable.
 		Name string
+
+		// Kind describes how this function should be called.
+		Kind FunctionKind
+
+		// Type is the signature of this function.
+		Type *FunctionType
+
+		// LocalTypes holds types of locals, set when Kind == FunctionKindWasm
+		LocalTypes []ValueType
+
+		// Body is the function body in WebAssembly Binary Format, set when Kind == FunctionKindWasm
+		Body []byte
+
+		// GoFunc holds the runtime representation of host functions.
+		// This is nil when Kind == FunctionKindWasm. Otherwise, all the above fields are ignored as they are
+		// specific to Wasm functions.
+		GoFunc *reflect.Value
+
+		// Fields above here are settable prior to instantiation. Below are set by the Store during instantiation.
+
+		// ModuleInstance holds the pointer to the module instance to which this function belongs.
+		Module *ModuleInstance
+		// TypeID is assigned by a store for FunctionType.
+		TypeID FunctionTypeID
+		// Index is the index of this function instance in Store.functions, and is exported because
+		// all function calls are made via funcaddr at runtime, not the index (scoped to a module).
+		//
+		// This is used by both host and non-host functions.
+		Index FunctionIndex
 	}
 
 	// TypeInstance is a store-specific representation of FunctionType where the function type
@@ -208,7 +218,7 @@ type (
 		// the target function instance in Store.FunctionInstances.
 		FunctionIndex FunctionIndex
 		// FunctionTypeID is the type ID of the target function's type, which
-		// equals store.Functions[FunctionIndex].FunctionType.TypeID.
+		// equals store.Functions[FunctionIndex].TypeID.
 		FunctionTypeID FunctionTypeID
 	}
 
@@ -262,8 +272,8 @@ func newModuleInstance(name string, module *Module, importedFunctions, functions
 	instance.Functions = append(instance.Functions, importedFunctions...)
 	for i, f := range functions {
 		// Associate each function with the type instance and the module instance's pointer.
-		f.FunctionType = typeInstances[module.FunctionSection[i]]
-		f.ModuleInstance = instance
+		f.Module = instance
+		f.TypeID = typeInstances[module.FunctionSection[i]].TypeID
 		instance.Functions = append(instance.Functions, f)
 	}
 
@@ -297,8 +307,8 @@ func (m *ModuleInstance) buildExportInstances(exports map[string]*Export) {
 			// The module instance of the host function is a fake that only includes the function and its types.
 			// We need to assign the ModuleInstance when re-exporting so that any memory defined in the target is
 			// available to the wasm.ModuleContext Memory.
-			if ei.Function.HostFunction != nil {
-				ei.Function.ModuleInstance = m
+			if ei.Function.GoFunc != nil {
+				ei.Function.Module = m
 			}
 		case ExternTypeGlobal:
 			ei = &ExportInstance{Type: exp.Type, Global: m.Globals[index]}
@@ -358,7 +368,7 @@ func (m *ModuleInstance) applyElements(elements []*ElementSegment) {
 			targetFunc := m.Functions[elm]
 			table[pos] = TableElement{
 				FunctionIndex:  targetFunc.Index,
-				FunctionTypeID: targetFunc.FunctionType.TypeID,
+				FunctionTypeID: targetFunc.TypeID,
 			}
 		}
 	}
@@ -714,8 +724,8 @@ func (s *Store) resolveImports(module *Module) (
 			}
 			expectedType := module.TypeSection[typeIndex]
 			f := exp.Function
-			if !bytes.Equal(expectedType.Results, f.FunctionType.Type.Results) || !bytes.Equal(expectedType.Params, f.FunctionType.Type.Params) {
-				err = fmt.Errorf("signature mimatch: %s != %s", expectedType, f.FunctionType.Type)
+			if !bytes.Equal(expectedType.Results, f.Type.Results) || !bytes.Equal(expectedType.Params, f.Type.Params) {
+				err = fmt.Errorf("signature mimatch: %s != %s", expectedType, f.Type)
 				return
 			}
 			functions = append(functions, f)
diff --git a/internal/wasm/text/type_parser.go b/internal/wasm/text/type_parser.go
index 80ed37e7..32b72b86 100644
--- a/internal/wasm/text/type_parser.go
+++ b/internal/wasm/text/type_parser.go
@@ -14,7 +14,7 @@ func newTypeParser(typeNamespace *indexNamespace, onType onType) *typeParser {
 
 type onType func(ft *wasm.FunctionType) tokenParser
 
-// typeParser parses a wasm.FunctionType from and dispatches to onType.
+// typeParser parses a wasm.Type from and dispatches to onType.
 //
 // Ex. `(module (type (func (param i32) (result i64)))`
 //      starts here --^                             ^
diff --git a/internal/wazeroir/compiler.go b/internal/wazeroir/compiler.go
index 60fd474d..5a4d8143 100644
--- a/internal/wazeroir/compiler.go
+++ b/internal/wazeroir/compiler.go
@@ -157,7 +157,7 @@ func Compile(f *wasm.FunctionInstance) (*CompilationResult, error) {
 	c := compiler{controlFrames: &controlFrames{}, f: f, result: CompilationResult{LabelCallers: map[string]uint32{}}}
 
 	// Push function arguments.
-	for _, t := range f.FunctionType.Type.Params {
+	for _, t := range f.Type.Params {
 		c.stackPush(wasmValueTypeToUnsignedType(t))
 	}
 	// Emit const expressions for locals.
@@ -169,13 +169,13 @@ func Compile(f *wasm.FunctionInstance) (*CompilationResult, error) {
 	}
 
 	// Insert the function control frame.
-	returns := make([]UnsignedType, 0, len(f.FunctionType.Type.Results))
-	for _, t := range f.FunctionType.Type.Results {
+	returns := make([]UnsignedType, 0, len(f.Type.Results))
+	for _, t := range f.Type.Results {
 		returns = append(returns, wasmValueTypeToUnsignedType(t))
 	}
 	c.controlFrames.push(&controlFrame{
 		frameID:          c.nextID(),
-		originalStackLen: len(f.FunctionType.Type.Params),
+		originalStackLen: len(f.Type.Params),
 		returns:          returns,
 		kind:             controlFrameKindFunction,
 	})
@@ -220,7 +220,7 @@ operatorSwitch:
 	case wasm.OpcodeNop:
 		// Nop is noop!
 	case wasm.OpcodeBlock:
-		bt, num, err := wasm.DecodeBlockType(c.f.ModuleInstance.Types,
+		bt, num, err := wasm.DecodeBlockType(c.f.Module.Types,
 			bytes.NewReader(c.f.Body[c.pc+1:]))
 		if err != nil {
 			return fmt.Errorf("reading block type for block instruction: %w", err)
@@ -246,7 +246,7 @@ operatorSwitch:
 		c.controlFrames.push(frame)
 
 	case wasm.OpcodeLoop:
-		bt, num, err := wasm.DecodeBlockType(c.f.ModuleInstance.Types,
+		bt, num, err := wasm.DecodeBlockType(c.f.Module.Types,
 			bytes.NewReader(c.f.Body[c.pc+1:]))
 		if err != nil {
 			return fmt.Errorf("reading block type for loop instruction: %w", err)
@@ -284,7 +284,7 @@ operatorSwitch:
 		)
 
 	case wasm.OpcodeIf:
-		bt, num, err := wasm.DecodeBlockType(c.f.ModuleInstance.Types,
+		bt, num, err := wasm.DecodeBlockType(c.f.Module.Types,
 			bytes.NewReader(c.f.Body[c.pc+1:]))
 		if err != nil {
 			return fmt.Errorf("reading block type for if instruction: %w", err)
diff --git a/internal/wazeroir/signature.go b/internal/wazeroir/signature.go
index c07dab6a..4e55f521 100644
--- a/internal/wazeroir/signature.go
+++ b/internal/wazeroir/signature.go
@@ -160,9 +160,9 @@ func wasmOpcodeSignature(f *wasm.FunctionInstance, op wasm.Opcode, index uint32)
 	case wasm.OpcodeReturn:
 		return signature_None_None, nil
 	case wasm.OpcodeCall:
-		return funcTypeToSignature(f.ModuleInstance.Functions[index].FunctionType.Type), nil
+		return funcTypeToSignature(f.Module.Functions[index].Type), nil
 	case wasm.OpcodeCallIndirect:
-		ret := funcTypeToSignature(f.ModuleInstance.Types[index].Type)
+		ret := funcTypeToSignature(f.Module.Types[index].Type)
 		ret.in = append(ret.in, UnsignedTypeI32)
 		return ret, nil
 	case wasm.OpcodeDrop:
@@ -170,54 +170,54 @@ func wasmOpcodeSignature(f *wasm.FunctionInstance, op wasm.Opcode, index uint32)
 	case wasm.OpcodeSelect:
 		return signature_UnknownUnkownI32_Unknown, nil
 	case wasm.OpcodeLocalGet:
-		inputLen := uint32(len(f.FunctionType.Type.Params))
+		inputLen := uint32(len(f.Type.Params))
 		if l := uint32(len(f.LocalTypes)) + inputLen; index >= l {
 			return nil, fmt.Errorf("invalid local index for local.get %d >= %d", index, l)
 		}
 		var t UnsignedType
 		if index < inputLen {
-			t = wasmValueTypeToUnsignedType(f.FunctionType.Type.Params[index])
+			t = wasmValueTypeToUnsignedType(f.Type.Params[index])
 		} else {
 			t = wasmValueTypeToUnsignedType(f.LocalTypes[index-inputLen])
 		}
 		return &signature{out: []UnsignedType{t}}, nil
 	case wasm.OpcodeLocalSet:
-		inputLen := uint32(len(f.FunctionType.Type.Params))
+		inputLen := uint32(len(f.Type.Params))
 		if l := uint32(len(f.LocalTypes)) + inputLen; index >= l {
 			return nil, fmt.Errorf("invalid local index for local.get %d >= %d", index, l)
 		}
 		var t UnsignedType
 		if index < inputLen {
-			t = wasmValueTypeToUnsignedType(f.FunctionType.Type.Params[index])
+			t = wasmValueTypeToUnsignedType(f.Type.Params[index])
 		} else {
 			t = wasmValueTypeToUnsignedType(f.LocalTypes[index-inputLen])
 		}
 		return &signature{in: []UnsignedType{t}}, nil
 	case wasm.OpcodeLocalTee:
-		inputLen := uint32(len(f.FunctionType.Type.Params))
+		inputLen := uint32(len(f.Type.Params))
 		if l := uint32(len(f.LocalTypes)) + inputLen; index >= l {
 			return nil, fmt.Errorf("invalid local index for local.get %d >= %d", index, l)
 		}
 		var t UnsignedType
 		if index < inputLen {
-			t = wasmValueTypeToUnsignedType(f.FunctionType.Type.Params[index])
+			t = wasmValueTypeToUnsignedType(f.Type.Params[index])
 		} else {
 			t = wasmValueTypeToUnsignedType(f.LocalTypes[index-inputLen])
 		}
 		return &signature{in: []UnsignedType{t}, out: []UnsignedType{t}}, nil
 	case wasm.OpcodeGlobalGet:
-		if len(f.ModuleInstance.Globals) <= int(index) {
-			return nil, fmt.Errorf("invalid global index for global.get %d >= %d", index, len(f.ModuleInstance.Globals))
+		if len(f.Module.Globals) <= int(index) {
+			return nil, fmt.Errorf("invalid global index for global.get %d >= %d", index, len(f.Module.Globals))
 		}
 		return &signature{
-			out: []UnsignedType{wasmValueTypeToUnsignedType(f.ModuleInstance.Globals[index].Type.ValType)},
+			out: []UnsignedType{wasmValueTypeToUnsignedType(f.Module.Globals[index].Type.ValType)},
 		}, nil
 	case wasm.OpcodeGlobalSet:
-		if len(f.ModuleInstance.Globals) <= int(index) {
-			return nil, fmt.Errorf("invalid global index for global.get %d >= %d", index, len(f.ModuleInstance.Globals))
+		if len(f.Module.Globals) <= int(index) {
+			return nil, fmt.Errorf("invalid global index for global.get %d >= %d", index, len(f.Module.Globals))
 		}
 		return &signature{
-			in: []UnsignedType{wasmValueTypeToUnsignedType(f.ModuleInstance.Globals[index].Type.ValType)},
+			in: []UnsignedType{wasmValueTypeToUnsignedType(f.Module.Globals[index].Type.ValType)},
 		}, nil
 	case wasm.OpcodeI32Load:
 		return signature_I32_I32, nil


Test patch:
diff --git a/internal/wasm/gofunc_test.go b/internal/wasm/gofunc_test.go
index e8d9e9f5..1d5319c0 100644
--- a/internal/wasm/gofunc_test.go
+++ b/internal/wasm/gofunc_test.go
@@ -103,7 +103,7 @@ func TestGetFunctionType(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			rVal := reflect.ValueOf(tc.inputFunc)
-			fk, ft, hasErrorResult, err := GetFunctionType("fn", &rVal, tc.allowErrorResult)
+			fk, ft, hasErrorResult, err := getFunctionType(&rVal, tc.allowErrorResult)
 			require.NoError(t, err)
 			require.Equal(t, tc.expectedKind, fk)
 			require.Equal(t, tc.expectedType, ft)
@@ -122,42 +122,42 @@ func TestGetFunctionTypeErrors(t *testing.T) {
 		{
 			name:        "not a func",
 			input:       struct{}{},
-			expectedErr: "fn is a struct, but should be a Func",
+			expectedErr: "kind != func: struct",
 		},
 		{
 			name:        "unsupported param",
 			input:       func(uint32, string) {},
-			expectedErr: "fn param[1] is unsupported: string",
+			expectedErr: "param[1] is unsupported: string",
 		},
 		{
 			name:        "unsupported result",
 			input:       func() string { return "" },
-			expectedErr: "fn result[0] is unsupported: string",
+			expectedErr: "result[0] is unsupported: string",
 		},
 		{
 			name:        "error result",
 			input:       func() error { return nil },
-			expectedErr: "fn result[0] is an error, which is unsupported",
+			expectedErr: "result[0] is an error, which is unsupported",
 		},
 		{
 			name:        "multiple results",
 			input:       func() (uint64, uint32) { return 0, 0 },
-			expectedErr: "fn has more than one result",
+			expectedErr: "multiple results are unsupported",
 		},
 		{
 			name:        "multiple context types",
 			input:       func(publicwasm.ModuleContext, context.Context) error { return nil },
-			expectedErr: "fn param[1] is a context.Context, which may be defined only once as param[0]",
+			expectedErr: "param[1] is a context.Context, which may be defined only once as param[0]",
 		},
 		{
 			name:        "multiple context.Context",
 			input:       func(context.Context, uint64, context.Context) error { return nil },
-			expectedErr: "fn param[2] is a context.Context, which may be defined only once as param[0]",
+			expectedErr: "param[2] is a context.Context, which may be defined only once as param[0]",
 		},
 		{
 			name:        "multiple wasm.ModuleContext",
 			input:       func(publicwasm.ModuleContext, uint64, publicwasm.ModuleContext) error { return nil },
-			expectedErr: "fn param[2] is a wasm.ModuleContext, which may be defined only once as param[0]",
+			expectedErr: "param[2] is a wasm.ModuleContext, which may be defined only once as param[0]",
 		},
 	}
 
@@ -166,7 +166,7 @@ func TestGetFunctionTypeErrors(t *testing.T) {
 
 		t.Run(tc.name, func(t *testing.T) {
 			rVal := reflect.ValueOf(tc.input)
-			_, _, _, err := GetFunctionType("fn", &rVal, tc.allowErrorResult)
+			_, _, _, err := getFunctionType(&rVal, tc.allowErrorResult)
 			require.EqualError(t, err, tc.expectedErr)
 		})
 	}
diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go
index 131e69dd..ccaf4773 100644
--- a/internal/wasm/host_test.go
+++ b/internal/wasm/host_test.go
@@ -477,6 +477,24 @@ func TestStore_NewHostModule(t *testing.T) {
 	require.EqualError(t, err, "module test has already been instantiated")
 }
 
+func TestStore_NewHostModule_Errors(t *testing.T) {
+	s := newStore()
+
+	t.Run("Adds export name to error message", func(t *testing.T) {
+		_, err := s.NewHostModule("test", map[string]interface{}{"fn": "hello"})
+		require.EqualError(t, err, "func[fn] kind != func: string")
+
+	})
+
+	t.Run("Fails if module name already in use", func(t *testing.T) {
+		_, err := s.NewHostModule("test", map[string]interface{}{"host_fn": func(wasm.ModuleContext) {}})
+		require.NoError(t, err)
+		// Trying to register it again should fail
+		_, err = s.NewHostModule("test", map[string]interface{}{"host_fn": func(wasm.ModuleContext) {}})
+		require.EqualError(t, err, "module test has already been instantiated")
+	})
+}
+
 func TestHostModule_String(t *testing.T) {
 	s := newStore()
 
diff --git a/internal/wasm/interpreter/interpreter_test.go b/internal/wasm/interpreter/interpreter_test.go
index a48c27f7..421d7c5a 100644
--- a/internal/wasm/interpreter/interpreter_test.go
+++ b/internal/wasm/interpreter/interpreter_test.go
@@ -89,15 +89,13 @@ func TestEngine_Call_HostFn(t *testing.T) {
 	module := &wasm.ModuleInstance{MemoryInstance: memory}
 	modCtx := wasm.NewModuleContext(context.Background(), e, module)
 	f := &wasm.FunctionInstance{
-		HostFunction: &hostFn,
-		FunctionKind: wasm.FunctionKindGoModuleContext,
-		FunctionType: &wasm.TypeInstance{
-			Type: &wasm.FunctionType{
-				Params:  []wasm.ValueType{wasm.ValueTypeI64},
-				Results: []wasm.ValueType{wasm.ValueTypeI64},
-			},
+		GoFunc: &hostFn,
+		Kind:   wasm.FunctionKindGoModuleContext,
+		Type: &wasm.FunctionType{
+			Params:  []wasm.ValueType{wasm.ValueTypeI64},
+			Results: []wasm.ValueType{wasm.ValueTypeI64},
 		},
-		ModuleInstance: module,
+		Module: module,
 	}
 	require.NoError(t, e.Compile(f))
 
diff --git a/internal/wasm/jit/engine_test.go b/internal/wasm/jit/engine_test.go
index a0c4da24..72dcbdb5 100644
--- a/internal/wasm/jit/engine_test.go
+++ b/internal/wasm/jit/engine_test.go
@@ -135,15 +135,13 @@ func TestEngine_Call_HostFn(t *testing.T) {
 	module := &wasm.ModuleInstance{MemoryInstance: memory}
 	modCtx := wasm.NewModuleContext(context.Background(), e, module)
 	f := &wasm.FunctionInstance{
-		HostFunction: &hostFn,
-		FunctionKind: wasm.FunctionKindGoModuleContext,
-		FunctionType: &wasm.TypeInstance{
-			Type: &wasm.FunctionType{
-				Params:  []wasm.ValueType{wasm.ValueTypeI64},
-				Results: []wasm.ValueType{wasm.ValueTypeI64},
-			},
+		GoFunc: &hostFn,
+		Kind:   wasm.FunctionKindGoModuleContext,
+		Type: &wasm.FunctionType{
+			Params:  []wasm.ValueType{wasm.ValueTypeI64},
+			Results: []wasm.ValueType{wasm.ValueTypeI64},
 		},
-		ModuleInstance: module,
+		Module: module,
 	}
 	require.NoError(t, e.Compile(f))
 
diff --git a/internal/wasm/jit/jit_amd64_test.go b/internal/wasm/jit/jit_amd64_test.go
index 5f138afb..ece42829 100644
--- a/internal/wasm/jit/jit_amd64_test.go
+++ b/internal/wasm/jit/jit_amd64_test.go
@@ -38,7 +38,7 @@ func (j *jitEnv) requireNewCompiler(t *testing.T) *amd64Compiler {
 	return &amd64Compiler{builder: b,
 		locationStack: newValueLocationStack(),
 		labels:        map[string]*labelInfo{},
-		f:             &wasm.FunctionInstance{ModuleInstance: j.moduleInstance, FunctionKind: wasm.FunctionKindWasm},
+		f:             &wasm.FunctionInstance{Module: j.moduleInstance, Kind: wasm.FunctionKindWasm},
 	}
 }
 
@@ -287,7 +287,7 @@ func TestAmd64Compiler_initializeModuleContext(t *testing.T) {
 			ce := env.callEngine()
 			compiler := env.requireNewCompiler(t)
 			compiler.initializeReservedStackBasePointer()
-			compiler.f.ModuleInstance = tc.moduleInstance
+			compiler.f.Module = tc.moduleInstance
 
 			require.Empty(t, compiler.locationStack.usedRegisters)
 			err := compiler.initializeModuleContext()
@@ -512,11 +512,11 @@ func TestAmd64Compiler_compileBrTable(t *testing.T) {
 
 func TestAmd64Compiler_pushFunctionInputs(t *testing.T) {
 	f := &wasm.FunctionInstance{
-		FunctionKind: wasm.FunctionKindWasm,
-		FunctionType: &wasm.TypeInstance{Type: &wasm.FunctionType{Params: []wasm.ValueType{wasm.ValueTypeF64, wasm.ValueTypeI32}}}}
+		Kind: wasm.FunctionKindWasm,
+		Type: &wasm.FunctionType{Params: []wasm.ValueType{wasm.ValueTypeF64, wasm.ValueTypeI32}}}
 	compiler := &amd64Compiler{locationStack: newValueLocationStack(), f: f}
 	compiler.pushFunctionParams()
-	require.Equal(t, uint64(len(f.FunctionType.Type.Params)), compiler.locationStack.sp)
+	require.Equal(t, uint64(len(f.Type.Params)), compiler.locationStack.sp)
 	loc := compiler.locationStack.pop()
 	require.Equal(t, uint64(1), loc.stackPointer)
 	loc = compiler.locationStack.pop()
@@ -5822,8 +5822,8 @@ func TestAmd64Compiler_compileGlobalGet(t *testing.T) {
 			env.addGlobals(globals...)
 			// Compiler needs global type information at compilation time.
 			compiler.f = &wasm.FunctionInstance{
-				ModuleInstance: &wasm.ModuleInstance{Globals: globals},
-				FunctionKind:   wasm.FunctionKindWasm,
+				Module: &wasm.ModuleInstance{Globals: globals},
+				Kind:   wasm.FunctionKindWasm,
 			}
 
 			// Emit the code.
@@ -5962,9 +5962,9 @@ func TestAmd64Compiler_callFunction(t *testing.T) {
 
 					compiler := env.requireNewCompiler(t)
 					compiler.f = &wasm.FunctionInstance{
-						FunctionKind:   wasm.FunctionKindWasm,
-						FunctionType:   &wasm.TypeInstance{Type: targetFunctionType},
-						ModuleInstance: moduleInstance,
+						Kind:   wasm.FunctionKindWasm,
+						Type:   targetFunctionType,
+						Module: moduleInstance,
 					}
 
 					err := compiler.compilePreamble()
@@ -6071,9 +6071,9 @@ func TestAmd64Compiler_compileCall(t *testing.T) {
 		// Call target function takes three i32 arguments and does ADD 2 times.
 		compiler := env.requireNewCompiler(t)
 		compiler.f = &wasm.FunctionInstance{
-			ModuleInstance: &wasm.ModuleInstance{},
-			FunctionKind:   wasm.FunctionKindWasm,
-			FunctionType:   &wasm.TypeInstance{Type: targetFunctionType},
+			Module: &wasm.ModuleInstance{},
+			Kind:   wasm.FunctionKindWasm,
+			Type:   targetFunctionType,
 		}
 		err := compiler.compilePreamble()
 		require.NoError(t, err)
@@ -6092,7 +6092,7 @@ func TestAmd64Compiler_compileCall(t *testing.T) {
 			codeInitialAddress: uintptr(unsafe.Pointer(&code[0])),
 		})
 		env.module().Functions = append(env.module().Functions,
-			&wasm.FunctionInstance{FunctionType: &wasm.TypeInstance{Type: targetFunctionType}, Index: targetFunctionIndex})
+			&wasm.FunctionInstance{Type: targetFunctionType, Index: targetFunctionIndex})
 	}
 
 	// Now we start building the caller's code.
@@ -6137,8 +6137,8 @@ func TestAmd64Compiler_compileCallIndirect(t *testing.T) {
 		targetOperation := &wazeroir.OperationCallIndirect{}
 		// Ensure that the module instance has the type information for targetOperation.TypeIndex.
 		compiler.f = &wasm.FunctionInstance{
-			FunctionKind:   wasm.FunctionKindWasm,
-			ModuleInstance: &wasm.ModuleInstance{Types: []*wasm.TypeInstance{{Type: &wasm.FunctionType{}}}},
+			Kind:   wasm.FunctionKindWasm,
+			Module: &wasm.ModuleInstance{Types: []*wasm.TypeInstance{{Type: &wasm.FunctionType{}}}},
 		}
 
 		// Place the offfset value.
@@ -6171,8 +6171,8 @@ func TestAmd64Compiler_compileCallIndirect(t *testing.T) {
 		targetOffset := &wazeroir.OperationConstI32{Value: uint32(0)}
 		// Ensure that the module instance has the type information for targetOperation.TypeIndex,
 		compiler.f = &wasm.FunctionInstance{
-			ModuleInstance: &wasm.ModuleInstance{Types: []*wasm.TypeInstance{{Type: &wasm.FunctionType{}, TypeID: 1000}}},
-			FunctionKind:   wasm.FunctionKindWasm,
+			Module: &wasm.ModuleInstance{Types: []*wasm.TypeInstance{{Type: &wasm.FunctionType{}, TypeID: 1000}}},
+			Kind:   wasm.FunctionKindWasm,
 		}
 		// and the typeID doesn't match the table[targetOffset]'s type ID.
 		table[0] = wasm.TableElement{FunctionTypeID: wasm.UninitializedTableElementTypeID}
@@ -6277,7 +6277,7 @@ func TestAmd64Compiler_compileCallIndirect(t *testing.T) {
 				require.NoError(t, err)
 
 				// Ensure that the module instance has the type information for targetOperation.TypeIndex,
-				compiler.f = &wasm.FunctionInstance{ModuleInstance: moduleInstance, FunctionKind: wasm.FunctionKindWasm}
+				compiler.f = &wasm.FunctionInstance{Module: moduleInstance, Kind: wasm.FunctionKindWasm}
 				// and the typeID  matches the table[targetOffset]'s type ID.
 
 				// Place the offfset value. Here we try calling a function of functionaddr == table[i].FunctionIndex.
diff --git a/internal/wasm/jit/jit_arm64_test.go b/internal/wasm/jit/jit_arm64_test.go
index fb5b0ae7..a92393b9 100644
--- a/internal/wasm/jit/jit_arm64_test.go
+++ b/internal/wasm/jit/jit_arm64_test.go
@@ -43,8 +43,8 @@ func requirePushTwoFloat32Consts(t *testing.T, x1, x2 float32, compiler *arm64Co
 
 func (j *jitEnv) requireNewCompiler(t *testing.T) *arm64Compiler {
 	cmp, done, err := newCompiler(&wasm.FunctionInstance{
-		ModuleInstance: j.moduleInstance,
-		FunctionKind:   wasm.FunctionKindWasm,
+		Module: j.moduleInstance,
+		Kind:   wasm.FunctionKindWasm,
 	}, nil)
 	require.NoError(t, err)
 	t.Cleanup(done)
@@ -1746,9 +1746,9 @@ func TestArm64Compiler_compileCall(t *testing.T) {
 				t.Run(fmt.Sprintf("compiling call target %d", i), func(t *testing.T) {
 					compiler := env.requireNewCompiler(t)
 					compiler.f = &wasm.FunctionInstance{
-						FunctionKind:   wasm.FunctionKindWasm,
-						FunctionType:   &wasm.TypeInstance{Type: targetFunctionType},
-						ModuleInstance: &wasm.ModuleInstance{},
+						Kind:   wasm.FunctionKindWasm,
+						Type:   targetFunctionType,
+						Module: &wasm.ModuleInstance{},
 					}
 
 					err := compiler.compilePreamble()
@@ -1769,7 +1769,7 @@ func TestArm64Compiler_compileCall(t *testing.T) {
 						codeInitialAddress: uintptr(unsafe.Pointer(&code[0])),
 					})
 					env.module().Functions = append(env.module().Functions,
-						&wasm.FunctionInstance{FunctionType: &wasm.TypeInstance{Type: targetFunctionType}, Index: index})
+						&wasm.FunctionInstance{Type: targetFunctionType, Index: index})
 				})
 			}
 
@@ -1830,8 +1830,8 @@ func TestArm64Compiler_compileCallIndirect(t *testing.T) {
 		targetOperation := &wazeroir.OperationCallIndirect{}
 		// Ensure that the module instance has the type information for targetOperation.TypeIndex.
 		compiler.f = &wasm.FunctionInstance{
-			FunctionKind:   wasm.FunctionKindWasm,
-			ModuleInstance: &wasm.ModuleInstance{Types: []*wasm.TypeInstance{{Type: &wasm.FunctionType{}}}},
+			Kind:   wasm.FunctionKindWasm,
+			Module: &wasm.ModuleInstance{Types: []*wasm.TypeInstance{{Type: &wasm.FunctionType{}}}},
 		}
 
 		// Place the offfset value.
@@ -1863,8 +1863,8 @@ func TestArm64Compiler_compileCallIndirect(t *testing.T) {
 		targetOffset := &wazeroir.OperationConstI32{Value: uint32(0)}
 		// Ensure that the module instance has the type information for targetOperation.TypeIndex,
 		compiler.f = &wasm.FunctionInstance{
-			ModuleInstance: &wasm.ModuleInstance{Types: []*wasm.TypeInstance{{Type: &wasm.FunctionType{}}}},
-			FunctionKind:   wasm.FunctionKindWasm,
+			Module: &wasm.ModuleInstance{Types: []*wasm.TypeInstance{{Type: &wasm.FunctionType{}}}},
+			Kind:   wasm.FunctionKindWasm,
 		}
 		// and the typeID doesn't match the table[targetOffset]'s type ID.
 		table := make([]wasm.TableElement, 10)
@@ -1984,7 +1984,7 @@ func TestArm64Compiler_compileCallIndirect(t *testing.T) {
 						err := compiler.compilePreamble()
 						require.NoError(t, err)
 
-						compiler.f = &wasm.FunctionInstance{ModuleInstance: moduleInstance, FunctionKind: wasm.FunctionKindWasm}
+						compiler.f = &wasm.FunctionInstance{Module: moduleInstance, Kind: wasm.FunctionKindWasm}
 
 						// Place the offfset value. Here we try calling a function of functionaddr == table[i].FunctionIndex.
 						err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: uint32(i)})
@@ -2194,7 +2194,7 @@ func TestArm64Compiler_compileModuleContextInitialization(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			env := newJITEnvironment()
 			compiler := env.requireNewCompiler(t)
-			compiler.f.ModuleInstance = tc.moduleInstance
+			compiler.f.Module = tc.moduleInstance
 
 			// The assembler skips the first instruction so we intentionally add NOP here.
 			// TODO: delete after #233
diff --git a/internal/wasm/jit/jit_test.go b/internal/wasm/jit/jit_test.go
index 51630ad7..45ed877b 100644
--- a/internal/wasm/jit/jit_test.go
+++ b/internal/wasm/jit/jit_test.go
@@ -112,8 +112,8 @@ func (j *jitEnv) exec(code []byte) {
 		codeSegment:        code,
 		codeInitialAddress: uintptr(unsafe.Pointer(&code[0])),
 		source: &wasm.FunctionInstance{
-			FunctionKind: wasm.FunctionKindWasm,
-			FunctionType: &wasm.TypeInstance{Type: &wasm.FunctionType{}},
+			Kind: wasm.FunctionKindWasm,
+			Type: &wasm.FunctionType{},
 		},
 	}
 
diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go
index d3534bf8..b1c70fc7 100644
--- a/internal/wasm/module_test.go
+++ b/internal/wasm/module_test.go
@@ -14,13 +14,13 @@ func TestFunctionType_String(t *testing.T) {
 		functype *FunctionType
 		exp      string
 	}{
-		{functype: &FunctionType{}, exp: "null_null"},
-		{functype: &FunctionType{Params: []ValueType{ValueTypeI32}}, exp: "i32_null"},
-		{functype: &FunctionType{Params: []ValueType{ValueTypeI32, ValueTypeF64}}, exp: "i32f64_null"},
-		{functype: &FunctionType{Params: []ValueType{ValueTypeF32, ValueTypeI32, ValueTypeF64}}, exp: "f32i32f64_null"},
-		{functype: &FunctionType{Results: []ValueType{ValueTypeI64}}, exp: "null_i64"},
-		{functype: &FunctionType{Results: []ValueType{ValueTypeI64, ValueTypeF32}}, exp: "null_i64f32"},
-		{functype: &FunctionType{Results: []ValueType{ValueTypeF32, ValueTypeI32, ValueTypeF64}}, exp: "null_f32i32f64"},
+		{functype: &FunctionType{}, exp: "v_v"},
+		{functype: &FunctionType{Params: []ValueType{ValueTypeI32}}, exp: "i32_v"},
+		{functype: &FunctionType{Params: []ValueType{ValueTypeI32, ValueTypeF64}}, exp: "i32f64_v"},
+		{functype: &FunctionType{Params: []ValueType{ValueTypeF32, ValueTypeI32, ValueTypeF64}}, exp: "f32i32f64_v"},
+		{functype: &FunctionType{Results: []ValueType{ValueTypeI64}}, exp: "v_i64"},
+		{functype: &FunctionType{Results: []ValueType{ValueTypeI64, ValueTypeF32}}, exp: "v_i64f32"},
+		{functype: &FunctionType{Results: []ValueType{ValueTypeF32, ValueTypeI32, ValueTypeF64}}, exp: "v_f32i32f64"},
 		{functype: &FunctionType{Params: []ValueType{ValueTypeI32}, Results: []ValueType{ValueTypeI64}}, exp: "i32_i64"},
 		{functype: &FunctionType{Params: []ValueType{ValueTypeI64, ValueTypeF32}, Results: []ValueType{ValueTypeI64, ValueTypeF32}}, exp: "i64f32_i64f32"},
 		{functype: &FunctionType{Params: []ValueType{ValueTypeI64, ValueTypeF32, ValueTypeF64}, Results: []ValueType{ValueTypeF32, ValueTypeI32, ValueTypeF64}}, exp: "i64f32f64_f32i32f64"},
@@ -28,6 +28,8 @@ func TestFunctionType_String(t *testing.T) {
 		tc := tc
 		t.Run(tc.functype.String(), func(t *testing.T) {
 			require.Equal(t, tc.exp, tc.functype.String())
+			require.Equal(t, tc.exp, tc.functype.key())
+			require.Equal(t, tc.exp, tc.functype.string)
 		})
 	}
 }
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index 068f0dad..e24325a5 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -130,7 +130,7 @@ func TestStore_ExportImportedHostFunction(t *testing.T) {
 	_, err := s.NewHostModule("", map[string]interface{}{"host_fn": func(wasm.ModuleContext) {}})
 	require.NoError(t, err)
 
-	t.Run("ModuleInstance is the importing module", func(t *testing.T) {
+	t.Run("Module is the importing module", func(t *testing.T) {
 		_, err = s.Instantiate(&Module{
 			TypeSection:   []*FunctionType{{}},
 			ImportSection: []*Import{{Type: ExternTypeFunc, Name: "host_fn", DescFunc: 0}},
@@ -147,7 +147,7 @@ func TestStore_ExportImportedHostFunction(t *testing.T) {
 		// We expect the host function to be called in context of the importing module.
 		// Otherwise, it would be the pseudo-module of the host, which only includes types and function definitions.
 		// Notably, this ensures the host function call context has the correct memory (from the importing module).
-		require.Equal(t, s.moduleInstances["test"], ei.Function.ModuleInstance)
+		require.Equal(t, s.moduleInstances["test"], ei.Function.Module)
 	})
 }
 
@@ -653,7 +653,7 @@ func TestStore_resolveImports(t *testing.T) {
 		t.Run("signature mismatch", func(t *testing.T) {
 			s := newStore()
 			s.moduleInstances[moduleName] = &ModuleInstance{Exports: map[string]*ExportInstance{name: {
-				Function: &FunctionInstance{FunctionType: &TypeInstance{Type: &FunctionType{}}},
+				Function: &FunctionInstance{Type: &FunctionType{}},
 			}}, Name: moduleName}
 			m := &Module{
 				TypeSection:   []*FunctionType{{Results: []ValueType{ValueTypeF32}}},
@@ -661,11 +661,11 @@ func TestStore_resolveImports(t *testing.T) {
 			}
 			_, _, _, _, _, err := s.resolveImports(m)
 			require.Error(t, err)
-			require.Contains(t, err.Error(), "signature mimatch: null_f32 != null_null")
+			require.Contains(t, err.Error(), "signature mimatch: v_f32 != v_v")
 		})
 		t.Run("ok", func(t *testing.T) {
 			s := newStore()
-			f := &FunctionInstance{FunctionType: &TypeInstance{Type: &FunctionType{Results: []ValueType{ValueTypeF32}}}}
+			f := &FunctionInstance{Type: &FunctionType{Results: []ValueType{ValueTypeF32}}}
 			s.moduleInstances[moduleName] = &ModuleInstance{Exports: map[string]*ExportInstance{name: {
 				Function: f,
 			}}, Name: moduleName}
@@ -903,8 +903,8 @@ func TestModuleInstance_applyElements(t *testing.T) {
 	}
 	targetAddr, targetOffset := uint32(1), byte(0)
 	targetAddr2, targetOffset2 := functionCounts-1, byte(0x8)
-	m.Functions[targetAddr] = &FunctionInstance{FunctionType: &TypeInstance{}, Index: FunctionIndex(targetAddr)}
-	m.Functions[targetAddr2] = &FunctionInstance{FunctionType: &TypeInstance{}, Index: FunctionIndex(targetAddr2)}
+	m.Functions[targetAddr] = &FunctionInstance{Type: &FunctionType{}, Index: FunctionIndex(targetAddr)}
+	m.Functions[targetAddr2] = &FunctionInstance{Type: &FunctionType{}, Index: FunctionIndex(targetAddr2)}
 	m.applyElements([]*ElementSegment{
 		{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{targetOffset}}, Init: []uint32{uint32(targetAddr)}},
 		{OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{targetOffset2}}, Init: []uint32{targetAddr2}},