Repository: tetratelabs/wazero
Pull request #132 (2022-01-14 06:09:04)

jit: support for call_indirect

mathetake: ok done with type comparison -- will mark ready for review after finish documenting 

Patch:
diff --git a/wasm/engine.go b/wasm/engine.go
index 00a27be2..eb274dca 100644
--- a/wasm/engine.go
+++ b/wasm/engine.go
@@ -9,12 +9,4 @@ type Engine interface {
 	Call(f *FunctionInstance, params ...uint64) (results []uint64, err error)
 	// Compile compiles down the function instance.
 	Compile(f *FunctionInstance) error
-	// PreCompile prepares the compilation for given function instances.
-	// This is called for all the instances in a module instance
-	// before Compile is called. That is necessary because
-	// JIT engine needs to assign unique ids to each function instance
-	// before it compiles each function. Concretely, the JIT engine
-	// uses the ids at the time when emitting call instructions against the yet-compiled
-	// function instances.
-	PreCompile(fs []*FunctionInstance) error
 }
diff --git a/wasm/jit/RATIONALE.md b/wasm/jit/RATIONALE.md
index 142e0222..13a34268 100644
--- a/wasm/jit/RATIONALE.md
+++ b/wasm/jit/RATIONALE.md
@@ -61,8 +61,8 @@ mov ... $100 ;; This is the beginning of program *after* function return.
 This way, the engine, which enters the native code via `jitcall`, can know the continuation address of the caller's function frame: 
 
 ```go
-case jitCallStatusCodeCallWasmFunction:
-    nextFunc := e.compiledWasmFunctions[e.functionCallIndex]
+case jitCallStatusCodeCallFunction:
+    nextFunc := e.compiledFunctions[e.functionCallIndex]
     // Calculate the continuation address so
     // we can resume this caller function frame.
     currentFrame.continuationAddress = currentFrame.f.codeInitialAddress + e.continuationAddressOffset
diff --git a/wasm/jit/compiler.go b/wasm/jit/compiler.go
index 21dacef4..8aa91f01 100644
--- a/wasm/jit/compiler.go
+++ b/wasm/jit/compiler.go
@@ -20,8 +20,10 @@ type compiler interface {
 	compileGlobalSet(o *wazeroir.OperationGlobalSet) error
 	compileBr(o *wazeroir.OperationBr) error
 	compileBrIf(o *wazeroir.OperationBrIf) error
+	compileBrTable(o *wazeroir.OperationBrTable) error
 	compileLabel(o *wazeroir.OperationLabel) error
 	compileCall(o *wazeroir.OperationCall) error
+	compileCallIndirect(o *wazeroir.OperationCallIndirect) error
 	compileDrop(o *wazeroir.OperationDrop) error
 	compileSelect() error
 	compilePick(o *wazeroir.OperationPick) error
diff --git a/wasm/jit/engine.go b/wasm/jit/engine.go
index 0bc17bf7..0e566f55 100644
--- a/wasm/jit/engine.go
+++ b/wasm/jit/engine.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"math"
 	"reflect"
+	"runtime/debug"
 	"strings"
 	"unsafe"
 
@@ -31,19 +32,23 @@ type engine struct {
 	stackBasePointer uint64
 	// Where we store the status code of JIT execution.
 	jitCallStatusCode jitCallStatusCode
-	// Set when statusCode == jitStatusCall{Function,BuiltInFunction,HostFunction}
+	// Set when statusCode == jitStatusCall{Function,BuiltInFunction}
 	// Indicating the function call index.
-	functionCallIndex int64
-	// Set when statusCode == jitStatusCall{Function,BuiltInFunction,HostFunction}
+	functionCallAddress wasm.FunctionAddress
+	// Set when statusCode == jitStatusCall{Function,BuiltInFunction}
 	// We use this value to continue the current function
 	// after calling the target function exits.
 	// Instructions after [base+continuationAddressOffset] must start with
 	// restoring reserved registeres.
 	continuationAddressOffset uintptr
-	// The current compiledWasmFunction.globalSliceAddress
+	// The current compiledFunction.globalSliceAddress
 	globalSliceAddress uintptr
 	// memorySliceLen stores the length of memory slice used by the currently executed function.
-	memorySliceLen int
+	memorySliceLen uint64
+	// The current compiledFunction.tableSliceAddress
+	tableSliceAddress uintptr
+	// tableSliceLen stores the length of the unique table used by the currently executed function.
+	tableSliceLen uint64
 	// Function call frames in linked list
 	callFrameStack *callFrame
 	// callFrameNum tracks the current number of call frames.
@@ -52,12 +57,8 @@ type engine struct {
 
 	// The following fields are only used during compilation.
 
-	// Store the compiled functions and indexes.
-	compiledWasmFunctions     []*compiledWasmFunction
-	compiledWasmFunctionIndex map[*wasm.FunctionInstance]int64
-	// Store the host functions and indexes.
-	compiledHostFunctions     []*compiledHostFunction
-	compiledHostFunctionIndex map[*wasm.FunctionInstance]int64
+	// Store the compiled functions.
+	compiledFunctions map[wasm.FunctionAddress]*compiledFunction
 }
 
 // Native code manipulates the engine's fields with these constants.
@@ -66,10 +67,12 @@ const (
 	enginestackPointerOffset        = 24
 	enginestackBasePointerOffset    = 32
 	engineJITCallStatusCodeOffset   = 40
-	engineFunctionCallIndexOffset   = 48
+	engineFunctionCallAddressOffset = 48
 	engineContinuationAddressOffset = 56
 	engineglobalSliceAddressOffset  = 64
 	engineMemorySliceLenOffset      = 72
+	engineTableSliceAddressOffset   = 80
+	engineTableSliceLenOffset       = 88
 )
 
 func (e *engine) Call(f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
@@ -90,6 +93,9 @@ func (e *engine) Call(f *wasm.FunctionInstance, params ...uint64) (results []uin
 	defer func() {
 		if shouldRecover {
 			if v := recover(); v != nil {
+				if buildoptions.IsDebugMode {
+					debug.PrintStack()
+				}
 				top := e.callFrameStack
 				var frames []string
 				var counter int
@@ -113,6 +119,7 @@ func (e *engine) Call(f *wasm.FunctionInstance, params ...uint64) (results []uin
 				e.callFrameStack = nil
 				e.stackBasePointer = 0
 				e.stackPointer = 0
+				e.callFrameNum = 0
 			}
 		}
 	}()
@@ -120,114 +127,37 @@ func (e *engine) Call(f *wasm.FunctionInstance, params ...uint64) (results []uin
 	for _, param := range params {
 		e.push(param)
 	}
-	// Note that there's no conflict between e.hostFunctionIndex and e.compiledWasmFunctionIndex,
-	// meaning that each *wasm.FunctionInstance is assigned to either host function index or wasm function one.
-	if index, ok := e.compiledHostFunctionIndex[f]; ok {
-		e.compiledHostFunctions[index].f(&wasm.HostFunctionCallContext{Memory: f.ModuleInstance.Memory})
-	} else if index, ok := e.compiledWasmFunctionIndex[f]; ok {
-		f := e.compiledWasmFunctions[index]
-		e.exec(f)
-	} else {
+
+	compiled, ok := e.compiledFunctions[f.Address]
+	if !ok {
 		err = fmt.Errorf("function not compiled")
 		return
 	}
+
+	if compiled.isHostFunction() {
+		e.execHostFunction(compiled.source.HostFunction, &wasm.HostFunctionCallContext{Memory: f.ModuleInstance.Memory})
+	} else {
+		e.execFunction(compiled)
+	}
+
 	// Note the top value is the tail of the results,
 	// so we assign them in reverse order.
-	results = make([]uint64, len(f.Signature.Results))
+	results = make([]uint64, len(f.FunctionType.Type.Results))
 	for i := range results {
 		results[len(results)-1-i] = e.pop()
 	}
 	return
 }
 
-// Here we assign unique ids to all the function instances,
-// so we can reference it when we compile each function instance.
-func (e *engine) PreCompile(fs []*wasm.FunctionInstance) error {
-	var newUniqueHostFunctions, newUniqueWasmFunctions int
-	for _, f := range fs {
-		if f.HostFunction != nil {
-			if _, ok := e.compiledHostFunctionIndex[f]; ok {
-				continue
-			}
-			id := getNewID(e.compiledHostFunctionIndex)
-			e.compiledHostFunctionIndex[f] = id
-			newUniqueHostFunctions++
-		} else {
-			if _, ok := e.compiledWasmFunctionIndex[f]; ok {
-				continue
-			}
-			id := getNewID(e.compiledWasmFunctionIndex)
-			e.compiledWasmFunctionIndex[f] = id
-			newUniqueWasmFunctions++
-		}
-	}
-	e.compiledHostFunctions = append(
-		e.compiledHostFunctions,
-		make([]*compiledHostFunction, newUniqueHostFunctions)...,
-	)
-	e.compiledWasmFunctions = append(
-		e.compiledWasmFunctions,
-		make([]*compiledWasmFunction, newUniqueWasmFunctions)...,
-	)
-	return nil
-}
-
-func getNewID(idMap map[*wasm.FunctionInstance]int64) int64 {
-	return int64(len(idMap))
-}
-
 func (e *engine) Compile(f *wasm.FunctionInstance) error {
-	if f.HostFunction != nil {
-		id := e.compiledHostFunctionIndex[f]
-		if e.compiledHostFunctions[id] != nil {
-			// Already compiled.
-			return nil
-		}
-		hf := func(ctx *wasm.HostFunctionCallContext) {
-			tp := f.HostFunction.Type()
-			in := make([]reflect.Value, tp.NumIn())
-			for i := len(in) - 1; i >= 1; i-- {
-				val := reflect.New(tp.In(i)).Elem()
-				raw := e.pop()
-				kind := tp.In(i).Kind()
-				switch kind {
-				case reflect.Float64, reflect.Float32:
-					val.SetFloat(math.Float64frombits(raw))
-				case reflect.Uint32, reflect.Uint64:
-					val.SetUint(raw)
-				case reflect.Int32, reflect.Int64:
-					val.SetInt(int64(raw))
-				}
-				in[i] = val
-			}
-			val := reflect.New(tp.In(0)).Elem()
-			val.Set(reflect.ValueOf(ctx))
-			in[0] = val
-			for _, ret := range f.HostFunction.Call(in) {
-				switch ret.Kind() {
-				case reflect.Float64, reflect.Float32:
-					e.push(math.Float64bits(ret.Float()))
-				case reflect.Uint32, reflect.Uint64:
-					e.push(ret.Uint())
-				case reflect.Int32, reflect.Int64:
-					e.push(uint64(ret.Int()))
-				default:
-					panic("invalid return type")
-				}
-			}
-		}
-		e.compiledHostFunctions[id] = &compiledHostFunction{f: hf, name: f.Name}
+	if f.IsHostFunction() {
+		e.compiledFunctions[f.Address] = &compiledFunction{source: f, memory: f.ModuleInstance.Memory}
 	} else {
-		id := e.compiledWasmFunctionIndex[f]
-		if e.compiledWasmFunctions[id] != nil {
-			// Already compiled.
-			return nil
-		}
 		cf, err := e.compileWasmFunction(f)
 		if err != nil {
 			return fmt.Errorf("failed to compile Wasm function: %w", err)
 		}
-		e.compiledWasmFunctions[id] = cf
+		e.compiledFunctions[f.Address] = cf
 	}
 	return nil
 }
@@ -240,9 +170,8 @@ const initialStackSize = 1024
 
 func newEngine() *engine {
 	e := &engine{
-		stack:                     make([]uint64, initialStackSize),
-		compiledWasmFunctionIndex: make(map[*wasm.FunctionInstance]int64),
-		compiledHostFunctionIndex: make(map[*wasm.FunctionInstance]int64),
+		stack:             make([]uint64, initialStackSize),
+		compiledFunctions: make(map[wasm.FunctionAddress]*compiledFunction),
 	}
 	return e
 }
@@ -260,22 +189,24 @@ func (e *engine) push(v uint64) {
 
 var callStackCeiling = uint64(buildoptions.CallStackCeiling)
 
-func (e *engine) callFramePush(next *callFrame) {
+func (e *engine) callFramePush(callee *callFrame) {
 	e.callFrameNum++
 	if callStackCeiling < e.callFrameNum {
 		panic(wasm.ErrCallStackOverflow)
 	}
 	// Push the new frame to the top of stack.
-	next.caller = e.callFrameStack
-	e.callFrameStack = next
+	callee.caller = e.callFrameStack
+	e.callFrameStack = callee
 
 	// Initialize the callframe-specific fields (stackBasePointer, stackPointer, globalSliceAddress, memorySliceLen).
-	e.stackBasePointer = next.stackBasePointer
+	e.stackBasePointer = callee.stackBasePointer
 	// Set the stack pointer so that base+sp would point to the top of function params.
-	e.stackPointer = next.wasmFunction.paramCount
-	e.globalSliceAddress = next.wasmFunction.globalSliceAddress
-	if next.wasmFunction.memory != nil {
-		e.memorySliceLen = len(next.wasmFunction.memory.Buffer)
+	e.stackPointer = callee.compiledFunction.paramCount
+	e.globalSliceAddress = callee.compiledFunction.globalSliceAddress
+	e.tableSliceLen = callee.compiledFunction.tableSliceLen
+	e.tableSliceAddress = callee.compiledFunction.tableSliceAddress
+	if callee.compiledFunction.memory != nil {
+		e.memorySliceLen = uint64(len(callee.compiledFunction.memory.Buffer))
 	}
 }
 
@@ -291,9 +222,11 @@ func (e *engine) callFramePop() {
 		// so we can go back to the exact state when the caller made the function call.
 		e.stackBasePointer = caller.stackBasePointer
 		e.stackPointer = caller.continuationStackPointer
-		e.globalSliceAddress = caller.wasmFunction.globalSliceAddress
-		if caller.wasmFunction.memory != nil {
-			e.memorySliceLen = len(caller.wasmFunction.memory.Buffer)
+		e.globalSliceAddress = caller.compiledFunction.globalSliceAddress
+		e.tableSliceLen = caller.compiledFunction.tableSliceLen
+		e.tableSliceAddress = caller.compiledFunction.tableSliceAddress
+		if caller.compiledFunction.memory != nil {
+			e.memorySliceLen = uint64(len(caller.compiledFunction.memory.Buffer))
 		}
 	}
 }
@@ -305,30 +238,30 @@ type jitCallStatusCode uint32
 const (
 	// jitStatusReturned means the jitcall reaches the end of function, and returns successfully.
 	jitCallStatusCodeReturned jitCallStatusCode = iota
-	// jitCallStatusCodeCallWasmFunction means the jitcall returns to make a regular Wasm function call.
-	jitCallStatusCodeCallWasmFunction
-	// jitCallStatusCodeCallWasmFunction means the jitcall returns to make a builtin function call.
+	// jitCallStatusCodeCallFunction means the jitcall returns to make a regular Wasm function call.
+	jitCallStatusCodeCallFunction
+	// jitCallStatusCodeCallFunction means the jitcall returns to make a builtin function call.
 	jitCallStatusCodeCallBuiltInFunction
-	// jitCallStatusCodeCallWasmFunction means the jitcall returns to make a host function call.
-	jitCallStatusCodeCallHostFunction
 	// jitCallStatusCodeUnreachable means the function invocation reaches "unreachable" instruction.
 	jitCallStatusCodeUnreachable
 	// jitCallStatusCodeInvalidFloatToIntConversion means a invalid conversion of integer to floats happened.
 	jitCallStatusCodeInvalidFloatToIntConversion
 	// jitCallStatusCodeMemoryOutOfBounds means a out of bounds memory access happened.
 	jitCallStatusCodeMemoryOutOfBounds
+	// jitCallStatusCodeTableOutOfBounds means the offset to table exceeds the length of table during call_indirect.
+	jitCallStatusCodeTableOutOfBounds
+	// jitCallStatusCodeTypeMismatchOnIndirectCall means the type check failed during call_indirect.
+	jitCallStatusCodeTypeMismatchOnIndirectCall
 )
 
 func (s jitCallStatusCode) String() (ret string) {
 	switch s {
 	case jitCallStatusCodeReturned:
 		ret = "returned"
-	case jitCallStatusCodeCallWasmFunction:
-		ret = "call_wasm_function"
+	case jitCallStatusCodeCallFunction:
+		ret = "call_function"
 	case jitCallStatusCodeCallBuiltInFunction:
 		ret = "call_builtin_function"
-	case jitCallStatusCodeCallHostFunction:
-		ret = "call_host_function"
 	case jitCallStatusCodeUnreachable:
 		ret = "unreachable"
 	}
@@ -339,8 +272,7 @@ type callFrame struct {
 	continuationAddress      uintptr
 	continuationStackPointer uint64
 	stackBasePointer         uint64
-	wasmFunction             *compiledWasmFunction
-	hostFunction             *compiledHostFunction
+	compiledFunction         *compiledFunction
 	caller                   *callFrame
 }
 
@@ -352,19 +284,10 @@ func (c *callFrame) String() string {
 }
 
 func (c *callFrame) getFunctionName() string {
-	if c.wasmFunction != nil {
-		return c.wasmFunction.source.Name
-	} else {
-		return c.hostFunction.name
-	}
-}
-
-type compiledHostFunction = struct {
-	f    func(ctx *wasm.HostFunctionCallContext)
-	name string
+	return c.compiledFunction.source.Name
 }
 
-type compiledWasmFunction struct {
+type compiledFunction struct {
 	// The source function instance from which this is compiled.
 	source                  *wasm.FunctionInstance
 	paramCount, resultCount uint64
@@ -381,13 +304,21 @@ type compiledWasmFunction struct {
 	memoryAddress uintptr
 	// globalSliceAddress is like codeInitialAddress, but for .globals.
 	globalSliceAddress uintptr
+	// tableSliceAddress and tableSliceLen are like codeInitialAddress, but for tables.
+	// Note we don't support multiple tables yet.
+	tableSliceAddress uintptr
+	tableSliceLen     uint64
 	// The max of the stack pointer this function can reach. Lazily applied via maybeGrowStack.
 	maxStackPointer uint64
 }
 
+func (f *compiledFunction) isHostFunction() bool {
+	return f.source.HostFunction != nil
+}
+
 const (
-	builtinFunctionIndexMemoryGrow = iota
-	builtinFunctionIndexMemorySize
+	builtinFunctionAddressMemoryGrow wasm.FunctionAddress = iota
+	builtinFunctionAddressMemorySize
 )
 
 // Grow the stack size according to maxStackPointer argument
@@ -408,9 +339,61 @@ func (e *engine) maybeGrowStack(maxStackPointer uint64) {
 	// TODO: maybe better think about how to shrink the stack as well.
 }
 
-func (e *engine) exec(f *compiledWasmFunction) {
+// execHostFunction executes the given host function represented as *reflect.Value.
+//
+// The arguments to the function are popped from the stack stack following the convension of
+// Wasm stack machine.
+// For example, if the host function F requires the (x1 uint32, x2 float32) parameters, and
+// the stack is [..., A, B], then the function is called as F(A, B) where A and B are interpreted
+// as uint32 and float32 respectively.
+//
+// After the execution, the result of host function is pushed onto the stack.
+//
+// ctx parameter is passed to the host function as a first argument.
+func (e *engine) execHostFunction(f *reflect.Value, ctx *wasm.HostFunctionCallContext) {
+	tp := f.Type()
+	in := make([]reflect.Value, tp.NumIn())
+
+	// We pop the value and pass them as arguments in a reverse order according to the
+	// stack machine convension.
+	for i := len(in) - 1; i >= 1; i-- {
+		val := reflect.New(tp.In(i)).Elem()
+		raw := e.pop()
+		kind := tp.In(i).Kind()
+		switch kind {
+		case reflect.Float64, reflect.Float32:
+			val.SetFloat(math.Float64frombits(raw))
+		case reflect.Uint32, reflect.Uint64:
+			val.SetUint(raw)
+		case reflect.Int32, reflect.Int64:
+			val.SetInt(int64(raw))
+		}
+		in[i] = val
+	}
+
+	// Host function must receive *wasm.HostFunctionCallContext as a first argument.
+	val := reflect.New(tp.In(0)).Elem()
+	val.Set(reflect.ValueOf(ctx))
+	in[0] = val
+
+	// Excute the host function and push back the call result onto the stack.
+	for _, ret := range f.Call(in) {
+		switch ret.Kind() {
+		case reflect.Float64, reflect.Float32:
+			e.push(math.Float64bits(ret.Float()))
+		case reflect.Uint32, reflect.Uint64:
+			e.push(ret.Uint())
+		case reflect.Int32, reflect.Int64:
+			e.push(uint64(ret.Int()))
+		default:
+			panic("invalid return type")
+		}
+	}
+}
+
+func (e *engine) execFunction(f *compiledFunction) {
 	// Push a new call frame for the target function.
-	e.callFramePush(&callFrame{continuationAddress: f.codeInitialAddress, wasmFunction: f})
+	e.callFramePush(&callFrame{continuationAddress: f.codeInitialAddress, compiledFunction: f})
 
 	// If the Go-allocated stack is running out, we grow it before calling into JITed code.
 	e.maybeGrowStack(f.maxStackPointer)
@@ -425,7 +408,7 @@ func (e *engine) exec(f *compiledWasmFunction) {
 		jitcall(
 			currentFrame.continuationAddress,
 			uintptr(unsafe.Pointer(e)),
-			currentFrame.wasmFunction.memoryAddress,
+			currentFrame.compiledFunction.memoryAddress,
 		)
 
 		// Check the status code from JIT code.
@@ -434,40 +417,40 @@ func (e *engine) exec(f *compiledWasmFunction) {
 			// Meaning that the current frame exits
 			// so restore the caller's frame.
 			e.callFramePop()
-		case jitCallStatusCodeCallWasmFunction:
-			// This never panics as we made sure that the index exists for all the referenced functions
-			// in a module.
-			nextFunc := e.compiledWasmFunctions[e.functionCallIndex]
+		case jitCallStatusCodeCallFunction:
+			// We consolidate host function calls with normal wasm function calls.
+			// This reduced the cost of checking isHost in the assembly as well as
+			// the cost of doing fully native function calls between wasm functions we will do later.
+			nextFunc := e.compiledFunctions[e.functionCallAddress]
 			// Calculate the continuation address so we can resume this caller function frame.
-			currentFrame.continuationAddress = currentFrame.wasmFunction.codeInitialAddress + e.continuationAddressOffset
-			currentFrame.continuationStackPointer = e.stackPointer + nextFunc.resultCount - nextFunc.paramCount
-			// Create the callee frame.
-			frame := &callFrame{
-				continuationAddress: nextFunc.codeInitialAddress,
-				wasmFunction:        nextFunc,
-				// Set the base pointer to the beginning of the function params
-				stackBasePointer: e.stackBasePointer + e.stackPointer - nextFunc.paramCount,
+			currentFrame.continuationAddress = currentFrame.compiledFunction.codeInitialAddress + e.continuationAddressOffset
+			if nextFunc.isHostFunction() {
+				// Push the call frame for this host function.
+				e.callFrameStack = &callFrame{compiledFunction: nextFunc, caller: currentFrame}
+				// Call into the host function.
+				e.execHostFunction(nextFunc.source.HostFunction, &wasm.HostFunctionCallContext{Memory: nextFunc.memory})
+				// Pop the call frame.
+				e.callFrameStack = currentFrame
+			} else {
+				currentFrame.continuationStackPointer = e.stackPointer + nextFunc.resultCount - nextFunc.paramCount
+				callee := &callFrame{
+					continuationAddress: nextFunc.codeInitialAddress,
+					compiledFunction:    nextFunc,
+					// Set the base pointer to the beginning of the function params
+					stackBasePointer: e.stackBasePointer + e.stackPointer - nextFunc.paramCount,
+				}
+				e.callFramePush(callee)
+				// If the Go-allocated stack is running out, we grow it before calling into JITed code.
+				e.maybeGrowStack(nextFunc.maxStackPointer)
 			}
-			e.callFramePush(frame)
-			// If the Go-allocated stack is running out, we grow it before calling into JITed code.
-			e.maybeGrowStack(nextFunc.maxStackPointer)
 		case jitCallStatusCodeCallBuiltInFunction:
-			switch e.functionCallIndex {
-			case builtinFunctionIndexMemoryGrow:
-				e.builtinFunctionMemoryGrow(currentFrame.wasmFunction)
-			case builtinFunctionIndexMemorySize:
-				e.builtinFunctionMemorySize(currentFrame.wasmFunction)
+			switch e.functionCallAddress {
+			case builtinFunctionAddressMemoryGrow:
+				e.builtinFunctionMemoryGrow(currentFrame.compiledFunction)
+			case builtinFunctionAddressMemorySize:
+				e.builtinFunctionMemorySize(currentFrame.compiledFunction)
 			}
-			currentFrame.continuationAddress = currentFrame.wasmFunction.codeInitialAddress + e.continuationAddressOffset
-		case jitCallStatusCodeCallHostFunction:
-			targetHostFunction := e.compiledHostFunctions[e.functionCallIndex]
-			currentFrame.continuationAddress = currentFrame.wasmFunction.codeInitialAddress + e.continuationAddressOffset
-			// Push the call frame for this host function.
-			e.callFrameStack = &callFrame{hostFunction: targetHostFunction, caller: currentFrame}
-			// Call into the host function.
-			targetHostFunction.f(&wasm.HostFunctionCallContext{Memory: currentFrame.wasmFunction.memory})
-			// Pop the call frame.
-			e.callFrameStack = currentFrame
+			currentFrame.continuationAddress = currentFrame.compiledFunction.codeInitialAddress + e.continuationAddressOffset
 		case jitCallStatusCodeInvalidFloatToIntConversion:
 			// TODO: have wasm.ErrInvalidFloatToIntConversion and use it here.
 			panic("invalid float to int conversion")
@@ -477,11 +460,17 @@ func (e *engine) exec(f *compiledWasmFunction) {
 		case jitCallStatusCodeMemoryOutOfBounds:
 			// TODO: have wasm.ErrMemoryOutOfBounds and use it here.
 			panic("out of bounds memory access")
+		case jitCallStatusCodeTableOutOfBounds:
+			// TODO: have wasm.ErrTableOutOfBounds and use it here.
+			panic("out of bounds table access")
+		case jitCallStatusCodeTypeMismatchOnIndirectCall:
+			// TODO: have wasm.ErrTypeMismatchOnIndirectCall and use it here.
+			panic("type mismatch on indirect function call")
 		}
 	}
 }
 
-func (e *engine) builtinFunctionMemoryGrow(f *compiledWasmFunction) {
+func (e *engine) builtinFunctionMemoryGrow(f *compiledFunction) {
 	newPages := e.pop()
 	max := uint64(math.MaxUint32)
 	if f.memory.Max != nil {
@@ -498,11 +487,11 @@ func (e *engine) builtinFunctionMemoryGrow(f *compiledWasmFunction) {
 	}
 }
 
-func (e *engine) builtinFunctionMemorySize(f *compiledWasmFunction) {
+func (e *engine) builtinFunctionMemorySize(f *compiledFunction) {
 	e.push(uint64(len(f.memory.Buffer)) / wasm.PageSize)
 }
 
-func (e *engine) compileWasmFunction(f *wasm.FunctionInstance) (*compiledWasmFunction, error) {
+func (e *engine) compileWasmFunction(f *wasm.FunctionInstance) (*compiledFunction, error) {
 	ir, err := wazeroir.Compile(f)
 	if err != nil {
 		return nil, fmt.Errorf("failed to lower to wazeroir: %w", err)
@@ -534,11 +523,11 @@ func (e *engine) compileWasmFunction(f *wasm.FunctionInstance) (*compiledWasmFun
 		case *wazeroir.OperationBrIf:
 			err = compiler.compileBrIf(o)
 		case *wazeroir.OperationBrTable:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compileBrTable(o)
 		case *wazeroir.OperationCall:
 			err = compiler.compileCall(o)
 		case *wazeroir.OperationCallIndirect:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compileCallIndirect(o)
 		case *wazeroir.OperationDrop:
 			err = compiler.compileDrop(o)
 		case *wazeroir.OperationSelect:
@@ -678,11 +667,11 @@ func (e *engine) compileWasmFunction(f *wasm.FunctionInstance) (*compiledWasmFun
 		fmt.Printf("compiled code in hex: %s\n", hex.EncodeToString(code))
 	}
 
-	cf := &compiledWasmFunction{
+	cf := &compiledFunction{
 		source:          f,
 		codeSegment:     code,
-		paramCount:      uint64(len(f.Signature.Params)),
-		resultCount:     uint64(len(f.Signature.Results)),
+		paramCount:      uint64(len(f.FunctionType.Type.Params)),
+		resultCount:     uint64(len(f.FunctionType.Type.Results)),
 		memory:          f.ModuleInstance.Memory,
 		maxStackPointer: maxStackPointer,
 	}
@@ -692,6 +681,15 @@ func (e *engine) compileWasmFunction(f *wasm.FunctionInstance) (*compiledWasmFun
 	if len(f.ModuleInstance.Globals) > 0 {
 		cf.globalSliceAddress = uintptr(unsafe.Pointer(&f.ModuleInstance.Globals[0]))
 	}
+	if tables := f.ModuleInstance.Tables; len(tables) > 0 {
+		// WebAssembly 1.0 (MVP) has at most 1 table
+		// See https://www.w3.org/TR/wasm-core-1/#tables%E2%91%A0
+		table := tables[0]
+		if len(table.Table) > 0 {
+			cf.tableSliceAddress = uintptr(unsafe.Pointer(&table.Table[0]))
+		}
+		cf.tableSliceLen = uint64(len(table.Table))
+	}
 	cf.codeInitialAddress = uintptr(unsafe.Pointer(&cf.codeSegment[0]))
 	return cf, nil
 }
diff --git a/wasm/jit/jit_amd64.go b/wasm/jit/jit_amd64.go
index 33450348..f55c3306 100644
--- a/wasm/jit/jit_amd64.go
+++ b/wasm/jit/jit_amd64.go
@@ -161,18 +161,25 @@ func (c *amd64Compiler) generate() ([]byte, uint64, error) {
 	if err != nil {
 		return nil, 0, err
 	}
-	// As we cannot read RIP register directly,
-	// we calculate now the offset to the next instruction
-	// relative to the beginning of this function body.
+	// As we cannot read RIP register directly, we calculate now the offset to the next
+	// instruction after return instruction for function call.
 	const operandSizeBytes = 8
-	for _, obj := range c.requireFunctionCallReturnAddressOffsetResolution {
+	for _, inst := range c.requireFunctionCallReturnAddressOffsetResolution {
+		afterReturnInst := inst
+		// Iterate the linked list of instructions until we find the return instruction.
+		for ; afterReturnInst != nil; afterReturnInst = afterReturnInst.Link {
+			if afterReturnInst.As == obj.ARET {
+				// Now we found the return instruction, move forward once again.
+				afterReturnInst = afterReturnInst.Link
+				break
+			}
+		}
+		if afterReturnInst == nil {
+			return nil, 0, fmt.Errorf("invalid function call at %v", inst)
+		}
 		// Skip MOV, and the register(rax): "0x49, 0xbd"
-		start := obj.Pc + 2
-		// obj.Link = setting offset to memory
-		// obj.Link.Link = writing back the stack pointer to eng.stackPointer.
-		// obj.Link.Link.Link = Return instruction.
-		// Therefore obj.Link.Link.Link.Link means the next instruction after the return.
-		afterReturnInst := obj.Link.Link.Link.Link
+		start := inst.Pc + 2
+		// Set the move target to the program counter (offset to the beginning to this binary).
 		binary.LittleEndian.PutUint64(code[start:start+operandSizeBytes], uint64(afterReturnInst.Pc))
 	}
 
@@ -187,7 +194,7 @@ func (c *amd64Compiler) generate() ([]byte, uint64, error) {
 }
 
 func (c *amd64Compiler) pushFunctionParams() {
-	for _, t := range c.f.Signature.Params {
+	for _, t := range c.f.FunctionType.Type.Params {
 		loc := c.locationStack.pushValueOnStack()
 		switch t {
 		case wasm.ValueTypeI32, wasm.ValueTypeI64:
@@ -542,6 +549,7 @@ func (c *amd64Compiler) compileBrIf(o *wazeroir.OperationBrIf) error {
 		jmpWithCond = c.newProg()
 		jmpWithCond.As = x86.AJNE
 		jmpWithCond.To.Type = obj.TYPE_BRANCH
+		c.locationStack.markRegisterUnused(cond.register)
 	}
 
 	// Make sure that the next coming label is the else jump target.
@@ -625,6 +633,10 @@ func (c *amd64Compiler) compileBrIf(o *wazeroir.OperationBrIf) error {
 	return nil
 }
 
+func (c *amd64Compiler) compileBrTable(o *wazeroir.OperationBrTable) error {
+	panic("TODO implement!")
+}
+
 // If a jump target has multiple callesr (origins),
 // we must have unique register states, so this function
 // must be called before such jump instruction.
@@ -693,25 +705,141 @@ func (c *amd64Compiler) compileCall(o *wazeroir.OperationCall) error {
 	}
 
 	target := c.f.ModuleInstance.Functions[o.FunctionIndex]
-	if target.HostFunction != nil {
-		index := c.eng.compiledHostFunctionIndex[target]
-		if err := c.callHostFunctionFromConstIndex(index); err != nil {
-			return err
-		}
-	} else {
-		index := c.eng.compiledWasmFunctionIndex[target]
-		if err := c.callFunctionFromConstIndex(index); err != nil {
-			return err
+	if err := c.compileFunctionCallFromAddress(jitCallStatusCodeCallFunction, target.Address); err != nil {
+		return err
+	}
+
+	// We consumed the function parameters from the stack after call.
+	for i := 0; i < len(target.FunctionType.Type.Params); i++ {
+		c.locationStack.pop()
+	}
+
+	// Also, the function results were pushed by the call.
+	for _, t := range target.FunctionType.Type.Results {
+		loc := c.locationStack.pushValueOnStack()
+		switch t {
+		case wasm.ValueTypeI32, wasm.ValueTypeI64:
+			loc.setRegisterType(generalPurposeRegisterTypeInt)
+		case wasm.ValueTypeF32, wasm.ValueTypeF64:
+			loc.setRegisterType(generalPurposeRegisterTypeFloat)
 		}
 	}
+	return nil
+}
+
+const (
+	tableElementFunctionAddressOffest = 0
+	tableElementTypeIDOffest          = 8
+)
+
+// compileCallIndirect adds instructions to perform call_indirect operation.
+// This consumes the one value from the top of stack (called "offset"),
+// and make a function call against the function whose function address equals "table[offset]".
+//
+// Note: This is called indirect function call in the sense that the target function is indirectly
+// determined by the current state (top value) of the stack.
+// Therefore, two checks are performed at runtime before entering the target function:
+// 1) If "offset" exceeds the length of table, "out of bounds table access" states (jitCallStatusCodeTableOutOfBounds) is returned.
+// 2) If the type of the function table[offset] doesn't match the specified function type, "type mismatch" status (jitCallStatusCodeTypeMismatchOnIndirectCall) is returned.
+// Otherwise, we successfully enter the target function.
+//
+// Note: WebAssembly 1.0 (MVP) supports at most one table, so this doesn't support multiple tables.
+func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) error {
+	offset := c.locationStack.pop()
+	if err := c.ensureOnGeneralPurposeRegister(offset); err != nil {
+		return nil
+	}
+
+	// First, we need to check if the offset doesn't exceed the length of table.
+	cmpLength := c.newProg()
+	cmpLength.As = x86.ACMPQ
+	cmpLength.To.Type = obj.TYPE_REG
+	cmpLength.To.Reg = offset.register
+	cmpLength.From.Type = obj.TYPE_MEM
+	cmpLength.From.Reg = reservedRegisterForEngine
+	cmpLength.From.Offset = engineTableSliceLenOffset
+	c.addInstruction(cmpLength)
+
+	notLengthExceedJump := c.newProg()
+	notLengthExceedJump.To.Type = obj.TYPE_BRANCH
+	notLengthExceedJump.As = x86.AJCC
+	c.addInstruction(notLengthExceedJump)
+
+	// If it exceeds, we return the function with jitCallStatusCodeTableOutOfBounds.
+	c.setJITStatus(jitCallStatusCodeTableOutOfBounds)
+	c.returnFunction()
+
+	// Next we check if the target's type matches the operation's one.
+	// In order to get the type instance's address, we have to multiply the offset
+	// by 16 as the offset is the "length" of table in Go's "[]wasm.TableElement",
+	// and size of wasm.TableInstance equals 128 bit (64-bit wasm.FunctionAddress and 64-bit wasm.TypeID).
+	getTypeInstanceAddress := c.newProg()
+	notLengthExceedJump.To.SetTarget(getTypeInstanceAddress)
+	getTypeInstanceAddress.As = x86.ASHLQ
+	getTypeInstanceAddress.To.Type = obj.TYPE_REG
+	getTypeInstanceAddress.To.Reg = offset.register
+	getTypeInstanceAddress.From.Type = obj.TYPE_CONST
+	getTypeInstanceAddress.From.Offset = 4
+	c.addInstruction(getTypeInstanceAddress)
+
+	// Adds the address of wasm.TableInstance[0] stored as engine.tableSliceAddress to the offset.
+	movTableSliceAddress := c.newProg()
+	movTableSliceAddress.As = x86.AADDQ
+	movTableSliceAddress.To.Type = obj.TYPE_REG
+	movTableSliceAddress.To.Reg = offset.register
+	movTableSliceAddress.From.Type = obj.TYPE_MEM
+	movTableSliceAddress.From.Reg = reservedRegisterForEngine
+	movTableSliceAddress.From.Offset = engineTableSliceAddressOffset
+	c.addInstruction(movTableSliceAddress)
+
+	// At this point offset.register holds the address of wasm.TableElement at wasm.TableInstance[offset]
+	// So the target type ID lives at offset+tableElementTypeIDOffest, and we compare it
+	// with targetFunctionType.TypeID.
+	targetFunctionType := c.f.ModuleInstance.Types[o.TypeIndex]
+	cmpTypeID := c.newProg()
+	cmpTypeID.As = x86.ACMPQ
+	cmpTypeID.From.Type = obj.TYPE_MEM
+	cmpTypeID.From.Reg = offset.register
+	cmpTypeID.From.Offset = tableElementTypeIDOffest
+	cmpTypeID.To.Type = obj.TYPE_CONST
+	cmpTypeID.To.Offset = int64(targetFunctionType.TypeID)
+	c.addInstruction(cmpTypeID)
+
+	// Jump if the type matches.
+	typeMatchJump := c.newProg()
+	typeMatchJump.To.Type = obj.TYPE_BRANCH
+	typeMatchJump.As = x86.AJEQ
+	c.addInstruction(typeMatchJump)
+
+	// Otherwise, we return the function with jitCallStatusCodeTypeMismatchOnIndirectCall.
+	c.setJITStatus(jitCallStatusCodeTypeMismatchOnIndirectCall)
+	c.returnFunction()
+
+	// Now all checks passeed, so we start making function call.
+	readValue := c.newProg()
+	typeMatchJump.To.SetTarget(readValue)
+	readValue.As = x86.AMOVQ
+	readValue.To.Type = obj.TYPE_REG
+	readValue.To.Reg = offset.register
+	readValue.From.Offset = tableElementFunctionAddressOffest
+	readValue.From.Type = obj.TYPE_MEM
+	readValue.From.Reg = offset.register
+	c.addInstruction(readValue)
+
+	if err := c.compileFunctionCallFromRegister(offset.register); 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)
 
 	// We consumed the function parameters from the stack after call.
-	for i := 0; i < len(target.Signature.Params); i++ {
+	for i := 0; i < len(targetFunctionType.Type.Params); i++ {
 		c.locationStack.pop()
 	}
 
 	// Also, the function results were pushed by the call.
-	for _, t := range target.Signature.Results {
+	for _, t := range targetFunctionType.Type.Results {
 		loc := c.locationStack.pushValueOnStack()
 		switch t {
 		case wasm.ValueTypeI32, wasm.ValueTypeI64:
@@ -3802,7 +3930,7 @@ func (c *amd64Compiler) compileMemoryGrow() error {
 	if err := c.maybeMoveTopConditionalToFreeGeneralPurposeRegister(); err != nil {
 		return err
 	}
-	return c.callBuiltinFunctionFromConstIndex(builtinFunctionIndexMemoryGrow)
+	return c.compileFunctionCallFromAddress(jitCallStatusCodeCallBuiltInFunction, builtinFunctionAddressMemoryGrow)
 }
 
 func (c *amd64Compiler) compileMemorySize() error {
@@ -3811,7 +3939,7 @@ func (c *amd64Compiler) compileMemorySize() error {
 	if err := c.maybeMoveTopConditionalToFreeGeneralPurposeRegister(); err != nil {
 		return err
 	}
-	if err := c.callBuiltinFunctionFromConstIndex(builtinFunctionIndexMemorySize); err != nil {
+	if err := c.compileFunctionCallFromAddress(jitCallStatusCodeCallBuiltInFunction, builtinFunctionAddressMemorySize); err != nil {
 		return err
 	}
 	loc := c.locationStack.pushValueOnStack() // The size is pushed on the top.
@@ -3819,20 +3947,6 @@ func (c *amd64Compiler) compileMemorySize() error {
 	return nil
 }
 
-func (c *amd64Compiler) callBuiltinFunctionFromConstIndex(index int64) error {
-	c.setJITStatus(jitCallStatusCodeCallBuiltInFunction)
-	c.setFunctionCallIndexFromConst(index)
-	// Release all the registers as our calling convention requires the callee-save.
-	if err := c.releaseAllRegistersToStack(); err != nil {
-		return err
-	}
-	c.setContinuationOffsetAtNextInstructionAndReturn()
-	// Once we return from the function call,
-	// we must setup the reserved registers again.
-	c.initializeReservedRegisters()
-	return nil
-}
-
 func (c *amd64Compiler) compileConstI32(o *wazeroir.OperationConstI32) error {
 	// If the top value is conditional one, we must save it before executing the following instructions
 	// as they clear the conditional flag, meaning that the conditional value might change.
@@ -4115,11 +4229,21 @@ func (c *amd64Compiler) setJITStatus(status jitCallStatusCode) {
 	c.addInstruction(prog)
 }
 
-func (c *amd64Compiler) callHostFunctionFromConstIndex(index int64) error {
-	// Set the jit status as jitCallStatusCodeCallHostFunction
-	c.setJITStatus(jitCallStatusCodeCallHostFunction)
-	// Set the function index.
-	c.setFunctionCallIndexFromConst(index)
+// compileFunctionCallFromAddress adds instructions to call a function whose address equals the addr parameter.
+// jitStatus is set before making call, and it should either jitCallStatusCodeCallBuiltInFunction or
+// jitCallStatusCodeCallFunction.
+func (c *amd64Compiler) compileFunctionCallFromAddress(jitStatus jitCallStatusCode, addr wasm.FunctionAddress) error {
+	c.setJITStatus(jitStatus)
+
+	prog := c.newProg()
+	prog.As = x86.AMOVQ
+	prog.From.Type = obj.TYPE_CONST
+	prog.From.Offset = int64(addr)
+	prog.To.Type = obj.TYPE_MEM
+	prog.To.Reg = reservedRegisterForEngine
+	prog.To.Offset = engineFunctionCallAddressOffset
+	c.addInstruction(prog)
+
 	// Release all the registers as our calling convention requires the callee-save.
 	if err := c.releaseAllRegistersToStack(); err != nil {
 		return err
@@ -4131,11 +4255,20 @@ func (c *amd64Compiler) callHostFunctionFromConstIndex(index int64) error {
 	return nil
 }
 
-func (c *amd64Compiler) callFunctionFromConstIndex(index int64) error {
-	// Set the jit status as jitCallStatusCodeCallWasmFunction
-	c.setJITStatus(jitCallStatusCodeCallWasmFunction)
-	// Set the function index.
-	c.setFunctionCallIndexFromConst(index)
+// compileFunctionCallFromRegister adds instructions to call a function whose address equals the value on
+// the functionCallAddressRegister.
+func (c *amd64Compiler) compileFunctionCallFromRegister(functionCallAddressRegister int16) error {
+	c.setJITStatus(jitCallStatusCodeCallFunction)
+
+	setFunctionAddressFromReg := c.newProg()
+	setFunctionAddressFromReg.As = x86.AMOVQ
+	setFunctionAddressFromReg.From.Type = obj.TYPE_REG
+	setFunctionAddressFromReg.From.Reg = functionCallAddressRegister
+	setFunctionAddressFromReg.To.Type = obj.TYPE_MEM
+	setFunctionAddressFromReg.To.Reg = reservedRegisterForEngine
+	setFunctionAddressFromReg.To.Offset = engineFunctionCallAddressOffset
+	c.addInstruction(setFunctionAddressFromReg)
+
 	// Release all the registers as our calling convention requires the callee-save.
 	if err := c.releaseAllRegistersToStack(); err != nil {
 		return err
@@ -4164,11 +4297,6 @@ func (c *amd64Compiler) releaseAllRegistersToStack() error {
 	return nil
 }
 
-// TODO: If this function call is the tail call,
-// we don't need to return back to this function.
-// Maybe better have another status for that case
-// so that we don't call back again to this function
-// and instead just release the call frame.
 func (c *amd64Compiler) setContinuationOffsetAtNextInstructionAndReturn() {
 	// setContinuationOffsetAtNextInstructionAndReturn is called after releasing
 	// all the registers, so at this point we always have free registers.
@@ -4210,18 +4338,7 @@ func (c *amd64Compiler) setFunctionCallIndexFromRegister(reg int16) {
 	prog.From.Reg = reg
 	prog.To.Type = obj.TYPE_MEM
 	prog.To.Reg = reservedRegisterForEngine
-	prog.To.Offset = engineFunctionCallIndexOffset
-	c.addInstruction(prog)
-}
-
-func (c *amd64Compiler) setFunctionCallIndexFromConst(index int64) {
-	prog := c.newProg()
-	prog.As = x86.AMOVL
-	prog.From.Type = obj.TYPE_CONST
-	prog.From.Offset = index
-	prog.To.Type = obj.TYPE_MEM
-	prog.To.Reg = reservedRegisterForEngine
-	prog.To.Offset = engineFunctionCallIndexOffset
+	prog.To.Offset = engineFunctionCallAddressOffset
 	c.addInstruction(prog)
 }
 
diff --git a/wasm/jit/jit_value_location_amd64.go b/wasm/jit/jit_value_location_amd64.go
index 73cbe506..95e57003 100644
--- a/wasm/jit/jit_value_location_amd64.go
+++ b/wasm/jit/jit_value_location_amd64.go
@@ -147,7 +147,11 @@ func (v *valueLocationStack) String() string {
 	for i := uint64(0); i < v.sp; i++ {
 		stackStr = append(stackStr, v.stack[i].String())
 	}
-	return fmt.Sprintf("sp=%d, stack=[%s]", v.sp, strings.Join(stackStr, ","))
+	var usedRegisters []string
+	for reg := range v.usedRegisters {
+		usedRegisters = append(usedRegisters, fmt.Sprintf("%d", reg))
+	}
+	return fmt.Sprintf("sp=%d, stack=[%s], used_registers=[%s]", v.sp, strings.Join(stackStr, ","), strings.Join(usedRegisters, ","))
 }
 
 func (s *valueLocationStack) clone() *valueLocationStack {
diff --git a/wasm/leb128/leb128.go b/wasm/leb128/leb128.go
index 0243a8f5..bbba5d89 100644
--- a/wasm/leb128/leb128.go
+++ b/wasm/leb128/leb128.go
@@ -41,7 +41,7 @@ func EncodeUint32(value uint32) (buf []byte) {
 	}
 }
 
-func DecodeUint32(r io.Reader) (ret uint32, num uint64, err error) {
+func DecodeUint32(r io.Reader) (ret uint32, bytesRead uint64, err error) {
 	// Derived from https://github.com/golang/go/blob/aafad20b617ee63d58fcd4f6e0d98fe27760678c/src/encoding/binary/varint.go
 	// with the modification on the overflow handling tailored for 32-bits.
 	var s uint32
@@ -63,7 +63,7 @@ func DecodeUint32(r io.Reader) (ret uint32, num uint64, err error) {
 	return 0, 0, errOverflow32
 }
 
-func DecodeUint64(r io.Reader) (ret uint64, num uint64, err error) {
+func DecodeUint64(r io.Reader) (ret uint64, bytesRead uint64, err error) {
 	// Derived from https://github.com/golang/go/blob/aafad20b617ee63d58fcd4f6e0d98fe27760678c/src/encoding/binary/varint.go
 	var s uint64
 	for i := 0; i < maxVarintLen64; i++ {
@@ -84,7 +84,7 @@ func DecodeUint64(r io.Reader) (ret uint64, num uint64, err error) {
 	return 0, 0, errOverflow64
 }
 
-func DecodeInt32(r io.Reader) (ret int32, num uint64, err error) {
+func DecodeInt32(r io.Reader) (ret int32, bytesRead uint64, err error) {
 	var shift int
 	var b byte
 	for {
@@ -94,18 +94,18 @@ func DecodeInt32(r io.Reader) (ret int32, num uint64, err error) {
 		}
 		ret |= (int32(b) & 0x7f) << shift
 		shift += 7
-		num++
+		bytesRead++
 		if b&0x80 == 0 {
 			if shift < 32 && (b&0x40) != 0 {
 				ret |= ^0 << shift
 			}
 			// Over flow checks.
 			// fixme: can be optimized.
-			if num > 5 {
+			if bytesRead > 5 {
 				return 0, 0, errOverflow32
-			} else if unused := b & 0b00110000; num == 5 && ret < 0 && unused != 0b00110000 {
+			} else if unused := b & 0b00110000; bytesRead == 5 && ret < 0 && unused != 0b00110000 {
 				return 0, 0, errOverflow32
-			} else if num == 5 && ret >= 0 && unused != 0x00 {
+			} else if bytesRead == 5 && ret >= 0 && unused != 0x00 {
 				return 0, 0, errOverflow32
 			}
 			return
@@ -113,7 +113,11 @@ func DecodeInt32(r io.Reader) (ret int32, num uint64, err error) {
 	}
 }
 
-func DecodeInt33AsInt64(r io.Reader) (ret int64, num uint64, err error) {
+// DecodeInt33AsInt64 is a special cased decoder for wasm.BlockType which is encoded as a positive signed integer, yet
+// still needs to fit the 32-bit range of allowed indices. Hence, this is 33, not 32-bit!
+//
+// See https://webassembly.github.io/spec/core/binary/instructions.html#control-instructions
+func DecodeInt33AsInt64(r io.Reader) (ret int64, bytesRead uint64, err error) {
 	const (
 		int33Mask  int64 = 1 << 7
 		int33Mask2       = ^int33Mask
@@ -133,7 +137,7 @@ func DecodeInt33AsInt64(r io.Reader) (ret int64, num uint64, err error) {
 		b = int64(rb)
 		ret |= (b & int33Mask2) << shift
 		shift += 7
-		num++
+		bytesRead++
 		if b&int33Mask == 0 {
 			break
 		}
@@ -151,17 +155,17 @@ func DecodeInt33AsInt64(r io.Reader) (ret int64, num uint64, err error) {
 	}
 	// Over flow checks.
 	// fixme: can be optimized.
-	if num > 5 {
+	if bytesRead > 5 {
 		return 0, 0, errOverflow33
-	} else if unused := b & 0b00100000; num == 5 && ret < 0 && unused != 0b00100000 {
+	} else if unused := b & 0b00100000; bytesRead == 5 && ret < 0 && unused != 0b00100000 {
 		return 0, 0, errOverflow33
-	} else if num == 5 && ret >= 0 && unused != 0x00 {
+	} else if bytesRead == 5 && ret >= 0 && unused != 0x00 {
 		return 0, 0, errOverflow33
 	}
-	return ret, num, nil
+	return ret, bytesRead, nil
 }
 
-func DecodeInt64(r io.Reader) (ret int64, num uint64, err error) {
+func DecodeInt64(r io.Reader) (ret int64, bytesRead uint64, err error) {
 	const (
 		int64Mask3 = 1 << 6
 		int64Mask4 = ^0
@@ -175,18 +179,18 @@ func DecodeInt64(r io.Reader) (ret int64, num uint64, err error) {
 		}
 		ret |= (int64(b) & 0x7f) << shift
 		shift += 7
-		num++
+		bytesRead++
 		if b&0x80 == 0 {
 			if shift < 64 && (b&int64Mask3) == int64Mask3 {
 				ret |= int64Mask4 << shift
 			}
 			// Over flow checks.
 			// fixme: can be optimized.
-			if num > 10 {
+			if bytesRead > 10 {
 				return 0, 0, errOverflow64
-			} else if unused := b & 0b00111110; num == 10 && ret < 0 && unused != 0b00111110 {
+			} else if unused := b & 0b00111110; bytesRead == 10 && ret < 0 && unused != 0b00111110 {
 				return 0, 0, errOverflow64
-			} else if num == 10 && ret >= 0 && unused != 0x00 {
+			} else if bytesRead == 10 && ret >= 0 && unused != 0x00 {
 				return 0, 0, errOverflow64
 			}
 			return
diff --git a/wasm/module.go b/wasm/module.go
index 5a2e40cd..eb490b7b 100644
--- a/wasm/module.go
+++ b/wasm/module.go
@@ -4,14 +4,15 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-
-	"github.com/tetratelabs/wazero/wasm/leb128"
 )
 
-const (
-	magic   = "\x00asm"
-	version = "\x01\x00\x00\x00"
-)
+// magic is the 4 byte preamble (literally "\0asm") of the binary format
+// See https://www.w3.org/TR/wasm-core-1/#binary-magic
+var magic = []byte{0x00, 0x61, 0x73, 0x6D}
+
+// version is format version and doesn't change between known specification versions
+// See https://www.w3.org/TR/wasm-core-1/#binary-version
+var version = []byte{0x01, 0x00, 0x00, 0x00}
 
 type reader struct {
 	binary []byte
@@ -90,11 +91,29 @@ type Module struct {
 	// CodeSection is index-correlated with FunctionSection and contains each function's locals and body.
 	//
 	// See https://www.w3.org/TR/wasm-core-1/#code-section%E2%91%A0
-	CodeSection    []*CodeSegment
-	DataSection    []*DataSegment
+	CodeSection []*CodeSegment
+	DataSection []*DataSegment
+	// NameSection is set when the custom section "name" was successfully decoded from the binary format.
+	//
+	// Note: This is the only custom section defined in the WebAssembly 1.0 (MVP) Binary Format. Others are in
+	// CustomSections
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#name-section%E2%91%A0
+	NameSection *NameSection
+	// CustomSections is set when at least one non-standard, or otherwise unsupported custom section was found in the
+	// binary format.
+	//
+	// Note: This never contains a "name" because that is standard and parsed into the NameSection.
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#custom-section%E2%91%A0
 	CustomSections map[string][]byte
 }
 
+// Encode encodes the given module into a byte slice. The result can be used directly or saved as a %.wasm file.
+func (m *Module) Encode() (bytes []byte) {
+	return m.encodeSections(append(magic, version...))
+}
+
 // DecodeModule decodes a `raw` module from io.Reader whose index spaces are yet to be initialized
 func DecodeModule(binary []byte) (*Module, error) {
 	r := &reader{binary: binary, buffer: bytes.NewBuffer(binary)}
@@ -120,7 +139,7 @@ func DecodeModule(binary []byte) (*Module, error) {
 		}
 	}
 
-	ret := &Module{CustomSections: map[string][]byte{}}
+	ret := &Module{}
 	if err := ret.readSections(r); err != nil {
 		return nil, fmt.Errorf("readSections failed: %w", err)
 	}
@@ -130,61 +149,3 @@ func DecodeModule(binary []byte) (*Module, error) {
 	}
 	return ret, nil
 }
-
-func (m *Module) DecodeCustomNameSection() (map[uint32]string, error) {
-	namesec, ok := m.CustomSections["name"]
-	if !ok {
-		return nil, fmt.Errorf("'name' %w", ErrCustomSectionNotFound)
-	}
-
-	r := bytes.NewReader(namesec)
-	for {
-		id, err := r.ReadByte()
-		if err != nil {
-			return nil, fmt.Errorf("failed to read subsection ID: %w", err)
-		}
-
-		size, _, err := leb128.DecodeUint32(r)
-		if err != nil {
-			return nil, fmt.Errorf("failed to read the size of subsection %d: %w", id, err)
-		}
-
-		if id == 1 {
-			// ID = 1 is the function name subsection.
-			break
-		} else {
-			// Skip other subsections.
-			_, err := r.Seek(int64(size), io.SeekCurrent)
-			if err != nil {
-				return nil, fmt.Errorf("failed to skip subsection %d: %w", id, err)
-			}
-		}
-	}
-
-	nameVectorSize, _, err := leb128.DecodeUint32(r)
-	if err != nil {
-		return nil, fmt.Errorf("failed to read the size of name vector: %w", err)
-	}
-
-	ret := make(map[uint32]string, nameVectorSize)
-	for i := uint32(0); i < nameVectorSize; i++ {
-		functionIndex, _, err := leb128.DecodeUint32(r)
-		if err != nil {
-			return nil, fmt.Errorf("failed to read function index: %w", err)
-		}
-
-		functionNameSize, _, err := leb128.DecodeUint32(r)
-		if err != nil {
-			return nil, fmt.Errorf("failed to read function name size: %w", err)
-		}
-
-		namebuf := make([]byte, functionNameSize)
-		_, err = io.ReadFull(r, namebuf)
-		if err != nil {
-			return nil, fmt.Errorf("failed to read function name: %w", err)
-		}
-		ret[functionIndex] = string(namebuf)
-	}
-
-	return ret, nil
-}
diff --git a/wasm/names.go b/wasm/names.go
new file mode 100644
index 00000000..f193c75c
--- /dev/null
+++ b/wasm/names.go
@@ -0,0 +1,282 @@
+package wasm
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"sort"
+
+	"github.com/tetratelabs/wazero/wasm/leb128"
+)
+
+// NameSection represent the known custom name subsections defined in the WebAssembly Binary Format
+// See https://www.w3.org/TR/wasm-core-1/#name-section%E2%91%A0
+// See https://github.com/tetratelabs/wazero/issues/134 about adding this to Module
+type NameSection struct {
+	// ModuleName is the symbolic identifier for a module. Ex. math
+	// The corresponding subsection is subsectionIDModuleName.
+	ModuleName string
+	// FunctionNames is an association of a function index to its symbolic identifier. Ex. add
+	// The corresponding subsection is subsectionIDFunctionNames.
+	//
+	// * the key (idx) is in the function namespace, where module defined functions are preceded by imported ones.
+	//
+	// Ex. Assuming the below text format is the second import, you would expect FunctionNames[1] = "mul"
+	//	(import "Math" "Mul" (func $mul (param $x f32) (param $y f32) (result f32)))
+	//
+	// Note: FunctionNames are a map because the specification requires function indices to be unique. These are sorted
+	// during EncodeData
+	FunctionNames map[uint32]string
+
+	// LocalNames is an association of a function index to any locals which have a symbolic identifier. Ex. add x
+	// The corresponding subsection is subsectionIDLocalNames.
+	//
+	// * the key (funcIndex) is in the function namespace, where module defined functions are preceded by imported ones.
+	// * the second key (idx) is in the local namespace, where parameters precede any function locals.
+	//
+	// Ex. Assuming the below text format is the second import, you would expect LocalNames[1][1] = "y"
+	//	(import "Math" "Mul" (func $mul (param $x f32) (param $y f32) (result f32)))
+	//
+	// Note: LocalNames are a map because the specification requires both function and local indices to be unique. These
+	// are sorted during EncodeData
+	LocalNames map[uint32]map[uint32]string
+}
+
+const (
+	// subsectionIDModuleName contains only the module name.
+	subsectionIDModuleName = uint8(0)
+	// subsectionIDFunctionNames is a map of indices to function names, in ascending order by function index
+	subsectionIDFunctionNames = uint8(1)
+	// subsectionIDLocalNames contain a map of function indices to a map of local indices to their names, in ascending
+	// order by function and local index
+	subsectionIDLocalNames = uint8(2)
+)
+
+// EncodeData serializes the data associated with the "name" key in SectionIDCustom according to the standard:
+//
+// Note: The result can be nil because this does not encode empty subsections
+//
+// See https://www.w3.org/TR/wasm-core-1/#binary-namesec
+func (n *NameSection) EncodeData() (data []byte) {
+	if n.ModuleName != "" {
+		data = append(data, encodeNameSubsection(subsectionIDModuleName, encodeSizePrefixed([]byte(n.ModuleName)))...)
+	}
+	if fd := n.encodeFunctionNameData(); len(fd) > 0 {
+		data = append(data, encodeNameSubsection(subsectionIDFunctionNames, fd)...)
+	}
+	if ld := n.encodeLocalNameData(); len(ld) > 0 {
+		data = append(data, encodeNameSubsection(subsectionIDLocalNames, ld)...)
+	}
+	return
+}
+
+// encodeFunctionNameData encodes the data for the function name subsection.
+// See https://www.w3.org/TR/wasm-core-1/#binary-funcnamesec
+func (n *NameSection) encodeFunctionNameData() []byte {
+	if len(n.FunctionNames) == 0 {
+		return nil
+	}
+	return encodeSortedAndSizePrefixed(n.FunctionNames)
+}
+
+func encodeSortedAndSizePrefixed(m map[uint32]string) []byte {
+	count := uint32(len(m))
+	data := leb128.EncodeUint32(count)
+
+	// Sort the keys so that they encode in ascending order
+	keys := make([]uint32, 0, count)
+	for k := range m {
+		keys = append(keys, k)
+	}
+	sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
+
+	for _, i := range keys {
+		data = append(data, encodeNameMapEntry(i, []byte(m[i]))...)
+	}
+	return data
+}
+
+// encodeLocalNameData encodes the data for the local name subsection.
+// See https://www.w3.org/TR/wasm-core-1/#binary-localnamesec
+func (n *NameSection) encodeLocalNameData() []byte {
+	if len(n.LocalNames) == 0 {
+		return nil
+	}
+
+	funcNameCount := uint32(len(n.LocalNames))
+	subsection := leb128.EncodeUint32(funcNameCount)
+
+	// Sort the function indices so that they encode in ascending order
+	funcIndex := make([]uint32, 0, funcNameCount)
+	for k := range n.LocalNames {
+		funcIndex = append(funcIndex, k)
+	}
+	sort.Slice(funcIndex, func(i, j int) bool { return funcIndex[i] < funcIndex[j] })
+
+	for _, i := range funcIndex {
+		locals := encodeSortedAndSizePrefixed(n.LocalNames[i])
+		subsection = append(subsection, append(leb128.EncodeUint32(i), locals...)...)
+	}
+	return subsection
+}
+
+// encodeNameSubsection returns a buffer encoding the given subsection
+// See https://www.w3.org/TR/wasm-core-1/#subsections%E2%91%A0
+func encodeNameSubsection(subsectionID uint8, content []byte) []byte {
+	contentSizeInBytes := leb128.EncodeUint32(uint32(len(content)))
+	result := []byte{subsectionID}
+	result = append(result, contentSizeInBytes...)
+	result = append(result, content...)
+	return result
+}
+
+// encodeNameMapEntry encodes the index and data prefixed by their size.
+// See https://www.w3.org/TR/wasm-core-1/#binary-namemap
+func encodeNameMapEntry(i uint32, data []byte) []byte {
+	return append(leb128.EncodeUint32(i), encodeSizePrefixed(data)...)
+}
+
+// encodeSizePrefixed encodes the data prefixed by their size.
+func encodeSizePrefixed(data []byte) []byte {
+	size := leb128.EncodeUint32(uint32(len(data)))
+	return append(size, data...)
+}
+
+// DecodeNameSection deserializes the data associated with the "name" key in SectionIDCustom according to the
+// standard:
+//
+// * ModuleName decode from subsection 0
+// * FunctionNames decode from subsection 1
+// * LocalNames decode from subsection 2
+//
+// See https://www.w3.org/TR/wasm-core-1/#binary-namesec
+func DecodeNameSection(data []byte) (result *NameSection, err error) {
+	// TODO: add leb128 functions that work on []byte and offset. While using a reader allows us to reuse reader-based
+	// leb128 functions, it is less efficient, causes untestable code and in some cases more complex vs plain []byte.
+	r := bytes.NewReader(data)
+	result = &NameSection{}
+
+	// subsectionID is decoded if known, and skipped if not
+	var subsectionID uint8
+	// subsectionSize is the length to skip when the subsectionID is unknown
+	var subsectionSize uint32
+	for {
+		if subsectionID, err = r.ReadByte(); err != nil {
+			if err == io.EOF {
+				return result, nil
+			}
+			// TODO: untestable as this can't fail for a reason beside EOF reading a byte from a buffer
+			return nil, fmt.Errorf("failed to read a subsection ID: %w", err)
+		}
+
+		// TODO: unused except when skipping. This means we can pass on a corrupt length of a known subsection
+		if subsectionSize, _, err = leb128.DecodeUint32(r); err != nil {
+			return nil, fmt.Errorf("failed to read the size of subsection[%d]: %w", subsectionID, err)
+		}
+
+		switch subsectionID {
+		case subsectionIDModuleName:
+			if result.ModuleName, err = decodeName(r, "module"); err != nil {
+				return nil, err
+			}
+		case subsectionIDFunctionNames:
+			if result.FunctionNames, err = decodeFunctionNames(r); err != nil {
+				return nil, err
+			}
+		case subsectionIDLocalNames:
+			if result.LocalNames, err = decodeLocalNames(r); err != nil {
+				return nil, err
+			}
+		default: // Skip other subsections.
+			// Note: Not Seek because it doesn't err when given an offset past EOF. Rather, it leads to undefined state.
+			if _, err := io.CopyN(io.Discard, r, int64(subsectionSize)); err != nil {
+				return nil, fmt.Errorf("failed to skip subsection[%d]: %w", subsectionID, err)
+			}
+		}
+	}
+}
+
+func decodeFunctionNames(r *bytes.Reader) (map[uint32]string, error) {
+	functionCount, err := decodeFunctionCount(r, subsectionIDFunctionNames)
+	if err != nil {
+		return nil, err
+	}
+
+	result := make(map[uint32]string, functionCount)
+	for i := uint32(0); i < functionCount; i++ {
+		functionIndex, err := decodeFunctionIndex(r, subsectionIDFunctionNames)
+		if err != nil {
+			return nil, err
+		}
+
+		if result[functionIndex], err = decodeName(r, "function[%d]", functionIndex); err != nil {
+			return nil, err
+		}
+	}
+	return result, nil
+}
+
+func decodeLocalNames(r *bytes.Reader) (map[uint32]map[uint32]string, error) {
+	functionCount, err := decodeFunctionCount(r, subsectionIDLocalNames)
+	if err != nil {
+		return nil, err
+	}
+
+	result := make(map[uint32]map[uint32]string, functionCount)
+	for i := uint32(0); i < functionCount; i++ {
+		functionIndex, err := decodeFunctionIndex(r, subsectionIDLocalNames)
+		if err != nil {
+			return nil, err
+		}
+
+		localCount, _, err := leb128.DecodeUint32(r)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read the local count for function[%d]: %w", functionIndex, err)
+		}
+
+		locals := make(map[uint32]string, localCount)
+		for i := uint32(0); i < localCount; i++ {
+			localIndex, _, err := leb128.DecodeUint32(r)
+			if err != nil {
+				return nil, fmt.Errorf("failed to read a local index of function[%d]: %w", functionIndex, err)
+			}
+			locals[localIndex], err = decodeName(r, "function[%d] local[%d]", functionIndex, localIndex)
+			if err != nil {
+				return nil, err
+			}
+		}
+		result[functionIndex] = locals
+	}
+	return result, nil
+}
+
+func decodeFunctionIndex(r *bytes.Reader, subsectionID uint8) (uint32, error) {
+	functionIndex, _, err := leb128.DecodeUint32(r)
+	if err != nil {
+		return 0, fmt.Errorf("failed to read a function index in subsection[%d]: %w", subsectionID, err)
+	}
+	return functionIndex, nil
+}
+
+func decodeFunctionCount(r *bytes.Reader, subsectionID uint8) (uint32, error) {
+	functionCount, _, err := leb128.DecodeUint32(r)
+	if err != nil {
+		return 0, fmt.Errorf("failed to read the function count of subsection[%d]: %w", subsectionID, err)
+	}
+	return functionCount, nil
+}
+
+// decodeName decodes a size prefixed string from the reader.
+// contextFormat and contextArgs apply an error format when present
+func decodeName(r *bytes.Reader, contextFormat string, contextArgs ...interface{}) (string, error) {
+	size, _, err := leb128.DecodeUint32(r)
+	if err != nil {
+		return "", fmt.Errorf("failed to read %s name size: %w", fmt.Sprintf(contextFormat, contextArgs...), err)
+	}
+
+	namebuf := make([]byte, size)
+	if _, err = io.ReadFull(r, namebuf); err != nil {
+		return "", fmt.Errorf("failed to read %s name: %w", fmt.Sprintf(contextFormat, contextArgs...), err)
+	}
+	return string(namebuf), nil
+}
diff --git a/wasm/section.go b/wasm/section.go
index 2924c214..b2d165a9 100644
--- a/wasm/section.go
+++ b/wasm/section.go
@@ -11,6 +11,7 @@ import (
 type SectionID = byte
 
 const (
+	// SectionIDCustom includes the standard defined NameSection and possibly others not defined in the standard.
 	SectionIDCustom   SectionID = 0
 	SectionIDType     SectionID = 1
 	SectionIDImport   SectionID = 2
@@ -25,24 +26,87 @@ const (
 	SectionIDData     SectionID = 11
 )
 
+// writeSections appends this module's sections into the buffer in the order required by the specification, custom
+// section first.
+// See https://www.w3.org/TR/wasm-core-1/#modules%E2%91%A0%E2%93%AA
+func (m *Module) encodeSections(buffer []byte) (bytes []byte) {
+	bytes = buffer
+	for name, data := range m.CustomSections {
+		bytes = append(bytes, encodeCustomSection(name, data)...)
+	}
+	if len(m.TypeSection) > 0 {
+		panic("TODO: TypeSection")
+	}
+	if len(m.ImportSection) > 0 {
+		panic("TODO: ImportSection")
+	}
+	if len(m.FunctionSection) > 0 {
+		panic("TODO: FunctionSection")
+	}
+	if len(m.TableSection) > 0 {
+		panic("TODO: TableSection")
+	}
+	if len(m.MemorySection) > 0 {
+		panic("TODO: MemorySection")
+	}
+	if len(m.GlobalSection) > 0 {
+		panic("TODO: GlobalSection")
+	}
+	if len(m.ExportSection) > 0 {
+		panic("TODO: ExportSection")
+	}
+	if m.StartSection != nil {
+		panic("TODO: StartSection")
+	}
+	if len(m.ElementSection) > 0 {
+		panic("TODO: ElementSection")
+	}
+	if len(m.CodeSection) > 0 {
+		panic("TODO: CodeSection")
+	}
+	if len(m.DataSection) > 0 {
+		panic("TODO: DataSection")
+	}
+	// >> The name section should appear only once in a module, and only after the data section.
+	// See https://www.w3.org/TR/wasm-core-1/#binary-namesec
+	if m.NameSection != nil {
+		bytes = append(bytes, encodeCustomSection("name", m.NameSection.EncodeData())...)
+	}
+	return
+}
+
+// encodeCustomSection encodes the opaque bytes for the given name as a SectionIDCustom
+// See https://www.w3.org/TR/wasm-core-1/#binary-customsec
+func encodeCustomSection(name string, data []byte) []byte {
+	// The contents of a custom section is the non-empty name followed by potentially empty opaque data
+	contents := append(encodeSizePrefixed([]byte(name)), data...)
+	return encodeSection(SectionIDCustom, contents)
+}
+
+// encodeSection encodes the sectionID, the size of its contents in bytes, followed by the contents.
+// See https://www.w3.org/TR/wasm-core-1/#sections%E2%91%A0
+func encodeSection(sectionID SectionID, contents []byte) []byte {
+	return append([]byte{sectionID}, encodeSizePrefixed(contents)...)
+}
+
 func (m *Module) readSections(r *reader) error {
 	for {
-		b := make([]byte, 1)
-		if _, err := io.ReadFull(r, b); err == io.EOF {
+		sectionID := make([]byte, 1)
+		if _, err := io.ReadFull(r, sectionID); err == io.EOF {
 			return nil
 		} else if err != nil {
 			return fmt.Errorf("read section id: %w", err)
 		}
 
-		ss, _, err := leb128.DecodeUint32(r)
+		sectionSize, _, err := leb128.DecodeUint32(r)
 		if err != nil {
-			return fmt.Errorf("get size of section for id=%d: %v", SectionID(b[0]), err)
+			return fmt.Errorf("get size of section for id=%d: %v", SectionID(sectionID[0]), err)
 		}
 
 		sectionContentStart := r.read
-		switch b[0] {
+		switch sectionID[0] {
 		case SectionIDCustom:
-			err = m.readSectionCustom(r, int(ss))
+			err = m.readSectionCustom(r, int(sectionSize))
 		case SectionIDType:
 			err = m.readSectionTypes(r)
 		case SectionIDImport:
@@ -69,18 +133,20 @@ func (m *Module) readSections(r *reader) error {
 			err = ErrInvalidSectionID
 		}
 
-		if err == nil && sectionContentStart+int(ss) != r.read {
-			err = fmt.Errorf("invalid section length: expected to be %d but got %d", ss, r.read-sectionContentStart)
+		if err == nil && sectionContentStart+int(sectionSize) != r.read {
+			err = fmt.Errorf("invalid section length: expected to be %d but got %d", sectionSize, r.read-sectionContentStart)
 		}
 
 		if err != nil {
-			return fmt.Errorf("section ID %d: %v", b[0], err)
+			return fmt.Errorf("section ID %d: %v", sectionID[0], err)
 		}
 	}
 }
 
+// readSectionCustom reads a custom section into Module.CustomSections unless it is the "name" section, which decodes
+// into Module.NameSection.
 func (m *Module) readSectionCustom(r *reader, sectionSize int) error {
-	nameLen, nameLenBytes, err := leb128.DecodeUint32(r)
+	nameLen, nameLenSize, err := leb128.DecodeUint32(r)
 	if err != nil {
 		return fmt.Errorf("cannot read custom section name length")
 	}
@@ -93,16 +159,33 @@ func (m *Module) readSectionCustom(r *reader, sectionSize int) error {
 	if !utf8.Valid(nameBuf) {
 		return fmt.Errorf("custom section name must be valid utf8")
 	}
-	contentLen := int(sectionSize) - int(nameLenBytes) - int(nameLen)
-	if contentLen < 0 {
-		return fmt.Errorf("malformed custom section %s", string(nameBuf))
+	name := string(nameBuf)
+
+	dataSize := sectionSize - int(nameLenSize) - int(nameLen)
+	if dataSize < 0 {
+		return fmt.Errorf("malformed custom section %s", name)
 	}
-	contents := make([]byte, contentLen)
-	_, err = io.ReadFull(r, contents)
+	data := make([]byte, dataSize)
+	_, err = io.ReadFull(r, data)
 	if err != nil {
-		return fmt.Errorf("cannot read custom section contents: %v", err)
+		return fmt.Errorf("cannot read custom section data: %v", err)
+	}
+
+	// Decode any known custom sections after checking for redundant keys
+	if name == "name" {
+		if m.NameSection != nil {
+			return fmt.Errorf("malformed custom section %s", name)
+		}
+		if m.NameSection, err = DecodeNameSection(data); err != nil {
+			return err
+		}
+	} else if _, ok := m.CustomSections[name]; ok {
+		return fmt.Errorf("malformed custom section %s", name)
+	} else if m.CustomSections == nil {
+		m.CustomSections = map[string][]byte{name: data}
+	} else {
+		m.CustomSections[name] = data
 	}
-	m.CustomSections[string(nameBuf)] = contents
 	return nil
 }
 
diff --git a/wasm/store.go b/wasm/store.go
index 7bb20820..dcf91e61 100644
--- a/wasm/store.go
+++ b/wasm/store.go
@@ -12,26 +12,67 @@ import (
 )
 
 type (
+	// Store is the runtime representation of "instantiated" Wasm module and objects.
+	// Multiple modules can be instantiated within a single store, and each instance,
+	// (e.g. function instance) can be referenced by other module instances in a Store via Module.ImportSection.
+	//
+	// Every type whose name ends with "Instance" suffix belongs to exactly one store.
+	//
+	// Note that store is not thread (concurrency) safe, meaning that using single Store
+	// via multiple goroutines might result in race conditions. In that case, the invocation
+	// and access to any methods and field of Store must be guarded by mutex.
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#store%E2%91%A0
 	Store struct {
-		engine          Engine
+		// The following fields are wazero-specific fields of Store.
+
+		// engine is a global context for a Store which is in reponsible for compilation and execution of Wasm modules.
+		engine Engine
+		// ModuleInstances holds the instantiated Wasm modules keyed on names given at Instantiate.
 		ModuleInstances map[string]*ModuleInstance
+		// TypeIDs maps each FunctionType.String() to a unique FunctionTypeID. This is used at runtime to
+		// do type-checks on indirect function calls.
+		TypeIDs map[string]FunctionTypeID
 
-		Functions []*FunctionInstance
-		Globals   []*GlobalInstance
-		Memories  []*MemoryInstance
-		Tables    []*TableInstance
-	}
+		// The followings fields match the definition of Store in the specification.
 
+		// Functions holds function instances (https://www.w3.org/TR/wasm-core-1/#function-instances%E2%91%A0),
+		// in this store.
+		// The slice index is to be interpreted as funcaddr (https://www.w3.org/TR/wasm-core-1/#syntax-funcaddr).
+		Functions []*FunctionInstance
+		// Globals holds global instances (https://www.w3.org/TR/wasm-core-1/#global-instances%E2%91%A0),
+		// in this store.
+		// The slice index is to be interpreted as globaladdr (https://www.w3.org/TR/wasm-core-1/#syntax-globaladdr).
+		Globals []*GlobalInstance
+		// Memories holds memory instances (https://www.w3.org/TR/wasm-core-1/#memory-instances%E2%91%A0),
+		// in this store.
+		// The slice index is to be interpreted as memaddr (https://www.w3.org/TR/wasm-core-1/#syntax-memaddr).
+		Memories []*MemoryInstance
+		// Tables holds table instances (https://www.w3.org/TR/wasm-core-1/#table-instances%E2%91%A0),
+		// in this store.
+		// The slice index is to be interpreted as tableaddr (https://www.w3.org/TR/wasm-core-1/#syntax-tableaddr).
+		Tables []*TableInstance
+	}
+
+	// ModuleInstance represents instantiated wasm module.
+	// The difference from the spec is that in wazero, a ModuleInstance holds pointers
+	// to the instances, rather than "addresses" (i.e. index to Store.Functions, Globals, etc) for convenience.
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#syntax-moduleinst
 	ModuleInstance struct {
 		Exports   map[string]*ExportInstance
 		Functions []*FunctionInstance
 		Globals   []*GlobalInstance
 		Memory    *MemoryInstance
 		Tables    []*TableInstance
-
-		Types []*FunctionType
+		Types     []*TypeInstance
 	}
 
+	// ExportInstance represents an exported instance in a Store.
+	// The difference from the spec is that in wazero, a ExportInstance holds pointers
+	// to the instances, rather than "addresses" (i.e. index to Store.Functions, Globals, etc) for convenience.
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#syntax-exportinst
 	ExportInstance struct {
 		Kind     byte
 		Function *FunctionInstance
@@ -40,59 +81,115 @@ type (
 		Table    *TableInstance
 	}
 
+	// FunctionInstance represents a function instance in a Store.
+	// See https://www.w3.org/TR/wasm-core-1/#function-instances%E2%91%A0
 	FunctionInstance struct {
-		Name           string
+		// ModuleInstance holds the pointer to the module instance to which this function belongs.
 		ModuleInstance *ModuleInstance
-		Body           []byte
-		Signature      *FunctionType
-		NumLocals      uint32
-		LocalTypes     []ValueType
-		HostFunction   *reflect.Value
-	}
-
-	HostFunctionCallContext struct {
-		Memory *MemoryInstance
-		// TODO: Add others if necessary.
-	}
-
-	FunctionInstanceBlock struct {
-		StartAt, ElseAt, EndAt uint64
-		BlockType              *FunctionType
-		BlockTypeBytes         uint64
-		IsLoop                 bool // TODO: might not be necessary
-		IsIf                   bool // TODO: might not be necessary
-	}
-
+		// 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
+		// HostFunction holds the runtime representation of host functions.
+		// If this is not nil, all the above fields are ignored as they are specific to non-host functions.
+		HostFunction *reflect.Value
+		// Address is the funcaddr(https://www.w3.org/TR/wasm-core-1/#syntax-funcaddr) of this function insntance.
+		// More precisely, this equals the index of this function instance in store.FunctionInstances.
+		// 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.
+		Address FunctionAddress
+		// 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
+		// Store.AddHostFunction. Ex. something.realistic
+		//
+		// Otherwise, this is the corresponding value in NameSection.FunctionNames or "unknown" if unavailable.
+		Name string
+	}
+
+	// TypeInstance is a store-specific representation of FunctionType where the function type
+	// is coupled with TypeID which is specific in a store.
+	TypeInstance struct {
+		Type *FunctionType
+		// TypeID is assigned by a store for FunctionType.
+		TypeID FunctionTypeID
+	}
+
+	// GlobalInstance represents a global instance in a store.
+	// See https://www.w3.org/TR/wasm-core-1/#global-instances%E2%91%A0
 	GlobalInstance struct {
 		Type *GlobalType
-		Val  uint64
+		// Val holds a 64-bit representation of the actual value.
+		Val uint64
 	}
 
+	// TableInstance represents a table instance in a store.
+	// See https://www.w3.org/TR/wasm-core-1/#table-instances%E2%91%A0
+	//
+	// Note this is fixed to function type until post MVP reference type is implemented.
 	TableInstance struct {
-		Table    []*TableInstanceElm
-		Min      uint32
-		Max      *uint32
+		// Table holds the table elements managed by this table instance.
+		//
+		// Note: we intentionally use "[]TableElement", not "[]*TableElement",
+		// because the JIT engine accesses this slice directly from assembly.
+		// If pointer type is used, the access becomes two level indirection (two hops of pointer jumps)
+		// which is a bit costly. TableElement is 128 bit (two 64bit fields) so the cost of using value type
+		// would be ignorable.
+		Table []TableElement
+		Min   uint32
+		Max   *uint32
+		// Currently fixed to 0x70 (funcref type).
 		ElemType byte
 	}
 
-	TableInstanceElm struct {
-		Function *FunctionInstance
-		// Other ref types will be added.
+	// TableElement represents an item in a table instance.
+	//
+	// Note: this is fixed to function type as it is the only supported type in WebAssembly 1.0 (MVP)
+	TableElement struct {
+		// FunctionAddress is funcaddr (https://www.w3.org/TR/wasm-core-1/#syntax-funcaddr)
+		// of the target function instance. More precisely, this equals the index of
+		// the target function instance in Store.FunctionInstances.
+		FunctionAddress FunctionAddress
+		// FunctionTypeID is the type ID of the target function's type, which
+		// equals store.Functions[FunctionAddress].FunctionType.TypeID.
+		FunctionTypeID FunctionTypeID
 	}
 
+	// MemoryInstance represents a memory instance in a store.
+	// See https://www.w3.org/TR/wasm-core-1/#memory-instances%E2%91%A0.
 	MemoryInstance struct {
 		Buffer []byte
 		Min    uint32
 		Max    *uint32
 	}
+
+	// FunctionAddress is funcaddr (https://www.w3.org/TR/wasm-core-1/#syntax-funcaddr),
+	// and the index to Store.Functions.
+	FunctionAddress uint64
+
+	// FunctionTypeID is an uniquely assigned integer for a function type.
+	// This is wazero specific runtime object and specific to a store,
+	// and used at runtime to do type-checks on indirect function calls.
+	FunctionTypeID uint64
 )
 
+func (f *FunctionInstance) IsHostFunction() bool {
+	return f.HostFunction != nil
+}
+
 func NewStore(engine Engine) *Store {
-	return &Store{ModuleInstances: map[string]*ModuleInstance{}, engine: engine}
+	return &Store{ModuleInstances: map[string]*ModuleInstance{}, TypeIDs: map[string]FunctionTypeID{}, engine: engine}
 }
 
 func (s *Store) Instantiate(module *Module, name string) error {
-	instance := &ModuleInstance{Types: module.TypeSection}
+	instance := &ModuleInstance{}
+	for _, t := range module.TypeSection {
+		instance.Types = append(instance.Types, s.getTypeInstance(t))
+	}
+
 	s.ModuleInstances[name] = instance
 	// Resolve the imports before doing the actual instantiation (mutating store).
 	if err := s.resolveImports(module, instance); err != nil {
@@ -133,13 +230,6 @@ func (s *Store) Instantiate(module *Module, name string) error {
 		return fmt.Errorf("exports: %w", err)
 	}
 
-	// We compile functions after successfully finished building all instances.
-	// This is not only because we want to do early feedback on malicious binaries,
-	// but also during the compilation phase, the compilers have to see all the possible
-	// instances (function, memory, table) in the module instance.
-	if err = s.engine.PreCompile(instance.Functions); err != nil {
-		return fmt.Errorf("failed to precompile: %w", err)
-	}
 	for i, f := range instance.Functions {
 		if err := s.engine.Compile(f); err != nil {
 			return fmt.Errorf("compilation failed at index %d/%d: %v", i, len(module.FunctionSection)-1, err)
@@ -147,14 +237,14 @@ func (s *Store) Instantiate(module *Module, name string) error {
 	}
 
 	// Check the start function is valid.
-	if module.StartSection != nil {
-		index := *module.StartSection
+	if startIndex := module.StartSection; startIndex != nil {
+		index := *startIndex
 		if int(index) >= len(instance.Functions) {
 			return fmt.Errorf("invalid start function index: %d", index)
 		}
-		signature := instance.Functions[index].Signature
-		if len(signature.Params) != 0 || len(signature.Results) != 0 {
-			return fmt.Errorf("start function must have the empty signature")
+		ft := instance.Functions[index].FunctionType
+		if len(ft.Type.Params) != 0 || len(ft.Type.Results) != 0 {
+			return fmt.Errorf("start function must have the empty function type")
 		}
 	}
 
@@ -162,8 +252,8 @@ func (s *Store) Instantiate(module *Module, name string) error {
 	rollbackFuncs = nil
 
 	// Execute the start function.
-	if module.StartSection != nil {
-		f := instance.Functions[*module.StartSection]
+	if startIndex := module.StartSection; startIndex != nil {
+		f := instance.Functions[*startIndex]
 		if _, err := s.engine.Call(f); err != nil {
 			return fmt.Errorf("calling start function failed: %v", err)
 		}
@@ -187,12 +277,17 @@ func (s *Store) CallFunction(moduleName, funcName string, params ...uint64) (res
 	}
 
 	f := exp.Function
-	if len(f.Signature.Params) != len(params) {
+	if len(f.FunctionType.Type.Params) != len(params) {
 		return nil, nil, fmt.Errorf("invalid number of parameters")
 	}
 
 	ret, err := s.engine.Call(f, params...)
-	return ret, f.Signature.Results, err
+	return ret, f.FunctionType.Type.Results, err
+}
+
+func (s *Store) addFunctionInstance(f *FunctionInstance) {
+	f.Address = FunctionAddress(len(s.Functions))
+	s.Functions = append(s.Functions, f)
 }
 
 func (s *Store) resolveImports(module *Module, target *ModuleInstance) error {
@@ -247,11 +342,11 @@ func (s *Store) applyFunctionImport(target *ModuleInstance, typeIndex uint32, ex
 	if int(typeIndex) >= len(target.Types) {
 		return fmt.Errorf("unknown type for function import")
 	}
-	iSig := target.Types[typeIndex]
-	if !HasSameSignature(iSig.Results, f.Signature.Results) {
-		return fmt.Errorf("return signature mimatch: %#x != %#x", iSig.Results, f.Signature.Results)
-	} else if !HasSameSignature(iSig.Params, f.Signature.Params) {
-		return fmt.Errorf("input signature mimatch: %#x != %#x", iSig.Params, f.Signature.Params)
+	expectedType := target.Types[typeIndex].Type
+	if !ValueTypesEqual(expectedType.Results, f.FunctionType.Type.Results) {
+		return fmt.Errorf("return signature mimatch: %#x != %#x", expectedType.Results, f.FunctionType.Type.Results)
+	} else if !ValueTypesEqual(expectedType.Params, f.FunctionType.Type.Params) {
+		return fmt.Errorf("input signature mimatch: %#x != %#x", expectedType.Params, f.FunctionType.Type.Params)
 	}
 	target.Functions = append(target.Functions, f)
 	return nil
@@ -433,12 +528,10 @@ func (s *Store) buildFunctionInstances(module *Module, target *ModuleInstance) (
 	memoryDeclarations = append(memoryDeclarations, module.MemorySection...)
 	tableDeclarations = append(tableDeclarations, module.TableSection...)
 
-	functionNames, _ := module.DecodeCustomNameSection()
-	if functionNames == nil {
-		// We cannot guarantee the existence of "name" custom section
-		// in the binary. That is because the custom section is optional
-		// in the spec. Also, some compilers optimize it out to reduce
-		// the binary size.
+	var functionNames map[uint32]string
+	if module.NameSection != nil && module.NameSection.FunctionNames != nil {
+		functionNames = module.NameSection.FunctionNames
+	} else {
 		functionNames = map[uint32]string{}
 	}
 
@@ -454,15 +547,14 @@ func (s *Store) buildFunctionInstances(module *Module, target *ModuleInstance) (
 
 		f := &FunctionInstance{
 			Name:           name,
-			Signature:      module.TypeSection[typeIndex],
+			FunctionType:   s.getTypeInstance(module.TypeSection[typeIndex]),
 			Body:           module.CodeSection[codeIndex].Body,
-			NumLocals:      module.CodeSection[codeIndex].NumLocals,
 			LocalTypes:     module.CodeSection[codeIndex].LocalTypes,
 			ModuleInstance: target,
 		}
 
 		if _, ok := analysisCache[codeIndex]; !ok {
-			err := analyzeFunction(
+			err := validateFunction(
 				module, f, functionDeclarations, globalDeclarations,
 				memoryDeclarations, tableDeclarations,
 			)
@@ -472,7 +564,7 @@ func (s *Store) buildFunctionInstances(module *Module, target *ModuleInstance) (
 		}
 
 		target.Functions = append(target.Functions, f)
-		s.Functions = append(s.Functions, f)
+		s.addFunctionInstance(f)
 	}
 	return rollbackFuncs, nil
 }
@@ -554,14 +646,9 @@ func (s *Store) buildMemoryInstances(module *Module, target *ModuleInstance) (ro
 func (s *Store) buildTableInstances(module *Module, target *ModuleInstance) (rollbackFuncs []func(), err error) {
 	// Allocate table instances.
 	for _, tableSeg := range module.TableSection {
-		tableInst := &TableInstance{
-			Table:    make([]*TableInstanceElm, tableSeg.Limit.Min),
-			Min:      tableSeg.Limit.Min,
-			Max:      tableSeg.Limit.Max,
-			ElemType: tableSeg.ElemType,
-		}
-		target.Tables = append(target.Tables, tableInst)
-		s.Tables = append(s.Tables, tableInst)
+		instance := newTableInstance(tableSeg.Limit.Min, tableSeg.Limit.Max)
+		target.Tables = append(target.Tables, instance)
+		s.Tables = append(s.Tables, instance)
 	}
 
 	for _, elem := range module.ElementSection {
@@ -611,8 +698,10 @@ func (s *Store) buildTableInstances(module *Module, target *ModuleInstance) (rol
 			rollbackFuncs = append(rollbackFuncs, func() {
 				tableInst.Table[pos] = original
 			})
-			tableInst.Table[pos] = &TableInstanceElm{
-				Function: target.Functions[elm],
+			targetFunc := target.Functions[elm]
+			tableInst.Table[pos] = TableElement{
+				FunctionAddress: targetFunc.Address,
+				FunctionTypeID:  targetFunc.FunctionType.TypeID,
 			}
 		}
 	}
@@ -772,17 +861,28 @@ func (s *valueTypeStack) String() string {
 		strings.Join(typeStrs, ", "), strings.Join(limits, ","))
 }
 
-type BlockType = FunctionType
+type functionBlock struct {
+	StartAt, ElseAt, EndAt uint64
+	BlockType              *FunctionType
+	BlockTypeBytes         uint64
+	IsLoop                 bool
+	IsIf                   bool
+}
 
-func analyzeFunction(
-	module *Module, f *FunctionInstance,
+// validateFunction validates the instruction sequence of a function instance body
+// following the specification https://www.w3.org/TR/wasm-core-1/#instructions%E2%91%A2.
+//
+// TODO: put this in a separate file like validate.go.
+func validateFunction(
+	module *Module,
+	f *FunctionInstance,
 	functionDeclarations []uint32,
 	globalDeclarations []*GlobalType,
 	memoryDeclarations []*MemoryType,
 	tableDeclarations []*TableType,
 ) error {
-	labelStack := []*FunctionInstanceBlock{
-		{BlockType: f.Signature, StartAt: math.MaxUint64},
+	labelStack := []*functionBlock{
+		{BlockType: f.FunctionType.Type, StartAt: math.MaxUint64},
 	}
 	valueTypeStack := &valueTypeStack{}
 	for pc := uint64(0); pc < uint64(len(f.Body)); pc++ {
@@ -1030,23 +1130,23 @@ func analyzeFunction(
 			pc += num - 1
 			switch Opcode(op) {
 			case OpcodeLocalGet:
-				inputLen := uint32(len(f.Signature.Params))
-				if l := f.NumLocals + inputLen; index >= l {
+				inputLen := uint32(len(f.FunctionType.Type.Params))
+				if l := uint32(len(f.LocalTypes)) + inputLen; index >= l {
 					return fmt.Errorf("invalid local index for local.get %d >= %d(=len(locals)+len(parameters))", index, l)
 				}
 				if index < inputLen {
-					valueTypeStack.push(f.Signature.Params[index])
+					valueTypeStack.push(f.FunctionType.Type.Params[index])
 				} else {
 					valueTypeStack.push(f.LocalTypes[index-inputLen])
 				}
 			case OpcodeLocalSet:
-				inputLen := uint32(len(f.Signature.Params))
-				if l := f.NumLocals + inputLen; index >= l {
+				inputLen := uint32(len(f.FunctionType.Type.Params))
+				if l := uint32(len(f.LocalTypes)) + inputLen; index >= l {
 					return fmt.Errorf("invalid local index for local.set %d >= %d(=len(locals)+len(parameters))", index, l)
 				}
 				var expType ValueType
 				if index < inputLen {
-					expType = f.Signature.Params[index]
+					expType = f.FunctionType.Type.Params[index]
 				} else {
 					expType = f.LocalTypes[index-inputLen]
 				}
@@ -1054,13 +1154,13 @@ func analyzeFunction(
 					return err
 				}
 			case OpcodeLocalTee:
-				inputLen := uint32(len(f.Signature.Params))
-				if l := f.NumLocals + inputLen; index >= l {
+				inputLen := uint32(len(f.FunctionType.Type.Params))
+				if l := uint32(len(f.LocalTypes)) + inputLen; index >= l {
 					return fmt.Errorf("invalid local index for local.tee %d >= %d(=len(locals)+len(parameters))", index, l)
 				}
 				var expType ValueType
 				if index < inputLen {
-					expType = f.Signature.Params[index]
+					expType = f.FunctionType.Type.Params[index]
 				} else {
 					expType = f.LocalTypes[index-inputLen]
 				}
@@ -1442,11 +1542,11 @@ func analyzeFunction(
 				return fmt.Errorf("invalid numeric instruction 0x%x", op)
 			}
 		} else if op == OpcodeBlock {
-			bt, num, err := ReadBlockType(module.TypeSection, bytes.NewBuffer(f.Body[pc+1:]))
+			bt, num, err := ReadBlockType(f.ModuleInstance.Types, bytes.NewBuffer(f.Body[pc+1:]))
 			if err != nil {
 				return fmt.Errorf("read block: %w", err)
 			}
-			labelStack = append(labelStack, &FunctionInstanceBlock{
+			labelStack = append(labelStack, &functionBlock{
 				StartAt:        pc,
 				BlockType:      bt,
 				BlockTypeBytes: num,
@@ -1454,11 +1554,11 @@ func analyzeFunction(
 			valueTypeStack.pushStackLimit()
 			pc += num
 		} else if op == OpcodeLoop {
-			bt, num, err := ReadBlockType(module.TypeSection, bytes.NewBuffer(f.Body[pc+1:]))
+			bt, num, err := ReadBlockType(f.ModuleInstance.Types, bytes.NewBuffer(f.Body[pc+1:]))
 			if err != nil {
 				return fmt.Errorf("read block: %w", err)
 			}
-			labelStack = append(labelStack, &FunctionInstanceBlock{
+			labelStack = append(labelStack, &functionBlock{
 				StartAt:        pc,
 				BlockType:      bt,
 				BlockTypeBytes: num,
@@ -1467,11 +1567,11 @@ func analyzeFunction(
 			valueTypeStack.pushStackLimit()
 			pc += num
 		} else if op == OpcodeIf {
-			bt, num, err := ReadBlockType(module.TypeSection, bytes.NewBuffer(f.Body[pc+1:]))
+			bt, num, err := ReadBlockType(f.ModuleInstance.Types, bytes.NewBuffer(f.Body[pc+1:]))
 			if err != nil {
 				return fmt.Errorf("read block: %w", err)
 			}
-			labelStack = append(labelStack, &FunctionInstanceBlock{
+			labelStack = append(labelStack, &functionBlock{
 				StartAt:        pc,
 				BlockType:      bt,
 				BlockTypeBytes: num,
@@ -1518,7 +1618,7 @@ func analyzeFunction(
 			// on values previously pushed by outer blocks.
 			valueTypeStack.popStackLimit()
 		} else if op == OpcodeReturn {
-			expTypes := f.Signature.Results
+			expTypes := f.FunctionType.Type.Results
 			for i := 0; i < len(expTypes); i++ {
 				if err := valueTypeStack.popAndVerifyType(expTypes[len(expTypes)-1-i]); err != nil {
 					return fmt.Errorf("return type mismatch on return: %v; want %v", err, expTypes)
@@ -1567,33 +1667,40 @@ func analyzeFunction(
 	return nil
 }
 
-func ReadBlockType(types []*FunctionType, r io.Reader) (*BlockType, uint64, error) {
+func ReadBlockType(types []*TypeInstance, r io.Reader) (*FunctionType, uint64, error) {
 	raw, num, err := leb128.DecodeInt33AsInt64(r)
 	if err != nil {
 		return nil, 0, fmt.Errorf("decode int33: %w", err)
 	}
 
-	var ret *BlockType
+	var ret *FunctionType
 	switch raw {
 	case -64: // 0x40 in original byte = nil
-		ret = &BlockType{}
+		ret = &FunctionType{}
 	case -1: // 0x7f in original byte = i32
-		ret = &BlockType{Results: []ValueType{ValueTypeI32}}
+		ret = &FunctionType{Results: []ValueType{ValueTypeI32}}
 	case -2: // 0x7e in original byte = i64
-		ret = &BlockType{Results: []ValueType{ValueTypeI64}}
+		ret = &FunctionType{Results: []ValueType{ValueTypeI64}}
 	case -3: // 0x7d in original byte = f32
-		ret = &BlockType{Results: []ValueType{ValueTypeF32}}
+		ret = &FunctionType{Results: []ValueType{ValueTypeF32}}
 	case -4: // 0x7c in original byte = f64
-		ret = &BlockType{Results: []ValueType{ValueTypeF64}}
+		ret = &FunctionType{Results: []ValueType{ValueTypeF64}}
 	default:
 		if raw < 0 || (raw >= int64(len(types))) {
 			return nil, 0, fmt.Errorf("invalid block type: %d", raw)
 		}
-		ret = types[raw]
+		ret = types[raw].Type
 	}
 	return ret, num, nil
 }
 
+// HostFunctionCallContext is the first argument of all host functions.
+type HostFunctionCallContext struct {
+	// Memory is the currently used memory instance at the time when the host function call is made.
+	Memory *MemoryInstance
+	// TODO: Add others if necessary.
+}
+
 func (s *Store) AddHostFunction(moduleName, funcName string, fn reflect.Value) error {
 	getTypeOf := func(kind reflect.Kind) (ValueType, error) {
 		switch kind {
@@ -1609,7 +1716,7 @@ func (s *Store) AddHostFunction(moduleName, funcName string, fn reflect.Value) e
 			return 0x00, fmt.Errorf("invalid type: %s", kind.String())
 		}
 	}
-	getSignature := func(p reflect.Type) (*FunctionType, error) {
+	getType := func(p reflect.Type) (*FunctionType, error) {
 		var err error
 		if p.NumIn() == 0 {
 			return nil, fmt.Errorf("host function must accept *wasm.HostFunctionCallContext as the first param")
@@ -1639,7 +1746,7 @@ func (s *Store) AddHostFunction(moduleName, funcName string, fn reflect.Value) e
 		return fmt.Errorf("name %s already exists in module %s", funcName, moduleName)
 	}
 
-	sig, err := getSignature(fn.Type())
+	sig, err := getType(fn.Type())
 	if err != nil {
 		return fmt.Errorf("invalid signature: %w", err)
 	}
@@ -1647,18 +1754,15 @@ func (s *Store) AddHostFunction(moduleName, funcName string, fn reflect.Value) e
 	f := &FunctionInstance{
 		Name:           fmt.Sprintf("%s.%s", moduleName, funcName),
 		HostFunction:   &fn,
-		Signature:      sig,
+		FunctionType:   s.getTypeInstance(sig),
 		ModuleInstance: m,
 	}
-	if err := s.engine.PreCompile([]*FunctionInstance{f}); err != nil {
-		return fmt.Errorf("failed to precompile %s: %v", f.Name, err)
-	}
 
 	if err := s.engine.Compile(f); err != nil {
 		return fmt.Errorf("failed to compile %s: %v", f.Name, err)
 	}
 	m.Exports[funcName] = &ExportInstance{Kind: ExportKindFunction, Function: f}
-	s.Functions = append(s.Functions, f)
+	s.addFunctionInstance(f)
 	return nil
 }
 
@@ -1686,14 +1790,9 @@ func (s *Store) AddTableInstance(moduleName, name string, min uint32, max *uint3
 		return fmt.Errorf("name %s already exists in module %s", name, moduleName)
 	}
 
-	table := &TableInstance{
-		Table:    make([]*TableInstanceElm, min),
-		Min:      min,
-		Max:      max,
-		ElemType: 0x70, // funcref
-	}
-	m.Exports[name] = &ExportInstance{Kind: ExportKindTable, Table: table}
-	s.Tables = append(s.Tables, table)
+	instance := newTableInstance(min, max)
+	m.Exports[name] = &ExportInstance{Kind: ExportKindTable, Table: instance}
+	s.Tables = append(s.Tables, instance)
 	return nil
 }
 
@@ -1715,6 +1814,16 @@ func (s *Store) AddMemoryInstance(moduleName, name string, min uint32, max *uint
 	return nil
 }
 
+func (s *Store) getTypeInstance(t *FunctionType) *TypeInstance {
+	key := t.String()
+	id, ok := s.TypeIDs[key]
+	if !ok {
+		id = FunctionTypeID(len(s.TypeIDs))
+		s.TypeIDs[key] = id
+	}
+	return &TypeInstance{Type: t, TypeID: id}
+}
+
 // getModuleInstance returns an existing ModuleInstance if exists, or assigns a new one.
 func (s *Store) getModuleInstance(name string) *ModuleInstance {
 	m, ok := s.ModuleInstances[name]
@@ -1724,3 +1833,19 @@ func (s *Store) getModuleInstance(name string) *ModuleInstance {
 	}
 	return m
 }
+
+func newTableInstance(min uint32, max *uint32) *TableInstance {
+	tableInst := &TableInstance{
+		Table:    make([]TableElement, min),
+		Min:      min,
+		Max:      max,
+		ElemType: 0x70, // funcref
+	}
+	for i := range tableInst.Table {
+		tableInst.Table[i] = TableElement{
+			// We use math.MaxUint64 to represent the uninitialized elements.
+			FunctionAddress: math.MaxUint32,
+		}
+	}
+	return tableInst
+}
diff --git a/wasm/type.go b/wasm/type.go
index 657da13a..c8b97e00 100644
--- a/wasm/type.go
+++ b/wasm/type.go
@@ -11,6 +11,23 @@ type FunctionType struct {
 	Params, Results []ValueType
 }
 
+func (t *FunctionType) String() (ret string) {
+	for _, b := range t.Params {
+		ret += formatValueType(b)
+	}
+	if len(t.Params) == 0 {
+		ret += "null"
+	}
+	ret += "_"
+	for _, b := range t.Results {
+		ret += formatValueType(b)
+	}
+	if len(t.Results) == 0 {
+		ret += "null"
+	}
+	return
+}
+
 func readFunctionType(r io.Reader) (*FunctionType, error) {
 	b := make([]byte, 1)
 	if _, err := io.ReadFull(r, b); err != nil {
diff --git a/wasm/value.go b/wasm/value.go
index bf7fa12b..53251914 100644
--- a/wasm/value.go
+++ b/wasm/value.go
@@ -8,6 +8,10 @@ import (
 	"github.com/tetratelabs/wazero/wasm/leb128"
 )
 
+// ValueType is the binary encoding of a type such as i32
+// See https://www.w3.org/TR/wasm-core-1/#binary-valtype
+//
+// Note: This is a type alias as it is easier to encode and decode in the binary format.
 type ValueType = byte
 
 const (
@@ -17,6 +21,20 @@ const (
 	ValueTypeF64 ValueType = 0x7c
 )
 
+func formatValueType(t ValueType) (ret string) {
+	switch t {
+	case ValueTypeI32:
+		ret = "i32"
+	case ValueTypeI64:
+		ret = "i64"
+	case ValueTypeF32:
+		ret = "f32"
+	case ValueTypeF64:
+		ret = "f64"
+	}
+	return
+}
+
 func readValueTypes(r io.Reader, num uint32) ([]ValueType, error) {
 	ret := make([]ValueType, num)
 	buf := make([]ValueType, num)
@@ -54,7 +72,7 @@ func readNameValue(r io.Reader) (string, error) {
 	return string(buf), nil
 }
 
-func HasSameSignature(a []ValueType, b []ValueType) bool {
+func ValueTypesEqual(a []ValueType, b []ValueType) bool {
 	if len(a) != len(b) {
 		return false
 	}
diff --git a/wasm/wat/bind.go b/wasm/wat/bind.go
index 15556d18..e22030c0 100644
--- a/wasm/wat/bind.go
+++ b/wasm/wat/bind.go
@@ -101,7 +101,7 @@ func bindSymbolicIDToNumericIndex(idToIndex map[string]uint32, idx *index, conte
 		context = fmt.Sprintf(context, contextArg0)
 	}
 	return &FormatError{idx.line, idx.col, context,
-		fmt.Errorf("unknown ID %s", idx.ID),
+		fmt.Errorf("unknown ID $%s", idx.ID), // re-attach '$' as that was in the text format!
 	}
 }
 
diff --git a/wasm/wat/convert.go b/wasm/wat/convert.go
index 4b358bd7..da8ceb7f 100644
--- a/wasm/wat/convert.go
+++ b/wasm/wat/convert.go
@@ -31,18 +31,51 @@ func TextToBinary(source []byte) (result *wasm.Module, err error) {
 
 	// Now, handle any imported functions. Notably, we retain the same insertion order as defined in the text format in
 	// case a numeric index is used for the start function (or another reason such as the call instruction).
-	for _, f := range m.importFuncs {
+	var functionNames map[uint32]string
+	var localNames map[uint32]map[uint32]string
+	for i, f := range m.importFuncs {
+		funcidx := uint32(i)
+		if f.funcName != "" {
+			if functionNames == nil {
+				functionNames = map[uint32]string{funcidx: f.funcName}
+			} else {
+				functionNames[funcidx] = f.funcName
+			}
+		}
+		if f.paramNames != nil {
+			locals := map[uint32]string{}
+			for _, pn := range f.paramNames {
+				locals[pn.index] = string(pn.name)
+			}
+			if localNames == nil {
+				localNames = map[uint32]map[uint32]string{funcidx: locals}
+			} else {
+				localNames[funcidx] = locals
+			}
+		}
 		result.ImportSection = append(result.ImportSection, &wasm.ImportSegment{
 			Module: f.module, Name: f.name,
-			Desc: &wasm.ImportDesc{Kind: wasm.ImportKindFunction, FuncTypeIndex: f.typeIndex.numeric},
+			Desc: &wasm.ImportDesc{
+				Kind:          wasm.ImportKindFunction,
+				FuncTypeIndex: f.typeIndex.numeric,
+			},
 		})
 	}
 
+	// TODO: module function and local names
+
 	// The start function is called on Module.Instantiate.
 	if m.startFunction != nil {
 		result.StartSection = &m.startFunction.numeric
 	}
 
-	// TODO: encode CustomSection["name"] with module function and local names
+	// Don't set the name section unless we found a name!
+	if m.name != "" || functionNames != nil || localNames != nil {
+		result.NameSection = &wasm.NameSection{
+			ModuleName:    m.name,
+			FunctionNames: functionNames,
+			LocalNames:    localNames,
+		}
+	}
 	return
 }
diff --git a/wasm/wat/index_parser.go b/wasm/wat/index_parser.go
index c71e5b0f..6fe5db4b 100644
--- a/wasm/wat/index_parser.go
+++ b/wasm/wat/index_parser.go
@@ -46,7 +46,7 @@ func (p *indexParser) parseIndex(tok tokenType, tokenBytes []byte, line, col uin
 		if p.currentIndex != nil {
 			return errors.New("redundant index")
 		}
-		p.currentIndex = &index{ID: string(tokenBytes), line: line, col: col}
+		p.currentIndex = &index{ID: string(stripDollar(tokenBytes)), line: line, col: col}
 	case tokenRParen: // end of this field
 		if p.currentIndex == nil {
 			return errors.New("missing index")
diff --git a/wasm/wat/names.go b/wasm/wat/names.go
deleted file mode 100644
index ab3e7975..00000000
--- a/wasm/wat/names.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package wat
-
-import (
-	"github.com/tetratelabs/wazero/wasm/leb128"
-)
-
-// encodeNameSection encodes a possibly empty buffer representing the "name" wasm.Module CustomSection.
-// See https://www.w3.org/TR/wasm-core-1/#name-section%E2%91%A0
-func encodeNameSection(m *module) []byte {
-	funcNameCount := uint32(0)
-	var funcNameEntries []byte
-	for _, f := range m.importFuncs {
-		if f.funcName != "" {
-			funcNameCount = funcNameCount + 1
-			funcNameEntries = append(funcNameEntries, encodeNameMapEntry(uint32(f.importIndex), f.funcName)...)
-		}
-	}
-	var nameSection []byte
-	if m.name != "" {
-		// See https://www.w3.org/TR/wasm-core-1/#binary-modulenamesec
-		nameSection = append(nameSection, encodeNameSubsection(uint8(0), encodeName(m.name))...)
-	}
-	if funcNameCount > 0 {
-		// See https://www.w3.org/TR/wasm-core-1/#binary-funcnamesec
-		content := leb128.EncodeUint32(funcNameCount)
-		content = append(content, funcNameEntries...)
-		nameSection = append(nameSection, encodeNameSubsection(uint8(1), content)...)
-	}
-	return nameSection
-}
-
-// This returns a buffer encoding the given subsection
-// See https://www.w3.org/TR/wasm-core-1/#subsections%E2%91%A0
-func encodeNameSubsection(subsectionID uint8, content []byte) []byte {
-	contentSizeInBytes := leb128.EncodeUint32(uint32(len(content)))
-	result := []byte{subsectionID}
-	result = append(result, contentSizeInBytes...)
-	result = append(result, content...)
-	return result
-}
-
-// encodeNameMapEntry encodes the index and name prefixed by their size.
-// See https://www.w3.org/TR/wasm-core-1/#binary-namemap
-func encodeNameMapEntry(i uint32, name string) []byte {
-	return append(leb128.EncodeUint32(i), encodeName(name)...)
-}
-
-// encodeName encodes the name prefixed by its size, stripping any leading '$'
-//
-// The WebAssembly 1.0 specification includes support for naming modules, functions, locals and tables via the custom
-// 'name' section: https://www.w3.org/TR/wasm-core-1/#binary-namesec However, how this round-trips between the text and
-// binary format is not discussed.
-//
-// We know that in the text format names must be dollar-sign prefixed to conform with tokenID conventions. However, we
-// don't know if the user's intent was a dollar-sign or not. For example, a function written in a higher level language,
-// targeting the binary format may end up prefixed with '$' for other reasons.
-//
-// This round-tripping concern materializes when a function written in the text format is transpiled into the binary
-// format (ex via `wat2wasm --debug-names`). For example, if a module name was encoded literally in the binary custom
-// 'name' section as "$Math", wabt tools would prefix it again, resulting in "$$Math".
-// https://github.com/WebAssembly/wabt/blob/e59cf9369004a521814222afbc05ae6b59446cd5/src/binary-reader-ir.cc#L1279
-//
-// Until the standard clarifies round-tripping concerns between the text and binary format, we chop off the leading '$'
-// when reading any names from the text format. This prevents awkwardness while wabt tools are in use.
-func encodeName(name string) []byte {
-	nameBytes := []byte(name[1:])
-	nameSize := leb128.EncodeUint32(uint32(len(nameBytes)))
-	content := append(nameSize, nameBytes...)
-	return content
-}
diff --git a/wasm/wat/parser.go b/wasm/wat/parser.go
index df72f7a0..c8292f7c 100644
--- a/wasm/wat/parser.go
+++ b/wasm/wat/parser.go
@@ -197,14 +197,13 @@ func (p *moduleParser) endField() {
 // next parser to parseModule. If the token isn't a tokenID, this calls parseModule.
 //
 // Ex. A module name is present `(module $math)`
-//                       records $math --^
+//                        records math --^
 //
 // Ex. No module name `(module)`
 //   calls parseModule here --^
 func (p *moduleParser) parseModuleName(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $Math
-		name := string(tokenBytes)
-		p.module.name = name
+		p.module.name = string(stripDollar(tokenBytes))
 		p.tokenParser = p.parseModule
 		return nil
 	}
@@ -230,14 +229,14 @@ func (p *moduleParser) parseModule(tok tokenType, tokenBytes []byte, _, _ uint32
 // if not found.
 //
 // Ex. A type name is present `(type $t0 (func (result i32)))`
-//                     records $t0 --^   ^
+//                      records t0 --^   ^
 //              parseType resumes here --+
 //
 // Ex. No type name `(type (func (result i32)))`
 //       calls parseType --^
 func (p *moduleParser) parseTypeName(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $v_v
-		p.currentValue0 = tokenBytes
+		p.currentValue0 = stripDollar(tokenBytes)
 		p.tokenParser = p.parseType
 		return nil
 	}
@@ -401,7 +400,7 @@ func (p *moduleParser) parseImport(tok tokenType, tokenBytes []byte, _, _ uint32
 // and sets the next parser to parseImportFunc. If the token isn't a tokenID, this calls parseImportFunc.
 //
 // Ex. A function name is present `(import "Math" "PI" (func $math.pi (result f32))`
-//                                   records $math.pi here --^
+//                                    records math.pi here --^
 //                                     parseImportFunc resumes here --^
 //
 // Ex. No function name `(import "Math" "PI" (func (result f32))`
@@ -409,7 +408,7 @@ func (p *moduleParser) parseImport(tok tokenType, tokenBytes []byte, _, _ uint32
 func (p *moduleParser) parseImportFuncName(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $main
 		fn := p.module.importFuncs[len(p.module.importFuncs)-1]
-		fn.funcName = string(tokenBytes)
+		fn.funcName = string(stripDollar(tokenBytes))
 		p.tokenParser = p.parseImportFunc
 		return nil
 	}
diff --git a/wasm/wat/token.go b/wasm/wat/token.go
index 5d9ae11d..f75c0ba5 100644
--- a/wasm/wat/token.go
+++ b/wasm/wat/token.go
@@ -172,3 +172,24 @@ func _idChar(ch byte) bool {
 	}
 	return false
 }
+
+// stripDollar returns the input without a leading '$'
+//
+// The WebAssembly 1.0 specification includes support for naming modules, functions, locals and tables via the custom
+// 'name' section: https://www.w3.org/TR/wasm-core-1/#binary-namesec However, how this round-trips between the text and
+// binary format is not discussed.
+//
+// We know that in the text format names must be dollar-sign prefixed to conform with tokenID conventions. However, we
+// don't know if the user's intent was a dollar-sign or not. For example, a function written in a higher level language,
+// targeting the binary format may end up prefixed with '$' for other reasons.
+//
+// This round-tripping concern materializes when a function written in the text format is transpiled into the binary
+// format (ex via `wat2wasm --debug-names`). For example, if a module name was encoded literally in the binary custom
+// 'name' section as "$Math", wabt tools would prefix it again, resulting in "$$Math".
+// https://github.com/WebAssembly/wabt/blob/e59cf9369004a521814222afbc05ae6b59446cd5/src/binary-reader-ir.cc#L1279
+//
+// Until the standard clarifies round-tripping concerns between the text and binary format, we chop off the leading '$'
+// when reading any names from the text format. This prevents awkwardness while wabt tools are in use.
+func stripDollar(tokenID []byte) []byte {
+	return tokenID[1:] // we don't check for leading '$' because we know the call sites must have one per tokenID
+}
diff --git a/wasm/wat/type_parser.go b/wasm/wat/type_parser.go
index f49ce01d..6760db9d 100644
--- a/wasm/wat/type_parser.go
+++ b/wasm/wat/type_parser.go
@@ -193,14 +193,14 @@ func (p *typeParser) parseMoreParamsOrResult(tok tokenType, tokenBytes []byte, l
 // found.
 //
 // Ex. A param name is present `(param $x i32)`
-//                        records $x --^  ^
+//                         records x --^  ^
 //              parseParam resumes here --+
 //
 // Ex. No param name `(param i32)`
 //        calls parseParam --^
 func (p *typeParser) parseParamName(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $len
-		p.currentParamName = tokenBytes
+		p.currentParamName = stripDollar(tokenBytes)
 		p.m.tokenParser = p.parseParam
 		return nil
 	}
diff --git a/wasm/wat/types.go b/wasm/wat/types.go
index a90cb1b9..9e95c00b 100644
--- a/wasm/wat/types.go
+++ b/wasm/wat/types.go
@@ -14,7 +14,7 @@ import (
 //
 // See https://www.w3.org/TR/wasm-core-1/#functions%E2%91%A7
 type module struct {
-	// name is optional and starts with '$'. For example, "$test".
+	// name is optional. For example, "test".
 	// See https://www.w3.org/TR/wasm-core-1/#modules%E2%91%A0%E2%91%A2
 	//
 	// Note: The name may also be stored in the wasm.Module CustomSection under the key "name" subsection 0.
@@ -91,7 +91,7 @@ type index struct {
 //
 // See https://www.w3.org/TR/wasm-core-1/#text-functype
 type typeFunc struct {
-	// name starts with '$'. For example, "$v_v", and only set when explicitly defined in module.types
+	// name is only set when explicitly defined in module.types. Ex. v_v
 	//
 	// name is only used for debugging. At runtime, types are called based on raw numeric index. The type index space
 	// begins those explicitly defined in module.types, followed by any inlined ones.
@@ -116,7 +116,7 @@ func funcTypeEquals(t *typeFunc, params []wasm.ValueType, result wasm.ValueType)
 }
 
 // paramNames are the possibly empty association of names that correspond with params. The index is to params and
-// the name will never be empty and always begin with '$' (ex. "$len") (tokenID). Ex. $x names (param $x i32)
+// the name will never be empty. Ex. x is the name of (param $x i32)
 //
 // paramNames are only used for debugging. At runtime, parameters are called based on raw numeric index.
 //
@@ -158,15 +158,15 @@ type importFunc struct {
 
 	// module is the possibly empty module name to import. Ex. "" or "Math"
 	//
-	// Note: This is not necessarily the module.name, so it does not need to begin with '$'!
+	// Note: This is not necessarily the module.name
 	module string
 
 	// name is the possibly empty entity name to import. Ex. "" or "PI"
 	//
-	// Note: This is not necessarily the funcName, so it does not need to begin with '$'!
+	// Note: This is not necessarily the funcName
 	name string
 
-	// funcName starts with '$'. For example, "$main".
+	// funcName is optional. Ex. main
 	//
 	// funcName is only used for debugging. At runtime, functions are called based on raw numeric index. The function
 	// index space begins with imported functions, followed by any defined in this module.
diff --git a/wasm/wazeroir/compiler.go b/wasm/wazeroir/compiler.go
index 366e5688..90a33b61 100644
--- a/wasm/wazeroir/compiler.go
+++ b/wasm/wazeroir/compiler.go
@@ -63,7 +63,7 @@ func (c *controlFrame) asBranchTarget() *BranchTarget {
 func (c *controlFrames) functionFrame() *controlFrame {
 	// No need to check stack bound
 	// as we can assume that all the operations
-	// are valid thanks to analyzeFunction
+	// are valid thanks to validateFunction
 	// at module validation phase.
 	return c.frames[0]
 }
@@ -71,7 +71,7 @@ func (c *controlFrames) functionFrame() *controlFrame {
 func (c *controlFrames) get(n int) *controlFrame {
 	// No need to check stack bound
 	// as we can assume that all the operations
-	// are valid thanks to analyzeFunction
+	// are valid thanks to validateFunction
 	// at module validation phase.
 	return c.frames[len(c.frames)-n-1]
 }
@@ -79,7 +79,7 @@ func (c *controlFrames) get(n int) *controlFrame {
 func (c *controlFrames) top() *controlFrame {
 	// No need to check stack bound
 	// as we can assume that all the operations
-	// are valid thanks to analyzeFunction
+	// are valid thanks to validateFunction
 	// at module validation phase.
 	return c.frames[len(c.frames)-1]
 }
@@ -91,7 +91,7 @@ func (c *controlFrames) empty() bool {
 func (c *controlFrames) pop() (frame *controlFrame) {
 	// No need to check stack bound
 	// as we can assume that all the operations
-	// are valid thanks to analyzeFunction
+	// are valid thanks to validateFunction
 	// at module validation phase.
 	frame = c.top()
 	c.frames = c.frames[:len(c.frames)-1]
@@ -148,7 +148,7 @@ func Compile(f *wasm.FunctionInstance) (*CompilationResult, error) {
 	c := compiler{controlFrames: &controlFrames{}, f: f, result: CompilationResult{LabelCallers: map[string]int{}}}
 
 	// Push function arguments.
-	for _, t := range f.Signature.Params {
+	for _, t := range f.FunctionType.Type.Params {
 		c.stackPush(wasmValueTypeToUnsignedType(t))
 	}
 	// Emit const expressions for locals.
@@ -160,13 +160,13 @@ func Compile(f *wasm.FunctionInstance) (*CompilationResult, error) {
 	}
 
 	// Insert the function control frame.
-	returns := make([]UnsignedType, 0, len(f.Signature.Results))
-	for _, t := range f.Signature.Results {
+	returns := make([]UnsignedType, 0, len(f.FunctionType.Type.Results))
+	for _, t := range f.FunctionType.Type.Results {
 		returns = append(returns, wasmValueTypeToUnsignedType(t))
 	}
 	c.controlFrames.push(&controlFrame{
 		frameID:          c.nextID(),
-		originalStackLen: len(f.Signature.Params),
+		originalStackLen: len(f.FunctionType.Type.Params),
 		returns:          returns,
 		kind:             controlFrameKindFunction,
 	})
@@ -1444,7 +1444,7 @@ func (c *compiler) applyToStack(opcode wasm.Opcode) (*uint32, error) {
 func (c *compiler) stackPop() (ret UnsignedType) {
 	// No need to check stack bound
 	// as we can assume that all the operations
-	// are valid thanks to analyzeFunction
+	// are valid thanks to validateFunction
 	// at module validation phase.
 	ret = c.stack[len(c.stack)-1]
 	c.stack = c.stack[:len(c.stack)-1]
diff --git a/wasm/wazeroir/engine.go b/wasm/wazeroir/engine.go
deleted file mode 100644
index 00a8c24e..00000000
--- a/wasm/wazeroir/engine.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package wazeroir
-
-import (
-	"github.com/tetratelabs/wazero/wasm"
-)
-
-func NewEngine() wasm.Engine {
-	// TODO: add option to use JIT instead of interpreter
-	return &interpreter{
-		functions:                  map[*wasm.FunctionInstance]*interpreterFunction{},
-		functionTypeIDs:            map[string]uint64{},
-		onCompilationDoneCallbacks: map[*wasm.FunctionInstance][]func(*interpreterFunction){},
-	}
-}
diff --git a/wasm/wazeroir/interpreter.go b/wasm/wazeroir/interpreter.go
index 427e8270..7545ddd9 100644
--- a/wasm/wazeroir/interpreter.go
+++ b/wasm/wazeroir/interpreter.go
@@ -8,7 +8,6 @@ import (
 	"reflect"
 	"runtime/debug"
 	"strings"
-	"unsafe"
 
 	"github.com/tetratelabs/wazero/wasm"
 	"github.com/tetratelabs/wazero/wasm/buildoptions"
@@ -20,21 +19,23 @@ var callStackCeiling = buildoptions.CallStackCeiling
 // This is the direct interpreter of wazeroir operations.
 type interpreter struct {
 	// Stores compiled functions.
-	functions map[*wasm.FunctionInstance]*interpreterFunction
-	// Cached function type IDs assigned to
-	// function signatures.
-	// Type IDs are used to check the signature of
-	// functions on call_indirect operation at runtime
-	functionTypeIDs map[string]uint64
+	functions map[wasm.FunctionAddress]*interpreterFunction
 	// stack contains the operands.
 	// Note that all the values are represented as uint64.
 	stack []uint64
 	// Function call stack.
 	frames []*interpreterFrame
-	// The callbacks when an function instance is compiled.
+	// onCompilationDoneCallbacks call back when a function instance is compiled.
 	// See the comment where this is used below for detail.
 	// Not used at runtime, and only in the compilation phase.
-	onCompilationDoneCallbacks map[*wasm.FunctionInstance][]func(*interpreterFunction)
+	onCompilationDoneCallbacks map[wasm.FunctionAddress][]func(*interpreterFunction)
+}
+
+func NewEngine() wasm.Engine {
+	return &interpreter{
+		functions:                  map[wasm.FunctionAddress]*interpreterFunction{},
+		onCompilationDoneCallbacks: map[wasm.FunctionAddress][]func(*interpreterFunction){},
+	}
 }
 
 func (it *interpreter) push(v uint64) {
@@ -44,7 +45,7 @@ func (it *interpreter) push(v uint64) {
 func (it *interpreter) pop() (v uint64) {
 	// No need to check stack bound
 	// as we can assume that all the operations
-	// are valid thanks to analyzeFunction
+	// are valid thanks to validateFunction
 	// at module validation phase
 	// and wazeroir translation
 	// before compilation.
@@ -56,7 +57,7 @@ func (it *interpreter) pop() (v uint64) {
 func (it *interpreter) drop(r *InclusiveRange) {
 	// No need to check stack bound
 	// as we can assume that all the operations
-	// are valid thanks to analyzeFunction
+	// are valid thanks to validateFunction
 	// at module validation phase
 	// and wazeroir translation
 	// before compilation.
@@ -79,7 +80,7 @@ func (it *interpreter) pushFrame(frame *interpreterFrame) {
 }
 
 func (it *interpreter) popFrame() (frame *interpreterFrame) {
-	// No need to check stack bound as we can assume that all the operations are valid thanks to analyzeFunction at
+	// No need to check stack bound as we can assume that all the operations are valid thanks to validateFunction at
 	// module validation phase and wazeroir translation before compilation.
 	oneLess := len(it.frames) - 1
 	frame = it.frames[oneLess]
@@ -99,8 +100,6 @@ type interpreterFunction struct {
 	funcInstance *wasm.FunctionInstance
 	body         []*interpreterOp
 	hostFn       *reflect.Value
-	signature    *wasm.FunctionType
-	typeID       uint64
 }
 
 // Non-interface union of all the wazeroir operations.
@@ -112,60 +111,39 @@ type interpreterOp struct {
 	f      *interpreterFunction
 }
 
-func (it *interpreter) PreCompile(fs []*wasm.FunctionInstance) error {
-	// We have nothing to do on the precompile phase, in contrast to JIT engine.
-	return nil
-}
-
 // Compile Implements wasm.Engine for interpreter.
 func (it *interpreter) Compile(f *wasm.FunctionInstance) error {
-	if _, ok := it.functions[f]; ok {
-		return nil
-	} else if f.HostFunction != nil {
+	funcaddr := f.Address
+
+	if f.IsHostFunction() {
 		ret := &interpreterFunction{
 			hostFn: f.HostFunction, funcInstance: f,
-			signature: f.Signature,
-		}
-		if id, ok := it.functionTypeIDs[funcTypeString(f.Signature)]; !ok {
-			id = uint64(len(it.functionTypeIDs))
-			it.functionTypeIDs[funcTypeString(f.Signature)] = id
-			ret.typeID = id
-		} else {
-			ret.typeID = id
 		}
-		it.functions[f] = ret
+		it.functions[funcaddr] = ret
 		return nil
-	}
-
-	ir, err := Compile(f)
-	if err != nil {
-		return fmt.Errorf("failed to lower Wasm to wazeroir: %w", err)
-	}
-
-	fn, err := it.lowerIROps(f, ir.Operations)
-	if err != nil {
-		return fmt.Errorf("failed to lower wazeroir operations to interpreter operations: %w", err)
-	}
+	} else {
+		ir, err := Compile(f)
+		if err != nil {
+			return fmt.Errorf("failed to compile Wasm to wazeroir: %w", err)
+		}
 
-	it.functions[f] = fn
-	for _, cb := range it.onCompilationDoneCallbacks[f] {
-		cb(fn)
+		fn, err := it.lowerIROps(f, ir.Operations)
+		if err != nil {
+			return fmt.Errorf("failed to convert wazeroir operations to interpreter ones: %w", err)
+		}
+		it.functions[funcaddr] = fn
+		for _, cb := range it.onCompilationDoneCallbacks[funcaddr] {
+			cb(fn)
+		}
+		delete(it.onCompilationDoneCallbacks, funcaddr)
 	}
-	delete(it.onCompilationDoneCallbacks, f)
 	return nil
 }
 
 // Lowers the wazeroir operations to interpreter friendly struct.
 func (it *interpreter) lowerIROps(f *wasm.FunctionInstance,
 	ops []Operation) (*interpreterFunction, error) {
-	ret := &interpreterFunction{funcInstance: f, signature: f.Signature}
-	if id, ok := it.functionTypeIDs[funcTypeString(f.Signature)]; !ok {
-		id = uint64(len(it.functionTypeIDs))
-		it.functionTypeIDs[funcTypeString(f.Signature)] = id
-		ret.typeID = id
-	} else {
-		ret.typeID = id
-	}
+	ret := &interpreterFunction{funcInstance: f}
 	labelAddress := map[string]uint64{}
 	onLabelAddressResolved := map[string][]func(addr uint64){}
 	for _, original := range ops {
@@ -255,27 +233,22 @@ func (it *interpreter) lowerIROps(f *wasm.FunctionInstance,
 				}
 			}
 		case *OperationCall:
-			targetInst := f.ModuleInstance.Functions[o.FunctionIndex]
-			target, ok := it.functions[targetInst]
+			target := f.ModuleInstance.Functions[o.FunctionIndex]
+			compiledTarget, ok := it.functions[target.Address]
 			if !ok {
 				// If the target function instance is not compiled,
 				// we set the callback so we can set the pointer to the target when the compilation done.
-				it.onCompilationDoneCallbacks[targetInst] = append(it.onCompilationDoneCallbacks[targetInst],
+				it.onCompilationDoneCallbacks[target.Address] = append(it.onCompilationDoneCallbacks[target.Address],
 					func(compiled *interpreterFunction) {
 						op.f = compiled
 					})
 			} else {
-				op.f = target
+				op.f = compiledTarget
 			}
 		case *OperationCallIndirect:
 			op.us = make([]uint64, 2)
 			op.us[0] = uint64(o.TableIndex)
-			key := funcTypeString(f.ModuleInstance.Types[o.TypeIndex])
-			typeid, ok := it.functionTypeIDs[key]
-			if !ok {
-				it.functionTypeIDs[key] = uint64(len(it.functionTypeIDs))
-			}
-			op.us[1] = typeid
+			op.us[1] = uint64(f.ModuleInstance.Types[o.TypeIndex].TypeID)
 		case *OperationDrop:
 			op.rs = make([]*InclusiveRange, 1)
 			op.rs[0] = o.Range
@@ -481,7 +454,7 @@ func (it *interpreter) Call(f *wasm.FunctionInstance, params ...uint64) (results
 		}
 	}()
 
-	g, ok := it.functions[f]
+	g, ok := it.functions[f.Address]
 	if !ok {
 		err = fmt.Errorf("function not compiled")
 		return
@@ -495,7 +468,7 @@ func (it *interpreter) Call(f *wasm.FunctionInstance, params ...uint64) (results
 		}
 		it.callNativeFunc(g)
 	}
-	results = make([]uint64, len(f.Signature.Results))
+	results = make([]uint64, len(f.FunctionType.Type.Results))
 	for i := range results {
 		results[len(results)-1-i] = it.pop()
 	}
@@ -547,9 +520,13 @@ func (it *interpreter) callHostFunc(f *interpreterFunction, _ ...uint64) {
 
 func (it *interpreter) callNativeFunc(f *interpreterFunction) {
 	frame := &interpreterFrame{f: f}
-	memoryInst := frame.f.funcInstance.ModuleInstance.Memory
-	globals := frame.f.funcInstance.ModuleInstance.Globals
-	tables := f.funcInstance.ModuleInstance.Tables
+	moduleInst := f.funcInstance.ModuleInstance
+	memoryInst := moduleInst.Memory
+	globals := moduleInst.Globals
+	var table *wasm.TableInstance
+	if len(moduleInst.Tables) > 0 {
+		table = moduleInst.Tables[0] // WebAssembly 1.0 (MVP) defines at most one table
+	}
 	it.pushFrame(frame)
 	bodyLen := uint64(len(frame.f.body))
 	for frame.pc < bodyLen {
@@ -588,7 +565,7 @@ func (it *interpreter) callNativeFunc(f *interpreterFunction) {
 		case OperationKindCall:
 			{
 				if op.f.hostFn != nil {
-					it.callHostFunc(op.f, it.stack[len(it.stack)-len(op.f.signature.Params):]...)
+					it.callHostFunc(op.f, it.stack[len(it.stack)-len(op.f.funcInstance.FunctionType.Type.Params):]...)
 				} else {
 					it.callNativeFunc(op.f)
 				}
@@ -596,15 +573,16 @@ func (it *interpreter) callNativeFunc(f *interpreterFunction) {
 			}
 		case OperationKindCallIndirect:
 			{
-				index := it.pop()
-				target := it.functions[tables[op.us[0]].Table[index].Function]
+				offset := it.pop()
+				target := it.functions[table.Table[offset].FunctionAddress]
 				// Type check.
-				if target.typeID != op.us[1] {
-					panic("function signature mismatch on call_indirect")
+				expType := target.funcInstance.FunctionType
+				if uint64(expType.TypeID) != op.us[1] {
+					panic("function type mismatch on call_indirect")
 				}
 				// Call in.
 				if target.hostFn != nil {
-					it.callHostFunc(target, it.stack[len(it.stack)-len(target.signature.Params):]...)
+					it.callHostFunc(target, it.stack[len(it.stack)-len(expType.Type.Params):]...)
 				} else {
 					it.callNativeFunc(target)
 				}
@@ -1470,16 +1448,6 @@ func (it *interpreter) callNativeFunc(f *interpreterFunction) {
 	it.popFrame()
 }
 
-func funcTypeString(t *wasm.FunctionType) string {
-	return fmt.Sprintf("%s-%s",
-		// Fast stringification of byte slice.
-		// This is safe anyway as the results are copied
-		// into the return value string.
-		*(*string)(unsafe.Pointer(&t.Params)),
-		*(*string)(unsafe.Pointer(&t.Results)),
-	)
-}
-
 // math.Min doen't comply with the Wasm spec, so we borrow from the original
 // with a change that either one of NaN results in NaN even if another is -Inf.
 // https://github.com/golang/go/blob/1d20a362d0ca4898d77865e314ef6f73582daef0/src/math/dim.go#L74-L91
diff --git a/wasm/wazeroir/signature.go b/wasm/wazeroir/signature.go
index 61eff100..6a10f690 100644
--- a/wasm/wazeroir/signature.go
+++ b/wasm/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].Signature), nil
+		return funcTypeToSignature(f.ModuleInstance.Functions[index].FunctionType.Type), nil
 	case wasm.OpcodeCallIndirect:
-		ret := funcTypeToSignature(f.ModuleInstance.Types[index])
+		ret := funcTypeToSignature(f.ModuleInstance.Types[index].Type)
 		ret.in = append(ret.in, UnsignedTypeI32)
 		return ret, nil
 	case wasm.OpcodeDrop:
@@ -170,37 +170,37 @@ 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.Signature.Params))
-		if l := f.NumLocals + inputLen; index >= l {
+		inputLen := uint32(len(f.FunctionType.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.Signature.Params[index])
+			t = wasmValueTypeToUnsignedType(f.FunctionType.Type.Params[index])
 		} else {
 			t = wasmValueTypeToUnsignedType(f.LocalTypes[index-inputLen])
 		}
 		return &signature{out: []UnsignedType{t}}, nil
 	case wasm.OpcodeLocalSet:
-		inputLen := uint32(len(f.Signature.Params))
-		if l := f.NumLocals + inputLen; index >= l {
+		inputLen := uint32(len(f.FunctionType.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.Signature.Params[index])
+			t = wasmValueTypeToUnsignedType(f.FunctionType.Type.Params[index])
 		} else {
 			t = wasmValueTypeToUnsignedType(f.LocalTypes[index-inputLen])
 		}
 		return &signature{in: []UnsignedType{t}}, nil
 	case wasm.OpcodeLocalTee:
-		inputLen := uint32(len(f.Signature.Params))
-		if l := f.NumLocals + inputLen; index >= l {
+		inputLen := uint32(len(f.FunctionType.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.Signature.Params[index])
+			t = wasmValueTypeToUnsignedType(f.FunctionType.Type.Params[index])
 		} else {
 			t = wasmValueTypeToUnsignedType(f.LocalTypes[index-inputLen])
 		}


Test patch:
diff --git a/wasm/jit/engine_test.go b/wasm/jit/engine_test.go
index 75dcaf62..136419d4 100644
--- a/wasm/jit/engine_test.go
+++ b/wasm/jit/engine_test.go
@@ -3,7 +3,6 @@ package jit
 import (
 	"errors"
 	"os"
-	"reflect"
 	"sync"
 	"testing"
 	"unsafe"
@@ -19,10 +18,12 @@ func TestEngine_veifyOffsetValue(t *testing.T) {
 	require.Equal(t, int(unsafe.Offsetof((&engine{}).stackPointer)), enginestackPointerOffset)
 	require.Equal(t, int(unsafe.Offsetof((&engine{}).stackBasePointer)), enginestackBasePointerOffset)
 	require.Equal(t, int(unsafe.Offsetof((&engine{}).jitCallStatusCode)), engineJITCallStatusCodeOffset)
-	require.Equal(t, int(unsafe.Offsetof((&engine{}).functionCallIndex)), engineFunctionCallIndexOffset)
+	require.Equal(t, int(unsafe.Offsetof((&engine{}).functionCallAddress)), engineFunctionCallAddressOffset)
 	require.Equal(t, int(unsafe.Offsetof((&engine{}).continuationAddressOffset)), engineContinuationAddressOffset)
 	require.Equal(t, int(unsafe.Offsetof((&engine{}).globalSliceAddress)), engineglobalSliceAddressOffset)
 	require.Equal(t, int(unsafe.Offsetof((&engine{}).memorySliceLen)), engineMemorySliceLenOffset)
+	require.Equal(t, int(unsafe.Offsetof((&engine{}).tableSliceAddress)), engineTableSliceAddressOffset)
+	require.Equal(t, int(unsafe.Offsetof((&engine{}).tableSliceLen)), engineTableSliceLenOffset)
 }
 
 func TestEngine_fibonacci(t *testing.T) {
@@ -128,38 +129,6 @@ func TestEngine_memory(t *testing.T) {
 	require.Equal(t, newPages, out[0])
 }
 
-func TestEngine_PreCompile(t *testing.T) {
-	eng := newEngine()
-	hf := reflect.ValueOf(func(*wasm.HostFunctionCallContext) {})
-	// Usually the function instances in a module consist of the maxture
-	// of host functions and wasm functions. And we treat a fcuntion instance
-	// as a native one when .HostFunction is nil.
-	fs := []*wasm.FunctionInstance{
-		{HostFunction: &hf},
-		{HostFunction: nil},
-		{HostFunction: nil},
-		{HostFunction: nil},
-	}
-	err := eng.PreCompile(fs)
-	require.NoError(t, err)
-	// Check the indexes.
-	require.Len(t, eng.compiledWasmFunctions, 3)
-	require.Len(t, eng.compiledWasmFunctionIndex, 3)
-	require.Len(t, eng.compiledHostFunctions, 1)
-	require.Len(t, eng.compiledHostFunctionIndex, 1)
-	prevCompiledFunctions := make([]*compiledWasmFunction, len(eng.compiledWasmFunctions))
-	prevHostFunctions := make([]*compiledHostFunction, len(eng.compiledHostFunctions))
-	copy(prevCompiledFunctions, eng.compiledWasmFunctions)
-	copy(prevHostFunctions, eng.compiledHostFunctions)
-	err = eng.PreCompile(fs)
-	// Precompiling same functions should be noop.
-	require.NoError(t, err)
-	require.Len(t, eng.compiledWasmFunctionIndex, 3)
-	require.Len(t, eng.compiledHostFunctionIndex, 1)
-	require.Equal(t, prevHostFunctions, eng.compiledHostFunctions)
-	require.Equal(t, prevCompiledFunctions, eng.compiledWasmFunctions)
-}
-
 func TestEngine_maybeGrowStack(t *testing.T) {
 	t.Run("grow", func(t *testing.T) {
 		eng := &engine{stack: make([]uint64, 10)}
diff --git a/wasm/jit/jit_amd64_test.go b/wasm/jit/jit_amd64_test.go
index 681a403d..22cb61d8 100644
--- a/wasm/jit/jit_amd64_test.go
+++ b/wasm/jit/jit_amd64_test.go
@@ -8,7 +8,6 @@ import (
 	"fmt"
 	"math"
 	"math/bits"
-	"reflect"
 	"runtime"
 	"testing"
 	"unsafe"
@@ -28,6 +27,7 @@ type jitEnv struct {
 	eng     *engine
 	mem     *wasm.MemoryInstance
 	globals []*wasm.GlobalInstance
+	table   *wasm.TableInstance
 }
 
 func (j *jitEnv) stackTopAsByte() byte {
@@ -73,8 +73,8 @@ func (j *jitEnv) jitStatus() jitCallStatusCode {
 	return j.eng.jitCallStatusCode
 }
 
-func (j *jitEnv) functionCallIndex() int64 {
-	return j.eng.functionCallIndex
+func (j *jitEnv) functionCallAddress() wasm.FunctionAddress {
+	return j.eng.functionCallAddress
 }
 
 func (j *jitEnv) continuationAddressOffset() uintptr {
@@ -97,11 +97,19 @@ func (j *jitEnv) getGlobal(index uint32) uint64 {
 	return j.globals[index].Val
 }
 
+func (j *jitEnv) setTable(table []wasm.TableElement) {
+	j.table.Table = table
+}
+
 func (j *jitEnv) exec(code []byte) {
-	j.eng.memorySliceLen = len(j.mem.Buffer)
+	j.eng.memorySliceLen = uint64(len(j.mem.Buffer))
 	if len(j.globals) > 0 {
 		j.eng.globalSliceAddress = uintptr(unsafe.Pointer(&j.globals[0]))
 	}
+	if l := len(j.table.Table); l > 0 {
+		j.eng.tableSliceAddress = uintptr(unsafe.Pointer(&j.table.Table[0]))
+		j.eng.tableSliceLen = uint64(l)
+	}
 	jitcall(
 		uintptr(unsafe.Pointer(&code[0])),
 		uintptr(unsafe.Pointer(j.eng)),
@@ -111,8 +119,9 @@ func (j *jitEnv) exec(code []byte) {
 
 func newJITEnvironment() *jitEnv {
 	return &jitEnv{
-		eng: newEngine(),
-		mem: &wasm.MemoryInstance{Buffer: make([]byte, 1024)},
+		eng:   newEngine(),
+		mem:   &wasm.MemoryInstance{Buffer: make([]byte, 1024)},
+		table: &wasm.TableInstance{},
 	}
 }
 
@@ -136,13 +145,62 @@ func (c *amd64Compiler) movIntConstToRegister(val int64, targetRegister int16) *
 	return prog
 }
 
+func TestAmd64Compiler_generate(t *testing.T) {
+	t.Run("ok", func(t *testing.T) {
+		compiler := requireNewCompiler(t)
+		compiler.initializeReservedRegisters()
+
+		// Emit a good move instruction which is followed by return and continuation.
+		inst := compiler.newProg()
+		inst.As = x86.AMOVQ
+		inst.From.Type = obj.TYPE_CONST
+		inst.To.Type = obj.TYPE_REG
+		inst.To.Reg = x86.REG_R10
+		inst.From.Offset = int64(1 << 33)
+		compiler.addInstruction(inst)
+		compiler.requireFunctionCallReturnAddressOffsetResolution = append(
+			compiler.requireFunctionCallReturnAddressOffsetResolution, inst)
+
+		ret := compiler.newProg()
+		ret.As = obj.ARET
+		compiler.addInstruction(ret)
+
+		afterFunctionCallInst := compiler.newProg()
+		afterFunctionCallInst.As = obj.ANOP
+		compiler.addInstruction(afterFunctionCallInst)
+
+		// Check the error is NOT returned.
+		_, _, err := compiler.generate()
+		require.NoError(t, err)
+	})
+	t.Run("invalid function call", func(t *testing.T) {
+		compiler := requireNewCompiler(t)
+		compiler.initializeReservedRegisters()
+
+		// Set the invalid function call origin which the return instruction doesn't follow.
+		invalidInst := compiler.newProg()
+		invalidInst.As = x86.AMOVQ
+		invalidInst.To.Type = obj.TYPE_REG
+		invalidInst.To.Reg = x86.REG_R10
+		invalidInst.To.Type = obj.TYPE_REG
+		invalidInst.To.Reg = x86.REG_R10
+		compiler.addInstruction(invalidInst)
+		compiler.requireFunctionCallReturnAddressOffsetResolution = append(
+			compiler.requireFunctionCallReturnAddressOffsetResolution, invalidInst)
+
+		// Check the error is returned.
+		_, _, err := compiler.generate()
+		require.Error(t, err)
+	})
+}
+
 func TestAmd64Compiler_pushFunctionInputs(t *testing.T) {
-	f := &wasm.FunctionInstance{Signature: &wasm.FunctionType{
+	f := &wasm.FunctionInstance{FunctionType: &wasm.TypeInstance{Type: &wasm.FunctionType{
 		Params: []wasm.ValueType{wasm.ValueTypeF64, wasm.ValueTypeI32},
-	}}
+	}}}
 	compiler := &amd64Compiler{locationStack: newValueLocationStack(), f: f}
 	compiler.pushFunctionParams()
-	require.Equal(t, uint64(len(f.Signature.Params)), compiler.locationStack.sp)
+	require.Equal(t, uint64(len(f.FunctionType.Type.Params)), compiler.locationStack.sp)
 	loc := compiler.locationStack.pop()
 	require.Equal(t, uint64(1), loc.stackPointer)
 	loc = compiler.locationStack.pop()
@@ -152,9 +210,9 @@ func TestAmd64Compiler_pushFunctionInputs(t *testing.T) {
 func Test_setJITStatus(t *testing.T) {
 	for _, s := range []jitCallStatusCode{
 		jitCallStatusCodeReturned,
-		jitCallStatusCodeCallWasmFunction,
+		jitCallStatusCodeCallFunction,
 		jitCallStatusCodeCallBuiltInFunction,
-		jitCallStatusCodeCallHostFunction,
+		jitCallStatusCodeCallFunction,
 		jitCallStatusCodeUnreachable,
 	} {
 		t.Run(s.String(), func(t *testing.T) {
@@ -179,35 +237,13 @@ func Test_setJITStatus(t *testing.T) {
 	}
 }
 
-func Test_setFunctionCallIndexFromConst(t *testing.T) {
-	// Build codes.
-	for _, index := range []int64{1, 5, 20} {
-		// Build codes.
-		compiler := requireNewCompiler(t)
-		compiler.initializeReservedRegisters()
-		compiler.setFunctionCallIndexFromConst(index)
-		compiler.returnFunction()
-
-		// Generate the code under test.
-		code, _, err := compiler.generate()
-		require.NoError(t, err)
-
-		// Run codes
-		env := newJITEnvironment()
-		env.exec(code)
-
-		// Check index.
-		require.Equal(t, index, env.functionCallIndex())
-	}
-}
-
 func Test_setFunctionCallIndexFromRegister(t *testing.T) {
 	reg := int16(x86.REG_R10)
-	for _, index := range []int64{1, 5, 20} {
+	for _, index := range []wasm.FunctionAddress{1, 5, 20} {
 		// Build codes.
 		compiler := requireNewCompiler(t)
 		compiler.initializeReservedRegisters()
-		compiler.movIntConstToRegister(index, reg)
+		compiler.movIntConstToRegister(int64(index), reg)
 		compiler.setFunctionCallIndexFromRegister(reg)
 		compiler.returnFunction()
 
@@ -220,7 +256,7 @@ func Test_setFunctionCallIndexFromRegister(t *testing.T) {
 		env.exec(code)
 
 		// Check index.
-		require.Equal(t, index, env.functionCallIndex())
+		require.Equal(t, index, env.functionCallAddress())
 	}
 }
 
@@ -239,7 +275,7 @@ func Test_setContinuationAtNextInstruction(t *testing.T) {
 	compiler.movIntConstToRegister(int64(50), tmpReg)
 	compiler.releaseRegisterToStack(loc)
 	require.NotContains(t, compiler.locationStack.usedRegisters, tmpReg)
-	compiler.setJITStatus(jitCallStatusCodeCallWasmFunction)
+	compiler.setJITStatus(jitCallStatusCodeCallFunction)
 	compiler.returnFunction()
 	// Generate the code under test.
 	code, _, err := compiler.generate()
@@ -255,7 +291,7 @@ func Test_setContinuationAtNextInstruction(t *testing.T) {
 	// Run code again on the continuation.
 	env.exec(code[env.continuationAddressOffset():])
 
-	require.Equal(t, jitCallStatusCodeCallWasmFunction, env.jitStatus())
+	require.Equal(t, jitCallStatusCodeCallFunction, env.jitStatus())
 	require.Equal(t, uint64(50), env.stack()[1])
 }
 
@@ -4402,82 +4438,33 @@ func TestAmd64Compiler_compile_min_max_copysign(t *testing.T) {
 }
 
 func TestAmd64Compiler_compileCall(t *testing.T) {
-	t.Run("host function", func(t *testing.T) {
-		const functionIndex = 5
-
-		env := newJITEnvironment()
-		compiler := requireNewCompiler(t)
-		compiler.f = &wasm.FunctionInstance{ModuleInstance: &wasm.ModuleInstance{}}
-
-		// Setup.
-		compiler.eng = env.eng
-		hostFuncRefValue := reflect.ValueOf(func() {})
-		hostFuncInstance := &wasm.FunctionInstance{HostFunction: &hostFuncRefValue, Signature: &wasm.FunctionType{}}
-		compiler.f.ModuleInstance.Functions = make([]*wasm.FunctionInstance, functionIndex+1)
-		compiler.f.ModuleInstance.Functions[functionIndex] = hostFuncInstance
-		compiler.eng.compiledHostFunctionIndex[hostFuncInstance] = functionIndex
-
-		// Build codes.
-		compiler.initializeReservedRegisters()
-		// Push the value onto stack.
-		_ = compiler.locationStack.pushValueOnStack() // dummy value, not actually used!
-		loc := compiler.locationStack.pushValueOnRegister(x86.REG_AX)
-		compiler.movIntConstToRegister(int64(50), loc.register)
-		err := compiler.compileCall(&wazeroir.OperationCall{FunctionIndex: functionIndex})
-		require.NoError(t, err)
-
-		// Generate the code under test.
-		code, _, err := compiler.generate()
-		require.NoError(t, err)
-
-		// Run code.
-		env.exec(code)
-
-		// Check the status.
-		require.Equal(t, jitCallStatusCodeCallHostFunction, env.jitStatus())
-		require.Equal(t, int64(functionIndex), env.functionCallIndex())
-		// All the registers must be written back to stack.
-		require.Equal(t, uint64(2), env.stackPointer())
-		require.Equal(t, uint64(50), env.stackTopAsUint64())
-	})
-	t.Run("wasm function", func(t *testing.T) {
-		const functionIndex = 20
+	const functionAddress wasm.FunctionAddress = 5 // arbitrary value for testing
+	env := newJITEnvironment()
+	compiler := requireNewCompiler(t)
+	compiler.f = &wasm.FunctionInstance{ModuleInstance: &wasm.ModuleInstance{}}
 
-		env := newJITEnvironment()
-		compiler := requireNewCompiler(t)
-		compiler.f = &wasm.FunctionInstance{ModuleInstance: &wasm.ModuleInstance{}}
+	// Setup.
+	compiler.eng = env.eng
+	wasmFuncInstance := &wasm.FunctionInstance{FunctionType: &wasm.TypeInstance{Type: &wasm.FunctionType{}}, Address: functionAddress}
+	compiler.f.ModuleInstance.Functions = []*wasm.FunctionInstance{wasmFuncInstance}
 
-		// Setup.
-		compiler.eng = env.eng
-		wasmFuncInstance := &wasm.FunctionInstance{Signature: &wasm.FunctionType{}}
-		compiler.f.ModuleInstance.Functions = make([]*wasm.FunctionInstance, functionIndex+1)
-		compiler.f.ModuleInstance.Functions[functionIndex] = wasmFuncInstance
-		env.eng.compiledWasmFunctionIndex[wasmFuncInstance] = functionIndex
+	compiler.initializeReservedRegisters()
 
-		// Build codes.
-		compiler.initializeReservedRegisters()
-		// Push the value onto stack.
-		_ = compiler.locationStack.pushValueOnStack() // dummy value, not actually used!
-		_ = compiler.locationStack.pushValueOnStack() // dummy value, not actually used!
-		loc := compiler.locationStack.pushValueOnRegister(x86.REG_AX)
-		compiler.movIntConstToRegister(int64(50), loc.register)
-		err := compiler.compileCall(&wazeroir.OperationCall{FunctionIndex: functionIndex})
-		require.NoError(t, err)
+	err := compiler.compileCall(&wazeroir.OperationCall{FunctionIndex: 0})
+	require.NoError(t, err)
 
-		// Generate the code under test.
-		code, _, err := compiler.generate()
-		require.NoError(t, err)
+	// Generate the code under test.
+	code, _, err := compiler.generate()
+	require.NoError(t, err)
 
-		// Run code.
-		env.exec(code)
+	// Run code.
+	env.exec(code)
 
-		// Check the status.
-		require.Equal(t, jitCallStatusCodeCallWasmFunction, env.jitStatus())
-		require.Equal(t, int64(functionIndex), env.functionCallIndex())
-		// All the registers must be written back to stack.
-		require.Equal(t, uint64(3), env.stackPointer())
-		require.Equal(t, uint64(50), env.stackTopAsUint64())
-	})
+	// Check the status.
+	require.Equal(t, jitCallStatusCodeCallFunction, env.jitStatus())
+	require.Equal(t, functionAddress, env.functionCallAddress())
+	// All the registers must be written back to stack.
+	require.Equal(t, uint64(0), env.stackPointer())
 }
 
 func TestAmd64Compiler_setupMemoryOffset(t *testing.T) {
@@ -5001,7 +4988,7 @@ func TestAmd64Compiler_compileMemoryGrow(t *testing.T) {
 	env.exec(code)
 
 	require.Equal(t, jitCallStatusCodeCallBuiltInFunction, env.jitStatus())
-	require.Equal(t, int64(builtinFunctionIndexMemoryGrow), env.functionCallIndex())
+	require.Equal(t, builtinFunctionAddressMemoryGrow, env.functionCallAddress())
 }
 
 func TestAmd64Compiler_compileMemorySize(t *testing.T) {
@@ -5023,7 +5010,7 @@ func TestAmd64Compiler_compileMemorySize(t *testing.T) {
 	env.exec(code)
 
 	require.Equal(t, jitCallStatusCodeCallBuiltInFunction, env.jitStatus())
-	require.Equal(t, int64(builtinFunctionIndexMemorySize), env.functionCallIndex())
+	require.Equal(t, wasm.FunctionAddress(builtinFunctionAddressMemorySize), env.functionCallAddress())
 }
 
 func TestAmd64Compiler_compileDrop(t *testing.T) {
@@ -5535,3 +5522,124 @@ func TestAmd64Compiler_compileGlobalSet(t *testing.T) {
 		})
 	}
 }
+
+func TestAmd64Compiler_compileCallIndirect(t *testing.T) {
+	// Ensure that the offset of wasm.TableInstance doesn't drift.
+	require.Equal(t, int(unsafe.Offsetof((&wasm.TableElement{}).FunctionAddress)), tableElementFunctionAddressOffest)
+	require.Equal(t, int(unsafe.Offsetof((&wasm.TableElement{}).FunctionTypeID)), tableElementTypeIDOffest)
+
+	t.Run("out of bounds", func(t *testing.T) {
+		env := newJITEnvironment()
+		env.setTable(make([]wasm.TableElement, 10))
+		compiler := requireNewCompiler(t)
+
+		targetOperation := &wazeroir.OperationCallIndirect{}
+		// 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{}}}}}
+
+		// Place the offfset value.
+		loc := compiler.locationStack.pushValueOnStack()
+		env.stack()[loc.stackPointer] = 1000000000
+
+		// Now emit the code.
+		compiler.initializeReservedRegisters()
+		require.NoError(t, compiler.compileCallIndirect(targetOperation))
+		compiler.returnFunction()
+
+		// Generate the code under test.
+		code, _, err := compiler.generate()
+		require.NoError(t, err)
+
+		// Run code.
+		env.exec(code)
+
+		require.Equal(t, jitCallStatusCodeTableOutOfBounds, env.jitStatus())
+	})
+
+	t.Run("type not match", func(t *testing.T) {
+		env := newJITEnvironment()
+		table := make([]wasm.TableElement, 10)
+		env.setTable(table)
+
+		compiler := requireNewCompiler(t)
+		targetOperation := &wazeroir.OperationCallIndirect{}
+		targetOffset := &wazeroir.OperationConstI32{Value: uint32(0)}
+		// Ensure that the module instance has the type information for targetOperation.TypeIndex,
+		compiler.f = &wasm.FunctionInstance{ModuleInstance: &wasm.ModuleInstance{Types: []*wasm.TypeInstance{{Type: &wasm.FunctionType{}, TypeID: 1000}}}}
+		// and the typeID doesn't match the table[targetOffset]'s type ID.
+		table[0] = wasm.TableElement{FunctionTypeID: 50}
+
+		// Place the offfset value.
+		err := compiler.compileConstI32(targetOffset)
+		require.NoError(t, err)
+
+		// Now emit the code.
+		compiler.initializeReservedRegisters()
+		require.NoError(t, compiler.compileCallIndirect(targetOperation))
+
+		// Generate the code under test.
+		compiler.returnFunction()
+		code, _, err := compiler.generate()
+		require.NoError(t, err)
+
+		// Run code.
+		env.exec(code)
+
+		require.Equal(t, jitCallStatusCodeTypeMismatchOnIndirectCall, env.jitStatus())
+	})
+
+	t.Run("ok", func(t *testing.T) {
+		table := make([]wasm.TableElement, 10)
+		for i := 0; i < len(table); i++ {
+			t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+				env := newJITEnvironment()
+				env.setTable(table)
+				compiler := requireNewCompiler(t)
+
+				targetType := &wasm.FunctionType{
+					Params:  []wasm.ValueType{wasm.ValueTypeI32, wasm.ValueTypeI32},
+					Results: []wasm.ValueType{wasm.ValueTypeF32, wasm.ValueTypeF32, wasm.ValueTypeF32, wasm.ValueTypeF32}}
+				targetOffset := &wazeroir.OperationConstI32{Value: uint32(i)}
+				targetOperation := &wazeroir.OperationCallIndirect{TypeIndex: 0}
+
+				// Ensure that the module instance has the type information for targetOperation.TypeIndex,
+				compiler.f = &wasm.FunctionInstance{ModuleInstance: &wasm.ModuleInstance{
+					Types: []*wasm.TypeInstance{{Type: targetType, TypeID: wasm.FunctionTypeID(i)}}},
+				}
+				// and the typeID  matches the table[targetOffset]'s type ID.
+				table[i] = wasm.TableElement{FunctionAddress: wasm.FunctionAddress(i), FunctionTypeID: wasm.FunctionTypeID(i)}
+
+				// Put the function call params.
+				for i := 0; i < len(targetType.Params); i++ {
+					compiler.locationStack.pushValueOnStack()
+				}
+
+				// Place the offfset value.
+				err := compiler.compileConstI32(targetOffset)
+				require.NoError(t, err)
+
+				// At this point, we should have three elements (function params + offset value) on the stack
+				require.Equal(t, uint64(3), compiler.locationStack.sp)
+
+				// Now emit the code.
+				compiler.initializeReservedRegisters()
+				require.NoError(t, compiler.compileCallIndirect(targetOperation))
+
+				// At this point, we consumed the function inputs and offset value, but the function result (four float values)
+				// are pushed onto the register.
+				require.Equal(t, uint64(4), compiler.locationStack.sp)
+
+				// Generate the code under test.
+				compiler.returnFunction()
+				code, _, err := compiler.generate()
+				require.NoError(t, err)
+
+				// Run code.
+				env.exec(code)
+
+				require.Equal(t, jitCallStatusCodeCallFunction, env.jitStatus())
+				require.Equal(t, wasm.FunctionAddress(i), env.functionCallAddress())
+			})
+		}
+	})
+}
diff --git a/wasm/module_test.go b/wasm/module_test.go
index 9be307ee..5f1099e1 100644
--- a/wasm/module_test.go
+++ b/wasm/module_test.go
@@ -6,27 +6,54 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestDecodeModule(t *testing.T) {
+func TestModule_Encode(t *testing.T) {
 	tests := []struct {
-		name        string
-		input       []byte
-		expected    *Module
-		expectedErr string
+		name     string
+		input    *Module
+		expected []byte
 	}{
 		{
 			name:     "empty",
-			input:    []byte("\x00asm\x01\x00\x00\x00"),
-			expected: &Module{CustomSections: map[string][]byte{}},
+			input:    &Module{},
+			expected: append(magic, version...),
 		},
 		{
-			name:        "wrong magic",
-			input:       []byte("wasm\x01\x00\x00\x00"),
-			expectedErr: "invalid magic number",
+			name:  "only name section",
+			input: &Module{NameSection: &NameSection{ModuleName: "simple"}},
+			expected: append(append(magic, version...),
+				SectionIDCustom, 0x0e, // 14 bytes in this section
+				0x04, 'n', 'a', 'm', 'e',
+				subsectionIDModuleName, 0x07, // 7 bytes in this subsection
+				0x06, // the Module name simple is 6 bytes long
+				's', 'i', 'm', 'p', 'l', 'e'),
 		},
 		{
-			name:        "wrong version",
-			input:       []byte("\x00asm\x01\x00\x00\x01"),
-			expectedErr: "invalid version header",
+			name: "only custom section",
+			input: &Module{CustomSections: map[string][]byte{
+				"meme": {1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+			}},
+			expected: append(append(magic, version...),
+				SectionIDCustom, 0xf, // 15 bytes in this section
+				0x04, 'm', 'e', 'm', 'e',
+				1, 2, 3, 4, 5, 6, 7, 8, 9, 0),
+		},
+		{
+			name: "name section and a custom section", // name should encode last
+			input: &Module{
+				NameSection: &NameSection{ModuleName: "simple"},
+				CustomSections: map[string][]byte{
+					"meme": {1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+				},
+			},
+			expected: append(append(magic, version...),
+				SectionIDCustom, 0xf, // 15 bytes in this section
+				0x04, 'm', 'e', 'm', 'e',
+				1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
+				SectionIDCustom, 0x0e, // 14 bytes in this section
+				0x04, 'n', 'a', 'm', 'e',
+				subsectionIDModuleName, 0x07, // 7 bytes in this subsection
+				0x06, // the Module name simple is 6 bytes long
+				's', 'i', 'm', 'p', 'l', 'e'),
 		},
 	}
 
@@ -34,78 +61,101 @@ func TestDecodeModule(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			m, e := DecodeModule(tc.input)
-			if tc.expectedErr != "" {
-				require.EqualError(t, e, tc.expectedErr)
-			} else {
-				require.NoError(t, e)
-				require.Equal(t, tc.expected, m)
-			}
+			bytes := tc.input.Encode()
+			require.Equal(t, tc.expected, bytes)
 		})
 	}
 }
 
-func TestModuleDecodeCustomNameSection(t *testing.T) {
-	m := Module{
-		CustomSections: map[string][]byte{},
+// TestDecodeModule relies on unit tests for Module.Encode, specifically that the encoding is both known and correct.
+// This avoids having to copy/paste or share variables to assert against byte arrays.
+func TestDecodeModule(t *testing.T) {
+	tests := []struct {
+		name  string
+		input *Module // round trip test!
+	}{
+		{
+			name:  "empty",
+			input: &Module{},
+		},
+		{
+			name:  "only name section",
+			input: &Module{NameSection: &NameSection{ModuleName: "simple"}},
+		},
+		{
+			name: "only custom section",
+			input: &Module{CustomSections: map[string][]byte{
+				"meme": {1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+			}},
+		},
+		{
+			name: "name section and a custom section",
+			input: &Module{
+				NameSection: &NameSection{ModuleName: "simple"},
+				CustomSections: map[string][]byte{
+					"meme": {1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+				},
+			},
+		},
 	}
-	// Name section not found.
-	_, err := m.DecodeCustomNameSection()
-	require.Error(t, err)
-
-	// Name section found, but cannot read subsection ID
-	m.CustomSections["name"] = []byte{}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read subsection")
-
-	// Cannot read the subsection size
-	m.CustomSections["name"] = []byte{0x04}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read the size of subsection 4")
 
-	// Function name subsection found, but name vector size not found.
-	m.CustomSections["name"] = []byte{0x01, 0x00}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read the size of name vector: EOF")
+	for _, tt := range tests {
+		tc := tt
 
-	// Function name subsection found with name vector size=1.
-	// But cannot read the vector content with EOF.
-	m.CustomSections["name"] = []byte{0x01, 0x01, 0x01}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read function index: EOF")
-	m.CustomSections["name"] = []byte{0x01, 0x01, 0x01, 0x00}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read function name size: EOF")
-	m.CustomSections["name"] = []byte{0x01, 0x01, 0x01, 0x00, 0x01}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read function name: EOF")
+		t.Run(tc.name, func(t *testing.T) {
+			m, e := DecodeModule(tc.input.Encode())
+			require.NoError(t, e)
+			require.Equal(t, tc.input, m)
+		})
+	}
+}
 
-	// Valid inputs.
-	m.CustomSections["name"] = []byte{
-		0x01 /* function subsection id */, 0x04, /* subsection size*/
-		0x01, /* size of name map */
-		0x00 /* function index*/, 0x01 /* size of name */, 'a',
+func TestDecodeModule_Errors(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       []byte
+		expectedErr string
+	}{
+		{
+			name:        "wrong magic",
+			input:       []byte("wasm\x01\x00\x00\x00"),
+			expectedErr: "invalid magic number",
+		},
+		{
+			name:        "wrong version",
+			input:       []byte("\x00asm\x01\x00\x00\x01"),
+			expectedErr: "invalid version header",
+		},
+		{
+			name: "redundant custom section",
+			input: append(append(magic, version...),
+				SectionIDCustom, 0x09, // 9 bytes in this section
+				0x04, 'm', 'e', 'm', 'e',
+				subsectionIDModuleName, 0x03, 0x01, 'x',
+				SectionIDCustom, 0x09, // 9 bytes in this section
+				0x04, 'm', 'e', 'm', 'e',
+				subsectionIDModuleName, 0x03, 0x01, 'y'),
+			expectedErr: "readSections failed: section ID 0: malformed custom section meme",
+		},
+		{
+			name: "redundant name section",
+			input: append(append(magic, version...),
+				SectionIDCustom, 0x09, // 9 bytes in this section
+				0x04, 'n', 'a', 'm', 'e',
+				subsectionIDModuleName, 0x03, 0x01, 'x',
+				SectionIDCustom, 0x09, // 9 bytes in this section
+				0x04, 'n', 'a', 'm', 'e',
+				subsectionIDModuleName, 0x03, 0x01, 'x'),
+			expectedErr: "readSections failed: section ID 0: malformed custom section name",
+		},
 	}
-	names, err := m.DecodeCustomNameSection()
-	require.NoError(t, err)
-	require.Equal(t, "a", names[0])
 
-	m.CustomSections["name"] = []byte{
-		0x00, 0x00, // other subsections.
-		0x03, 0x01, 0x00, // other subsections.
-		0x01 /* function subsection id */, 0x04, /* subsection size*/
-		0x02, /* size of name map */
-		0x00 /* function index*/, 0x01 /* size of name */, 'a',
-		0x01 /* function index*/, 0x02 /* size of name */, 'a', 'b',
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			_, e := DecodeModule(tc.input)
+			require.EqualError(t, e, tc.expectedErr)
+		})
 	}
-	names, err = m.DecodeCustomNameSection()
-	require.NoError(t, err)
-	require.Equal(t, "a", names[0])
-	require.Equal(t, "ab", names[1])
 }
diff --git a/wasm/names_test.go b/wasm/names_test.go
new file mode 100644
index 00000000..6c89713d
--- /dev/null
+++ b/wasm/names_test.go
@@ -0,0 +1,318 @@
+package wasm
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestNameSection_EncodeData(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    *NameSection
+		expected []byte
+	}{
+		{
+			name:  "empty",
+			input: &NameSection{},
+		},
+		{
+			name: "only module",
+			// Ex. (module $simple )
+			input: &NameSection{ModuleName: "simple"},
+			expected: []byte{
+				subsectionIDModuleName, 0x07, // 7 bytes
+				0x06, // the Module name simple is 6 bytes long
+				's', 'i', 'm', 'p', 'l', 'e',
+			},
+		},
+		{
+			name: "module and function name",
+			//	(module $simple
+			//		(import "" "Hello" (func $hello))
+			//		(start $hello)
+			//	)
+			input: &NameSection{
+				ModuleName:    "simple",
+				FunctionNames: map[uint32]string{0x00: "hello"},
+			},
+			expected: []byte{
+				subsectionIDModuleName, 0x07, // 7 bytes
+				0x06, // the Module name simple is 6 bytes long
+				's', 'i', 'm', 'p', 'l', 'e',
+				subsectionIDFunctionNames, 0x08, // 8 bytes
+				0x01, // one function name
+				0x00, // the function index is zero
+				0x05, // the function name hello is 5 bytes long
+				'h', 'e', 'l', 'l', 'o',
+			},
+		},
+		{
+			name: "two function names", // Ex. TinyGo which at one point didn't set a module name
+			//	(module
+			//		(import "wasi_snapshot_preview1" "args_sizes_get" (func $runtime.args_sizes_get (param i32, i32) (result i32)))
+			//		(import "wasi_snapshot_preview1" "fd_write" (func $runtime.fd_write (param i32, i32, i32, i32) (result i32)))
+			//	)
+			input: &NameSection{
+				FunctionNames: map[uint32]string{
+					0x00: "runtime.args_sizes_get",
+					0x01: "runtime.fd_write",
+				},
+			},
+			expected: []byte{
+				subsectionIDFunctionNames, 0x2b, // 43 bytes
+				0x02, // two function names
+				0x00, // the function index is zero
+				0x16, // the function name runtime.args_sizes_get is 22 bytes long
+				'r', 'u', 'n', 't', 'i', 'm', 'e', '.', 'a', 'r', 'g', 's', '_', 's', 'i', 'z', 'e', 's', '_', 'g', 'e', 't',
+				0x01, // the function index is one
+				0x10, // the function name runtime.fd_write is 16 bytes long
+				'r', 'u', 'n', 't', 'i', 'm', 'e', '.', 'f', 'd', '_', 'w', 'r', 'i', 't', 'e',
+			},
+		},
+		{
+			name: "function with local names",
+			//	(module
+			//		(import "Math" "Mul" (func $mul (param $x f32) (param $y f32) (result f32)))
+			//		(import "Math" "Add" (func $add (param $l f32) (param $r f32) (result f32)))
+			//	)
+			input: &NameSection{
+				FunctionNames: map[uint32]string{
+					0x00: "mul",
+					0x01: "add",
+				},
+				LocalNames: map[uint32]map[uint32]string{
+					0x00: {0x00: "x", 0x01: "y"},
+					0x01: {0x00: "l", 0x01: "r"},
+				},
+			},
+			expected: []byte{
+				subsectionIDFunctionNames, 0x0b, // 7 bytes
+				0x02,                      // two function names
+				0x00, 0x03, 'm', 'u', 'l', // index 0, size of "mul", "mul"
+				0x01, 0x03, 'a', 'd', 'd', // index 1, size of "add", "add"
+				subsectionIDLocalNames, 0x11, // 17 bytes
+				0x02,       // two functions
+				0x00, 0x02, // index 0 has 2 locals
+				0x00, 0x01, 'x', // index 0, size of "x", "x"
+				0x01, 0x01, 'y', // index 1, size of "y", "y"
+				0x01, 0x02, // index 1 has 2 locals
+				0x00, 0x01, 'l', // index 0, size of "l", "l"
+				0x01, 0x01, 'r', // index 1, size of "r", "r"
+			},
+		},
+		{
+			name: "function with local names - out of order",
+			// Names are associated with functions and parameters which are ordered in the module, but decoupled via
+			// NameSection. The impact is they can become out-of-order, so we have to sort during encode.
+			//
+			// Note: We can't force map iteration out of order. However, reversing the order of the same values across
+			// two tests improves the likelihood of needing to sort!
+			input: &NameSection{
+				FunctionNames: map[uint32]string{
+					0x01: "add",
+					0x00: "mul",
+				},
+				LocalNames: map[uint32]map[uint32]string{
+					0x01: {0x01: "r", 0x00: "l"},
+					0x00: {0x01: "y", 0x00: "x"},
+				},
+			},
+			expected: (&NameSection{
+				FunctionNames: map[uint32]string{
+					0x00: "mul",
+					0x01: "add",
+				},
+				LocalNames: map[uint32]map[uint32]string{
+					0x00: {0x00: "x", 0x01: "y"},
+					0x01: {0x00: "l", 0x01: "r"},
+				},
+			}).EncodeData(),
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			bytes := tc.input.EncodeData()
+			require.Equal(t, tc.expected, bytes)
+		})
+	}
+}
+
+func TestEncodeNameSubsection(t *testing.T) {
+	subsectionID := uint8(1)
+	name := []byte("simple")
+	require.Equal(t, []byte{
+		subsectionID,
+		byte(1 + 6), // 1 is the size of 6 in LEB128 encoding
+		6, 's', 'i', 'm', 'p', 'l', 'e'}, encodeNameSubsection(subsectionID, encodeSizePrefixed(name)))
+}
+
+func TestEncodeNameMapEntry(t *testing.T) {
+	index := uint32(1)
+	name := []byte("hello")
+	require.Equal(t, []byte{byte(index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameMapEntry(index, name))
+}
+
+func TestEncodeSizePrefixed(t *testing.T) {
+	// We expect size in bytes (LEB128 encoded) then the bytes
+	require.Equal(t, []byte{5, 'h', 'e', 'l', 'l', 'o'}, encodeSizePrefixed([]byte("hello")))
+}
+
+// TestDecodeNameSection relies on unit tests for NameSection.EncodeData, specifically that the encoding is
+// both known and correct. This avoids having to copy/paste or share variables to assert against byte arrays.
+func TestDecodeNameSection(t *testing.T) {
+	tests := []struct {
+		name  string
+		input *NameSection // round trip test!
+	}{{
+		name:  "empty",
+		input: &NameSection{},
+	},
+		{
+			name:  "only module",
+			input: &NameSection{ModuleName: "simple"},
+		},
+		{
+			name: "module and function name",
+			input: &NameSection{
+				ModuleName:    "simple",
+				FunctionNames: map[uint32]string{0x00: "hello"},
+			},
+		},
+		{
+			name: "two function names",
+			input: &NameSection{
+				FunctionNames: map[uint32]string{
+					0x00: "runtime.args_sizes_get",
+					0x01: "runtime.fd_write",
+				},
+			},
+		},
+		{
+			name: "function with local names",
+			input: &NameSection{
+				FunctionNames: map[uint32]string{
+					0x00: "mul",
+					0x01: "add",
+				},
+				LocalNames: map[uint32]map[uint32]string{
+					0x00: {0x00: "x", 0x01: "y"},
+					0x01: {0x00: "l", 0x01: "r"},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			ns, err := DecodeNameSection(tc.input.EncodeData())
+			require.NoError(t, err)
+			require.Equal(t, tc.input, ns)
+		})
+	}
+}
+
+func TestDecodeNameSection_Errors(t *testing.T) {
+	// currently, we ignore the size of known subsections
+	ignoredSubsectionSize := byte(50)
+	tests := []struct {
+		name        string
+		input       []byte
+		expectedErr string
+	}{
+		{
+			name:        "EOF after module name subsection ID",
+			input:       []byte{subsectionIDModuleName},
+			expectedErr: "failed to read the size of subsection[0]: EOF",
+		},
+		{
+			name:        "EOF after function names subsection ID",
+			input:       []byte{subsectionIDFunctionNames},
+			expectedErr: "failed to read the size of subsection[1]: EOF",
+		},
+		{
+			name:        "EOF after local names subsection ID",
+			input:       []byte{subsectionIDLocalNames},
+			expectedErr: "failed to read the size of subsection[2]: EOF",
+		},
+		{
+			name:        "EOF after unknown subsection ID",
+			input:       []byte{4},
+			expectedErr: "failed to read the size of subsection[4]: EOF",
+		},
+		{
+			name:        "EOF after module name subsection size",
+			input:       []byte{subsectionIDModuleName, ignoredSubsectionSize},
+			expectedErr: "failed to read module name size: EOF",
+		},
+		{
+			name:        "EOF after function names subsection size",
+			input:       []byte{subsectionIDFunctionNames, ignoredSubsectionSize},
+			expectedErr: "failed to read the function count of subsection[1]: EOF",
+		},
+		{
+			name:        "EOF after local names subsection size",
+			input:       []byte{subsectionIDLocalNames, ignoredSubsectionSize},
+			expectedErr: "failed to read the function count of subsection[2]: EOF",
+		},
+		{
+			name:        "EOF skipping unknown subsection size",
+			input:       []byte{4, 100},
+			expectedErr: "failed to skip subsection[4]: EOF",
+		},
+		{
+			name:        "EOF after module name size",
+			input:       []byte{subsectionIDModuleName, ignoredSubsectionSize, 5},
+			expectedErr: "failed to read module name: EOF",
+		},
+		{
+			name:        "EOF after function name count",
+			input:       []byte{subsectionIDFunctionNames, ignoredSubsectionSize, 2},
+			expectedErr: "failed to read a function index in subsection[1]: EOF",
+		},
+		{
+			name:        "EOF after local names function count",
+			input:       []byte{subsectionIDLocalNames, ignoredSubsectionSize, 2},
+			expectedErr: "failed to read a function index in subsection[2]: EOF",
+		},
+		{
+			name:        "EOF after function name index",
+			input:       []byte{subsectionIDFunctionNames, ignoredSubsectionSize, 2, 0},
+			expectedErr: "failed to read function[0] name size: EOF",
+		},
+		{
+			name:        "EOF after local names function index",
+			input:       []byte{subsectionIDLocalNames, ignoredSubsectionSize, 2, 0},
+			expectedErr: "failed to read the local count for function[0]: EOF",
+		},
+		{
+			name:        "EOF after function name size",
+			input:       []byte{subsectionIDFunctionNames, ignoredSubsectionSize, 2, 0, 5},
+			expectedErr: "failed to read function[0] name: EOF",
+		},
+		{
+			name:        "EOF after local names count for a function index",
+			input:       []byte{subsectionIDLocalNames, ignoredSubsectionSize, 2, 0, 2},
+			expectedErr: "failed to read a local index of function[0]: EOF",
+		},
+		{
+			name:        "EOF after local name size",
+			input:       []byte{subsectionIDLocalNames, ignoredSubsectionSize, 2, 0, 2, 1},
+			expectedErr: "failed to read function[0] local[1] name size: EOF",
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := DecodeNameSection(tc.input)
+			require.EqualError(t, err, tc.expectedErr)
+		})
+	}
+}
diff --git a/wasm/spectests/spec_test.go b/wasm/spectests/spec_test.go
index 3393b28f..d1a5c41e 100644
--- a/wasm/spectests/spec_test.go
+++ b/wasm/spectests/spec_test.go
@@ -191,14 +191,14 @@ func TestJIT(t *testing.T) {
 		"break-drop.wast":             false, // Needs br_table support
 		"br_if.wast":                  false, // Needs br_table support
 		"br.wast":                     false, // Needs br_table support
-		"call_indirect.wast":          false, // Needs call_indirect support
-		"call.wast":                   false, // Needs call_indirect support
+		"call_indirect.wast":          false, // Needs br_table support
+		"call.wast":                   false, // Needs br_table support
 		"comments.wast":               true,
 		"const.wast":                  true,
 		"conversions.wast":            true,
 		"custom.wast":                 true,
 		"data.wast":                   true,
-		"elem.wast":                   false, // Needs call_indirect support
+		"elem.wast":                   true,
 		"endianness.wast":             true,
 		"exports.wast":                true,
 		"f32_bitwise.wast":            true,
@@ -213,25 +213,25 @@ func TestJIT(t *testing.T) {
 		"float_memory.wast":           true,
 		"float_misc.wast":             true,
 		"forward.wast":                true,
-		"func_ptrs.wast":              false, // Needs call_indirect support
+		"func_ptrs.wast":              true,
 		"func.wast":                   false, // Needs br_table support
 		"globals.wast":                false, // Needs br_table support
 		"i32.wast":                    true,
 		"i64.wast":                    true,
 		"if.wast":                     false, // Needs br_table support
-		"imports.wast":                false, // Needs call_indirect support
+		"imports.wast":                true,
 		"inline-module.wast":          true,
 		"int_exprs.wast":              true,
 		"int_literals.wast":           true,
 		"labels.wast":                 false, // Needs br_table support
 		"left-to-right.wast":          false, // Needs br_table support
-		"linking.wast":                false, // Needs call_indirect support
-		"load.wast":                   false, // Needs call_indirect support
+		"linking.wast":                true,
+		"load.wast":                   false, // Needs br_table support
 		"local_get.wast":              false, // Needs br_table support
 		"local_set.wast":              false, // Needs br_table support
 		"local_tee.wast":              false, // Needs br_table support
-		"loop.wast":                   false, // Needs call_indirect support
-		"memory_grow.wast":            false, // Needs call_indirect support
+		"loop.wast":                   false, // Needs br_table support
+		"memory_grow.wast":            false, // Needs br_table support
 		"memory_redundancy.wast":      true,
 		"memory_size.wast":            true,
 		"memory_trap.wast":            true,
@@ -241,7 +241,7 @@ func TestJIT(t *testing.T) {
 		"return.wast":                 true,
 		"select.wast":                 false, // Needs br_table support
 		"skip-stack-guard-page.wast":  true,
-		"stack.wast":                  false, // Needs call_indirect support
+		"stack.wast":                  true,
 		"start.wast":                  true,
 		"store.wast":                  false, // Needs br_table support
 		"switch.wast":                 false, // Needs br_table support
diff --git a/wasm/type_test.go b/wasm/type_test.go
new file mode 100644
index 00000000..22c23f78
--- /dev/null
+++ b/wasm/type_test.go
@@ -0,0 +1,36 @@
+package wasm
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestFunctionType_String(t *testing.T) {
+	tp := FunctionType{}
+	require.Equal(t, "null_null", tp.String())
+
+	// With params.
+	tp = FunctionType{Params: []ValueType{ValueTypeI32}}
+	require.Equal(t, "i32_null", tp.String())
+	tp = FunctionType{Params: []ValueType{ValueTypeI32, ValueTypeF64}}
+	require.Equal(t, "i32f64_null", tp.String())
+	tp = FunctionType{Params: []ValueType{ValueTypeF32, ValueTypeI32, ValueTypeF64}}
+	require.Equal(t, "f32i32f64_null", tp.String())
+
+	// With results.
+	tp = FunctionType{Results: []ValueType{ValueTypeI64}}
+	require.Equal(t, "null_i64", tp.String())
+	tp = FunctionType{Results: []ValueType{ValueTypeI64, ValueTypeF32}}
+	require.Equal(t, "null_i64f32", tp.String())
+	tp = FunctionType{Results: []ValueType{ValueTypeF32, ValueTypeI32, ValueTypeF64}}
+	require.Equal(t, "null_f32i32f64", tp.String())
+
+	// With params and results.
+	tp = FunctionType{Params: []ValueType{ValueTypeI32}, Results: []ValueType{ValueTypeI64}}
+	require.Equal(t, "i32_i64", tp.String())
+	tp = FunctionType{Params: []ValueType{ValueTypeI64, ValueTypeF32}, Results: []ValueType{ValueTypeI64, ValueTypeF32}}
+	require.Equal(t, "i64f32_i64f32", tp.String())
+	tp = FunctionType{Params: []ValueType{ValueTypeI64, ValueTypeF32, ValueTypeF64}, Results: []ValueType{ValueTypeF32, ValueTypeI32, ValueTypeF64}}
+	require.Equal(t, "i64f32f64_f32i32f64", tp.String())
+}
diff --git a/wasm/wat/bind_test.go b/wasm/wat/bind_test.go
index 050ce1a9..4bf62918 100644
--- a/wasm/wat/bind_test.go
+++ b/wasm/wat/bind_test.go
@@ -23,14 +23,14 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeInlined: &inlinedTypeFunc{paramI32I32I32I32ResultI32, 0, 0}},
 				},
 			},
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexZero},
 				},
 			},
@@ -40,18 +40,18 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeInlined: &inlinedTypeFunc{paramI32I32ResultI32, 0, 0}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeInlined: &inlinedTypeFunc{paramI32I32I32I32ResultI32, 0, 0}},
 				},
 			},
 			expected: &module{
 				types: []*typeFunc{paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: indexZero},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexOne},
 				},
 			},
@@ -61,18 +61,18 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "runtime.args_get",
 						typeInlined: &inlinedTypeFunc{paramI32I32ResultI32, 0, 0}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeInlined: &inlinedTypeFunc{paramI32I32ResultI32, 0, 0}},
 				},
 			},
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "runtime.args_get",
 						typeIndex: indexOne},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: indexOne},
 				},
 			},
@@ -82,26 +82,26 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{
 					typeFuncEmpty,
-					{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
-					{name: "$i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
+					{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+					{name: "i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
-						typeIndex: &index{ID: "$i32i32_i32", line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
-						typeIndex: &index{ID: "$i32i32i32i32_i32", line: 6, col: 76}},
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
+						typeIndex: &index{ID: "i32i32_i32", line: 5, col: 86}},
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
+						typeIndex: &index{ID: "i32i32i32i32_i32", line: 6, col: 76}},
 				},
 			},
 			expected: &module{
 				types: []*typeFunc{
 					typeFuncEmpty,
-					{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
-					{name: "$i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
+					{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+					{name: "i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 6, col: 76}},
 				},
 			},
@@ -111,18 +111,18 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 6, col: 76}},
 				},
 			},
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 6, col: 76}},
 				},
 			},
@@ -132,16 +132,16 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{typeFuncEmpty},
 				importFuncs: []*importFunc{
-					{funcName: "$one", typeIndex: indexZero},
-					{funcName: "$two", typeIndex: indexZero},
+					{funcName: "one", typeIndex: indexZero},
+					{funcName: "two", typeIndex: indexZero},
 				},
-				startFunction: &index{ID: "$two", line: 3, col: 9},
+				startFunction: &index{ID: "two", line: 3, col: 9},
 			},
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty},
 				importFuncs: []*importFunc{
-					{funcName: "$one", typeIndex: indexZero},
-					{funcName: "$two", typeIndex: indexZero},
+					{funcName: "one", typeIndex: indexZero},
+					{funcName: "two", typeIndex: indexZero},
 				},
 				startFunction: &index{numeric: 1, line: 3, col: 9},
 			},
@@ -192,7 +192,7 @@ func TestBindIndices_Errors(t *testing.T) {
 			name: "function type points nowhere",
 			input: &module{
 				types:       []*typeFunc{typeFuncEmpty},
-				importFuncs: []*importFunc{{name: "hello", typeIndex: &index{ID: "$main", line: 3, col: 9}}},
+				importFuncs: []*importFunc{{name: "hello", typeIndex: &index{ID: "main", line: 3, col: 9}}},
 			},
 			expectedErr: "3:9: unknown ID $main in module.import[0].func.type",
 		},
@@ -208,7 +208,7 @@ func TestBindIndices_Errors(t *testing.T) {
 		{
 			name: "start points nowhere",
 			input: &module{
-				startFunction: &index{ID: "$main", line: 1, col: 16},
+				startFunction: &index{ID: "main", line: 1, col: 16},
 			},
 			expectedErr: "1:16: unknown ID $main in module.start",
 		},
diff --git a/wasm/wat/convert_test.go b/wasm/wat/convert_test.go
index e1e33c19..3d0349d6 100644
--- a/wasm/wat/convert_test.go
+++ b/wasm/wat/convert_test.go
@@ -66,6 +66,12 @@ func TestTextToBinary(t *testing.T) {
 						},
 					},
 				},
+				NameSection: &wasm.NameSection{
+					FunctionNames: map[uint32]string{
+						0: "runtime.path_open",
+						1: "runtime.fd_write",
+					},
+				},
 			},
 		},
 		{
@@ -98,6 +104,12 @@ func TestTextToBinary(t *testing.T) {
 						},
 					},
 				},
+				NameSection: &wasm.NameSection{
+					FunctionNames: map[uint32]string{
+						0: "runtime.arg_sizes_get",
+						1: "runtime.fd_write",
+					},
+				},
 			},
 		},
 		{
@@ -116,6 +128,11 @@ func TestTextToBinary(t *testing.T) {
 					},
 				}},
 				StartSection: &zero,
+				NameSection: &wasm.NameSection{
+					FunctionNames: map[uint32]string{
+						0: "hello",
+					},
+				},
 			},
 		},
 		{
@@ -180,6 +197,20 @@ func TestTextToBinary(t *testing.T) {
 					},
 				},
 				StartSection: &four,
+				NameSection: &wasm.NameSection{
+					ModuleName: "example",
+					FunctionNames: map[uint32]string{
+						0: "runtime.arg_sizes_get",
+						1: "runtime.fd_write",
+						2: "mul",
+						3: "add",
+						4: "hello",
+					},
+					LocalNames: map[uint32]map[uint32]string{
+						2: {0: "x", 1: "y"},
+						3: {0: "l", 1: "r"},
+					},
+				},
 			},
 		},
 	}
diff --git a/wasm/wat/index_parser_test.go b/wasm/wat/index_parser_test.go
index 0b3d9689..e3e35125 100644
--- a/wasm/wat/index_parser_test.go
+++ b/wasm/wat/index_parser_test.go
@@ -12,7 +12,7 @@ func TestParseIndex(t *testing.T) {
 		expected    *index
 	}{
 		{"numeric", "(1)", &index{numeric: 0x1, line: 1, col: 2}},
-		{"symbolic", "($v_v)", &index{ID: "$v_v", line: 1, col: 2}},
+		{"symbolic", "($v_v)", &index{ID: "v_v", line: 1, col: 2}},
 	}
 
 	for _, tt := range tests {
diff --git a/wasm/wat/names_test.go b/wasm/wat/names_test.go
deleted file mode 100644
index 9baf8b69..00000000
--- a/wasm/wat/names_test.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package wat
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/require"
-
-	"github.com/tetratelabs/wazero/wasm"
-)
-
-func TestEncodeNameSection(t *testing.T) {
-	m, err := parseModule([]byte(`(module $simple
-	(import "" "hello" (func $hello))
-	(start $hello)
-)`))
-	require.NoError(t, err)
-
-	// TIP: the below is the binary suffix of `wat2wasm --debug-names --debug-parser -v simple.wat` where simple.wat
-	// contains the same text as the above inlined text format.
-	require.Equal(t, []byte{
-		0x00, // module subsection ID zero
-		0x07, // 7 bytes to follow
-		0x06, // the module name simple is 6 characters long
-		's', 'i', 'm', 'p', 'l', 'e',
-		0x01, // function subsection ID one
-		0x08, // 8 bytes to follow
-		0x01, // one function name
-		0x00, // the function index is zero
-		0x05, // the function name hello is 5 characters long
-		'h', 'e', 'l', 'l', 'o',
-	}, encodeNameSection(m))
-}
-
-// TestEncodeNameSection_OnlyFuncName shows that we don't rely on the module name being present. For example, this isn't
-// encoded in TinyGo.
-func TestEncodeNameSection_OnlyFuncName(t *testing.T) {
-	func0, func1 := "runtime.args_sizes_get", "runtime.fd_write" // note: no leading '$'
-	i32 := wasm.ValueTypeI32
-	type1 := &typeFunc{params: []wasm.ValueType{i32, i32}, result: i32}
-	type2 := &typeFunc{params: []wasm.ValueType{i32, i32, i32, i32}, result: i32}
-	m := &module{
-		types: []*typeFunc{type1, type2},
-		importFuncs: []*importFunc{
-			{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_sizes_get", funcName: "$" + func0,
-				typeIndex: &index{ /* TODO 0*/ }},
-			{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$" + func1,
-				typeIndex: &index{ /* TODO 1*/ }},
-		},
-	}
-
-	expected := append(append([]byte{
-		0x01, // function subsection ID one
-		// length includes overhead for size in bytes of the function name count, plus index + length prefix per name
-		byte(1 + 2 + 2 + len(func0) + len(func1)),
-		0x02, // two function names
-	},
-		append([]byte{0x00 /* funcIndex */, byte(len(func0))}, func0...)...),
-		append([]byte{0x01 /* funcIndex */, byte(len(func1))}, func1...)...)
-
-	require.Equal(t, expected, encodeNameSection(m))
-}
-
-func TestEncodeNameSubsection(t *testing.T) {
-	subsectionID := uint8(1)
-	name := "$simple"
-	require.Equal(t, []byte{
-		subsectionID,
-		byte(1 + 6), // 1 is the size of 6 in LEB128 encoding
-		6, 's', 'i', 'm', 'p', 'l', 'e'}, encodeNameSubsection(subsectionID, encodeName(name)))
-}
-
-func TestEncodeNameMapEntry(t *testing.T) {
-	index := uint32(1)
-	name := "$hello"
-	require.Equal(t, []byte{byte(index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameMapEntry(index, name))
-}
-
-func TestEncodeName(t *testing.T) {
-	// We expect a length (in LEB128) prefixed string encoding
-	require.Equal(t, []byte{5, 'h', 'e', 'l', 'l', 'o'}, encodeName("$hello"))
-}
diff --git a/wasm/wat/parser_test.go b/wasm/wat/parser_test.go
index 33b96caa..7a47f21d 100644
--- a/wasm/wat/parser_test.go
+++ b/wasm/wat/parser_test.go
@@ -53,14 +53,14 @@ func TestParseModule(t *testing.T) {
 		{
 			name:     "only name",
 			input:    "(module $tools)",
-			expected: &module{name: "$tools"},
+			expected: &module{name: "tools"},
 		},
 		{
 			name:  "type func one empty",
 			input: "(module (type $i32i32_i32 (func (param i32 i32) (result i32))) (type (func)))",
 			expected: &module{
 				types: []*typeFunc{
-					{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+					{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
 					typeFuncEmpty,
 				},
 			},
@@ -70,13 +70,13 @@ func TestParseModule(t *testing.T) {
 			input: "(module (type $mul (func (param $x f32) (param $y f32) (result f32))))",
 			expected: &module{
 				types: []*typeFunc{
-					{name: "$mul", params: []wasm.ValueType{f32, f32}, result: f32},
+					{name: "mul", params: []wasm.ValueType{f32, f32}, result: f32},
 				},
 				typeParamNames: []*typeParamNames{{
 					index: uint32(0),
 					paramNames: paramNames{
-						&paramNameIndex{uint32(0), []byte{'$', 'x'}},
-						&paramNameIndex{uint32(1), []byte{'$', 'y'}},
+						&paramNameIndex{uint32(0), []byte{'x'}},
+						&paramNameIndex{uint32(1), []byte{'y'}},
 					},
 				}},
 			},
@@ -91,8 +91,8 @@ func TestParseModule(t *testing.T) {
 				typeParamNames: []*typeParamNames{{
 					index: uint32(0),
 					paramNames: paramNames{
-						&paramNameIndex{uint32(2), []byte{'$', 'v'}},
-						&paramNameIndex{uint32(4), []byte{'$', 't'}},
+						&paramNameIndex{uint32(2), []byte{'v'}},
+						&paramNameIndex{uint32(4), []byte{'t'}},
 					},
 				}},
 			},
@@ -106,23 +106,23 @@ func TestParseModule(t *testing.T) {
 )`,
 			expected: &module{
 				types: []*typeFunc{
-					{name: "$mul", params: []wasm.ValueType{f32, f32}, result: f32},
+					{name: "mul", params: []wasm.ValueType{f32, f32}, result: f32},
 					typeFuncEmpty,
-					{name: "$add", params: []wasm.ValueType{f32, f32}, result: f32},
+					{name: "add", params: []wasm.ValueType{f32, f32}, result: f32},
 				},
 				typeParamNames: []*typeParamNames{
 					{
 						index: uint32(0),
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'x'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'y'}},
+							&paramNameIndex{uint32(0), []byte{'x'}},
+							&paramNameIndex{uint32(1), []byte{'y'}},
 						},
 					},
 					{
 						index: uint32(2),
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'l'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'r'}},
+							&paramNameIndex{uint32(0), []byte{'l'}},
+							&paramNameIndex{uint32(1), []byte{'r'}},
 						},
 					},
 				},
@@ -140,7 +140,7 @@ func TestParseModule(t *testing.T) {
 					paramI32I32I32I32ResultI32,
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexOne},
 				},
 			},
@@ -220,7 +220,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexZero},
 				},
 			},
@@ -233,7 +233,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexZero},
 				},
 			},
@@ -248,7 +248,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexZero},
 				},
 			},
@@ -261,7 +261,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "proc_exit", funcName: "$runtime.proc_exit",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "proc_exit", funcName: "runtime.proc_exit",
 						typeIndex: indexZero},
 				},
 			},
@@ -282,7 +282,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32I32I64I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "$runtime.path_open",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "runtime.path_open",
 						typeIndex: indexZero},
 				},
 			},
@@ -295,7 +295,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32I32I64I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "$runtime.path_open",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "runtime.path_open",
 						typeIndex: indexZero},
 				},
 			},
@@ -309,9 +309,9 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: indexZero},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexOne},
 				},
 			},
@@ -328,13 +328,13 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{
 					typeFuncEmpty,
-					{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
-					{name: "$i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
+					{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+					{name: "i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 6, col: 76}},
 				},
 			},
@@ -351,9 +351,9 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 6, col: 76}},
 				},
 			},
@@ -368,9 +368,9 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "runtime.args_get",
 						typeIndex: indexOne},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: indexOne},
 				},
 			},
@@ -386,9 +386,9 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "runtime.args_get",
 						typeIndex: &index{numeric: 1, line: 4, col: 76}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
 				},
 			},
@@ -404,9 +404,9 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "runtime.args_get",
 						typeIndex: &index{numeric: 1, line: 3, col: 76}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 4, col: 86}},
 				},
 			},
@@ -419,11 +419,11 @@ func TestParseModule(t *testing.T) {
 					{params: []wasm.ValueType{f32, f32}, result: f32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "Math", name: "Mul", funcName: "$mul",
+					{importIndex: 0, module: "Math", name: "Mul", funcName: "mul",
 						typeIndex: indexZero,
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'x'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'y'}},
+							&paramNameIndex{uint32(0), []byte{'x'}},
+							&paramNameIndex{uint32(1), []byte{'y'}},
 						},
 					}},
 			},
@@ -439,18 +439,18 @@ func TestParseModule(t *testing.T) {
 					{params: []wasm.ValueType{f32, f32}, result: f32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "Math", name: "Mul", funcName: "$mul",
+					{importIndex: 0, module: "Math", name: "Mul", funcName: "mul",
 						typeIndex: indexZero,
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'x'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'y'}},
+							&paramNameIndex{uint32(0), []byte{'x'}},
+							&paramNameIndex{uint32(1), []byte{'y'}},
 						},
 					},
-					{importIndex: 1, module: "Math", name: "Add", funcName: "$add",
+					{importIndex: 1, module: "Math", name: "Add", funcName: "add",
 						typeIndex: indexZero,
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'l'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'r'}},
+							&paramNameIndex{uint32(0), []byte{'l'}},
+							&paramNameIndex{uint32(1), []byte{'r'}},
 						},
 					},
 				},
@@ -465,8 +465,8 @@ func TestParseModule(t *testing.T) {
 				},
 				importFuncs: []*importFunc{
 					{importIndex: 0, typeIndex: indexZero, paramNames: paramNames{
-						&paramNameIndex{uint32(2), []byte{'$', 'v'}},
-						&paramNameIndex{uint32(4), []byte{'$', 't'}},
+						&paramNameIndex{uint32(2), []byte{'v'}},
+						&paramNameIndex{uint32(4), []byte{'t'}},
 					}}},
 			},
 		},
@@ -478,7 +478,7 @@ func TestParseModule(t *testing.T) {
 )`,
 			expected: &module{
 				types:         []*typeFunc{typeFuncEmpty},
-				importFuncs:   []*importFunc{{name: "hello", funcName: "$hello", typeIndex: indexZero}},
+				importFuncs:   []*importFunc{{name: "hello", funcName: "hello", typeIndex: indexZero}},
 				startFunction: &index{numeric: 0, line: 3, col: 9},
 			},
 		},
@@ -498,37 +498,37 @@ func TestParseModule(t *testing.T) {
 			name:  "example",
 			input: string(example),
 			expected: &module{
-				name: "$example",
+				name: "example",
 				types: []*typeFunc{
-					{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+					{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
 					typeFuncEmpty, // Note: inlined types come after explicit ones even if the latter are defined later
 					{params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
 					{params: []wasm.ValueType{f32, f32}, result: f32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 0, line: 3, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 3, col: 81}},
-					{importIndex: 2, module: "Math", name: "Mul", funcName: "$mul",
+					{importIndex: 2, module: "Math", name: "Mul", funcName: "mul",
 						typeIndex: &index{numeric: 3, line: 3, col: 81},
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'x'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'y'}},
+							&paramNameIndex{uint32(0), []byte{'x'}},
+							&paramNameIndex{uint32(1), []byte{'y'}},
 						},
 					},
-					{importIndex: 3, module: "Math", name: "Add", funcName: "$add",
+					{importIndex: 3, module: "Math", name: "Add", funcName: "add",
 						typeIndex: &index{numeric: 0, line: 6, col: 40},
 						typeInlined: &inlinedTypeFunc{
-							typeFunc: &typeFunc{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+							typeFunc: &typeFunc{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
 							line:     6, col: 35,
 						},
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'l'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'r'}},
+							&paramNameIndex{uint32(0), []byte{'l'}},
+							&paramNameIndex{uint32(1), []byte{'r'}},
 						},
 					},
-					{importIndex: 4, module: "", name: "hello", funcName: "$hello",
+					{importIndex: 4, module: "", name: "hello", funcName: "hello",
 						typeIndex: &index{numeric: 1, line: 8, col: 40}},
 				},
 				startFunction: &index{numeric: 4, line: 9, col: 9},