Repository: tetratelabs/wazero
Pull request #712 (2022-07-22 06:06:46)

Makes CacheNumInUint64 lazy and stops crashing in assemblyscript
This makes CacheNumInUint64 lazy so that all tests for function types
don't need to handle it. This also changes the assemblyscript special
functions so they don't crash when attempting to log. Finally, this
refactors `wasm.Func` so that it can enclose the parameter names as it
is more sensible than defining them elsewhere


Patch:
diff --git a/assemblyscript/assemblyscript.go b/assemblyscript/assemblyscript.go
index 70797fc5..d234989b 100644
--- a/assemblyscript/assemblyscript.go
+++ b/assemblyscript/assemblyscript.go
@@ -38,6 +38,12 @@ import (
 	"github.com/tetratelabs/wazero/sys"
 )
 
+const (
+	functionAbort = "abort"
+	functionTrace = "trace"
+	functionSeed  = "seed"
+)
+
 // Instantiate instantiates the "env" module used by AssemblyScript into the
 // runtime default namespace.
 //
@@ -77,65 +83,33 @@ type FunctionExporter interface {
 
 // NewFunctionExporter returns a FunctionExporter object with trace disabled.
 func NewFunctionExporter() FunctionExporter {
-	return &functionExporter{traceMode: traceDisabled}
+	return &functionExporter{abortFn: abortMessageEnabled, traceFn: traceDisabled}
 }
 
-type traceMode int
-
-const (
-	traceDisabled traceMode = 0
-	traceStdout   traceMode = 1
-	traceStderr   traceMode = 2
-)
-
 type functionExporter struct {
-	abortMessageDisabled bool
-	traceMode            traceMode
+	abortFn, traceFn *wasm.Func
 }
 
 // WithAbortMessageDisabled implements FunctionExporter.WithAbortMessageDisabled
 func (e *functionExporter) WithAbortMessageDisabled() FunctionExporter {
-	ret := *e // copy
-	ret.abortMessageDisabled = true
-	return &ret
+	return &functionExporter{abortFn: abortMessageDisabled, traceFn: e.traceFn}
 }
 
 // WithTraceToStdout implements FunctionExporter.WithTraceToStdout
 func (e *functionExporter) WithTraceToStdout() FunctionExporter {
-	ret := *e // copy
-	ret.traceMode = traceStdout
-	return &ret
+	return &functionExporter{abortFn: e.abortFn, traceFn: traceStdout}
 }
 
 // WithTraceToStderr implements FunctionExporter.WithTraceToStderr
 func (e *functionExporter) WithTraceToStderr() FunctionExporter {
-	ret := *e // copy
-	ret.traceMode = traceStderr
-	return &ret
+	return &functionExporter{abortFn: e.abortFn, traceFn: traceStderr}
 }
 
 // ExportFunctions implements FunctionExporter.ExportFunctions
 func (e *functionExporter) ExportFunctions(builder wazero.ModuleBuilder) {
-	var abortFn fnAbort
-	if e.abortMessageDisabled {
-		abortFn = abort
-	} else {
-		abortFn = abortWithMessage
-	}
-	var traceFn interface{}
-	switch e.traceMode {
-	case traceDisabled:
-		traceFn = traceNoop
-	case traceStdout:
-		traceFn = traceToStdout
-	case traceStderr:
-		traceFn = traceToStderr
-	}
-	builder.ExportFunction("abort", abortFn, "~lib/builtins/abort",
-		"message", "fileName", "lineNumber", "columnNumber")
-	builder.ExportFunction("trace", traceFn, "~lib/builtins/trace",
-		"message", "nArgs", "arg0", "arg1", "arg2", "arg3", "arg4")
-	builder.ExportFunction("seed", seed, "~lib/builtins/seed")
+	builder.ExportFunction(functionAbort, e.abortFn)
+	builder.ExportFunction(functionTrace, e.traceFn)
+	builder.ExportFunction(functionSeed, seed)
 }
 
 // abort is called on unrecoverable errors. This is typically present in Wasm
@@ -150,18 +124,26 @@ func (e *functionExporter) ExportFunctions(builder wazero.ModuleBuilder) {
 //	(import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
 //
 // See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L18
-type fnAbort func(
-	ctx context.Context, mod api.Module, message, fileName, lineNumber, columnNumber uint32,
+var abortMessageEnabled = wasm.NewGoFunc(
+	"abort", "~lib/builtins/abort",
+	[]string{"message", "fileName", "lineNumber", "columnNumber"},
+	abortWithMessage,
 )
 
+var abortMessageDisabled = abortMessageEnabled.WithGoFunc(abort)
+
 // abortWithMessage implements fnAbort
 func abortWithMessage(
 	ctx context.Context, mod api.Module, message, fileName, lineNumber, columnNumber uint32,
 ) {
 	sysCtx := mod.(*wasm.CallContext).Sys
-	msg := requireAssemblyScriptString(ctx, mod, "message", message)
-	fn := requireAssemblyScriptString(ctx, mod, "fileName", fileName)
-	_, _ = fmt.Fprintf(sysCtx.Stderr(), "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber)
+	mem := mod.Memory()
+	// Don't panic if there was a problem reading the message
+	if msg, msgOk := readAssemblyScriptString(ctx, mem, message); msgOk {
+		if fn, fnOk := readAssemblyScriptString(ctx, mem, fileName); fnOk {
+			_, _ = fmt.Fprintf(sysCtx.Stderr(), "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber)
+		}
+	}
 	abort(ctx, mod, message, fileName, lineNumber, columnNumber)
 }
 
@@ -180,25 +162,25 @@ func abort(
 	panic(sys.NewExitError(mod.Name(), exitCode))
 }
 
-// traceNoop implements trace in wasm to avoid host call overhead on no-op.
-var traceNoop = &wasm.Func{
-	Type: wasm.MustFunctionType(traceToStdout),
-	Code: &wasm.Code{Body: []byte{wasm.OpcodeEnd}},
-}
+// traceDisabled ignores the input.
+var traceDisabled = traceStdout.WithWasm([]byte{wasm.OpcodeEnd})
 
-// traceToStdout implements trace to the configured Stdout.
-func traceToStdout(
-	ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64,
-) {
-	traceTo(ctx, mod, message, nArgs, arg0, arg1, arg2, arg3, arg4, mod.(*wasm.CallContext).Sys.Stdout())
-}
+// traceStdout implements trace to the configured Stdout.
+var traceStdout = wasm.NewGoFunc(functionTrace, "~lib/builtins/trace",
+	[]string{"message", "nArgs", "arg0", "arg1", "arg2", "arg3", "arg4"},
+	func(
+		ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64,
+	) {
+		traceTo(ctx, mod, message, nArgs, arg0, arg1, arg2, arg3, arg4, mod.(*wasm.CallContext).Sys.Stdout())
+	},
+)
 
-// traceToStdout implements trace to the configured Stderr.
-func traceToStderr(
+// traceStderr implements trace to the configured Stderr.
+var traceStderr = traceStdout.WithGoFunc(func(
 	ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64,
 ) {
 	traceTo(ctx, mod, message, nArgs, arg0, arg1, arg2, arg3, arg4, mod.(*wasm.CallContext).Sys.Stderr())
-}
+})
 
 // traceTo implements the function "trace" in AssemblyScript. Ex.
 //	trace('Hello World!')
@@ -212,9 +194,13 @@ func traceTo(
 	ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64,
 	writer io.Writer,
 ) {
+	msg, ok := readAssemblyScriptString(ctx, mod.Memory(), message)
+	if !ok {
+		return // don't panic if unable to trace
+	}
 	var ret strings.Builder
 	ret.WriteString("trace: ")
-	ret.WriteString(requireAssemblyScriptString(ctx, mod, "message", message))
+	ret.WriteString(msg)
 	if nArgs >= 1 {
 		ret.WriteString(" ")
 		ret.WriteString(formatFloat(arg0))
@@ -236,9 +222,7 @@ func traceTo(
 		ret.WriteString(formatFloat(arg4))
 	}
 	ret.WriteByte('\n')
-	if _, err := writer.Write([]byte(ret.String())); err != nil {
-		panic(err)
-	}
+	_, _ = writer.Write([]byte(ret.String())) // don't crash if trace logging fails
 }
 
 func formatFloat(f float64) string {
@@ -253,30 +237,29 @@ func formatFloat(f float64) string {
 //	(import "env" "seed" (func $~lib/builtins/seed (result f64)))
 //
 // See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L111
-func seed(mod api.Module) float64 {
-	randSource := mod.(*wasm.CallContext).Sys.RandSource()
-	v, err := ieee754.DecodeFloat64(randSource)
-	if err != nil {
-		panic(fmt.Errorf("error reading random seed: %w", err))
-	}
-	return v
-}
+var seed = wasm.NewGoFunc(functionSeed, "~lib/builtins/seed", []string{},
+	func(mod api.Module) float64 {
+		randSource := mod.(*wasm.CallContext).Sys.RandSource()
+		v, err := ieee754.DecodeFloat64(randSource)
+		if err != nil {
+			panic(fmt.Errorf("error reading random seed: %w", err))
+		}
+		return v
+	},
+)
 
-// requireAssemblyScriptString reads a UTF-16 string created by AssemblyScript.
-func requireAssemblyScriptString(ctx context.Context, mod api.Module, fieldName string, offset uint32) string {
+// readAssemblyScriptString reads a UTF-16 string created by AssemblyScript.
+func readAssemblyScriptString(ctx context.Context, mem api.Memory, offset uint32) (string, bool) {
 	// Length is four bytes before pointer.
-	byteCount, ok := mod.Memory().ReadUint32Le(ctx, offset-4)
-	if !ok {
-		panic(fmt.Errorf("out of memory reading %s", fieldName))
-	}
-	if byteCount%2 != 0 {
-		panic(fmt.Errorf("invalid UTF-16 reading %s", fieldName))
+	byteCount, ok := mem.ReadUint32Le(ctx, offset-4)
+	if !ok || byteCount%2 != 0 {
+		return "", false
 	}
-	buf, ok := mod.Memory().Read(ctx, offset, byteCount)
+	buf, ok := mem.Read(ctx, offset, byteCount)
 	if !ok {
-		panic(fmt.Errorf("out of memory reading %s", fieldName))
+		return "", false
 	}
-	return decodeUTF16(buf)
+	return decodeUTF16(buf), true
 }
 
 func decodeUTF16(b []byte) string {
diff --git a/builder.go b/builder.go
index a832922c..e086d7d9 100644
--- a/builder.go
+++ b/builder.go
@@ -324,6 +324,8 @@ func (b *moduleBuilder) Compile(ctx context.Context, cConfig CompileConfig) (Com
 	module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.funcToNames, b.nameToMemory, b.nameToGlobal, b.r.enabledFeatures)
 	if err != nil {
 		return nil, err
+	} else if err = module.Validate(b.r.enabledFeatures); err != nil {
+		return nil, err
 	}
 
 	c := &compiledModule{module: module, compiledEngine: b.r.store.Engine}
diff --git a/emscripten/emscripten.go b/emscripten/emscripten.go
index 7ba6db47..78f689ec 100644
--- a/emscripten/emscripten.go
+++ b/emscripten/emscripten.go
@@ -51,8 +51,7 @@ type functionExporter struct{}
 
 // ExportFunctions implements FunctionExporter.ExportFunctions
 func (e *functionExporter) ExportFunctions(builder wazero.ModuleBuilder) {
-	builder.ExportFunction("emscripten_notify_memory_growth", emscriptenNotifyMemoryGrowth,
-		"emscripten_notify_memory_growth", "memory_index")
+	builder.ExportFunction(notifyMemoryGrowth.Name, notifyMemoryGrowth)
 }
 
 // emscriptenNotifyMemoryGrowth is called when wasm is compiled with
@@ -70,7 +69,12 @@ func (e *functionExporter) ExportFunctions(builder wazero.ModuleBuilder) {
 //
 // See https://github.com/emscripten-core/emscripten/blob/3.1.16/system/lib/standalone/standalone.c#L118
 // and https://emscripten.org/docs/api_reference/emscripten.h.html#abi-functions
-var emscriptenNotifyMemoryGrowth = &wasm.Func{
-	Type: &wasm.FunctionType{Params: []wasm.ValueType{wasm.ValueTypeI32}},
-	Code: &wasm.Code{Body: []byte{wasm.OpcodeEnd}},
+const functionNotifyMemoryGrowth = "emscripten_notify_memory_growth"
+
+var notifyMemoryGrowth = &wasm.Func{
+	ExportNames: []string{functionNotifyMemoryGrowth},
+	Name:        functionNotifyMemoryGrowth,
+	ParamTypes:  []wasm.ValueType{wasm.ValueTypeI32},
+	ParamNames:  []string{"memory_index"},
+	Code:        &wasm.Code{Body: []byte{wasm.OpcodeEnd}},
 }
diff --git a/internal/wasm/binary/function.go b/internal/wasm/binary/function.go
index 67d4a2b9..3a246e2f 100644
--- a/internal/wasm/binary/function.go
+++ b/internal/wasm/binary/function.go
@@ -96,7 +96,5 @@ func decodeFunctionType(enabledFeatures wasm.Features, r *bytes.Reader) (*wasm.F
 		Params:  paramTypes,
 		Results: resultTypes,
 	}
-
-	ret.CacheNumInUint64()
 	return ret, nil
 }
diff --git a/internal/wasm/gofunc.go b/internal/wasm/gofunc.go
index 60cef086..2f1ab806 100644
--- a/internal/wasm/gofunc.go
+++ b/internal/wasm/gofunc.go
@@ -148,17 +148,6 @@ func newModuleVal(m api.Module) reflect.Value {
 	return val
 }
 
-// MustFunctionType returns the function type corresponding to the function
-// signature or panics if invalid.
-func MustFunctionType(fn interface{}) *FunctionType {
-	fnV := reflect.ValueOf(fn)
-	_, ft, err := getFunctionType(&fnV)
-	if err != nil {
-		panic(err)
-	}
-	return ft
-}
-
 // getFunctionType returns the function type corresponding to the function signature or errs if invalid.
 func getFunctionType(fn *reflect.Value) (fk FunctionKind, ft *FunctionType, err error) {
 	p := fn.Type()
@@ -181,8 +170,6 @@ func getFunctionType(fn *reflect.Value) (fk FunctionKind, ft *FunctionType, err
 	rCount := p.NumOut()
 
 	ft = &FunctionType{Params: make([]ValueType, p.NumIn()-pOffset), Results: make([]ValueType, rCount)}
-	ft.CacheNumInUint64()
-
 	for i := 0; i < len(ft.Params); i++ {
 		pI := p.In(i + pOffset)
 		if t, ok := getTypeOf(pI.Kind()); ok {
diff --git a/internal/wasm/host.go b/internal/wasm/host.go
index 4f74ec21..7cf4eec5 100644
--- a/internal/wasm/host.go
+++ b/internal/wasm/host.go
@@ -10,15 +10,59 @@ import (
 )
 
 // Func is a function with an inlined type, typically used for NewHostModule.
+// Any corresponding FunctionType will be reused or added to the Module.
 type Func struct {
-	// Type is the equivalent function in the SectionIDType.
-	// This will resolve to an existing or new element.
-	Type *FunctionType
+	// ExportNames is equivalent to  the same method on api.FunctionDefinition.
+	ExportNames []string
+
+	// Name is equivalent to  the same method on api.FunctionDefinition.
+	Name string
+
+	// ParamTypes is equivalent to  the same method on api.FunctionDefinition.
+	ParamTypes []ValueType
+
+	// ParamNames is equivalent to  the same method on api.FunctionDefinition.
+	ParamNames []string
+
+	// ResultTypes is equivalent to  the same method on api.FunctionDefinition.
+	ResultTypes []ValueType
 
 	// Code is the equivalent function in the SectionIDCode.
 	Code *Code
 }
 
+// NewGoFunc returns a Func for the given parameters or panics.
+func NewGoFunc(exportName string, name string, paramNames []string, fn interface{}) *Func {
+	fnV := reflect.ValueOf(fn)
+	_, ft, err := getFunctionType(&fnV)
+	if err != nil {
+		panic(err)
+	}
+	return &Func{
+		ExportNames: []string{exportName},
+		Name:        name,
+		ParamTypes:  ft.Params,
+		ResultTypes: ft.Results,
+		ParamNames:  paramNames,
+		Code:        &Code{GoFunc: &fnV},
+	}
+}
+
+// WithGoFunc returns a copy of the function, replacing its Code.GoFunc.
+func (f *Func) WithGoFunc(fn interface{}) *Func {
+	ret := *f
+	fnV := reflect.ValueOf(fn)
+	ret.Code = &Code{GoFunc: &fnV}
+	return &ret
+}
+
+// WithWasm returns a copy of the function, replacing its Code.Body.
+func (f *Func) WithWasm(body []byte) *Func {
+	ret := *f
+	ret.Code = &Code{Body: body}
+	return &ret
+}
+
 // NewHostModule is defined internally for use in WASI tests and to keep the code size in the root directory small.
 func NewHostModule(
 	moduleName string,
@@ -90,67 +134,73 @@ func addFuncs(
 	funcToNames map[string][]string,
 	enabledFeatures Features,
 ) (err error) {
-	funcCount := uint32(len(nameToGoFunc))
-	funcNames := make([]string, 0, funcCount)
 	if m.NameSection == nil {
 		m.NameSection = &NameSection{}
 	}
 	moduleName := m.NameSection.ModuleName
-	m.NameSection.FunctionNames = make([]*NameAssoc, 0, funcCount)
-	m.FunctionSection = make([]Index, 0, funcCount)
-	m.CodeSection = make([]*Code, 0, funcCount)
-	m.FunctionDefinitionSection = make([]*FunctionDefinition, 0, funcCount)
+	nameToFunc := make(map[string]*Func, len(nameToGoFunc))
+	funcNames := make([]string, len(nameToFunc))
+	for k, v := range nameToGoFunc {
+		if hf, ok := v.(*Func); !ok {
+			fn := reflect.ValueOf(v)
+			_, ft, ftErr := getFunctionType(&fn)
+			if ftErr != nil {
+				return fmt.Errorf("func[%s.%s] %w", moduleName, k, ftErr)
+			}
+			hf = &Func{
+				ExportNames: []string{k},
+				Name:        k,
+				ParamTypes:  ft.Params,
+				ResultTypes: ft.Results,
+				Code:        &Code{GoFunc: &fn},
+			}
+			if names := funcToNames[k]; names != nil {
+				namesLen := len(names)
+				if namesLen > 1 && namesLen-1 != len(ft.Params) {
+					return fmt.Errorf("func[%s.%s] has %d params, but %d param names", moduleName, k, namesLen-1, len(ft.Params))
+				}
+				hf.Name = names[0]
+				hf.ParamNames = names[1:]
+			}
+			nameToFunc[k] = hf
+			funcNames = append(funcNames, k)
+		} else {
+			nameToFunc[hf.Name] = hf
+			funcNames = append(funcNames, hf.Name)
+		}
+	}
 
 	// Sort names for consistent iteration
-	for k := range nameToGoFunc {
-		funcNames = append(funcNames, k)
-	}
 	sort.Strings(funcNames)
 
-	for idx := Index(0); idx < funcCount; idx++ {
-		exportName := funcNames[idx]
-		debugName := wasmdebug.FuncName(moduleName, exportName, idx)
-
-		gf := nameToGoFunc[exportName]
-		var ft *FunctionType
-		if hf, ok := gf.(*Func); ok {
-			ft = hf.Type
-			m.CodeSection = append(m.CodeSection, hf.Code)
-		} else {
-			fn := reflect.ValueOf(gf)
-			_, ft, err = getFunctionType(&fn)
-			if err != nil {
-				return fmt.Errorf("func[%s] %w", debugName, err)
-			}
-			m.CodeSection = append(m.CodeSection, &Code{GoFunc: &fn})
-		}
-
-		m.FunctionSection = append(m.FunctionSection, m.maybeAddType(ft))
+	funcCount := uint32(len(nameToFunc))
+	m.NameSection.FunctionNames = make([]*NameAssoc, 0, funcCount)
+	m.FunctionSection = make([]Index, 0, funcCount)
+	m.CodeSection = make([]*Code, 0, funcCount)
+	m.FunctionDefinitionSection = make([]*FunctionDefinition, 0, funcCount)
 
-		names := funcToNames[exportName]
-		namesLen := len(names)
-		if namesLen > 1 && namesLen-1 != len(ft.Params) {
-			return fmt.Errorf("func[%s] has %d params, but %d param names", debugName, namesLen-1, len(ft.Params))
+	idx := Index(0)
+	for _, name := range funcNames {
+		hf := nameToFunc[name]
+		debugName := wasmdebug.FuncName(moduleName, name, idx)
+		typeIdx, typeErr := m.maybeAddType(hf.ParamTypes, hf.ResultTypes, enabledFeatures)
+		if typeErr != nil {
+			return fmt.Errorf("func[%s] %v", debugName, typeErr)
 		}
-		if len(ft.Results) > 1 {
-			// Guard >1.0 feature multi-value
-			if err = enabledFeatures.Require(FeatureMultiValue); err != nil {
-				err = fmt.Errorf("func[%s] multiple result types invalid as %v", debugName, err)
-				return
-			}
+		m.FunctionSection = append(m.FunctionSection, typeIdx)
+		m.CodeSection = append(m.CodeSection, hf.Code)
+		for _, export := range hf.ExportNames {
+			m.ExportSection = append(m.ExportSection, &Export{Type: ExternTypeFunc, Name: export, Index: idx})
 		}
-
-		m.ExportSection = append(m.ExportSection, &Export{Type: ExternTypeFunc, Name: exportName, Index: idx})
-		if namesLen > 0 {
-			m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: names[0]})
+		m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: hf.Name})
+		if len(hf.ParamNames) > 0 {
 			localNames := &NameMapAssoc{Index: idx}
-			for i, n := range names[1:] {
+			for i, n := range hf.ParamNames {
 				localNames.NameMap = append(localNames.NameMap, &NameAssoc{Index: Index(i), Name: n})
 			}
 			m.NameSection.LocalNames = append(m.NameSection.LocalNames, localNames)
-		} else {
-			m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: exportName})
 		}
+		idx++
 	}
 	return nil
 }
@@ -199,14 +249,21 @@ func addGlobals(m *Module, globals map[string]*Global) error {
 	return nil
 }
 
-func (m *Module) maybeAddType(ft *FunctionType) Index {
+func (m *Module) maybeAddType(params, results []ValueType, enabledFeatures Features) (Index, error) {
+	if len(results) > 1 {
+		// Guard >1.0 feature multi-value
+		if err := enabledFeatures.Require(FeatureMultiValue); err != nil {
+			return 0, fmt.Errorf("multiple result types invalid as %v", err)
+		}
+	}
 	for i, t := range m.TypeSection {
-		if t.EqualsSignature(ft.Params, ft.Results) {
-			return Index(i)
+		if t.EqualsSignature(params, results) {
+			return Index(i), nil
 		}
 	}
 
 	result := m.SectionElementCount(SectionIDType)
-	m.TypeSection = append(m.TypeSection, ft)
-	return result
+	toAdd := &FunctionType{Params: params, Results: results}
+	m.TypeSection = append(m.TypeSection, toAdd)
+	return result, nil
 }
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index 2aeb0e8a..446cd438 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -227,6 +227,10 @@ func (m *Module) TypeOfFunction(funcIdx Index) *FunctionType {
 }
 
 func (m *Module) Validate(enabledFeatures Features) error {
+	for _, tp := range m.TypeSection {
+		tp.CacheNumInUint64()
+	}
+
 	if err := m.validateStartSection(); err != nil {
 		return err
 	}
@@ -325,7 +329,9 @@ func (m *Module) validateFunctions(enabledFeatures Features, functions []Index,
 		if typeIndex >= typeCount {
 			return fmt.Errorf("invalid %s: type section index %d out of range", m.funcDesc(SectionIDFunction, Index(idx)), typeIndex)
 		}
-
+		if m.CodeSection[idx].GoFunc != nil {
+			continue
+		}
 		if err = m.validateFunction(enabledFeatures, Index(idx), functions, globals, memory, tables, declaredFuncIndexes); err != nil {
 			return fmt.Errorf("invalid %s: %w", m.funcDesc(SectionIDFunction, Index(idx)), err)
 		}
@@ -667,17 +673,21 @@ type FunctionType struct {
 }
 
 func (f *FunctionType) CacheNumInUint64() {
-	for _, tp := range f.Params {
-		f.ParamNumInUint64++
-		if tp == ValueTypeV128 {
+	if f.ParamNumInUint64 == 0 {
+		for _, tp := range f.Params {
 			f.ParamNumInUint64++
+			if tp == ValueTypeV128 {
+				f.ParamNumInUint64++
+			}
 		}
 	}
 
-	for _, tp := range f.Results {
-		f.ResultNumInUint64++
-		if tp == ValueTypeV128 {
+	if f.ResultNumInUint64 == 0 {
+		for _, tp := range f.Results {
 			f.ResultNumInUint64++
+			if tp == ValueTypeV128 {
+				f.ResultNumInUint64++
+			}
 		}
 	}
 }
diff --git a/internal/watzero/internal/decoder.go b/internal/watzero/internal/decoder.go
index 089e1836..2cce6610 100644
--- a/internal/watzero/internal/decoder.go
+++ b/internal/watzero/internal/decoder.go
@@ -143,10 +143,6 @@ func DecodeModule(
 	if names.ModuleName == "" && names.FunctionNames == nil && names.LocalNames == nil {
 		module.NameSection = nil
 	}
-
-	for _, tp := range module.TypeSection {
-		tp.CacheNumInUint64()
-	}
 	return
 }
 
diff --git a/wasi_snapshot_preview1/args.go b/wasi_snapshot_preview1/args.go
index e0f6928b..69203ceb 100644
--- a/wasi_snapshot_preview1/args.go
+++ b/wasi_snapshot_preview1/args.go
@@ -44,10 +44,14 @@ const (
 // See argsSizesGet
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_get
 // See https://en.wikipedia.org/wiki/Null-terminated_string
-func argsGet(ctx context.Context, mod api.Module, argv, argvBuf uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	return writeOffsetsAndNullTerminatedValues(ctx, mod.Memory(), sysCtx.Args(), argv, argvBuf)
-}
+var argsGet = wasm.NewGoFunc(
+	functionArgsGet, functionArgsGet,
+	[]string{"argv", "argv_buf"},
+	func(ctx context.Context, mod api.Module, argv, argvBuf uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		return writeOffsetsAndNullTerminatedValues(ctx, mod.Memory(), sysCtx.Args(), argv, argvBuf)
+	},
+)
 
 // argsSizesGet is the WASI function named functionArgsSizesGet that reads
 // command-line argument sizes.
@@ -78,15 +82,19 @@ func argsGet(ctx context.Context, mod api.Module, argv, argvBuf uint32) Errno {
 // See argsGet
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_sizes_get
 // See https://en.wikipedia.org/wiki/Null-terminated_string
-func argsSizesGet(ctx context.Context, mod api.Module, resultArgc, resultArgvBufSize uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	mem := mod.Memory()
+var argsSizesGet = wasm.NewGoFunc(
+	functionArgsSizesGet, functionArgsSizesGet,
+	[]string{"result.argc", "result.argv_buf_size"},
+	func(ctx context.Context, mod api.Module, resultArgc, resultArgvBufSize uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		mem := mod.Memory()
 
-	if !mem.WriteUint32Le(ctx, resultArgc, uint32(len(sysCtx.Args()))) {
-		return ErrnoFault
-	}
-	if !mem.WriteUint32Le(ctx, resultArgvBufSize, sysCtx.ArgsSize()) {
-		return ErrnoFault
-	}
-	return ErrnoSuccess
-}
+		if !mem.WriteUint32Le(ctx, resultArgc, uint32(len(sysCtx.Args()))) {
+			return ErrnoFault
+		}
+		if !mem.WriteUint32Le(ctx, resultArgvBufSize, sysCtx.ArgsSize()) {
+			return ErrnoFault
+		}
+		return ErrnoSuccess
+	},
+)
diff --git a/wasi_snapshot_preview1/clock.go b/wasi_snapshot_preview1/clock.go
index 7d01f6d2..f0121525 100644
--- a/wasi_snapshot_preview1/clock.go
+++ b/wasi_snapshot_preview1/clock.go
@@ -53,28 +53,32 @@ const (
 // Note: This is similar to `clock_getres` in POSIX.
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_res_getid-clockid---errno-timestamp
 // See https://linux.die.net/man/3/clock_getres
-func clockResGet(ctx context.Context, mod api.Module, id uint32, resultResolution uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
+var clockResGet = wasm.NewGoFunc(
+	functionClockResGet, functionClockResGet,
+	[]string{"id", "result.resolution"},
+	func(ctx context.Context, mod api.Module, id uint32, resultResolution uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
 
-	var resolution uint64 // ns
-	switch id {
-	case clockIDRealtime:
-		resolution = uint64(sysCtx.WalltimeResolution())
-	case clockIDMonotonic:
-		resolution = uint64(sysCtx.NanotimeResolution())
-	case clockIDProcessCputime, clockIDThreadCputime:
-		// Similar to many other runtimes, we only support realtime and
-		// monotonic clocks. Other types are slated to be removed from the next
-		// version of WASI.
-		return ErrnoNotsup
-	default:
-		return ErrnoInval
-	}
-	if !mod.Memory().WriteUint64Le(ctx, resultResolution, resolution) {
-		return ErrnoFault
-	}
-	return ErrnoSuccess
-}
+		var resolution uint64 // ns
+		switch id {
+		case clockIDRealtime:
+			resolution = uint64(sysCtx.WalltimeResolution())
+		case clockIDMonotonic:
+			resolution = uint64(sysCtx.NanotimeResolution())
+		case clockIDProcessCputime, clockIDThreadCputime:
+			// Similar to many other runtimes, we only support realtime and
+			// monotonic clocks. Other types are slated to be removed from the next
+			// version of WASI.
+			return ErrnoNotsup
+		default:
+			return ErrnoInval
+		}
+		if !mod.Memory().WriteUint64Le(ctx, resultResolution, resolution) {
+			return ErrnoFault
+		}
+		return ErrnoSuccess
+	},
+)
 
 // clockTimeGet is the WASI function named functionClockTimeGet that returns
 // the time value of a name (time.Now).
@@ -107,28 +111,32 @@ func clockResGet(ctx context.Context, mod api.Module, id uint32, resultResolutio
 // Note: This is similar to `clock_gettime` in POSIX.
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_time_getid-clockid-precision-timestamp---errno-timestamp
 // See https://linux.die.net/man/3/clock_gettime
-func clockTimeGet(ctx context.Context, mod api.Module, id uint32, precision uint64, resultTimestamp uint32) Errno {
-	// TODO: precision is currently ignored.
-	sysCtx := mod.(*wasm.CallContext).Sys
+var clockTimeGet = wasm.NewGoFunc(
+	functionClockTimeGet, functionClockTimeGet,
+	[]string{"id", "precision", "result.timestamp"},
+	func(ctx context.Context, mod api.Module, id uint32, precision uint64, resultTimestamp uint32) Errno {
+		// TODO: precision is currently ignored.
+		sysCtx := mod.(*wasm.CallContext).Sys
 
-	var val uint64
-	switch id {
-	case clockIDRealtime:
-		sec, nsec := sysCtx.Walltime(ctx)
-		val = (uint64(sec) * uint64(time.Second.Nanoseconds())) + uint64(nsec)
-	case clockIDMonotonic:
-		val = uint64(sysCtx.Nanotime(ctx))
-	case clockIDProcessCputime, clockIDThreadCputime:
-		// Similar to many other runtimes, we only support realtime and
-		// monotonic clocks. Other types are slated to be removed from the next
-		// version of WASI.
-		return ErrnoNotsup
-	default:
-		return ErrnoInval
-	}
+		var val uint64
+		switch id {
+		case clockIDRealtime:
+			sec, nsec := sysCtx.Walltime(ctx)
+			val = (uint64(sec) * uint64(time.Second.Nanoseconds())) + uint64(nsec)
+		case clockIDMonotonic:
+			val = uint64(sysCtx.Nanotime(ctx))
+		case clockIDProcessCputime, clockIDThreadCputime:
+			// Similar to many other runtimes, we only support realtime and
+			// monotonic clocks. Other types are slated to be removed from the next
+			// version of WASI.
+			return ErrnoNotsup
+		default:
+			return ErrnoInval
+		}
 
-	if !mod.Memory().WriteUint64Le(ctx, resultTimestamp, val) {
-		return ErrnoFault
-	}
-	return ErrnoSuccess
-}
+		if !mod.Memory().WriteUint64Le(ctx, resultTimestamp, val) {
+			return ErrnoFault
+		}
+		return ErrnoSuccess
+	},
+)
diff --git a/wasi_snapshot_preview1/environ.go b/wasi_snapshot_preview1/environ.go
index 027adcc3..adb34a0f 100644
--- a/wasi_snapshot_preview1/environ.go
+++ b/wasi_snapshot_preview1/environ.go
@@ -44,10 +44,14 @@ const (
 // See environSizesGet
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_get
 // See https://en.wikipedia.org/wiki/Null-terminated_string
-func environGet(ctx context.Context, mod api.Module, environ uint32, environBuf uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	return writeOffsetsAndNullTerminatedValues(ctx, mod.Memory(), sysCtx.Environ(), environ, environBuf)
-}
+var environGet = wasm.NewGoFunc(
+	functionEnvironGet, functionEnvironGet,
+	[]string{"environ", "environ_buf"},
+	func(ctx context.Context, mod api.Module, environ uint32, environBuf uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		return writeOffsetsAndNullTerminatedValues(ctx, mod.Memory(), sysCtx.Environ(), environ, environBuf)
+	},
+)
 
 // environSizesGet is the WASI function named functionEnvironSizesGet that
 // reads environment variable sizes.
@@ -80,16 +84,20 @@ func environGet(ctx context.Context, mod api.Module, environ uint32, environBuf
 // See environGet
 // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_sizes_get
 // and https://en.wikipedia.org/wiki/Null-terminated_string
-func environSizesGet(ctx context.Context, mod api.Module, resultEnvironc uint32, resultEnvironBufSize uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	mem := mod.Memory()
+var environSizesGet = wasm.NewGoFunc(
+	functionEnvironSizesGet, functionEnvironSizesGet,
+	[]string{"result.environc", "result.environBufSize"},
+	func(ctx context.Context, mod api.Module, resultEnvironc uint32, resultEnvironBufSize uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		mem := mod.Memory()
 
-	if !mem.WriteUint32Le(ctx, resultEnvironc, uint32(len(sysCtx.Environ()))) {
-		return ErrnoFault
-	}
-	if !mem.WriteUint32Le(ctx, resultEnvironBufSize, sysCtx.EnvironSize()) {
-		return ErrnoFault
-	}
+		if !mem.WriteUint32Le(ctx, resultEnvironc, uint32(len(sysCtx.Environ()))) {
+			return ErrnoFault
+		}
+		if !mem.WriteUint32Le(ctx, resultEnvironBufSize, sysCtx.EnvironSize()) {
+			return ErrnoFault
+		}
 
-	return ErrnoSuccess
-}
+		return ErrnoSuccess
+	},
+)
diff --git a/wasi_snapshot_preview1/fs.go b/wasi_snapshot_preview1/fs.go
index 39b09ee5..94249abc 100644
--- a/wasi_snapshot_preview1/fs.go
+++ b/wasi_snapshot_preview1/fs.go
@@ -50,13 +50,21 @@ const (
 // advisory information on a file descriptor.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_advisefd-fd-offset-filesize-len-filesize-advice-advice---errno
-var fdAdvise = stubFunction(i32, i64, i64, i32) // stubbed for GrainLang per #271.
+var fdAdvise = stubFunction(
+	functionFdAdvise,
+	[]wasm.ValueType{i32, i64, i64, i32},
+	[]string{"fd", "offset", "len", "result.advice"},
+)
 
 // fdAllocate is the WASI function named functionFdAllocate which forces the
 // allocation of space in a file.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_allocatefd-fd-offset-filesize-len-filesize---errno
-var fdAllocate = stubFunction(i32, i64, i64) // stubbed for GrainLang per #271.
+var fdAllocate = stubFunction(
+	functionFdAllocate,
+	[]wasm.ValueType{i32, i64, i64},
+	[]string{"fd", "offset", "len"},
+)
 
 // fdClose is the WASI function named functionFdClose which closes a file
 // descriptor.
@@ -73,20 +81,28 @@ var fdAllocate = stubFunction(i32, i64, i64) // stubbed for GrainLang per #271.
 // Note: This is similar to `close` in POSIX.
 // See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_close
 // and https://linux.die.net/man/3/close
-func fdClose(ctx context.Context, mod api.Module, fd uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	if ok := sysCtx.FS(ctx).CloseFile(ctx, fd); !ok {
-		return ErrnoBadf
-	}
+var fdClose = wasm.NewGoFunc(
+	functionFdClose, functionFdClose,
+	[]string{"fd"},
+	func(ctx context.Context, mod api.Module, fd uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		if ok := sysCtx.FS(ctx).CloseFile(ctx, fd); !ok {
+			return ErrnoBadf
+		}
 
-	return ErrnoSuccess
-}
+		return ErrnoSuccess
+	},
+)
 
 // fdDatasync is the WASI function named functionFdDatasync which synchronizes
 // the data of a file to disk.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_datasyncfd-fd---errno
-var fdDatasync = stubFunction(i32) // stubbed for GrainLang per #271.
+var fdDatasync = stubFunction(
+	functionFdDatasync,
+	[]wasm.ValueType{i32},
+	[]string{"fd"},
+)
 
 // fdFdstatGet is the WASI function named functionFdFdstatGet which returns the
 // attributes of a file descriptor.
@@ -125,20 +141,28 @@ var fdDatasync = stubFunction(i32) // stubbed for GrainLang per #271.
 // well as additional fields.
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fdstat
 // and https://linux.die.net/man/3/fsync
-func fdFdstatGet(ctx context.Context, mod api.Module, fd uint32, resultStat uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	if _, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok {
-		return ErrnoBadf
-	}
-	// TODO: actually write the fdstat!
-	return ErrnoSuccess
-}
+var fdFdstatGet = wasm.NewGoFunc(
+	functionFdFdstatGet, functionFdFdstatGet,
+	[]string{"fd", "result.stat"},
+	func(ctx context.Context, mod api.Module, fd uint32, resultStat uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		if _, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok {
+			return ErrnoBadf
+		}
+		// TODO: actually write the fdstat!
+		return ErrnoSuccess
+	},
+)
 
 // fdFdstatSetFlags is the WASI function named functionFdFdstatSetFlags which
 // adjusts the flags associated with a file descriptor.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_fdstat_set_flagsfd-fd-flags-fdflags---errnoand is stubbed for GrainLang per #271
-var fdFdstatSetFlags = stubFunction(i32, i32) // stubbed for GrainLang per #271.
+var fdFdstatSetFlags = stubFunction(
+	functionFdFdstatSetFlags,
+	[]wasm.ValueType{i32, i32},
+	[]string{"fd", "flags"},
+)
 
 // fdFdstatSetRights is the WASI function named functionFdFdstatSetRights which
 // adjusts the rights associated with a file descriptor.
@@ -146,31 +170,51 @@ var fdFdstatSetFlags = stubFunction(i32, i32) // stubbed for GrainLang per #271.
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_fdstat_set_rightsfd-fd-fs_rights_base-rights-fs_rights_inheriting-rights---errno
 //
 // Note: This will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844
-var fdFdstatSetRights = stubFunction(i32, i64, i64) // stubbed for GrainLang per #271.
+var fdFdstatSetRights = stubFunction(
+	functionFdFdstatSetRights,
+	[]wasm.ValueType{i32, i64, i64},
+	[]string{"fd", "fs_rights_base", "fs_rights_inheriting"},
+)
 
 // fdFilestatGet is the WASI function named functionFdFilestatGet which returns
 // the attributes of an open file.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_getfd-fd---errno-filestat
-var fdFilestatGet = stubFunction(i32, i32) // stubbed for GrainLang per #271.
+var fdFilestatGet = stubFunction(
+	functionFdFilestatGet,
+	[]wasm.ValueType{i32, i32},
+	[]string{"fd", "result.buf"},
+)
 
 // fdFilestatSetSize is the WASI function named functionFdFilestatSetSize which
 // adjusts the size of an open file.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_set_sizefd-fd-size-filesize---errno
-var fdFilestatSetSize = stubFunction(i32, i64) // stubbed for GrainLang per #271.
+var fdFilestatSetSize = stubFunction(
+	functionFdFilestatSetSize,
+	[]wasm.ValueType{i32, i64},
+	[]string{"fd", "size"},
+)
 
 // fdFilestatSetTimes is the WASI function named functionFdFilestatSetTimes
 // which adjusts the times of an open file.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_set_timesfd-fd-atim-timestamp-mtim-timestamp-fst_flags-fstflags---errno
-var fdFilestatSetTimes = stubFunction(i32, i64, i64, i32) // stubbed for GrainLang per #271.
+var fdFilestatSetTimes = stubFunction(
+	functionFdFilestatSetTimes,
+	[]wasm.ValueType{i32, i64, i64, i32},
+	[]string{"fd", "atim", "mtim", "fst_flags"},
+)
 
 // fdPread is the WASI function named functionFdPread which reads from a file
 // descriptor, without using and updating the file descriptor's offset.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_preadfd-fd-iovs-iovec_array-offset-filesize---errno-size
-var fdPread = stubFunction(i32, i32, i32, i64, i32) // stubbed for GrainLang per #271.
+var fdPread = stubFunction(
+	functionFdPread,
+	[]wasm.ValueType{i32, i32, i32, i64, i32},
+	[]string{"fd", "iovs", "iovs_len", "offset", "result.nread"},
+)
 
 // fdPrestatGet is the WASI function named functionFdPrestatGet which returns
 // the prestat data of a file descriptor.
@@ -203,24 +247,28 @@ var fdPread = stubFunction(i32, i32, i32, i64, i32) // stubbed for GrainLang per
 //
 // See fdPrestatDirName and
 // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#prestat
-func fdPrestatGet(ctx context.Context, mod api.Module, fd uint32, resultPrestat uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	entry, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd)
-	if !ok {
-		return ErrnoBadf
-	}
-
-	// Zero-value 8-bit tag, and 3-byte zero-value paddings, which is uint32le(0) in short.
-	if !mod.Memory().WriteUint32Le(ctx, resultPrestat, uint32(0)) {
-		return ErrnoFault
-	}
-	// Write the length of the directory name at offset 4.
-	if !mod.Memory().WriteUint32Le(ctx, resultPrestat+4, uint32(len(entry.Path))) {
-		return ErrnoFault
-	}
-
-	return ErrnoSuccess
-}
+var fdPrestatGet = wasm.NewGoFunc(
+	functionFdPrestatGet, functionFdPrestatGet,
+	[]string{"fd", "result.prestat"},
+	func(ctx context.Context, mod api.Module, fd uint32, resultPrestat uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		entry, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd)
+		if !ok {
+			return ErrnoBadf
+		}
+
+		// Zero-value 8-bit tag, and 3-byte zero-value paddings, which is uint32le(0) in short.
+		if !mod.Memory().WriteUint32Le(ctx, resultPrestat, uint32(0)) {
+			return ErrnoFault
+		}
+		// Write the length of the directory name at offset 4.
+		if !mod.Memory().WriteUint32Le(ctx, resultPrestat+4, uint32(len(entry.Path))) {
+			return ErrnoFault
+		}
+
+		return ErrnoSuccess
+	},
+)
 
 // fdPrestatDirName is the WASI function named functionFdPrestatDirName which
 // returns the path of the pre-opened directory of a file descriptor.
@@ -252,30 +300,37 @@ func fdPrestatGet(ctx context.Context, mod api.Module, fd uint32, resultPrestat
 //
 // See fdPrestatGet
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name
-func fdPrestatDirName(ctx context.Context, mod api.Module, fd uint32, pathPtr uint32, pathLen uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd)
-	if !ok {
-		return ErrnoBadf
-	}
-
-	// Some runtimes may have another semantics. See /RATIONALE.md
-	if uint32(len(f.Path)) < pathLen {
-		return ErrnoNametoolong
-	}
-
-	// TODO: fdPrestatDirName may have to return ErrnoNotdir if the type of the prestat data of `fd` is not a PrestatDir.
-	if !mod.Memory().Write(ctx, pathPtr, []byte(f.Path)[:pathLen]) {
-		return ErrnoFault
-	}
-	return ErrnoSuccess
-}
+var fdPrestatDirName = wasm.NewGoFunc(
+	functionFdPrestatDirName, functionFdPrestatDirName,
+	[]string{"fd", "path", "path_len"},
+	func(ctx context.Context, mod api.Module, fd uint32, pathPtr uint32, pathLen uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd)
+		if !ok {
+			return ErrnoBadf
+		}
+
+		// Some runtimes may have another semantics. See /RATIONALE.md
+		if uint32(len(f.Path)) < pathLen {
+			return ErrnoNametoolong
+		}
+
+		// TODO: fdPrestatDirName may have to return ErrnoNotdir if the type of the prestat data of `fd` is not a PrestatDir.
+		if !mod.Memory().Write(ctx, pathPtr, []byte(f.Path)[:pathLen]) {
+			return ErrnoFault
+		}
+		return ErrnoSuccess
+	},
+)
 
 // fdPwrite is the WASI function named functionFdPwrite which writes to a file
 // descriptor, without using and updating the file descriptor's offset.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_pwritefd-fd-iovs-ciovec_array-offset-filesize---errno-size
-var fdPwrite = stubFunction(i32, i32, i32, i64, i32) // stubbed for GrainLang per #271.
+var fdPwrite = stubFunction(functionFdPwrite,
+	[]wasm.ValueType{i32, i32, i32, i64, i32},
+	[]string{"fd", "iovs", "iovs_len", "offset", "result.nwritten"},
+)
 
 // fdRead is the WASI function named functionFdRead which reads from a file
 // descriptor.
@@ -326,53 +381,65 @@ var fdPwrite = stubFunction(i32, i32, i32, i64, i32) // stubbed for GrainLang pe
 //
 // See fdWrite
 // and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_read
-func fdRead(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	reader := internalsys.FdReader(ctx, sysCtx, fd)
-	if reader == nil {
-		return ErrnoBadf
-	}
-
-	var nread uint32
-	for i := uint32(0); i < iovsCount; i++ {
-		iovPtr := iovs + i*8
-		offset, ok := mod.Memory().ReadUint32Le(ctx, iovPtr)
-		if !ok {
-			return ErrnoFault
+var fdRead = wasm.NewGoFunc(
+	functionFdRead, functionFdRead,
+	[]string{"fd", "iovs", "iovs_len", "result.size"},
+	func(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		reader := internalsys.FdReader(ctx, sysCtx, fd)
+		if reader == nil {
+			return ErrnoBadf
 		}
-		l, ok := mod.Memory().ReadUint32Le(ctx, iovPtr+4)
-		if !ok {
-			return ErrnoFault
+
+		var nread uint32
+		for i := uint32(0); i < iovsCount; i++ {
+			iovPtr := iovs + i*8
+			offset, ok := mod.Memory().ReadUint32Le(ctx, iovPtr)
+			if !ok {
+				return ErrnoFault
+			}
+			l, ok := mod.Memory().ReadUint32Le(ctx, iovPtr+4)
+			if !ok {
+				return ErrnoFault
+			}
+			b, ok := mod.Memory().Read(ctx, offset, l)
+			if !ok {
+				return ErrnoFault
+			}
+			n, err := reader.Read(b) // Note: n <= l
+			nread += uint32(n)
+			if errors.Is(err, io.EOF) {
+				break
+			} else if err != nil {
+				return ErrnoIo
+			}
 		}
-		b, ok := mod.Memory().Read(ctx, offset, l)
-		if !ok {
+		if !mod.Memory().WriteUint32Le(ctx, resultSize, nread) {
 			return ErrnoFault
 		}
-		n, err := reader.Read(b) // Note: n <= l
-		nread += uint32(n)
-		if errors.Is(err, io.EOF) {
-			break
-		} else if err != nil {
-			return ErrnoIo
-		}
-	}
-	if !mod.Memory().WriteUint32Le(ctx, resultSize, nread) {
-		return ErrnoFault
-	}
-	return ErrnoSuccess
-}
+		return ErrnoSuccess
+	},
+)
 
 // fdReaddir is the WASI function named functionFdReaddir which reads directory
 // entries from a directory.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_readdirfd-fd-buf-pointeru8-buf_len-size-cookie-dircookie---errno-size
-var fdReaddir = stubFunction(i32, i32, i32, i64, i32) // stubbed for GrainLang per #271.
+var fdReaddir = stubFunction(
+	functionFdReaddir,
+	[]wasm.ValueType{i32, i32, i32, i64, i32},
+	[]string{"fd", "buf", "buf_len", "cookie", "result.bufused"},
+)
 
 // fdRenumber is the WASI function named functionFdRenumber which atomically
 // replaces a file descriptor by renumbering another file descriptor.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno
-var fdRenumber = stubFunction(i32, i32) // stubbed for GrainLang per #271.
+var fdRenumber = stubFunction(
+	functionFdRenumber,
+	[]wasm.ValueType{i32, i32},
+	[]string{"fd", "to"},
+)
 
 // fdSeek is the WASI function named functionFdSeek which moves the offset of a
 // file descriptor.
@@ -411,43 +478,55 @@ var fdRenumber = stubFunction(i32, i32) // stubbed for GrainLang per #271.
 //
 // See io.Seeker
 // and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_seek
-func fdSeek(ctx context.Context, mod api.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	var seeker io.Seeker
-	// Check to see if the file descriptor is available
-	if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok || f.File == nil {
-		return ErrnoBadf
-		// fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it.
-	} else if seeker, ok = f.File.(io.Seeker); !ok {
-		return ErrnoBadf
-	}
-
-	if whence > io.SeekEnd /* exceeds the largest valid whence */ {
-		return ErrnoInval
-	}
-	newOffset, err := seeker.Seek(int64(offset), int(whence))
-	if err != nil {
-		return ErrnoIo
-	}
-
-	if !mod.Memory().WriteUint32Le(ctx, resultNewoffset, uint32(newOffset)) {
-		return ErrnoFault
-	}
-
-	return ErrnoSuccess
-}
+var fdSeek = wasm.NewGoFunc(
+	functionFdSeek, functionFdSeek,
+	[]string{"fd", "offset", "whence", "result.newoffset"},
+	func(ctx context.Context, mod api.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		var seeker io.Seeker
+		// Check to see if the file descriptor is available
+		if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok || f.File == nil {
+			return ErrnoBadf
+			// fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it.
+		} else if seeker, ok = f.File.(io.Seeker); !ok {
+			return ErrnoBadf
+		}
+
+		if whence > io.SeekEnd /* exceeds the largest valid whence */ {
+			return ErrnoInval
+		}
+		newOffset, err := seeker.Seek(int64(offset), int(whence))
+		if err != nil {
+			return ErrnoIo
+		}
+
+		if !mod.Memory().WriteUint32Le(ctx, resultNewoffset, uint32(newOffset)) {
+			return ErrnoFault
+		}
+
+		return ErrnoSuccess
+	},
+)
 
 // fdSync is the WASI function named functionFdSync which synchronizes the data
 // and metadata of a file to disk.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_syncfd-fd---errno
-var fdSync = stubFunction(i32) // stubbed for GrainLang per #271.
+var fdSync = stubFunction(
+	functionFdSync,
+	[]wasm.ValueType{i32},
+	[]string{"fd"},
+)
 
 // fdTell is the WASI function named functionFdTell which returns the current
 // offset of a file descriptor.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_tellfd-fd---errno-filesize
-var fdTell = stubFunction(i32, i32) // stubbed for GrainLang per #271.
+var fdTell = stubFunction(
+	functionFdTell,
+	[]wasm.ValueType{i32, i32},
+	[]string{"fd", "result.offset"},
+)
 
 // fdWrite is the WASI function named functionFdWrite which writes to a file
 // descriptor.
@@ -506,65 +585,85 @@ var fdTell = stubFunction(i32, i32) // stubbed for GrainLang per #271.
 // See fdRead
 // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#ciovec
 // and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write
-func fdWrite(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	writer := internalsys.FdWriter(ctx, sysCtx, fd)
-	if writer == nil {
-		return ErrnoBadf
-	}
-
-	var nwritten uint32
-	for i := uint32(0); i < iovsCount; i++ {
-		iovPtr := iovs + i*8
-		offset, ok := mod.Memory().ReadUint32Le(ctx, iovPtr)
-		if !ok {
-			return ErrnoFault
+var fdWrite = wasm.NewGoFunc(
+	functionFdWrite, functionFdWrite,
+	[]string{"fd", "iovs", "iovs_len", "result.size"},
+	func(ctx context.Context, mod api.Module, fd, iovs, iovsCount, resultSize uint32) Errno {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		writer := internalsys.FdWriter(ctx, sysCtx, fd)
+		if writer == nil {
+			return ErrnoBadf
 		}
-		// Note: emscripten has been known to write zero length iovec. However,
-		// it is not common in other compilers, so we don't optimize for it.
-		l, ok := mod.Memory().ReadUint32Le(ctx, iovPtr+4)
-		if !ok {
-			return ErrnoFault
+
+		var nwritten uint32
+		for i := uint32(0); i < iovsCount; i++ {
+			iovPtr := iovs + i*8
+			offset, ok := mod.Memory().ReadUint32Le(ctx, iovPtr)
+			if !ok {
+				return ErrnoFault
+			}
+			// Note: emscripten has been known to write zero length iovec. However,
+			// it is not common in other compilers, so we don't optimize for it.
+			l, ok := mod.Memory().ReadUint32Le(ctx, iovPtr+4)
+			if !ok {
+				return ErrnoFault
+			}
+			b, ok := mod.Memory().Read(ctx, offset, l)
+			if !ok {
+				return ErrnoFault
+			}
+			n, err := writer.Write(b)
+			if err != nil {
+				return ErrnoIo
+			}
+			nwritten += uint32(n)
 		}
-		b, ok := mod.Memory().Read(ctx, offset, l)
-		if !ok {
+		if !mod.Memory().WriteUint32Le(ctx, resultSize, nwritten) {
 			return ErrnoFault
 		}
-		n, err := writer.Write(b)
-		if err != nil {
-			return ErrnoIo
-		}
-		nwritten += uint32(n)
-	}
-	if !mod.Memory().WriteUint32Le(ctx, resultSize, nwritten) {
-		return ErrnoFault
-	}
-	return ErrnoSuccess
-}
+		return ErrnoSuccess
+	},
+)
 
 // pathCreateDirectory is the WASI function named functionPathCreateDirectory
 // which creates a directory.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_create_directoryfd-fd-path-string---errno
-var pathCreateDirectory = stubFunction(i32, i32, i32) // stubbed for GrainLang per #271.
+var pathCreateDirectory = stubFunction(
+	functionPathCreateDirectory,
+	[]wasm.ValueType{i32, i32, i32},
+	[]string{"fd", "path", "path_len"},
+)
 
 // pathFilestatGet is the WASI function named functionPathFilestatGet which
 // returns the attributes of a file or directory.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_filestat_getfd-fd-flags-lookupflags-path-string---errno-filestat
-var pathFilestatGet = stubFunction(i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
+var pathFilestatGet = stubFunction(
+	functionPathFilestatGet,
+	[]wasm.ValueType{i32, i32, i32, i32, i32},
+	[]string{"fd", "flags", "path", "path_len", "result.buf"},
+)
 
 // pathFilestatSetTimes is the WASI function named functionPathFilestatSetTimes
 // which adjusts the timestamps of a file or directory.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_filestat_set_timesfd-fd-flags-lookupflags-path-string-atim-timestamp-mtim-timestamp-fst_flags-fstflags---errno
-var pathFilestatSetTimes = stubFunction(i32, i32, i32, i32, i64, i64, i32) // stubbed for GrainLang per #271.
+var pathFilestatSetTimes = stubFunction(
+	functionPathFilestatSetTimes,
+	[]wasm.ValueType{i32, i32, i32, i32, i64, i64, i32},
+	[]string{"fd", "flags", "path", "path_len", "atim", "mtim", "fst_flags"},
+)
 
 // pathLink is the WASI function named functionPathLink which adjusts the
 // timestamps of a file or directory.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#path_link
-var pathLink = stubFunction(i32, i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
+var pathLink = stubFunction(
+	functionPathLink,
+	[]wasm.ValueType{i32, i32, i32, i32, i32, i32, i32},
+	[]string{"old_fd", "old_flags", "old_path", "old_path_len", "new_fd", "new_path", "new_path_len"},
+)
 
 // pathOpen is the WASI function named functionPathOpen which opens a file or
 // directory. This returns ErrnoBadf if the fd is invalid.
@@ -620,59 +719,83 @@ var pathLink = stubFunction(i32, i32, i32, i32, i32, i32, i32) // stubbed for Gr
 //	* Rights will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844
 //
 // See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#path_open
-func pathOpen(ctx context.Context, mod api.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase,
-	fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno Errno) {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	fsc := sysCtx.FS(ctx)
-	if _, ok := fsc.OpenedFile(ctx, fd); !ok {
-		return ErrnoBadf
-	}
-
-	b, ok := mod.Memory().Read(ctx, pathPtr, pathLen)
-	if !ok {
-		return ErrnoFault
-	}
-
-	if newFD, err := fsc.OpenFile(ctx, string(b)); err != nil {
-		switch {
-		case errors.Is(err, fs.ErrNotExist):
-			return ErrnoNoent
-		case errors.Is(err, fs.ErrExist):
-			return ErrnoExist
-		default:
-			return ErrnoIo
+var pathOpen = wasm.NewGoFunc(
+	functionPathOpen, functionPathOpen,
+	[]string{"fd", "dirflags", "path", "path_len", "oflags", "fs_rights_base", "fs_rights_inheriting", "fdflags", "result.opened_fd"},
+	func(ctx context.Context, mod api.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase,
+		fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno Errno) {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		fsc := sysCtx.FS(ctx)
+		if _, ok := fsc.OpenedFile(ctx, fd); !ok {
+			return ErrnoBadf
+		}
+
+		b, ok := mod.Memory().Read(ctx, pathPtr, pathLen)
+		if !ok {
+			return ErrnoFault
 		}
-	} else if !mod.Memory().WriteUint32Le(ctx, resultOpenedFd, newFD) {
-		_ = fsc.CloseFile(ctx, newFD)
-		return ErrnoFault
-	}
-	return ErrnoSuccess
-}
+
+		if newFD, err := fsc.OpenFile(ctx, string(b)); err != nil {
+			switch {
+			case errors.Is(err, fs.ErrNotExist):
+				return ErrnoNoent
+			case errors.Is(err, fs.ErrExist):
+				return ErrnoExist
+			default:
+				return ErrnoIo
+			}
+		} else if !mod.Memory().WriteUint32Le(ctx, resultOpenedFd, newFD) {
+			_ = fsc.CloseFile(ctx, newFD)
+			return ErrnoFault
+		}
+		return ErrnoSuccess
+	},
+)
 
 // pathReadlink is the WASI function named functionPathReadlink that reads the
 // contents of a symbolic link.
 //
 // See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_readlinkfd-fd-path-string-buf-pointeru8-buf_len-size---errno-size
-var pathReadlink = stubFunction(i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
+var pathReadlink = stubFunction(
+	functionPathReadlink,
+	[]wasm.ValueType{i32, i32, i32, i32, i32, i32},
+	[]string{"fd", "path", "path_len", "buf", "buf_len", "result.bufused"},
+)
 
 // pathRemoveDirectory is the WASI function named functionPathRemoveDirectory
 // which removes a directory.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_remove_directoryfd-fd-path-string---errno
-var pathRemoveDirectory = stubFunction(i32, i32, i32) // stubbed for GrainLang per #271.
+var pathRemoveDirectory = stubFunction(
+	functionPathRemoveDirectory,
+	[]wasm.ValueType{i32, i32, i32},
+	[]string{"fd", "path", "path_len"},
+)
 
 // pathRename is the WASI function named functionPathRename which renames a
 // file or directory.
-var pathRename = stubFunction(i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
+var pathRename = stubFunction(
+	functionPathRename,
+	[]wasm.ValueType{i32, i32, i32, i32, i32, i32},
+	[]string{"fd", "old_path", "old_path_len", "new_fd", "new_path", "new_path_len"},
+)
 
 // pathSymlink is the WASI function named functionPathSymlink which creates a
 // symbolic link.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#path_symlink
-var pathSymlink = stubFunction(i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
+var pathSymlink = stubFunction(
+	functionPathSymlink,
+	[]wasm.ValueType{i32, i32, i32, i32, i32},
+	[]string{"old_path", "old_path_len", "fd", "new_path", "new_path_len"},
+)
 
 // pathUnlinkFile is the WASI function named functionPathUnlinkFile which
 // unlinks a file.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_unlink_filefd-fd-path-string---errno
-var pathUnlinkFile = stubFunction(i32, i32, i32) // stubbed for GrainLang per #271.
+var pathUnlinkFile = stubFunction(
+	functionPathUnlinkFile,
+	[]wasm.ValueType{i32, i32, i32},
+	[]string{"fd", "path", "path_len"},
+)
diff --git a/wasi_snapshot_preview1/poll.go b/wasi_snapshot_preview1/poll.go
index d05b6093..192fcde0 100644
--- a/wasi_snapshot_preview1/poll.go
+++ b/wasi_snapshot_preview1/poll.go
@@ -46,57 +46,61 @@ const (
 //	* This is similar to `poll` in POSIX.
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#poll_oneoff
 // See https://linux.die.net/man/3/poll
-func pollOneoff(ctx context.Context, mod api.Module, in, out, nsubscriptions, resultNevents uint32) Errno {
-	if nsubscriptions == 0 {
-		return ErrnoInval
-	}
-
-	mem := mod.Memory()
+var pollOneoff = wasm.NewGoFunc(
+	functionPollOneoff, functionPollOneoff,
+	[]string{"in", "out", "nsubscriptions", "result.nevents"},
+	func(ctx context.Context, mod api.Module, in, out, nsubscriptions, resultNevents uint32) Errno {
+		if nsubscriptions == 0 {
+			return ErrnoInval
+		}
 
-	// Ensure capacity prior to the read loop to reduce error handling.
-	inBuf, ok := mem.Read(ctx, in, nsubscriptions*48)
-	if !ok {
-		return ErrnoFault
-	}
-	outBuf, ok := mem.Read(ctx, out, nsubscriptions*32)
-	if !ok {
-		return ErrnoFault
-	}
+		mem := mod.Memory()
 
-	// Eagerly write the number of events which will equal subscriptions unless
-	// there's a fault in parsing (not processing).
-	if !mod.Memory().WriteUint32Le(ctx, resultNevents, nsubscriptions) {
-		return ErrnoFault
-	}
+		// Ensure capacity prior to the read loop to reduce error handling.
+		inBuf, ok := mem.Read(ctx, in, nsubscriptions*48)
+		if !ok {
+			return ErrnoFault
+		}
+		outBuf, ok := mem.Read(ctx, out, nsubscriptions*32)
+		if !ok {
+			return ErrnoFault
+		}
 
-	// Loop through all subscriptions and write their output.
-	for sub := uint32(0); sub < nsubscriptions; sub++ {
-		inOffset := sub * 48
-		outOffset := sub * 32
-
-		var errno Errno
-		eventType := inBuf[inOffset+8] // +8 past userdata
-		switch eventType {
-		case eventTypeClock: // handle later
-			// +8 past userdata +8 name alignment
-			errno = processClockEvent(ctx, mod, inBuf[inOffset+8+8:])
-		case eventTypeFdRead, eventTypeFdWrite:
-			// +8 past userdata +4 FD alignment
-			errno = processFDEvent(ctx, mod, eventType, inBuf[inOffset+8+4:])
-		default:
-			return ErrnoInval
+		// Eagerly write the number of events which will equal subscriptions unless
+		// there's a fault in parsing (not processing).
+		if !mod.Memory().WriteUint32Le(ctx, resultNevents, nsubscriptions) {
+			return ErrnoFault
 		}
 
-		// Write the event corresponding to the processed subscription.
-		// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-event-struct
-		copy(outBuf, inBuf[inOffset:inOffset+8]) // userdata
-		outBuf[outOffset+8] = byte(errno)        // uint16, but safe as < 255
-		outBuf[outOffset+9] = 0
-		binary.LittleEndian.PutUint32(outBuf[outOffset+10:], uint32(eventType))
-		// TODO: When FD events are supported, write outOffset+16
-	}
-	return ErrnoSuccess
-}
+		// Loop through all subscriptions and write their output.
+		for sub := uint32(0); sub < nsubscriptions; sub++ {
+			inOffset := sub * 48
+			outOffset := sub * 32
+
+			var errno Errno
+			eventType := inBuf[inOffset+8] // +8 past userdata
+			switch eventType {
+			case eventTypeClock: // handle later
+				// +8 past userdata +8 name alignment
+				errno = processClockEvent(ctx, mod, inBuf[inOffset+8+8:])
+			case eventTypeFdRead, eventTypeFdWrite:
+				// +8 past userdata +4 FD alignment
+				errno = processFDEvent(ctx, mod, eventType, inBuf[inOffset+8+4:])
+			default:
+				return ErrnoInval
+			}
+
+			// Write the event corresponding to the processed subscription.
+			// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-event-struct
+			copy(outBuf, inBuf[inOffset:inOffset+8]) // userdata
+			outBuf[outOffset+8] = byte(errno)        // uint16, but safe as < 255
+			outBuf[outOffset+9] = 0
+			binary.LittleEndian.PutUint32(outBuf[outOffset+10:], uint32(eventType))
+			// TODO: When FD events are supported, write outOffset+16
+		}
+		return ErrnoSuccess
+	},
+)
 
 // processClockEvent supports only relative name events, as that's what's used
 // to implement sleep in various compilers including Rust, Zig and TinyGo.
diff --git a/wasi_snapshot_preview1/proc.go b/wasi_snapshot_preview1/proc.go
index a539547c..db610cb2 100644
--- a/wasi_snapshot_preview1/proc.go
+++ b/wasi_snapshot_preview1/proc.go
@@ -4,6 +4,7 @@ import (
 	"context"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/sys"
 )
 
@@ -21,18 +22,22 @@ const (
 //	* exitCode: exit code.
 //
 // See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#proc_exit
-func procExit(ctx context.Context, mod api.Module, exitCode uint32) {
-	// Ensure other callers see the exit code.
-	_ = mod.CloseWithExitCode(ctx, exitCode)
+var procExit = wasm.NewGoFunc(
+	functionProcExit, functionProcExit,
+	[]string{"rval"},
+	func(ctx context.Context, mod api.Module, exitCode uint32) {
+		// Ensure other callers see the exit code.
+		_ = mod.CloseWithExitCode(ctx, exitCode)
 
-	// Prevent any code from executing after this function. For example, LLVM
-	// inserts unreachable instructions after calls to exit.
-	// See: https://github.com/emscripten-core/emscripten/issues/12322
-	panic(sys.NewExitError(mod.Name(), exitCode))
-}
+		// Prevent any code from executing after this function. For example, LLVM
+		// inserts unreachable instructions after calls to exit.
+		// See: https://github.com/emscripten-core/emscripten/issues/12322
+		panic(sys.NewExitError(mod.Name(), exitCode))
+	},
+)
 
 // procRaise is the WASI function named functionProcRaise which sends a signal
 // to the process of the calling thread.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-proc_raisesig-signal---errno
-var procRaise = stubFunction(i32) // stubbed for GrainLang per #271.
+var procRaise = stubFunction(functionProcRaise, []wasm.ValueType{i32}, []string{"sig"})
diff --git a/wasi_snapshot_preview1/random.go b/wasi_snapshot_preview1/random.go
index e00eec9e..e685c209 100644
--- a/wasi_snapshot_preview1/random.go
+++ b/wasi_snapshot_preview1/random.go
@@ -34,19 +34,23 @@ const functionRandomGet = "random_get"
 //              buf --^
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-bufLen-size---errno
-func randomGet(ctx context.Context, mod api.Module, buf uint32, bufLen uint32) (errno Errno) {
-	sysCtx := mod.(*wasm.CallContext).Sys
-	randSource := sysCtx.RandSource()
+var randomGet = wasm.NewGoFunc(
+	functionRandomGet, functionRandomGet,
+	[]string{"buf", "buf_len"},
+	func(ctx context.Context, mod api.Module, buf uint32, bufLen uint32) (errno Errno) {
+		sysCtx := mod.(*wasm.CallContext).Sys
+		randSource := sysCtx.RandSource()
 
-	randomBytes, ok := mod.Memory().Read(ctx, buf, bufLen)
-	if !ok { // out-of-range
-		return ErrnoFault
-	}
+		randomBytes, ok := mod.Memory().Read(ctx, buf, bufLen)
+		if !ok { // out-of-range
+			return ErrnoFault
+		}
 
-	// We can ignore the returned n as it only != byteCount on error
-	if _, err := io.ReadAtLeast(randSource, randomBytes, int(bufLen)); err != nil {
-		return ErrnoIo
-	}
+		// We can ignore the returned n as it only != byteCount on error
+		if _, err := io.ReadAtLeast(randSource, randomBytes, int(bufLen)); err != nil {
+			return ErrnoIo
+		}
 
-	return ErrnoSuccess
-}
+		return ErrnoSuccess
+	},
+)
diff --git a/wasi_snapshot_preview1/sched.go b/wasi_snapshot_preview1/sched.go
index 3f6f0409..65e74bf5 100644
--- a/wasi_snapshot_preview1/sched.go
+++ b/wasi_snapshot_preview1/sched.go
@@ -6,4 +6,4 @@ const functionSchedYield = "sched_yield"
 // yields execution of the calling thread.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sched_yield---errno
-var schedYield = stubFunction() // stubbed for GrainLang per #271.
+var schedYield = stubFunction(functionSchedYield, nil, nil)
diff --git a/wasi_snapshot_preview1/sock.go b/wasi_snapshot_preview1/sock.go
index 6344517b..55a847b3 100644
--- a/wasi_snapshot_preview1/sock.go
+++ b/wasi_snapshot_preview1/sock.go
@@ -1,5 +1,7 @@
 package wasi_snapshot_preview1
 
+import "github.com/tetratelabs/wazero/internal/wasm"
+
 const (
 	functionSockRecv     = "sock_recv"
 	functionSockSend     = "sock_send"
@@ -10,16 +12,28 @@ const (
 // message from a socket.
 //
 // See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_recvfd-fd-ri_data-iovec_array-ri_flags-riflags---errno-size-roflags
-var sockRecv = stubFunction(i32, i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
+var sockRecv = stubFunction(
+	functionSockRecv,
+	[]wasm.ValueType{i32, i32, i32, i32, i32, i32},
+	[]string{"fd", "ri_data", "ri_data_count", "ri_flags", "result.ro_datalen", "result.ro_flags"},
+)
 
 // sockSend is the WASI function named functionSockSend which sends a message
 // on a socket.
 //
 // See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_sendfd-fd-si_data-ciovec_array-si_flags-siflags---errno-size
-var sockSend = stubFunction(i32, i32, i32, i32, i32) // stubbed for GrainLang per #271.
+var sockSend = stubFunction(
+	functionSockSend,
+	[]wasm.ValueType{i32, i32, i32, i32, i32},
+	[]string{"fd", "si_data", "si_data_count", "si_flags", "result.so_datalen"},
+)
 
 // sockShutdown is the WASI function named functionSockShutdown which shuts
 // down socket send and receive channels.
 //
 // See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_shutdownfd-fd-how-sdflags---errno
-var sockShutdown = stubFunction(i32, i32) // stubbed for GrainLang per #271.
+var sockShutdown = stubFunction(
+	functionSockShutdown,
+	[]wasm.ValueType{i32, i32},
+	[]string{"fd", "how"},
+)
diff --git a/wasi_snapshot_preview1/wasi.go b/wasi_snapshot_preview1/wasi.go
index 5153441a..45747cbf 100644
--- a/wasi_snapshot_preview1/wasi.go
+++ b/wasi_snapshot_preview1/wasi.go
@@ -111,96 +111,51 @@ func (b *builder) Instantiate(ctx context.Context, ns wazero.Namespace) (api.Clo
 func exportFunctions(builder wazero.ModuleBuilder) {
 	// Note:se are ordered per spec for consistency even if the resulting map can't guarantee that.
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#functions
-	builder.ExportFunction(functionArgsGet, argsGet,
-		functionArgsGet, "argv", "argv_buf")
-	builder.ExportFunction(functionArgsSizesGet, argsSizesGet,
-		functionArgsSizesGet, "result.argc", "result.argv_buf_size")
-	builder.ExportFunction(functionEnvironGet, environGet,
-		functionEnvironGet, "environ", "environ_buf")
-	builder.ExportFunction(functionEnvironSizesGet, environSizesGet,
-		functionEnvironSizesGet, "result.environc", "result.environBufSize")
-	builder.ExportFunction(functionClockResGet, clockResGet,
-		functionClockResGet, "id", "result.resolution")
-	builder.ExportFunction(functionClockTimeGet, clockTimeGet,
-		functionClockTimeGet, "id", "precision", "result.timestamp")
-	builder.ExportFunction(functionFdAdvise, fdAdvise,
-		functionFdAdvise, "fd", "offset", "len", "result.advice")
-	builder.ExportFunction(functionFdAllocate, fdAllocate,
-		functionFdAllocate, "fd", "offset", "len")
-	builder.ExportFunction(functionFdClose, fdClose,
-		functionFdClose, "fd")
-	builder.ExportFunction(functionFdDatasync, fdDatasync,
-		functionFdDatasync, "fd")
-	builder.ExportFunction(functionFdFdstatGet, fdFdstatGet,
-		functionFdFdstatGet, "fd", "result.stat")
-	builder.ExportFunction(functionFdFdstatSetFlags, fdFdstatSetFlags,
-		functionFdFdstatSetFlags, "fd", "flags")
-	builder.ExportFunction(functionFdFdstatSetRights, fdFdstatSetRights,
-		functionFdFdstatSetRights, "fd", "fs_rights_base", "fs_rights_inheriting")
-	builder.ExportFunction(functionFdFilestatGet, fdFilestatGet,
-		functionFdFilestatGet, "fd", "result.buf")
-	builder.ExportFunction(functionFdFilestatSetSize, fdFilestatSetSize,
-		functionFdFilestatSetSize, "fd", "size")
-	builder.ExportFunction(functionFdFilestatSetTimes, fdFilestatSetTimes,
-		functionFdFilestatSetTimes, "fd", "atim", "mtim", "fst_flags")
-	builder.ExportFunction(functionFdPread, fdPread,
-		functionFdPread, "fd", "iovs", "iovs_len", "offset", "result.nread")
-	builder.ExportFunction(functionFdPrestatGet, fdPrestatGet,
-		functionFdPrestatGet, "fd", "result.prestat")
-	builder.ExportFunction(functionFdPrestatDirName, fdPrestatDirName,
-		functionFdPrestatDirName, "fd", "path", "path_len")
-	builder.ExportFunction(functionFdPwrite, fdPwrite,
-		functionFdPwrite, "fd", "iovs", "iovs_len", "offset", "result.nwritten")
-	builder.ExportFunction(functionFdRead, fdRead,
-		functionFdRead, "fd", "iovs", "iovs_len", "result.size")
-	builder.ExportFunction(functionFdReaddir, fdReaddir,
-		functionFdReaddir, "fd", "buf", "buf_len", "cookie", "result.bufused")
-	builder.ExportFunction(functionFdRenumber, fdRenumber,
-		functionFdRenumber, "fd", "to")
-	builder.ExportFunction(functionFdSeek, fdSeek,
-		functionFdSeek, "fd", "offset", "whence", "result.newoffset")
-	builder.ExportFunction(functionFdSync, fdSync,
-		functionFdSync, "fd")
-	builder.ExportFunction(functionFdTell, fdTell,
-		functionFdTell, "fd", "result.offset")
-	builder.ExportFunction(functionFdWrite, fdWrite,
-		functionFdWrite, "fd", "iovs", "iovs_len", "result.size")
-	builder.ExportFunction(functionPathCreateDirectory, pathCreateDirectory,
-		functionPathCreateDirectory, "fd", "path", "path_len")
-	builder.ExportFunction(functionPathFilestatGet, pathFilestatGet,
-		functionPathFilestatGet, "fd", "flags", "path", "path_len", "result.buf")
-	builder.ExportFunction(functionPathFilestatSetTimes, pathFilestatSetTimes,
-		functionPathFilestatSetTimes, "fd", "flags", "path", "path_len", "atim", "mtim", "fst_flags")
-	builder.ExportFunction(functionPathLink, pathLink,
-		functionPathLink, "old_fd", "old_flags", "old_path", "old_path_len", "new_fd", "new_path", "new_path_len")
-	builder.ExportFunction(functionPathOpen, pathOpen,
-		functionPathOpen, "fd", "dirflags", "path", "path_len", "oflags", "fs_rights_base", "fs_rights_inheriting", "fdflags", "result.opened_fd")
-	builder.ExportFunction(functionPathReadlink, pathReadlink,
-		functionPathReadlink, "fd", "path", "path_len", "buf", "buf_len", "result.bufused")
-	builder.ExportFunction(functionPathRemoveDirectory, pathRemoveDirectory,
-		functionPathRemoveDirectory, "fd", "path", "path_len")
-	builder.ExportFunction(functionPathRename, pathRename,
-		functionPathRename, "fd", "old_path", "old_path_len", "new_fd", "new_path", "new_path_len")
-	builder.ExportFunction(functionPathSymlink, pathSymlink,
-		functionPathSymlink, "old_path", "old_path_len", "fd", "new_path", "new_path_len")
-	builder.ExportFunction(functionPathUnlinkFile, pathUnlinkFile,
-		functionPathUnlinkFile, "fd", "path", "path_len")
-	builder.ExportFunction(functionPollOneoff, pollOneoff,
-		functionPollOneoff, "in", "out", "nsubscriptions", "result.nevents")
-	builder.ExportFunction(functionProcExit, procExit,
-		functionProcExit, "rval")
-	builder.ExportFunction(functionProcRaise, procRaise,
-		functionProcRaise, "sig")
-	builder.ExportFunction(functionSchedYield, schedYield,
-		functionSchedYield)
-	builder.ExportFunction(functionRandomGet, randomGet,
-		functionRandomGet, "buf", "buf_len")
-	builder.ExportFunction(functionSockRecv, sockRecv,
-		functionSockRecv, "fd", "ri_data", "ri_data_count", "ri_flags", "result.ro_datalen", "result.ro_flags")
-	builder.ExportFunction(functionSockSend, sockSend,
-		functionSockSend, "fd", "si_data", "si_data_count", "si_flags", "result.so_datalen")
-	builder.ExportFunction(functionSockShutdown, sockShutdown,
-		functionSockShutdown, "fd", "how")
+	builder.ExportFunction(argsGet.Name, argsGet)
+	builder.ExportFunction(argsSizesGet.Name, argsSizesGet)
+	builder.ExportFunction(environGet.Name, environGet)
+	builder.ExportFunction(environSizesGet.Name, environSizesGet)
+	builder.ExportFunction(clockResGet.Name, clockResGet)
+	builder.ExportFunction(clockTimeGet.Name, clockTimeGet)
+	builder.ExportFunction(fdAdvise.Name, fdAdvise)
+	builder.ExportFunction(fdAllocate.Name, fdAllocate)
+	builder.ExportFunction(fdClose.Name, fdClose)
+	builder.ExportFunction(fdDatasync.Name, fdDatasync)
+	builder.ExportFunction(fdFdstatGet.Name, fdFdstatGet)
+	builder.ExportFunction(fdFdstatSetFlags.Name, fdFdstatSetFlags)
+	builder.ExportFunction(fdFdstatSetRights.Name, fdFdstatSetRights)
+	builder.ExportFunction(fdFilestatGet.Name, fdFilestatGet)
+	builder.ExportFunction(fdFilestatSetSize.Name, fdFilestatSetSize)
+	builder.ExportFunction(fdFilestatSetTimes.Name, fdFilestatSetTimes)
+	builder.ExportFunction(fdPread.Name, fdPread)
+	builder.ExportFunction(fdPrestatGet.Name, fdPrestatGet)
+	builder.ExportFunction(fdPrestatDirName.Name, fdPrestatDirName)
+	builder.ExportFunction(fdPwrite.Name, fdPwrite)
+	builder.ExportFunction(fdRead.Name, fdRead)
+	builder.ExportFunction(fdReaddir.Name, fdReaddir)
+	builder.ExportFunction(fdRenumber.Name, fdRenumber)
+	builder.ExportFunction(fdSeek.Name, fdSeek)
+	builder.ExportFunction(fdSync.Name, fdSync)
+	builder.ExportFunction(fdTell.Name, fdTell)
+	builder.ExportFunction(fdWrite.Name, fdWrite)
+	builder.ExportFunction(pathCreateDirectory.Name, pathCreateDirectory)
+	builder.ExportFunction(pathFilestatGet.Name, pathFilestatGet)
+	builder.ExportFunction(pathFilestatSetTimes.Name, pathFilestatSetTimes)
+	builder.ExportFunction(pathLink.Name, pathLink)
+	builder.ExportFunction(pathOpen.Name, pathOpen)
+	builder.ExportFunction(pathReadlink.Name, pathReadlink)
+	builder.ExportFunction(pathRemoveDirectory.Name, pathRemoveDirectory)
+	builder.ExportFunction(pathRename.Name, pathRename)
+	builder.ExportFunction(pathSymlink.Name, pathSymlink)
+	builder.ExportFunction(pathUnlinkFile.Name, pathUnlinkFile)
+	builder.ExportFunction(pollOneoff.Name, pollOneoff)
+	builder.ExportFunction(procExit.Name, procExit)
+	builder.ExportFunction(procRaise.Name, procRaise)
+	builder.ExportFunction(schedYield.Name, schedYield)
+	builder.ExportFunction(randomGet.Name, randomGet)
+	builder.ExportFunction(sockRecv.Name, sockRecv)
+	builder.ExportFunction(sockSend.Name, sockSend)
+	builder.ExportFunction(sockShutdown.Name, sockShutdown)
 }
 
 func writeOffsetsAndNullTerminatedValues(ctx context.Context, mem api.Memory, values []string, offsets, bytes uint32) Errno {
@@ -225,15 +180,14 @@ func writeOffsetsAndNullTerminatedValues(ctx context.Context, mem api.Memory, va
 	return ErrnoSuccess
 }
 
-// stubFunction returns a function for the given params which returns ErrnoNosys.
-func stubFunction(params ...wasm.ValueType) *wasm.Func {
+// stubFunction stubs for GrainLang per #271.
+func stubFunction(name string, paramTypes []wasm.ValueType, paramNames []string) *wasm.Func {
 	return &wasm.Func{
-		Type: &wasm.FunctionType{
-			Params:            params,
-			Results:           []wasm.ValueType{wasm.ValueTypeI32},
-			ParamNumInUint64:  len(params),
-			ResultNumInUint64: 1,
-		},
-		Code: &wasm.Code{Body: []byte{wasm.OpcodeI32Const, byte(ErrnoNosys), wasm.OpcodeEnd}},
+		Name:        name,
+		ExportNames: []string{name},
+		ParamTypes:  paramTypes,
+		ParamNames:  paramNames,
+		ResultTypes: []wasm.ValueType{i32},
+		Code:        &wasm.Code{Body: []byte{wasm.OpcodeI32Const, byte(ErrnoNosys), wasm.OpcodeEnd}},
 	}
 }


Test patch:
diff --git a/assemblyscript/assemblyscript_test.go b/assemblyscript/assemblyscript_test.go
index e45d677f..e3215c19 100644
--- a/assemblyscript/assemblyscript_test.go
+++ b/assemblyscript/assemblyscript_test.go
@@ -15,7 +15,8 @@ import (
 	"github.com/tetratelabs/wazero/api"
 	. "github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/internal/testing/require"
-	"github.com/tetratelabs/wazero/internal/watzero"
+	"github.com/tetratelabs/wazero/internal/u64"
+	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/sys"
 )
 
@@ -23,25 +24,18 @@ import (
 var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
 
 func TestAbort(t *testing.T) {
-	var stderr bytes.Buffer
-	mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(&stderr))
-	defer r.Close(testCtx)
-
 	tests := []struct {
 		name     string
-		abortFn  fnAbort
 		exporter FunctionExporter
 		expected string
 	}{
 		{
 			name:     "enabled",
-			abortFn:  abortWithMessage,
 			exporter: NewFunctionExporter(),
 			expected: "message at filename:1:2\n",
 		},
 		{
 			name:     "disabled",
-			abortFn:  abort,
 			exporter: NewFunctionExporter().WithAbortMessageDisabled(),
 			expected: "",
 		},
@@ -51,15 +45,19 @@ func TestAbort(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			defer stderr.Reset()
+			var stderr bytes.Buffer
+			mod, r, log := requireModule(t, tc.exporter, wazero.NewModuleConfig().WithStderr(&stderr))
+			defer r.Close(testCtx)
 
 			messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename"))
 
-			err := require.CapturePanic(func() {
-				tc.abortFn(testCtx, mod, messageOff, filenameOff, 1, 2)
-			})
+			_, err := mod.ExportedFunction(functionAbort).
+				Call(testCtx, uint64(messageOff), uint64(filenameOff), uint64(1), uint64(2))
 			require.Error(t, err)
 			require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode())
+			require.Equal(t, `
+==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
+`, "\n"+log.String())
 
 			require.Equal(t, tc.expected, stderr.String())
 		})
@@ -68,7 +66,7 @@ func TestAbort(t *testing.T) {
 
 func TestAbort_Error(t *testing.T) {
 	var stderr bytes.Buffer
-	mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(&stderr))
+	mod, r, log := requireModule(t, NewFunctionExporter(), wazero.NewModuleConfig().WithStderr(&stderr))
 	defer r.Close(testCtx)
 
 	tests := []struct {
@@ -81,16 +79,16 @@ func TestAbort_Error(t *testing.T) {
 			name:          "bad message",
 			messageUTF16:  encodeUTF16("message")[:5],
 			fileNameUTF16: encodeUTF16("filename"),
-			expectedLog: `==> env.~lib/builtins/abort(message=4,fileName=13,lineNumber=1,columnNumber=2)
-<== ()
+			expectedLog: `
+==> env.~lib/builtins/abort(message=4,fileName=13,lineNumber=1,columnNumber=2)
 `,
 		},
 		{
 			name:          "bad filename",
 			messageUTF16:  encodeUTF16("message"),
 			fileNameUTF16: encodeUTF16("filename")[:5],
-			expectedLog: `==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
-<== ()
+			expectedLog: `
+==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
 `,
 		},
 	}
@@ -99,25 +97,35 @@ func TestAbort_Error(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
+			defer log.Reset()
 			defer stderr.Reset()
 
 			messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
 
-			// Since abort panics, any opcodes afterwards cannot be reached.
-			_ = require.CapturePanic(func() {
-				abortWithMessage(testCtx, mod, messageOff, filenameOff, 1, 2)
-			})
+			_, err := mod.ExportedFunction(functionAbort).
+				Call(testCtx, uint64(messageOff), uint64(filenameOff), uint64(1), uint64(2))
+			require.Error(t, err)
+			require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode())
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
+
 			require.Equal(t, "", stderr.String()) // nothing output if strings fail to read.
 		})
 	}
 }
 
 func TestSeed(t *testing.T) {
-	mod, r := requireModule(t, wazero.NewModuleConfig().
-		WithRandSource(bytes.NewReader([]byte{0, 1, 2, 3, 4, 5, 6, 7})))
+	b := []byte{0, 1, 2, 3, 4, 5, 6, 7}
+	mod, r, log := requireModule(t, NewFunctionExporter(), wazero.NewModuleConfig().WithRandSource(bytes.NewReader(b)))
 	defer r.Close(testCtx)
 
-	require.Equal(t, 7.949928895127363e-275, seed(mod))
+	ret, err := mod.ExportedFunction(functionSeed).Call(testCtx)
+	require.NoError(t, err)
+	require.Equal(t, `
+==> env.~lib/builtins/seed()
+<== (7.949928895127363e-275)
+`, "\n"+log.String())
+
+	require.Equal(t, b, u64.LeBytes(ret[0]))
 }
 
 func TestSeed_error(t *testing.T) {
@@ -127,14 +135,18 @@ func TestSeed_error(t *testing.T) {
 		expectedErr string
 	}{
 		{
-			name:        "not 8 bytes",
-			source:      bytes.NewReader([]byte{0, 1}),
-			expectedErr: `error reading random seed: unexpected EOF`,
+			name:   "not 8 bytes",
+			source: bytes.NewReader([]byte{0, 1}),
+			expectedErr: `error reading random seed: unexpected EOF (recovered by wazero)
+wasm stack trace:
+	env.~lib/builtins/seed() f64`,
 		},
 		{
-			name:        "error reading",
-			source:      iotest.ErrReader(errors.New("ice cream")),
-			expectedErr: `error reading random seed: ice cream`,
+			name:   "error reading",
+			source: iotest.ErrReader(errors.New("ice cream")),
+			expectedErr: `error reading random seed: ice cream (recovered by wazero)
+wasm stack trace:
+	env.~lib/builtins/seed() f64`,
 		},
 	}
 
@@ -142,11 +154,14 @@ func TestSeed_error(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			mod, r := requireModule(t, wazero.NewModuleConfig().WithRandSource(tc.source))
+			mod, r, log := requireModule(t, NewFunctionExporter(), wazero.NewModuleConfig().WithRandSource(tc.source))
 			defer r.Close(testCtx)
 
-			err := require.CapturePanic(func() { seed(mod) })
+			_, err := mod.ExportedFunction(functionSeed).Call(testCtx)
 			require.EqualError(t, err, tc.expectedErr)
+			require.Equal(t, `
+==> env.~lib/builtins/seed()
+`, "\n"+log.String())
 		})
 	}
 }
@@ -154,13 +169,17 @@ func TestSeed_error(t *testing.T) {
 // TestFunctionExporter_Trace ensures the trace output is according to configuration.
 func TestFunctionExporter_Trace(t *testing.T) {
 	noArgs := []uint64{4, 0, 0, 0, 0, 0, 0}
-	noArgsLog := `==> env.~lib/builtins/trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0)
+	noArgsLog := `
+==> env.~lib/builtins/trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0)
 <== ()
 `
+
 	tests := []struct {
 		name                  string
 		exporter              FunctionExporter
 		params                []uint64
+		message               []byte
+		outErr                bool
 		expected, expectedLog string
 	}{
 		{
@@ -190,7 +209,8 @@ func TestFunctionExporter_Trace(t *testing.T) {
 			exporter: NewFunctionExporter().WithTraceToStdout(),
 			params:   []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0},
 			expected: "trace: hello 1\n",
-			expectedLog: `==> env.~lib/builtins/trace(message=4,nArgs=1,arg0=1,arg1=0,arg2=0,arg3=0,arg4=0)
+			expectedLog: `
+==> env.~lib/builtins/trace(message=4,nArgs=1,arg0=1,arg1=0,arg2=0,arg3=0,arg4=0)
 <== ()
 `,
 		},
@@ -199,7 +219,8 @@ func TestFunctionExporter_Trace(t *testing.T) {
 			exporter: NewFunctionExporter().WithTraceToStdout(),
 			params:   []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0},
 			expected: "trace: hello 1,2\n",
-			expectedLog: `==> env.~lib/builtins/trace(message=4,nArgs=2,arg0=1,arg1=2,arg2=0,arg3=0,arg4=0)
+			expectedLog: `
+==> env.~lib/builtins/trace(message=4,nArgs=2,arg0=1,arg1=2,arg2=0,arg3=0,arg4=0)
 <== ()
 `,
 		},
@@ -216,81 +237,24 @@ func TestFunctionExporter_Trace(t *testing.T) {
 				api.EncodeF64(5),
 			},
 			expected: "trace: hello 1,2,3.3,4.4,5\n",
-			expectedLog: `==> env.~lib/builtins/trace(message=4,nArgs=5,arg0=1,arg1=2,arg2=3.3,arg3=4.4,arg4=5)
+			expectedLog: `
+==> env.~lib/builtins/trace(message=4,nArgs=5,arg0=1,arg1=2,arg2=3.3,arg3=4.4,arg4=5)
 <== ()
 `,
 		},
-	}
-
-	for _, tt := range tests {
-		tc := tt
-
-		t.Run(tc.name, func(t *testing.T) {
-			var stderr, functionLog bytes.Buffer
-
-			// Set context to one that has an experimental listener
-			ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&functionLog))
-
-			r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
-			defer r.Close(ctx)
-
-			envBuilder := r.NewModuleBuilder("env")
-			tc.exporter.ExportFunctions(envBuilder)
-			_, err := envBuilder.Instantiate(ctx, r)
-			require.NoError(t, err)
-
-			traceWasm, err := watzero.Wat2Wasm(`(module
-  (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64)))
-  (memory 1 1)
-  (export "trace" (func 0))
-)`)
-			require.NoError(t, err)
-
-			code, err := r.CompileModule(ctx, traceWasm, wazero.NewCompileConfig())
-			require.NoError(t, err)
-
-			config := wazero.NewModuleConfig()
-			if strings.Contains("ToStderr", tc.name) {
-				config = config.WithStderr(&stderr)
-			} else {
-				config = config.WithStdout(&stderr)
-			}
-
-			mod, err := r.InstantiateModule(ctx, code, config)
-			require.NoError(t, err)
-
-			message := encodeUTF16("hello")
-			ok := mod.Memory().WriteUint32Le(ctx, 0, uint32(len(message)))
-			require.True(t, ok)
-			ok = mod.Memory().Write(ctx, uint32(4), message)
-			require.True(t, ok)
-
-			_, err = mod.ExportedFunction("trace").Call(ctx, tc.params...)
-			require.NoError(t, err)
-			require.Equal(t, tc.expected, stderr.String())
-			require.Equal(t, tc.expectedLog, functionLog.String())
-		})
-	}
-}
-
-func TestTrace_error(t *testing.T) {
-	tests := []struct {
-		name        string
-		message     []byte
-		stderr      io.Writer
-		expectedErr string
-	}{
 		{
 			name:        "not 8 bytes",
+			exporter:    NewFunctionExporter().WithTraceToStderr(),
 			message:     encodeUTF16("hello")[:5],
-			stderr:      &bytes.Buffer{},
-			expectedErr: `invalid UTF-16 reading message`,
+			params:      noArgs,
+			expectedLog: noArgsLog,
 		},
 		{
 			name:        "error writing",
-			message:     encodeUTF16("hello"),
-			stderr:      &errWriter{err: errors.New("ice cream")},
-			expectedErr: `ice cream`,
+			exporter:    NewFunctionExporter().WithTraceToStderr(),
+			outErr:      true,
+			params:      noArgs,
+			expectedLog: noArgsLog,
 		},
 	}
 
@@ -298,32 +262,45 @@ func TestTrace_error(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(tc.stderr))
+			var out bytes.Buffer
+
+			config := wazero.NewModuleConfig()
+			if strings.Contains("ToStderr", tc.name) {
+				config = config.WithStderr(&out)
+			} else {
+				config = config.WithStdout(&out)
+			}
+			if tc.outErr {
+				config = config.WithStderr(&errWriter{err: errors.New("ice cream")})
+			}
+
+			mod, r, log := requireModule(t, tc.exporter, config)
 			defer r.Close(testCtx)
 
-			ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(tc.message)))
+			message := tc.message
+			if message == nil {
+				message = encodeUTF16("hello")
+			}
+			ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(message)))
 			require.True(t, ok)
-			ok = mod.Memory().Write(testCtx, uint32(4), tc.message)
+			ok = mod.Memory().Write(testCtx, uint32(4), message)
 			require.True(t, ok)
 
-			err := require.CapturePanic(func() {
-				traceToStderr(testCtx, mod, 4, 0, 0, 0, 0, 0, 0)
-			})
-			require.EqualError(t, err, tc.expectedErr)
+			_, err := mod.ExportedFunction(functionTrace).Call(testCtx, tc.params...)
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, out.String())
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
 		})
 	}
 }
 
-func Test_requireAssemblyScriptString(t *testing.T) {
-	var stderr bytes.Buffer
-	mod, r := requireModule(t, wazero.NewModuleConfig().WithStderr(&stderr))
-	defer r.Close(testCtx)
-
+func Test_readAssemblyScriptString(t *testing.T) {
 	tests := []struct {
-		name                  string
-		memory                func(context.Context, api.Memory)
-		offset                int
-		expected, expectedErr string
+		name       string
+		memory     func(context.Context, api.Memory)
+		offset     int
+		expected   string
+		expectedOk bool
 	}{
 		{
 			name: "success",
@@ -332,8 +309,9 @@ func Test_requireAssemblyScriptString(t *testing.T) {
 				b := encodeUTF16("hello")
 				memory.Write(testCtx, 4, b)
 			},
-			offset:   4,
-			expected: "hello",
+			offset:     4,
+			expected:   "hello",
+			expectedOk: true,
 		},
 		{
 			name: "can't read size",
@@ -341,8 +319,8 @@ func Test_requireAssemblyScriptString(t *testing.T) {
 				b := encodeUTF16("hello")
 				memory.Write(testCtx, 0, b)
 			},
-			offset:      0, // will attempt to read size from offset -4
-			expectedErr: "out of memory reading message",
+			offset:     0, // will attempt to read size from offset -4
+			expectedOk: false,
 		},
 		{
 			name: "odd size",
@@ -351,8 +329,8 @@ func Test_requireAssemblyScriptString(t *testing.T) {
 				b := encodeUTF16("hello")
 				memory.Write(testCtx, 4, b)
 			},
-			offset:      4,
-			expectedErr: "invalid UTF-16 reading message",
+			offset:     4,
+			expectedOk: false,
 		},
 		{
 			name: "can't read string",
@@ -361,8 +339,8 @@ func Test_requireAssemblyScriptString(t *testing.T) {
 				b := encodeUTF16("hello")
 				memory.Write(testCtx, 4, b)
 			},
-			offset:      4,
-			expectedErr: "out of memory reading message",
+			offset:     4,
+			expectedOk: false,
 		},
 	}
 
@@ -370,17 +348,12 @@ func Test_requireAssemblyScriptString(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			tc.memory(testCtx, mod.Memory())
+			mem := wasm.NewMemoryInstance(&wasm.Memory{Min: 1, Cap: 1, Max: 1})
+			tc.memory(testCtx, mem)
 
-			if tc.expectedErr != "" {
-				err := require.CapturePanic(func() {
-					_ = requireAssemblyScriptString(testCtx, mod, "message", uint32(tc.offset))
-				})
-				require.EqualError(t, err, tc.expectedErr)
-			} else {
-				s := requireAssemblyScriptString(testCtx, mod, "message", uint32(tc.offset))
-				require.Equal(t, tc.expected, s)
-			}
+			s, ok := readAssemblyScriptString(testCtx, mem, uint32(tc.offset))
+			require.Equal(t, tc.expectedOk, ok)
+			require.Equal(t, tc.expected, s)
 		})
 	}
 }
@@ -421,15 +394,21 @@ func (w *errWriter) Write([]byte) (int, error) {
 	return 0, w.err
 }
 
-func requireModule(t *testing.T, config wazero.ModuleConfig) (api.Module, api.Closer) {
+func requireModule(t *testing.T, fns FunctionExporter, config wazero.ModuleConfig) (api.Module, api.Closer, *bytes.Buffer) {
+	var log bytes.Buffer
+
+	// Set context to one that has an experimental listener
+	ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, NewLoggingListenerFactory(&log))
+
 	r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
 
-	compiled, err := r.NewModuleBuilder(t.Name()).
-		ExportMemoryWithMax("memory", 1, 1).
-		Compile(testCtx, wazero.NewCompileConfig())
+	builder := r.NewModuleBuilder("env").
+		ExportMemoryWithMax("memory", 1, 1)
+	fns.ExportFunctions(builder)
+	compiled, err := builder.Compile(ctx, wazero.NewCompileConfig())
 	require.NoError(t, err)
 
-	mod, err := r.InstantiateModule(testCtx, compiled, config)
+	mod, err := r.InstantiateModule(ctx, compiled, config)
 	require.NoError(t, err)
-	return mod, r
+	return mod, r, &log
 }
diff --git a/builder_test.go b/builder_test.go
index f57f70d3..c1c772ed 100644
--- a/builder_test.go
+++ b/builder_test.go
@@ -51,7 +51,7 @@ func TestNewModuleBuilder_Compile(t *testing.T) {
 			},
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
-					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
+					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
 				},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{{GoFunc: &fnUint32_uint32}},
@@ -70,7 +70,7 @@ func TestNewModuleBuilder_Compile(t *testing.T) {
 			},
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
-					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
+					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
 				},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{{GoFunc: &fnUint32_uint32}},
@@ -90,7 +90,7 @@ func TestNewModuleBuilder_Compile(t *testing.T) {
 			},
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
-					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
+					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
 				},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{{GoFunc: &fnUint64_uint32}},
@@ -110,8 +110,8 @@ func TestNewModuleBuilder_Compile(t *testing.T) {
 			},
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
-					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
-					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
+					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
+					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
 				},
 				FunctionSection: []wasm.Index{0, 1},
 				CodeSection:     []*wasm.Code{{GoFunc: &fnUint32_uint32}, {GoFunc: &fnUint64_uint32}},
@@ -134,8 +134,8 @@ func TestNewModuleBuilder_Compile(t *testing.T) {
 			},
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
-					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
-					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
+					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
+					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
 				},
 				FunctionSection: []wasm.Index{0, 1},
 				CodeSection:     []*wasm.Code{{GoFunc: &fnUint32_uint32}, {GoFunc: &fnUint64_uint32}},
@@ -159,8 +159,8 @@ func TestNewModuleBuilder_Compile(t *testing.T) {
 			},
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
-					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
-					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
+					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
+					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
 				},
 				FunctionSection: []wasm.Index{0, 1},
 				CodeSection:     []*wasm.Code{{GoFunc: &fnUint32_uint32}, {GoFunc: &fnUint64_uint32}},
@@ -448,6 +448,9 @@ func TestNewModuleBuilder_Instantiate_Errors(t *testing.T) {
 // requireHostModuleEquals is redefined from internal/wasm/host_test.go to avoid an import cycle extracting it.
 func requireHostModuleEquals(t *testing.T, expected, actual *wasm.Module) {
 	// `require.Equal(t, expected, actual)` fails reflect pointers don't match, so brute compare:
+	for _, tp := range expected.TypeSection {
+		tp.CacheNumInUint64()
+	}
 	require.Equal(t, expected.TypeSection, actual.TypeSection)
 	require.Equal(t, expected.ImportSection, actual.ImportSection)
 	require.Equal(t, expected.FunctionSection, actual.FunctionSection)
diff --git a/internal/integration_test/bench/codec_test.go b/internal/integration_test/bench/codec_test.go
index cabc7a65..47341a5c 100644
--- a/internal/integration_test/bench/codec_test.go
+++ b/internal/integration_test/bench/codec_test.go
@@ -21,12 +21,12 @@ func newExample() *wasm.Module {
 	f32, i32, i64 := wasm.ValueTypeF32, wasm.ValueTypeI32, wasm.ValueTypeI64
 	return &wasm.Module{
 		TypeSection: []*wasm.FunctionType{
-			{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1},
-			{ParamNumInUint64: 0, ResultNumInUint64: 0},
-			{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 4, ResultNumInUint64: 1},
-			{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}, ParamNumInUint64: 1, ResultNumInUint64: 1},
-			{Params: []wasm.ValueType{f32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
-			{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}, ParamNumInUint64: 2, ResultNumInUint64: 2},
+			{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}},
+			{},
+			{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}},
+			{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}},
+			{Params: []wasm.ValueType{f32}, Results: []wasm.ValueType{i32}},
+			{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}},
 		},
 		ImportSection: []*wasm.Import{
 			{
diff --git a/internal/wasm/binary/decoder_test.go b/internal/wasm/binary/decoder_test.go
index 68efa1d9..a55c28fc 100644
--- a/internal/wasm/binary/decoder_test.go
+++ b/internal/wasm/binary/decoder_test.go
@@ -30,14 +30,8 @@ func TestDecodeModule(t *testing.T) {
 			input: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
 					{},
-					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32},
-						ParamNumInUint64:  2,
-						ResultNumInUint64: 1,
-					},
-					{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32},
-						ParamNumInUint64:  4,
-						ResultNumInUint64: 1,
-					},
+					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}},
+					{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}},
 				},
 			},
 		},
@@ -45,14 +39,8 @@ func TestDecodeModule(t *testing.T) {
 			name: "type and import section",
 			input: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
-					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32},
-						ParamNumInUint64:  2,
-						ResultNumInUint64: 1,
-					},
-					{Params: []wasm.ValueType{f32, f32}, Results: []wasm.ValueType{f32},
-						ParamNumInUint64:  2,
-						ResultNumInUint64: 1,
-					},
+					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}},
+					{Params: []wasm.ValueType{f32, f32}, Results: []wasm.ValueType{f32}},
 				},
 				ImportSection: []*wasm.Import{
 					{
diff --git a/internal/wasm/binary/function_test.go b/internal/wasm/binary/function_test.go
index efeb30af..8acbdf32 100644
--- a/internal/wasm/binary/function_test.go
+++ b/internal/wasm/binary/function_test.go
@@ -23,52 +23,52 @@ func TestFunctionType(t *testing.T) {
 		},
 		{
 			name:     "one param no result",
-			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32}, ParamNumInUint64: 1},
+			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32}},
 			expected: []byte{0x60, 1, i32, 0},
 		},
 		{
 			name:     "no param one result",
-			input:    &wasm.FunctionType{Results: []wasm.ValueType{i32}, ResultNumInUint64: 1},
+			input:    &wasm.FunctionType{Results: []wasm.ValueType{i32}},
 			expected: []byte{0x60, 0, 1, i32},
 		},
 		{
 			name:     "one param one result",
-			input:    &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
+			input:    &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32}},
 			expected: []byte{0x60, 1, i64, 1, i32},
 		},
 		{
 			name:     "two params no result",
-			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, ParamNumInUint64: 2},
+			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}},
 			expected: []byte{0x60, 2, i32, i64, 0},
 		},
 		{
 			name:     "two param one result",
-			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1},
+			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32}},
 			expected: []byte{0x60, 2, i32, i64, 1, i32},
 		},
 		{
 			name:     "no param two results",
-			input:    &wasm.FunctionType{Results: []wasm.ValueType{i32, i64}, ResultNumInUint64: 2},
+			input:    &wasm.FunctionType{Results: []wasm.ValueType{i32, i64}},
 			expected: []byte{0x60, 0, 2, i32, i64},
 		},
 		{
 			name:     "one param two results",
-			input:    &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32, i64}, ParamNumInUint64: 1, ResultNumInUint64: 2},
+			input:    &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32, i64}},
 			expected: []byte{0x60, 1, i64, 2, i32, i64},
 		},
 		{
 			name:     "two param two results",
-			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32, i64}, ParamNumInUint64: 2, ResultNumInUint64: 2},
+			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32, i64}},
 			expected: []byte{0x60, 2, i32, i64, 2, i32, i64},
 		},
 		{
 			name:     "two param two results with funcrefs",
-			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32, funcRef}, Results: []wasm.ValueType{funcRef, i64}, ParamNumInUint64: 2, ResultNumInUint64: 2},
+			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32, funcRef}, Results: []wasm.ValueType{funcRef, i64}},
 			expected: []byte{0x60, 2, i32, funcRef, 2, funcRef, i64},
 		},
 		{
 			name:     "two param two results with externrefs",
-			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32, externRef}, Results: []wasm.ValueType{externRef, i64}, ParamNumInUint64: 2, ResultNumInUint64: 2},
+			input:    &wasm.FunctionType{Params: []wasm.ValueType{i32, externRef}, Results: []wasm.ValueType{externRef, i64}},
 			expected: []byte{0x60, 2, i32, externRef, 2, externRef, i64},
 		},
 	}
diff --git a/internal/wasm/gofunc_test.go b/internal/wasm/gofunc_test.go
index 57c215bb..d9c696ad 100644
--- a/internal/wasm/gofunc_test.go
+++ b/internal/wasm/gofunc_test.go
@@ -49,7 +49,7 @@ func TestGetFunctionType(t *testing.T) {
 			name:         "all supported params and i32 result",
 			inputFunc:    func(uint32, uint64, float32, float64, uintptr) uint32 { return 0 },
 			expectedKind: FunctionKindGoNoContext,
-			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64, externref}, Results: []ValueType{i32}, ParamNumInUint64: 5, ResultNumInUint64: 1},
+			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64, externref}, Results: []ValueType{i32}},
 		},
 		{
 			name: "all supported params and all supported results",
@@ -58,28 +58,27 @@ func TestGetFunctionType(t *testing.T) {
 			},
 			expectedKind: FunctionKindGoNoContext,
 			expectedType: &FunctionType{
-				Params:           []ValueType{i32, i64, f32, f64, externref},
-				Results:          []ValueType{i32, i64, f32, f64, externref},
-				ParamNumInUint64: 5, ResultNumInUint64: 5,
+				Params:  []ValueType{i32, i64, f32, f64, externref},
+				Results: []ValueType{i32, i64, f32, f64, externref},
 			},
 		},
 		{
 			name:         "all supported params and i32 result - wasm.Module",
 			inputFunc:    func(api.Module, uint32, uint64, float32, float64, uintptr) uint32 { return 0 },
 			expectedKind: FunctionKindGoModule,
-			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64, externref}, Results: []ValueType{i32}, ParamNumInUint64: 5, ResultNumInUint64: 1},
+			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64, externref}, Results: []ValueType{i32}},
 		},
 		{
 			name:         "all supported params and i32 result - context.Context",
 			inputFunc:    func(context.Context, uint32, uint64, float32, float64, uintptr) uint32 { return 0 },
 			expectedKind: FunctionKindGoContext,
-			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64, externref}, Results: []ValueType{i32}, ParamNumInUint64: 5, ResultNumInUint64: 1},
+			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64, externref}, Results: []ValueType{i32}},
 		},
 		{
 			name:         "all supported params and i32 result - context.Context and api.Module",
 			inputFunc:    func(context.Context, api.Module, uint32, uint64, float32, float64, uintptr) uint32 { return 0 },
 			expectedKind: FunctionKindGoContextModule,
-			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64, externref}, Results: []ValueType{i32}, ParamNumInUint64: 5, ResultNumInUint64: 1},
+			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64, externref}, Results: []ValueType{i32}},
 		},
 	}
 	for _, tt := range tests {
diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go
index 941f0bab..03981b03 100644
--- a/internal/wasm/host_test.go
+++ b/internal/wasm/host_test.go
@@ -30,8 +30,6 @@ func swap(x, y uint32) (uint32, uint32) {
 }
 
 func TestNewHostModule(t *testing.T) {
-	i32 := ValueTypeI32
-
 	a := wasiAPI{}
 	functionArgsSizesGet := "args_sizes_get"
 	fnArgsSizesGet := reflect.ValueOf(a.ArgsSizesGet)
@@ -65,8 +63,8 @@ func TestNewHostModule(t *testing.T) {
 			},
 			expected: &Module{
 				TypeSection: []*FunctionType{
-					{Params: []ValueType{i32, i32}, Results: []ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1},
-					{Params: []ValueType{i32, i32, i32, i32}, Results: []ValueType{i32}, ParamNumInUint64: 4, ResultNumInUint64: 1},
+					{Params: []ValueType{i32, i32}, Results: []ValueType{i32}},
+					{Params: []ValueType{i32, i32, i32, i32}, Results: []ValueType{i32}},
 				},
 				FunctionSection: []Index{0, 1},
 				CodeSection:     []*Code{{GoFunc: &fnArgsSizesGet}, {GoFunc: &fnFdWrite}},
@@ -90,7 +88,7 @@ func TestNewHostModule(t *testing.T) {
 				functionSwap: swap,
 			},
 			expected: &Module{
-				TypeSection:     []*FunctionType{{Params: []ValueType{i32, i32}, Results: []ValueType{i32, i32}, ParamNumInUint64: 2, ResultNumInUint64: 2}},
+				TypeSection:     []*FunctionType{{Params: []ValueType{i32, i32}, Results: []ValueType{i32, i32}}},
 				FunctionSection: []Index{0},
 				CodeSection:     []*Code{{GoFunc: &fnSwap}},
 				ExportSection:   []*Export{{Name: "swap", Type: ExternTypeFunc, Index: 0}},
@@ -151,7 +149,7 @@ func TestNewHostModule(t *testing.T) {
 			},
 			expected: &Module{
 				TypeSection: []*FunctionType{
-					{Params: []ValueType{i32, i32}, Results: []ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1},
+					{Params: []ValueType{i32, i32}, Results: []ValueType{i32}},
 				},
 				FunctionSection: []Index{0},
 				CodeSection:     []*Code{{GoFunc: &fnArgsSizesGet}},
diff --git a/internal/watzero/internal/decoder_test.go b/internal/watzero/internal/decoder_test.go
index 1ad9feff..d58bd207 100644
--- a/internal/watzero/internal/decoder_test.go
+++ b/internal/watzero/internal/decoder_test.go
@@ -63,7 +63,7 @@ func TestDecodeModule(t *testing.T) {
 			input: "(module (type (func (param i32 i32) (result i32 i32))))",
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
-					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}, ParamNumInUint64: 2, ResultNumInUint64: 2},
+					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}},
 				},
 			},
 		},
@@ -72,7 +72,7 @@ func TestDecodeModule(t *testing.T) {
 			input: "(module (type (func (param i32) (param i32) (result i32) (result i32))))",
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
-					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}, ParamNumInUint64: 2, ResultNumInUint64: 2},
+					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}},
 				},
 			},
 		},
@@ -362,8 +362,8 @@ func TestDecodeModule(t *testing.T) {
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
 					v_v,
-					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1},
-					{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 4, ResultNumInUint64: 1},
+					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}},
+					{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}},
 				},
 				ImportSection: []*wasm.Import{
 					{
@@ -549,7 +549,7 @@ func TestDecodeModule(t *testing.T) {
 			input: "(module (import \"\" \"\" (func (param i32 i32) (param $v i32) (param i64) (param $t f32))))",
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
-					{Params: []wasm.ValueType{i32, i32, i32, i64, f32}, ParamNumInUint64: 5},
+					{Params: []wasm.ValueType{i32, i32, i32, i64, f32}},
 				},
 				ImportSection: []*wasm.Import{{Type: wasm.ExternTypeFunc, DescFunc: 0}},
 				NameSection: &wasm.NameSection{
@@ -569,8 +569,8 @@ func TestDecodeModule(t *testing.T) {
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
 					v_v,
-					{Params: []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 9, ResultNumInUint64: 1},
-					{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 4, ResultNumInUint64: 1},
+					{Params: []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32}, Results: []wasm.ValueType{i32}},
+					{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}},
 				},
 				ImportSection: []*wasm.Import{
 					{
@@ -598,7 +598,7 @@ func TestDecodeModule(t *testing.T) {
 	(import "foo" "bar" (func (type 0) (param i32)))
 )`,
 			expected: &wasm.Module{
-				TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}, ParamNumInUint64: 1}},
+				TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}},
 				ImportSection: []*wasm.Import{{
 					Module: "foo", Name: "bar",
 					Type:     wasm.ExternTypeFunc,
@@ -613,7 +613,7 @@ func TestDecodeModule(t *testing.T) {
 	(type $i32 (func (param i32)))
 )`,
 			expected: &wasm.Module{
-				TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}, ParamNumInUint64: 1}},
+				TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}},
 				ImportSection: []*wasm.Import{{
 					Module: "foo", Name: "bar",
 					Type:     wasm.ExternTypeFunc,
@@ -628,7 +628,7 @@ func TestDecodeModule(t *testing.T) {
 	(import "foo" "bar" (func (type $i32) (param i32)))
 )`,
 			expected: &wasm.Module{
-				TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}, ParamNumInUint64: 1}},
+				TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}},
 				ImportSection: []*wasm.Import{{
 					Module: "foo", Name: "bar",
 					Type:     wasm.ExternTypeFunc,
@@ -643,7 +643,7 @@ func TestDecodeModule(t *testing.T) {
 	(type $i32 (func (param i32)))
 )`,
 			expected: &wasm.Module{
-				TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}, ParamNumInUint64: 1}},
+				TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}},
 				ImportSection: []*wasm.Import{{
 					Module: "foo", Name: "bar",
 					Type:     wasm.ExternTypeFunc,
@@ -655,7 +655,7 @@ func TestDecodeModule(t *testing.T) {
 			name:  "import func multiple abbreviated results",
 			input: `(module (import "misc" "swap" (func $swap (param i32 i32) (result i32 i32))))`,
 			expected: &wasm.Module{
-				TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}, ParamNumInUint64: 2, ResultNumInUint64: 2}},
+				TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}}},
 				ImportSection: []*wasm.Import{{
 					Module: "misc", Name: "swap",
 					Type:     wasm.ExternTypeFunc,
@@ -856,8 +856,8 @@ func TestDecodeModule(t *testing.T) {
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
 					v_v,
-					{Params: []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 9, ResultNumInUint64: 1},
-					{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 4, ResultNumInUint64: 1},
+					{Params: []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32}, Results: []wasm.ValueType{i32}},
+					{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}},
 				},
 				FunctionSection: []wasm.Index{1, 2},
 				CodeSection:     []*wasm.Code{{Body: localGet0End}, {Body: localGet0End}},
@@ -881,8 +881,8 @@ func TestDecodeModule(t *testing.T) {
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
 					v_v,
-					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1},
-					{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 4, ResultNumInUint64: 1},
+					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}},
+					{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}},
 				},
 				FunctionSection: []wasm.Index{1, 2},
 				CodeSection:     []*wasm.Code{{Body: localGet0End}, {Body: localGet0End}},
@@ -922,7 +922,7 @@ func TestDecodeModule(t *testing.T) {
 	(func (type 0) (param i32))
 )`,
 			expected: &wasm.Module{
-				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32}, ParamNumInUint64: 1}},
+				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{codeEnd},
 			},
@@ -934,7 +934,7 @@ func TestDecodeModule(t *testing.T) {
 	(type $i32 (func (param i32)))
 )`,
 			expected: &wasm.Module{
-				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32}, ParamNumInUint64: 1}},
+				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{codeEnd},
 			},
@@ -946,7 +946,7 @@ func TestDecodeModule(t *testing.T) {
 	(func (type $i32) (param i32))
 )`,
 			expected: &wasm.Module{
-				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32}, ParamNumInUint64: 1}},
+				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{codeEnd},
 			},
@@ -958,7 +958,7 @@ func TestDecodeModule(t *testing.T) {
 	(type $i32 (func (param i32)))
 )`,
 			expected: &wasm.Module{
-				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32}, ParamNumInUint64: 1}},
+				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32}}},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{codeEnd},
 			},
@@ -1101,7 +1101,7 @@ func TestDecodeModule(t *testing.T) {
 			name:  "func param IDs",
 			input: "(module (func $one (param $x i32) (param $y i32) (result i32) local.get 0))",
 			expected: &wasm.Module{
-				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1}},
+				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{{Body: localGet0End}},
 				NameSection: &wasm.NameSection{
@@ -1119,7 +1119,7 @@ func TestDecodeModule(t *testing.T) {
 			(func (param $l i32) (param $r i32) (result i32) local.get 0)
 		)`,
 			expected: &wasm.Module{
-				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1}},
+				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}},
 				FunctionSection: []wasm.Index{0, 0},
 				CodeSection:     []*wasm.Code{{Body: localGet0End}, {Body: localGet0End}},
 				NameSection: &wasm.NameSection{
@@ -1134,7 +1134,7 @@ func TestDecodeModule(t *testing.T) {
 			name:  "func mixed param IDs", // Verifies we can handle less param fields than Params
 			input: "(module (func (param i32 i32) (param $v i32) (param i64) (param $t f32)))",
 			expected: &wasm.Module{
-				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32, i32, i64, f32}, ParamNumInUint64: 5}},
+				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32, i32, i64, f32}}},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{{Body: end}},
 				NameSection: &wasm.NameSection{
@@ -1148,7 +1148,7 @@ func TestDecodeModule(t *testing.T) {
 			name:  "func multiple abbreviated results",
 			input: "(module (func $swap (param i32 i32) (result i32 i32) local.get 1 local.get 0))",
 			expected: &wasm.Module{
-				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}, ParamNumInUint64: 2, ResultNumInUint64: 2}},
+				TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}}},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{{Body: []byte{wasm.OpcodeLocalGet, 0x01, wasm.OpcodeLocalGet, 0x00, wasm.OpcodeEnd}}},
 				NameSection: &wasm.NameSection{
@@ -1393,7 +1393,7 @@ func TestDecodeModule(t *testing.T) {
 )`,
 			expected: &wasm.Module{
 				TypeSection: []*wasm.FunctionType{
-					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1},
+					{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}},
 				},
 				FunctionSection: []wasm.Index{0},
 				CodeSection: []*wasm.Code{
diff --git a/internal/watzero/internal/type_parser_test.go b/internal/watzero/internal/type_parser_test.go
index 529a6a55..96339763 100644
--- a/internal/watzero/internal/type_parser_test.go
+++ b/internal/watzero/internal/type_parser_test.go
@@ -9,44 +9,19 @@ import (
 
 var (
 	f32, f64, i32, i64 = wasm.ValueTypeF32, wasm.ValueTypeF64, wasm.ValueTypeI32, wasm.ValueTypeI64
-	i32_v              = &wasm.FunctionType{Params: []wasm.ValueType{i32},
-		ParamNumInUint64: 1,
-	}
-	v_i32 = &wasm.FunctionType{Results: []wasm.ValueType{i32},
-		ResultNumInUint64: 1,
-	}
-	v_i32i64 = &wasm.FunctionType{Results: []wasm.ValueType{i32, i64},
-		ResultNumInUint64: 2,
-	}
-	f32_i32 = &wasm.FunctionType{Params: []wasm.ValueType{f32}, Results: []wasm.ValueType{i32},
-		ParamNumInUint64:  1,
-		ResultNumInUint64: 1,
-	}
-	i64_i64 = &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64},
-		ParamNumInUint64:  1,
-		ResultNumInUint64: 1,
-	}
-	i32i64_v = &wasm.FunctionType{Params: []wasm.ValueType{i32, i64},
-		ParamNumInUint64: 2,
-	}
-	i32i32_i32 = &wasm.FunctionType{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32},
-		ParamNumInUint64:  2,
-		ResultNumInUint64: 1,
-	}
-	i32i64_i32 = &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32},
-		ParamNumInUint64:  2,
-		ResultNumInUint64: 1,
-	}
-	i32i32i32i32_i32 = &wasm.FunctionType{
-		Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32},
-		ParamNumInUint64:  4,
-		ResultNumInUint64: 1,
-	}
+	i32_v              = &wasm.FunctionType{Params: []wasm.ValueType{i32}}
+	v_i32              = &wasm.FunctionType{Results: []wasm.ValueType{i32}}
+	v_i32i64           = &wasm.FunctionType{Results: []wasm.ValueType{i32, i64}}
+	f32_i32            = &wasm.FunctionType{Params: []wasm.ValueType{f32}, Results: []wasm.ValueType{i32}}
+	i64_i64            = &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}}
+	i32i64_v           = &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}}
+	i32i32_i32         = &wasm.FunctionType{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}
+	i32i64_i32         = &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32}}
+	i32i32i32i32_i32   = &wasm.FunctionType{
+		Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}}
 	i32i32i32i32i32i64i64i32i32_i32 = &wasm.FunctionType{
-		Params:            []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32},
-		Results:           []wasm.ValueType{i32},
-		ParamNumInUint64:  9,
-		ResultNumInUint64: 1,
+		Params:  []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32},
+		Results: []wasm.ValueType{i32},
 	}
 )
 
@@ -125,7 +100,7 @@ func TestTypeParser(t *testing.T) {
 		{
 			name:     "mixed param abbreviation", // Verifies we can handle less param fields than param types
 			input:    "(type (func (param i32 i32) (param i32) (param i64) (param f32)))",
-			expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i32, i32, i64, f32}, ParamNumInUint64: 5},
+			expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i32, i32, i64, f32}},
 		},
 
 		// Below are changes to test/core/br.wast from the commit that added "multi-value" support.
@@ -134,58 +109,55 @@ func TestTypeParser(t *testing.T) {
 		{
 			name:     "multi-value - v_i64f32 abbreviated",
 			input:    "(type (func (result i64 f32)))",
-			expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}, ResultNumInUint64: 2},
+			expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}},
 		},
 		{
 			name:     "multi-value - i32i64_f32f64 abbreviated",
 			input:    "(type (func (param i32 i64) (result f32 f64)))",
-			expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}, ParamNumInUint64: 2, ResultNumInUint64: 2},
+			expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
 		},
 		{
 			name:     "multi-value - v_i64f32",
 			input:    "(type (func (result i64) (result f32)))",
-			expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}, ResultNumInUint64: 2},
+			expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}},
 		},
 		{
 			name:     "multi-value - i32i64_f32f64",
 			input:    "(type (func (param i32) (param i64) (result f32) (result f64)))",
-			expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}, ParamNumInUint64: 2, ResultNumInUint64: 2},
+			expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
 		},
 		{
 			name:     "multi-value - i32i64_f32f64 named",
 			input:    "(type (func (param $x i32) (param $y i64) (result f32) (result f64)))",
-			expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}, ParamNumInUint64: 2, ResultNumInUint64: 2},
+			expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
 		},
 		{
 			name:     "multi-value - i64i64f32_f32i32 results abbreviated in groups",
 			input:    "(type (func (result i64 i64 f32) (result f32 i32)))",
-			expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32, f32, i32}, ResultNumInUint64: 5},
+			expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32, f32, i32}},
 		},
 		{
 			name:  "multi-value - i32i32i64i32_f32f64f64i32 params and results abbreviated in groups",
 			input: "(type (func (param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32)))",
 			expected: &wasm.FunctionType{
-				Params:           []wasm.ValueType{i32, i32, i64, i32},
-				Results:          []wasm.ValueType{f32, f64, f64, i32},
-				ParamNumInUint64: 4, ResultNumInUint64: 4,
+				Params:  []wasm.ValueType{i32, i32, i64, i32},
+				Results: []wasm.ValueType{f32, f64, f64, i32},
 			},
 		},
 		{
 			name:  "multi-value - i32i32i64i32_f32f64f64i32 abbreviated in groups",
 			input: "(type (func (param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32)))",
 			expected: &wasm.FunctionType{
-				Params:           []wasm.ValueType{i32, i32, i64, i32},
-				Results:          []wasm.ValueType{f32, f64, f64, i32},
-				ParamNumInUint64: 4, ResultNumInUint64: 4,
+				Params:  []wasm.ValueType{i32, i32, i64, i32},
+				Results: []wasm.ValueType{f32, f64, f64, i32},
 			},
 		},
 		{
 			name:  "multi-value - i32i32i64i32_f32f64f64i32 abbreviated in groups",
 			input: "(type (func (param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32)))",
 			expected: &wasm.FunctionType{
-				Params:           []wasm.ValueType{i32, i32, i64, i32},
-				Results:          []wasm.ValueType{f32, f64, f64, i32},
-				ParamNumInUint64: 4, ResultNumInUint64: 4,
+				Params:  []wasm.ValueType{i32, i32, i64, i32},
+				Results: []wasm.ValueType{f32, f64, f64, i32},
 			},
 		},
 		{
@@ -193,7 +165,7 @@ func TestTypeParser(t *testing.T) {
 			input: "(type (func (result) (result) (result i64 i64) (result) (result f32) (result)))",
 			// Abbreviations have min length zero, which implies no-op results are ok.
 			// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#abbreviations%E2%91%A2
-			expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32}, ResultNumInUint64: 3},
+			expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32}},
 		},
 		{
 			name: "multi-value - empty abbreviated params and results",
@@ -204,9 +176,8 @@ func TestTypeParser(t *testing.T) {
 			// Abbreviations have min length zero, which implies no-op results are ok.
 			// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#abbreviations%E2%91%A2
 			expected: &wasm.FunctionType{
-				Params:           []wasm.ValueType{i32, i32, i64, i32, i32},
-				Results:          []wasm.ValueType{f32, f64, f64, i32},
-				ParamNumInUint64: 5, ResultNumInUint64: 4,
+				Params:  []wasm.ValueType{i32, i32, i64, i32, i32},
+				Results: []wasm.ValueType{f32, f64, f64, i32},
 			},
 		},
 	}
@@ -408,9 +379,6 @@ func parseFunctionType(
 	tp := newTypeParser(enabledFeatures, typeNamespace, setFunc)
 	// typeParser starts after the '(type', so we need to eat it first!
 	_, _, err := lex(skipTokens(2, tp.begin), []byte(input))
-	if parsed != nil {
-		parsed.CacheNumInUint64()
-	}
 	return parsed, tp, err
 }
 
diff --git a/internal/watzero/internal/typeuse_parser_test.go b/internal/watzero/internal/typeuse_parser_test.go
index e75e2d6f..ac152f12 100644
--- a/internal/watzero/internal/typeuse_parser_test.go
+++ b/internal/watzero/internal/typeuse_parser_test.go
@@ -74,7 +74,7 @@ func TestTypeUseParser_InlinesTypesWhenNotYetAdded(t *testing.T) {
 		{
 			name:                "mixed param abbreviation", // Verifies we can handle less param fields than param types
 			input:               "((param i32 i32) (param i32) (param i64) (param f32))",
-			expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i32, i32, i64, f32}, ParamNumInUint64: 5},
+			expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i32, i32, i64, f32}},
 		},
 
 		// Below are changes to test/core/br.wast from the commit that added "multi-value" support.
@@ -83,73 +83,56 @@ func TestTypeUseParser_InlinesTypesWhenNotYetAdded(t *testing.T) {
 		{
 			name:                "multi-value - v_i64f32 abbreviated",
 			input:               "((result i64 f32))",
-			expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}, ResultNumInUint64: 2},
+			expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}},
 		},
 		{
-			name:  "multi-value - i32i64_f32f64 abbreviated",
-			input: "((param i32 i64) (result f32 f64))",
-			expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64},
-				ParamNumInUint64:  2,
-				ResultNumInUint64: 2,
-			},
+			name:                "multi-value - i32i64_f32f64 abbreviated",
+			input:               "((param i32 i64) (result f32 f64))",
+			expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
 		},
 		{
 			name:                "multi-value - v_i64f32",
 			input:               "((result i64) (result f32))",
-			expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}, ResultNumInUint64: 2},
+			expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}},
 		},
 		{
-			name:  "multi-value - i32i64_f32f64",
-			input: "((param i32) (param i64) (result f32) (result f64))",
-			expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64},
-				ParamNumInUint64:  2,
-				ResultNumInUint64: 2,
-			},
+			name:                "multi-value - i32i64_f32f64",
+			input:               "((param i32) (param i64) (result f32) (result f64))",
+			expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
 		},
 		{
-			name:  "multi-value - i32i64_f32f64 named",
-			input: "((param $x i32) (param $y i64) (result f32) (result f64))",
-			expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64},
-				ParamNumInUint64:  2,
-				ResultNumInUint64: 2,
-			},
-			expectedParamNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "x"}, &wasm.NameAssoc{Index: 1, Name: "y"}},
+			name:                "multi-value - i32i64_f32f64 named",
+			input:               "((param $x i32) (param $y i64) (result f32) (result f64))",
+			expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
+			expectedParamNames:  wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "x"}, &wasm.NameAssoc{Index: 1, Name: "y"}},
 		},
 		{
-			name:  "multi-value - i64i64f32_f32i32 results abbreviated in groups",
-			input: "((result i64 i64 f32) (result f32 i32))",
-			expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32, f32, i32},
-				ResultNumInUint64: 5,
-			},
+			name:                "multi-value - i64i64f32_f32i32 results abbreviated in groups",
+			input:               "((result i64 i64 f32) (result f32 i32))",
+			expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32, f32, i32}},
 		},
 		{
 			name:  "multi-value - i32i32i64i32_f32f64f64i32 params and results abbreviated in groups",
 			input: "((param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32))",
 			expectedInlinedType: &wasm.FunctionType{
-				Params:            []wasm.ValueType{i32, i32, i64, i32},
-				Results:           []wasm.ValueType{f32, f64, f64, i32},
-				ParamNumInUint64:  4,
-				ResultNumInUint64: 4,
+				Params:  []wasm.ValueType{i32, i32, i64, i32},
+				Results: []wasm.ValueType{f32, f64, f64, i32},
 			},
 		},
 		{
 			name:  "multi-value - i32i32i64i32_f32f64f64i32 abbreviated in groups",
 			input: "((param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32))",
 			expectedInlinedType: &wasm.FunctionType{
-				Params:            []wasm.ValueType{i32, i32, i64, i32},
-				Results:           []wasm.ValueType{f32, f64, f64, i32},
-				ParamNumInUint64:  4,
-				ResultNumInUint64: 4,
+				Params:  []wasm.ValueType{i32, i32, i64, i32},
+				Results: []wasm.ValueType{f32, f64, f64, i32},
 			},
 		},
 		{
 			name:  "multi-value - i32i32i64i32_f32f64f64i32 abbreviated in groups",
 			input: "((param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32))",
 			expectedInlinedType: &wasm.FunctionType{
-				Params:            []wasm.ValueType{i32, i32, i64, i32},
-				Results:           []wasm.ValueType{f32, f64, f64, i32},
-				ParamNumInUint64:  4,
-				ResultNumInUint64: 4,
+				Params:  []wasm.ValueType{i32, i32, i64, i32},
+				Results: []wasm.ValueType{f32, f64, f64, i32},
 			},
 		},
 		{
@@ -157,9 +140,7 @@ func TestTypeUseParser_InlinesTypesWhenNotYetAdded(t *testing.T) {
 			input: "((result) (result) (result i64 i64) (result) (result f32) (result))",
 			// Abbreviations have min length zero, which implies no-op results are ok.
 			// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#abbreviations%E2%91%A2
-			expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32},
-				ResultNumInUint64: 3,
-			},
+			expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32}},
 		},
 		{
 			name: "multi-value - empty abbreviated params and results",
@@ -170,10 +151,8 @@ func TestTypeUseParser_InlinesTypesWhenNotYetAdded(t *testing.T) {
 			// Abbreviations have min length zero, which implies no-op results are ok.
 			// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#abbreviations%E2%91%A2
 			expectedInlinedType: &wasm.FunctionType{
-				Params:            []wasm.ValueType{i32, i32, i64, i32, i32},
-				Results:           []wasm.ValueType{f32, f64, f64, i32},
-				ParamNumInUint64:  5,
-				ResultNumInUint64: 4,
+				Params:  []wasm.ValueType{i32, i32, i64, i32, i32},
+				Results: []wasm.ValueType{f32, f64, f64, i32},
 			},
 			expectedParamNames: wasm.NameMap{&wasm.NameAssoc{Index: 4, Name: "x"}},
 		},
@@ -185,8 +164,6 @@ func TestTypeUseParser_InlinesTypesWhenNotYetAdded(t *testing.T) {
 		return tp, func(t *testing.T) {
 			// We should have inlined the type, and it is the first type use, which means the inlined index is zero
 			require.Zero(t, tp.inlinedTypeIndices[0].inlinedIdx)
-			exp := tp.inlinedTypes[0]
-			exp.CacheNumInUint64()
 			require.Equal(t, []*wasm.FunctionType{tc.expectedInlinedType}, tp.inlinedTypes)
 		}
 	})
@@ -225,9 +202,7 @@ func TestTypeUseParser_UnresolvedType(t *testing.T) {
 			if tc.expectedInlinedType == nil {
 				require.Zero(t, len(tp.inlinedTypes), "expected no inlinedTypes")
 			} else {
-				exp := tp.inlinedTypes[0]
-				exp.CacheNumInUint64()
-				require.Equal(t, tc.expectedInlinedType, exp)
+				require.Equal(t, tc.expectedInlinedType, tp.inlinedTypes[0])
 			}
 		}
 	})
@@ -372,9 +347,6 @@ func TestTypeUseParser_ReuseExistingInlinedType(t *testing.T) {
 		require.NoError(t, parseTypeUse(tp, tc.input, ignoreTypeUse))
 
 		return tp, func(t *testing.T) {
-			for _, it := range tp.inlinedTypes {
-				it.CacheNumInUint64()
-			}
 			// verify it wasn't duplicated
 			require.Equal(t, []*wasm.FunctionType{i32i64_v, tc.expectedInlinedType}, tp.inlinedTypes)
 			// last two inlined types are the same
@@ -420,9 +392,6 @@ func TestTypeUseParser_BeginResets(t *testing.T) {
 		require.NoError(t, parseTypeUse(tp, tc.input, ignoreTypeUse))
 
 		return tp, func(t *testing.T) {
-			for _, it := range tp.inlinedTypes {
-				it.CacheNumInUint64()
-			}
 			// this is the second inlined type
 			require.Equal(t, []*wasm.FunctionType{i32i64_i32, tc.expectedInlinedType}, tp.inlinedTypes)
 		}
diff --git a/internal/watzero/watzero_test.go b/internal/watzero/watzero_test.go
index 325270bf..965ae1d1 100644
--- a/internal/watzero/watzero_test.go
+++ b/internal/watzero/watzero_test.go
@@ -22,7 +22,7 @@ func newExample() *wasm.Module {
 	return &wasm.Module{
 		TypeSection: []*wasm.FunctionType{
 			{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 2, ResultNumInUint64: 1},
-			{ParamNumInUint64: 0, ResultNumInUint64: 0},
+			{},
 			{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 4, ResultNumInUint64: 1},
 			{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}, ParamNumInUint64: 1, ResultNumInUint64: 1},
 			{Params: []wasm.ValueType{f32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1},
diff --git a/internal/wazeroir/compiler_test.go b/internal/wazeroir/compiler_test.go
index aecd974f..450f2476 100644
--- a/internal/wazeroir/compiler_test.go
+++ b/internal/wazeroir/compiler_test.go
@@ -119,7 +119,9 @@ func TestCompile(t *testing.T) {
 			if enabledFeatures == 0 {
 				enabledFeatures = wasm.Features20220419
 			}
-
+			for _, tp := range tc.module.TypeSection {
+				tp.CacheNumInUint64()
+			}
 			res, err := CompileFunctions(ctx, enabledFeatures, tc.module)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, res[0])
@@ -535,6 +537,9 @@ func TestCompile_MultiValue(t *testing.T) {
 			if enabledFeatures == 0 {
 				enabledFeatures = wasm.Features20220419
 			}
+			for _, tp := range tc.module.TypeSection {
+				tp.CacheNumInUint64()
+			}
 			res, err := CompileFunctions(ctx, enabledFeatures, tc.module)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, res[0])
@@ -565,7 +570,9 @@ func TestCompile_NonTrappingFloatToIntConversion(t *testing.T) {
 		Types:        []*wasm.FunctionType{f32_i32},
 		TableTypes:   []wasm.RefType{},
 	}
-
+	for _, tp := range module.TypeSection {
+		tp.CacheNumInUint64()
+	}
 	res, err := CompileFunctions(ctx, wasm.FeatureNonTrappingFloatToIntConversion, module)
 	require.NoError(t, err)
 	require.Equal(t, expected, res[0])
@@ -590,7 +597,9 @@ func TestCompile_SignExtensionOps(t *testing.T) {
 		Types:        []*wasm.FunctionType{i32_i32},
 		TableTypes:   []wasm.RefType{},
 	}
-
+	for _, tp := range module.TypeSection {
+		tp.CacheNumInUint64()
+	}
 	res, err := CompileFunctions(ctx, wasm.FeatureSignExtensionOps, module)
 	require.NoError(t, err)
 	require.Equal(t, expected, res[0])
diff --git a/wasi_snapshot_preview1/clock_test.go b/wasi_snapshot_preview1/clock_test.go
index c5928995..bbb6ba42 100644
--- a/wasi_snapshot_preview1/clock_test.go
+++ b/wasi_snapshot_preview1/clock_test.go
@@ -191,16 +191,28 @@ func Test_clockTimeGet_Unsupported(t *testing.T) {
 			name:          "process cputime",
 			clockID:       2,
 			expectedErrno: ErrnoNotsup,
+			expectedLog: `
+==> wasi_snapshot_preview1.clock_time_get(id=2,precision=0,result.timestamp=1)
+<== ENOTSUP
+`,
 		},
 		{
 			name:          "thread cputime",
 			clockID:       3,
 			expectedErrno: ErrnoNotsup,
+			expectedLog: `
+==> wasi_snapshot_preview1.clock_time_get(id=3,precision=0,result.timestamp=1)
+<== ENOTSUP
+`,
 		},
 		{
 			name:          "undefined",
 			clockID:       100,
 			expectedErrno: ErrnoInval,
+			expectedLog: `
+==> wasi_snapshot_preview1.clock_time_get(id=100,precision=0,result.timestamp=1)
+<== EINVAL
+`,
 		},
 	}
 
@@ -211,9 +223,9 @@ func Test_clockTimeGet_Unsupported(t *testing.T) {
 			defer log.Reset()
 
 			resultTimestamp := uint32(1) // arbitrary offset
-			errno := clockTimeGet(testCtx, mod, tc.clockID, 0 /* TODO: precision */, resultTimestamp)
-			require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno))
-			require.Equal(t, tc.expectedLog, log.String())
+
+			requireErrno(t, tc.expectedErrno, mod, functionClockTimeGet, uint64(tc.clockID), uint64(0) /* TODO: precision */, uint64(resultTimestamp))
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
 		})
 	}
 }
@@ -232,11 +244,18 @@ func Test_clockTimeGet_Errors(t *testing.T) {
 		{
 			name:            "resultTimestamp out-of-memory",
 			resultTimestamp: memorySize,
+			expectedLog: `
+==> wasi_snapshot_preview1.clock_time_get(id=0,precision=0,result.timestamp=65536)
+<== EFAULT
+`,
 		},
-
 		{
 			name:            "resultTimestamp exceeds the maximum valid address by 1",
 			resultTimestamp: memorySize - 4 + 1, // 4 is the size of uint32, the type of the count of args
+			expectedLog: `
+==> wasi_snapshot_preview1.clock_time_get(id=0,precision=0,result.timestamp=65533)
+<== EFAULT
+`,
 		},
 	}
 
@@ -246,9 +265,8 @@ func Test_clockTimeGet_Errors(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			defer log.Reset()
 
-			errno := clockTimeGet(testCtx, mod, 0 /* TODO: id */, 0 /* TODO: precision */, tc.resultTimestamp)
-			require.Equal(t, ErrnoFault, errno, ErrnoName(errno))
-			require.Equal(t, tc.expectedLog, log.String())
+			requireErrno(t, ErrnoFault, mod, functionClockTimeGet, uint64(0) /* TODO: id */, uint64(0) /* TODO: precision */, uint64(tc.resultTimestamp))
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
 		})
 	}
 }
diff --git a/wasi_snapshot_preview1/environ_test.go b/wasi_snapshot_preview1/environ_test.go
index 708c93ef..1f7dd55d 100644
--- a/wasi_snapshot_preview1/environ_test.go
+++ b/wasi_snapshot_preview1/environ_test.go
@@ -55,23 +55,39 @@ func Test_environGet_Errors(t *testing.T) {
 			name:       "out-of-memory environPtr",
 			environ:    memorySize,
 			environBuf: validAddress,
+			expectedLog: `
+==> wasi_snapshot_preview1.environ_get(environ=65536,environ_buf=0)
+<== EFAULT
+`,
 		},
 		{
 			name:       "out-of-memory environBufPtr",
 			environ:    validAddress,
 			environBuf: memorySize,
+			expectedLog: `
+==> wasi_snapshot_preview1.environ_get(environ=0,environ_buf=65536)
+<== EFAULT
+`,
 		},
 		{
 			name: "environPtr exceeds the maximum valid address by 1",
 			// 4*envCount is the expected length for environPtr, 4 is the size of uint32
 			environ:    memorySize - 4*2 + 1,
 			environBuf: validAddress,
+			expectedLog: `
+==> wasi_snapshot_preview1.environ_get(environ=65529,environ_buf=0)
+<== EFAULT
+`,
 		},
 		{
 			name:    "environBufPtr exceeds the maximum valid address by 1",
 			environ: validAddress,
 			// "a=bc", "b=cd" size = size of "a=bc0b=cd0" = 10
 			environBuf: memorySize - 10 + 1,
+			expectedLog: `
+==> wasi_snapshot_preview1.environ_get(environ=0,environ_buf=65527)
+<== EFAULT
+`,
 		},
 	}
 
@@ -81,9 +97,8 @@ func Test_environGet_Errors(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			defer log.Reset()
 
-			errno := environGet(testCtx, mod, tc.environ, tc.environBuf)
-			require.Equal(t, ErrnoFault, errno, ErrnoName(errno))
-			require.Equal(t, tc.expectedLog, log.String())
+			requireErrno(t, ErrnoFault, mod, functionEnvironGet, uint64(tc.environ), uint64(tc.environBuf))
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
 		})
 	}
 }
@@ -134,21 +149,37 @@ func Test_environSizesGet_Errors(t *testing.T) {
 			name:           "out-of-memory environCountPtr",
 			environc:       memorySize,
 			environBufSize: validAddress,
+			expectedLog: `
+==> wasi_snapshot_preview1.environ_sizes_get(result.environc=65536,result.environBufSize=0)
+<== EFAULT
+`,
 		},
 		{
 			name:           "out-of-memory environBufSizePtr",
 			environc:       validAddress,
 			environBufSize: memorySize,
+			expectedLog: `
+==> wasi_snapshot_preview1.environ_sizes_get(result.environc=0,result.environBufSize=65536)
+<== EFAULT
+`,
 		},
 		{
 			name:           "environCountPtr exceeds the maximum valid address by 1",
 			environc:       memorySize - 4 + 1, // 4 is the size of uint32, the type of the count of environ
 			environBufSize: validAddress,
+			expectedLog: `
+==> wasi_snapshot_preview1.environ_sizes_get(result.environc=65533,result.environBufSize=0)
+<== EFAULT
+`,
 		},
 		{
 			name:           "environBufSizePtr exceeds the maximum valid size by 1",
 			environc:       validAddress,
 			environBufSize: memorySize - 4 + 1, // 4 is count of bytes to encode uint32le
+			expectedLog: `
+==> wasi_snapshot_preview1.environ_sizes_get(result.environc=0,result.environBufSize=65533)
+<== EFAULT
+`,
 		},
 	}
 
@@ -156,9 +187,10 @@ func Test_environSizesGet_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			errno := environSizesGet(testCtx, mod, tc.environc, tc.environBufSize)
-			require.Equal(t, ErrnoFault, errno, ErrnoName(errno))
-			require.Equal(t, tc.expectedLog, log.String())
+			defer log.Reset()
+
+			requireErrno(t, ErrnoFault, mod, functionEnvironSizesGet, uint64(tc.environc), uint64(tc.environBufSize))
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
 		})
 	}
 }
diff --git a/wasi_snapshot_preview1/fs_test.go b/wasi_snapshot_preview1/fs_test.go
index a4b7e01d..c928afe5 100644
--- a/wasi_snapshot_preview1/fs_test.go
+++ b/wasi_snapshot_preview1/fs_test.go
@@ -113,22 +113,38 @@ func Test_fdFdstatGet(t *testing.T) {
 			name: "file",
 			fd:   fileFd,
 			// TODO: expectedMem for a file
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=4,result.stat=0)
+<== ESUCCESS
+`,
 		},
 		{
 			name: "dir",
 			fd:   dirFd,
 			// TODO: expectedMem for a dir
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=5,result.stat=0)
+<== ESUCCESS
+`,
 		},
 		{
 			name:          "bad FD",
 			fd:            math.MaxUint32,
 			expectedErrno: ErrnoBadf,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=4294967295,result.stat=0)
+<== EBADF
+`,
 		},
 		{
 			name:       "resultStat exceeds the maximum valid address by 1",
 			fd:         dirFd,
 			resultStat: memorySize - 24 + 1,
 			// TODO: ErrnoFault
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=5,result.stat=65513)
+<== ESUCCESS
+`,
 		},
 	}
 
@@ -138,9 +154,8 @@ func Test_fdFdstatGet(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			defer log.Reset()
 
-			errno := fdFdstatGet(testCtx, mod, tc.fd, tc.resultStat)
-			require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno))
-			require.Equal(t, tc.expectedLog, log.String())
+			requireErrno(t, tc.expectedErrno, mod, functionFdFdstatGet, uint64(tc.fd), uint64(tc.resultStat))
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
 		})
 	}
 }
@@ -238,18 +253,27 @@ func Test_fdPrestatGet_Errors(t *testing.T) {
 		fd            uint32
 		resultPrestat uint32
 		expectedErrno Errno
+		expectedLog   string
 	}{
 		{
 			name:          "invalid FD",
 			fd:            42, // arbitrary invalid FD
 			resultPrestat: 0,  // valid offset
 			expectedErrno: ErrnoBadf,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_prestat_get(fd=42,result.prestat=0)
+<== EBADF
+`,
 		},
 		{
 			name:          "out-of-memory resultPrestat",
 			fd:            fd,
 			resultPrestat: memorySize,
 			expectedErrno: ErrnoFault,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_prestat_get(fd=4,result.prestat=65536)
+<== EFAULT
+`,
 		},
 		// TODO: non pre-opened file == api.ErrnoBadf
 	}
@@ -260,8 +284,8 @@ func Test_fdPrestatGet_Errors(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			defer log.Reset()
 
-			errno := fdPrestatGet(testCtx, mod, tc.fd, tc.resultPrestat)
-			require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno))
+			requireErrno(t, tc.expectedErrno, mod, functionFdPrestatGet, uint64(tc.fd), uint64(tc.resultPrestat))
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
 		})
 	}
 }
@@ -307,6 +331,7 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 		path          uint32
 		pathLen       uint32
 		expectedErrno Errno
+		expectedLog   string
 	}{
 		{
 			name:          "out-of-memory path",
@@ -314,6 +339,10 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 			path:          memorySize,
 			pathLen:       pathLen,
 			expectedErrno: ErrnoFault,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=4,path=65536,path_len=4)
+<== EFAULT
+`,
 		},
 		{
 			name:          "path exceeds the maximum valid address by 1",
@@ -321,6 +350,10 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 			path:          memorySize - pathLen + 1,
 			pathLen:       pathLen,
 			expectedErrno: ErrnoFault,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=4,path=65533,path_len=4)
+<== EFAULT
+`,
 		},
 		{
 			name:          "pathLen exceeds the length of the dir name",
@@ -328,6 +361,10 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 			path:          validAddress,
 			pathLen:       pathLen + 1,
 			expectedErrno: ErrnoNametoolong,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=4,path=0,path_len=5)
+<== ENAMETOOLONG
+`,
 		},
 		{
 			name:          "invalid fd",
@@ -335,6 +372,10 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 			path:          validAddress,
 			pathLen:       pathLen,
 			expectedErrno: ErrnoBadf,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=42,path=0,path_len=4)
+<== EBADF
+`,
 		},
 		// TODO: non pre-opened file == ErrnoBadf
 	}
@@ -345,8 +386,8 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			defer log.Reset()
 
-			errno := fdPrestatDirName(testCtx, mod, tc.fd, tc.path, tc.pathLen)
-			require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno))
+			requireErrno(t, tc.expectedErrno, mod, functionFdPrestatDirName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen))
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
 		})
 	}
 }
@@ -410,11 +451,16 @@ func Test_fdRead_Errors(t *testing.T) {
 		fd, iovs, iovsCount, resultSize uint32
 		memory                          []byte
 		expectedErrno                   Errno
+		expectedLog                     string
 	}{
 		{
 			name:          "invalid fd",
 			fd:            42, // arbitrary invalid fd
 			expectedErrno: ErrnoBadf,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_read(fd=42,iovs=65536,iovs_len=65536,result.size=65536)
+<== EBADF
+`,
 		},
 		{
 			name:          "out-of-memory reading iovs[0].offset",
@@ -422,6 +468,10 @@ func Test_fdRead_Errors(t *testing.T) {
 			iovs:          1,
 			memory:        []byte{'?'},
 			expectedErrno: ErrnoFault,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_read(fd=4,iovs=65536,iovs_len=65535,result.size=65535)
+<== EFAULT
+`,
 		},
 		{
 			name: "out-of-memory reading iovs[0].length",
@@ -432,6 +482,10 @@ func Test_fdRead_Errors(t *testing.T) {
 				9, 0, 0, 0, // = iovs[0].offset
 			},
 			expectedErrno: ErrnoFault,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_read(fd=4,iovs=65532,iovs_len=65532,result.size=65531)
+<== EFAULT
+`,
 		},
 		{
 			name: "iovs[0].offset is outside memory",
@@ -443,6 +497,10 @@ func Test_fdRead_Errors(t *testing.T) {
 				1, 0, 0, 0, // = iovs[0].length
 			},
 			expectedErrno: ErrnoFault,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_read(fd=4,iovs=65528,iovs_len=65528,result.size=65527)
+<== EFAULT
+`,
 		},
 		{
 			name: "length to read exceeds memory by 1",
@@ -455,6 +513,10 @@ func Test_fdRead_Errors(t *testing.T) {
 				'?',
 			},
 			expectedErrno: ErrnoFault,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_read(fd=4,iovs=65527,iovs_len=65527,result.size=65526)
+<== EFAULT
+`,
 		},
 		{
 			name: "resultSize offset is outside memory",
@@ -468,6 +530,10 @@ func Test_fdRead_Errors(t *testing.T) {
 				'?',
 			},
 			expectedErrno: ErrnoFault,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_read(fd=4,iovs=65527,iovs_len=65527,result.size=65536)
+<== EFAULT
+`,
 		},
 	}
 
@@ -481,8 +547,8 @@ func Test_fdRead_Errors(t *testing.T) {
 			memoryWriteOK := mod.Memory().Write(testCtx, offset, tc.memory)
 			require.True(t, memoryWriteOK)
 
-			errno := fdRead(testCtx, mod, tc.fd, tc.iovs+offset, tc.iovsCount+offset, tc.resultSize+offset)
-			require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno))
+			requireErrno(t, tc.expectedErrno, mod, functionFdRead, uint64(tc.fd), uint64(tc.iovs+offset), uint64(tc.iovsCount+offset), uint64(tc.resultSize+offset))
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
 		})
 	}
 }
diff --git a/wasi_snapshot_preview1/proc_test.go b/wasi_snapshot_preview1/proc_test.go
index 5462d619..9465dc5d 100644
--- a/wasi_snapshot_preview1/proc_test.go
+++ b/wasi_snapshot_preview1/proc_test.go
@@ -20,10 +20,16 @@ func Test_procExit(t *testing.T) {
 		{
 			name:     "success (exitcode 0)",
 			exitCode: 0,
+			expectedLog: `
+==> wasi_snapshot_preview1.proc_exit(rval=0)
+`,
 		},
 		{
 			name:     "arbitrary non-zero exitcode",
 			exitCode: 42,
+			expectedLog: `
+==> wasi_snapshot_preview1.proc_exit(rval=42)
+`,
 		},
 	}
 
@@ -34,9 +40,9 @@ func Test_procExit(t *testing.T) {
 			defer log.Reset()
 
 			// Since procExit panics, any opcodes afterwards cannot be reached.
-			err := require.CapturePanic(func() { procExit(testCtx, mod, tc.exitCode) })
+			_, err := mod.ExportedFunction(functionProcExit).Call(testCtx, uint64(tc.exitCode))
 			require.Equal(t, tc.exitCode, err.(*sys.ExitError).ExitCode())
-			require.Equal(t, tc.expectedLog, log.String())
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
 		})
 	}
 }
diff --git a/wasi_snapshot_preview1/random_test.go b/wasi_snapshot_preview1/random_test.go
index ff20967a..068cfa70 100644
--- a/wasi_snapshot_preview1/random_test.go
+++ b/wasi_snapshot_preview1/random_test.go
@@ -92,10 +92,18 @@ func Test_randomGet_SourceError(t *testing.T) {
 		{
 			name:       "error",
 			randSource: iotest.ErrReader(errors.New("RandSource error")),
+			expectedLog: `
+==> wasi_snapshot_preview1.random_get(buf=1,buf_len=5)
+<== EIO
+`,
 		},
 		{
 			name:       "incomplete",
 			randSource: bytes.NewReader([]byte{1, 2}),
+			expectedLog: `
+==> wasi_snapshot_preview1.random_get(buf=1,buf_len=5)
+<== EIO
+`,
 		},
 	}
 
@@ -106,9 +114,8 @@ func Test_randomGet_SourceError(t *testing.T) {
 				WithRandSource(tc.randSource))
 			defer r.Close(testCtx)
 
-			errno := randomGet(testCtx, mod, uint32(1), uint32(5)) // arbitrary offset and length
-			require.Equal(t, ErrnoIo, errno, ErrnoName(errno))
-			require.Equal(t, tc.expectedLog, log.String())
+			requireErrno(t, ErrnoIo, mod, functionRandomGet, uint64(1), uint64(5)) // arbitrary offset and length
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
 		})
 	}
 }
diff --git a/wasi_snapshot_preview1/wasi_bench_test.go b/wasi_snapshot_preview1/wasi_bench_test.go
index eb4b1859..0d5d7c76 100644
--- a/wasi_snapshot_preview1/wasi_bench_test.go
+++ b/wasi_snapshot_preview1/wasi_bench_test.go
@@ -53,8 +53,13 @@ func Benchmark_EnvironGet(b *testing.B) {
 
 	b.Run("environGet", func(b *testing.B) {
 		for i := 0; i < b.N; i++ {
-			if environGet(testCtx, mod, 0, 4) != ErrnoSuccess {
-				b.Fatal()
+			results, err := mod.ExportedFunction(functionEnvironGet).Call(testCtx, uint64(0), uint64(4))
+			if err != nil {
+				b.Fatal(err)
+			}
+			errno := Errno(results[0])
+			if errno != ErrnoSuccess {
+				b.Fatal(ErrnoName(errno))
 			}
 		}
 	})