Repository: tetratelabs/wazero
Pull request #125 (2022-01-10 08:38:55)

wat: refactors in preparation of module defined types
This refactors the code in preparation of parsing module types. There is
a lot of nuance around type signatures, so this restructures the code to
fit that idea and enhances the documentation in preparation.

codefromthecrypt: PS this takes an early punch on state tracking, so perf dipped a little to track the indexes even if we aren't yet using them.

Ex full conversion on trivial example before:
```
1233 ns/op	    1096 B/op	      35 allocs/op
```

now
```
1370 ns/op	    1232 B/op	      38 allocs/op
```

Once we can parse the whole structure, I will revisit perf as a whole as I think there are enough common patterns to improve it later

Patch:
diff --git a/wasm/wat/convert.go b/wasm/wat/convert.go
index 9e73f7b3..9e704f6c 100644
--- a/wasm/wat/convert.go
+++ b/wasm/wat/convert.go
@@ -24,24 +24,28 @@ func TextToBinary(source []byte) (result *wasm.Module, err error) {
 	// Next, we need to convert the types from the text format into the binary one. This is easy because the only
 	// difference is that the text format has type names and the binary format does not.
 	result = &wasm.Module{}
-	for _, t := range m.types {
+	typeToIndex := map[*typeFunc]uint32{}
+	for i, t := range m.typeFuncs {
 		var results []wasm.ValueType
 		if t.result != 0 {
 			results = []wasm.ValueType{t.result}
 		}
+		typeToIndex[t] = uint32(i)
 		result.TypeSection = append(result.TypeSection, &wasm.FunctionType{Params: t.params, Results: results})
 	}
 
 	// Now, handle any imported functions. Notably, we retain the same insertion order as defined in the text format in
 	// case a numeric index is used for the start function (or another reason such as the call instruction).
 	for _, f := range m.importFuncs {
-		result.ImportSection = append(result.ImportSection, &wasm.ImportSegment{
+		i := &wasm.ImportSegment{
 			Module: f.module, Name: f.name,
-			Desc: &wasm.ImportDesc{
-				Kind:          wasm.ImportKindFunction,
-				FuncTypeIndex: f.typeIndex,
-			},
-		})
+			Desc: &wasm.ImportDesc{Kind: wasm.ImportKindFunction},
+		}
+		if f.typeInlined != nil {
+			i.Desc.FuncTypeIndex = typeToIndex[f.typeInlined]
+		}
+
+		result.ImportSection = append(result.ImportSection, i)
 	}
 
 	// The start function is called on Module.Instantiate. To prevent runtime errors, resolve it into a valid function
diff --git a/wasm/wat/parser.go b/wasm/wat/parser.go
index 947fba6c..2ef303a8 100644
--- a/wasm/wat/parser.go
+++ b/wasm/wat/parser.go
@@ -52,7 +52,7 @@ type moduleParser struct {
 	// Ex. for fieldModuleImport, this would be PI if (import "Math" "PI" ...)
 	currentValue1 []byte
 
-	// currentImportIndex allows us to track the relative position of imports regardless of position in the source.
+	// currentImportIndex allows us to track the relative position of module.importFuncs regardless of position in the source.
 	currentImportIndex uint32
 
 	typeParser *typeParser
@@ -83,6 +83,12 @@ func parseModule(source []byte) (*module, error) {
 	if err != nil {
 		return nil, &FormatError{line, col, p.errorContext(), err}
 	}
+
+	// TODO: inlined types can contain "verification types" basically where typeIndex != nil if there is a type we need
+	// to verify it matches the corresponding index exactly.
+
+	// Add any types implicitly defined from type use. Ex. (module (import (func (param i32)...
+	p.module.typeFuncs = append(p.module.typeFuncs, p.typeParser.inlinedTypes...)
 	return p.module, nil
 }
 
@@ -121,7 +127,7 @@ func (p *moduleParser) beginField(tok tokenType, fieldName []byte, _, _ uint32)
 		// TODO: "types"
 		case "import":
 			p.currentField = fieldModuleImport
-			p.tokenParser = p.parseImport
+			p.tokenParser = p.parseImportModule
 		case "start":
 			if p.module.startFunction != nil {
 				return errors.New("redundant start")
@@ -166,6 +172,14 @@ func (p *moduleParser) endField() {
 	}
 }
 
+// parseModuleName is the first parser inside the module field. This records the module.name if present and sets the
+// next parser to parseModule. If the token isn't a tokenID, this calls parseModule.
+//
+// Ex. A module name is present `(module $math)`
+//                       records $math --^
+//
+// Ex. No module name `(module)`
+//   calls parseModule here --^
 func (p *moduleParser) parseModuleName(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $Math
 		name := string(tokenBytes)
@@ -191,35 +205,74 @@ func (p *moduleParser) parseModule(tok tokenType, tokenBytes []byte, _, _ uint32
 	return nil
 }
 
+// parseImportModule is the first parser inside the import field. This records the importFunc.module, then sets the next
+// parser to parseImportName. Since the imported module name is required, this errs on anything besides tokenString.
+//
+// Ex. Imported module name is present `(import "Math" "PI" (func (result f32)))`
+//                                records Math --^     ^
+//                      parseImportName resumes here --+
+//
+// Ex. Imported module name is absent `(import (func (result f32)))`
+//                                 errs here --^
+func (p *moduleParser) parseImportModule(tok tokenType, tokenBytes []byte, _, _ uint32) error {
+	switch tok {
+	case tokenString: // Ex. "" or "Math"
+		p.currentValue0 = tokenBytes[1 : len(tokenBytes)-1] // unquote
+		p.tokenParser = p.parseImportName
+		return nil
+	case tokenLParen, tokenRParen:
+		return errors.New("missing module and name")
+	default:
+		return unexpectedToken(tok, tokenBytes)
+	}
+}
+
+// parseImportName is the second parser inside the import field. This records the importFunc.name, then sets the next
+// parser to parseImport. Since the imported function name is required, this errs on anything besides tokenString.
+//
+// Ex. Imported function name is present `(import "Math" "PI" (func (result f32)))`
+//                                         starts here --^    ^
+//                                           records PI --^   |
+//                                 parseImport resumes here --+
+//
+// Ex. Imported function name is absent `(import "Math" (func (result f32)))`
+//                                          errs here --+
+func (p *moduleParser) parseImportName(tok tokenType, tokenBytes []byte, _, _ uint32) error {
+	switch tok {
+	case tokenString: // Ex. "" or "PI"
+		p.currentValue1 = tokenBytes[1 : len(tokenBytes)-1] // unquote
+		p.tokenParser = p.parseImport
+		return nil
+	case tokenLParen, tokenRParen:
+		return errors.New("missing name")
+	default:
+		return unexpectedToken(tok, tokenBytes)
+	}
+}
+
+// parseImport is the last parser inside the import field. This records the description field, ex. (func) or errs if
+// missing. When complete, this sets the next parser to parseModule.
+//
+// Ex. Description is present `(module (import "Math" "PI" (func (result f32))))`
+//                                           starts here --^                   ^
+//                                                  parseModule resumes here --+
+//
+// Ex. Description is missing `(import "Math" "PI")`
+//                                    errs here --^
 func (p *moduleParser) parseImport(tok tokenType, tokenBytes []byte, _, _ uint32) error {
 	switch tok {
-	case tokenString:
-		// Note: tokenString is minimum length two on account of quotes. Ex. "" or "foo"
-		name := tokenBytes[1 : len(tokenBytes)-1] // unquote
-		if p.currentValue0 == nil {               // Ex. (module ""
-			p.currentValue0 = name
-		} else if p.currentValue1 == nil { // Ex. (module "" ""
-			p.currentValue1 = name
-		} else { // Ex. (module "" "" ""
-			return fmt.Errorf("redundant name: %s", name)
-		}
+	case tokenString: // Ex. (import "Math" "PI" "PI"
+		return fmt.Errorf("redundant name: %s", tokenBytes[1:len(tokenBytes)-1]) // unquote
 	case tokenLParen: // start fields, ex. (func
 		// Err if there's a second description. Ex. (import "" "" (func) (func))
 		if uint32(len(p.module.importFuncs)) > p.currentImportIndex {
 			return unexpectedToken(tok, tokenBytes)
 		}
-		// Err if there are not enough names when we reach a description. Ex. (import func())
-		if err := p.validateImportModuleAndName(); err != nil {
-			return err
-		}
 		p.tokenParser = p.beginField
 		return nil
 	case tokenRParen: // end of this import
 		// Err if we never reached a description...
 		if uint32(len(p.module.importFuncs)) == p.currentImportIndex {
-			if err := p.validateImportModuleAndName(); err != nil {
-				return err // Ex. missing (func) and names: (import) or (import "Math")
-			}
 			return errors.New("missing description field") // Ex. missing (func): (import "Math" "Pi")
 		}
 
@@ -235,16 +288,15 @@ func (p *moduleParser) parseImport(tok tokenType, tokenBytes []byte, _, _ uint32
 	return nil
 }
 
-// validateImportModuleAndName ensures we read both the module and name in the text format, even if they were empty.
-func (p *moduleParser) validateImportModuleAndName() error {
-	if p.currentValue0 == nil && p.currentValue1 == nil {
-		return errors.New("missing module and name")
-	} else if p.currentValue1 == nil {
-		return errors.New("missing name")
-	}
-	return nil
-}
-
+// parseImportFuncName is the first parser inside an imported function field. This records the typeFunc.name if present
+// and sets the next parser to parseImportFunc. If the token isn't a tokenID, this calls parseImportFunc.
+//
+// Ex. A function name is present `(import "Math" "PI" (func $math.pi (result f32))`
+//                                   records $math.pi here --^
+//                                     parseImportFunc resumes here --^
+//
+// Ex. No function name `(import "Math" "PI" (func (result f32))`
+//                    calls parseImportFunc here --^
 func (p *moduleParser) parseImportFuncName(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $main
 		name := string(tokenBytes)
@@ -256,35 +308,39 @@ func (p *moduleParser) parseImportFuncName(tok tokenType, tokenBytes []byte, lin
 	return p.parseImportFunc(tok, tokenBytes, line, col)
 }
 
+// parseImportFunc is the second parser inside the imported function field. This passes control to the typeParser until
+// any signature is read, then sets the next parser to parseImportFuncAfterType.
+//
+// Ex. `(import "Math" "PI" (func $math.pi (result f32)))`
+//                           starts here --^           ^
+//             parseImportFuncAfterType resumes here --+
+//
+// Ex. If there is no signature `(import "" "main" (func))`
+//               calls parseImportFuncAfterType here ---^
 func (p *moduleParser) parseImportFunc(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	switch tok {
 	case tokenID: // Ex. (func $main $main)
-		return fmt.Errorf("redundant name: %s", string(tokenBytes))
+		return fmt.Errorf("redundant name: %s", tokenBytes)
 	case tokenLParen: // start fields, ex. (param or (result
-		p.typeParser.beginParsingParamsOrResult(p.parseImportFuncAfterType)
+		p.typeParser.beginParsingTypeUse(p.parseImportFuncAfterType)
 		return nil
 	}
 	return p.parseImportFuncAfterType(tok, tokenBytes, line, col)
 }
 
-// parseImportFuncAfterType handles tokens after any type signature was read. The only valid token is tokenRParen
-// because no fields are defined except those in the function type.
-func (p *moduleParser) parseImportFuncAfterType(tok tokenType, tokenBytes []byte, line, col uint32) error {
+// parseImportFuncAfterType is the last parser inside the imported function field. This records the importFunc.typeIndex
+// and/or importFunc.typeInlined and sets the next parser to parseImport.
+func (p *moduleParser) parseImportFuncAfterType(tok tokenType, tokenBytes []byte, _, _ uint32) error {
 	if tok == tokenRParen {
-		p.endImportFunc()
+		fn := p.module.importFuncs[len(p.module.importFuncs)-1]
+		fn.typeIndex, fn.typeInlined = p.typeParser.getTypeUse()
+		p.currentField = fieldModuleImport
+		p.tokenParser = p.parseImport
 		return nil
 	}
 	return unexpectedToken(tok, tokenBytes)
 }
 
-// endImportFunc adds the module.types index for this function regardless of whether one was defined inline or not
-func (p *moduleParser) endImportFunc() {
-	fn := p.module.importFuncs[len(p.module.importFuncs)-1]
-	fn.typeIndex = p.typeParser.findOrAddType(p.module)
-	p.currentField = fieldModuleImport
-	p.tokenParser = p.parseImport
-}
-
 func (p *moduleParser) parseStart(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	switch tok {
 	case tokenUN, tokenID: // Ex. $main or 2
@@ -320,12 +376,12 @@ func (p *moduleParser) errorContext() string {
 		return ""
 	case fieldModule:
 		return "module"
-	case fieldModuleStart:
-		return "module.start"
 	case fieldModuleImport:
 		return fmt.Sprintf("module.import[%d]", p.currentImportIndex)
 	case fieldModuleImportFunc: // TODO: table, memory or global
 		return fmt.Sprintf("module.import[%d].func%s", p.currentImportIndex, p.typeParser.errorContext())
+	case fieldModuleStart:
+		return "module.start"
 	default: // currentField is an enum, we expect to have handled all cases above. panic if we didn't
 		panic(fmt.Errorf("BUG: unhandled parsing state: %v", p.currentField))
 	}
diff --git a/wasm/wat/type_parser.go b/wasm/wat/type_parser.go
index e6054ff0..72602a7b 100644
--- a/wasm/wat/type_parser.go
+++ b/wasm/wat/type_parser.go
@@ -12,26 +12,29 @@ import (
 type typeParsingState byte
 
 const (
-	parsingParamOrResult typeParsingState = iota
+	parsingTypeUse typeParsingState = iota
+	parsingParamOrResult
 	parsingParam
 	parsingResult
 	parsingComplete
 )
 
-// typeParser parses an inlined type from a field such as "type" or "func". Unlike normal parsers, this is not used for
-// an entire field (enclosed by parens). Rather, this only handles "param" and "result" inner fields in the correct
-// order.
+// typeParser parses an inlined type from a field such as "type" or "func" and dispatches to onTypeEnd.
 //
-// Ex. `(module (import (func $main (param i32))))`
-//   This parses after the name     ^---------^
+// Ex. `(import "Math" "PI" (func $math.pi (result f32))`
+//                           starts here --^           ^
+//                            onTypeEnd resumes here --+
 //
-// Ex. `(module (type (param i32) (result i64)))`
-//   This parses here ^----------------------^
+// Ex. `(type (func (param i32) (result i64)))`
+//    starts here --^                       ^
+//                 onTypeEnd resumes here --+
 //
-// Ex. `(module (import (func)))`
-//   This parses nothing (because there is no type)
+// Ex. `(module (import "" "" (func $main)))`
+//                calls onTypeEnd here --^
 //
-// typeParser is reusable. The caller resets when reaching the appropriate tokenRParen via beginParsingParamsOrResult.
+// Note: Unlike normal parsers, this is not used for an entire field (enclosed by parens). Rather, this only handles
+// "param" and "result" inner fields in the correct order.
+// Note: typeParser is reusable. The caller resets when reaching the appropriate tokenRParen via beginParsingTypeUse.
 type typeParser struct {
 	// m is used as a function pointer to moduleParser.tokenParser. This updates based on state changes.
 	m *moduleParser
@@ -45,6 +48,20 @@ type typeParser struct {
 	// state is initially parsingParamOrResult and updated alongside tokenParser
 	state typeParsingState
 
+	// inlinedTypes are a collection of types currently known to be inlined.
+	// Ex. `(param i32)` in `(import (func (param i32)))`
+	//
+	// Note: The Text Format requires imports first, not types first. This resolution has to be done later. The impact
+	// of this is types here can be removed if later discovered to be an explicitly defined type.
+	//
+	// For example, here the `(param i32)` type is initially considered inlined until the module type with the same
+	// signature is read later: (module (import (func (param i32))) (type (func (param i32))))`
+	inlinedTypes []*typeFunc
+
+	// currentTypeIndex is set when there's a "type" field in a type use
+	// See https://www.w3.org/TR/wasm-core-1/#type-uses%E2%91%A0
+	currentTypeIndex []byte
+
 	// currentParams allow us to accumulate typeFunc.params across multiple fields, as well support abbreviated
 	// anonymous parameters. ex. both (param i32) (param i32) and (param i32 i32) formats.
 	// See https://www.w3.org/TR/wasm-core-1/#abbreviations%E2%91%A2
@@ -61,19 +78,19 @@ type typeParser struct {
 	currentResult wasm.ValueType
 }
 
-// beginParsingParamsOrResult sets moduleParser.tokenParser to beginParamOrResult after resetting internal fields.
+// beginParsingTypeUse sets moduleParser.tokenParser to parsingTypeUse after resetting internal fields.
 // This should only be called when reaching the first tokenLParen after the optional field name (tokenID).
 //
 // Ex. Given the source `(module (import (func $main (param i32))))`
-//         Set this result to the next parser here --^
-//             This result restores control to onTypeEnd here --^
+//                  beginParamOrResult starts here --^          ^
+//                                     onTypeEnd resumes here --+
 //
 // The onTypeEnd parameter is invoked once any "param" and "result" fields have been consumed.
 //
 // NOTE: An empty function is valid and will not reach a tokenLParen! Ex. `(module (import (func)))`
-func (p *typeParser) beginParsingParamsOrResult(onTypeEnd tokenParser) {
+func (p *typeParser) beginParsingTypeUse(onTypeEnd tokenParser) {
 	p.onTypeEnd = onTypeEnd
-	p.state = parsingParamOrResult
+	p.state = parsingTypeUse
 	p.m.tokenParser = p.beginParamOrResult
 	p.currentParams = nil
 	p.currentParamIndex = 0
@@ -172,21 +189,34 @@ func (p *typeParser) errorContext() string {
 	return ""
 }
 
-// findOrAddType returns the index in module.types of a possibly empty type matching any current params or result.
-func (p *typeParser) findOrAddType(m *module) (typeIndex uint32) {
-	// search for an existing signature that matches the current type
-	for i, t := range m.types {
-		if bytes.Equal(p.currentParams, t.params) && p.currentResult == t.result {
-			return uint32(i)
+var typeFuncEmpty = &typeFunc{}
+
+// getTypeUse finalizes any current params or result and returns the current typeIndex and/or type
+func (p *typeParser) getTypeUse() (typeIndex []byte, sig *typeFunc) {
+	typeIndex = p.currentTypeIndex
+
+	// Search for an existing signature that matches the current type in the pending inlined types
+	for _, t := range p.inlinedTypes {
+		if p.currentEqualsType(t) {
+			sig = t
+			return
 		}
 	}
 
-	// if we didn't find a match, we need to insert a new type and use it
-	typeIndex = uint32(len(m.types))
-	m.types = append(m.types, &typeFunc{p.currentParams, p.currentResult})
+	sig = &typeFunc{"", p.currentParams, p.currentResult}
+
+	// If we didn't find a match, we need to insert an inlined type to use it. We don't do this when there is a type
+	// index because in this case, the signature is only used for verification on an existing type.
+	if typeIndex == nil {
+		p.inlinedTypes = append(p.inlinedTypes, sig)
+	}
 	return
 }
 
+func (p *typeParser) currentEqualsType(t *typeFunc) bool {
+	return bytes.Equal(p.currentParams, t.params) && p.currentResult == t.result
+}
+
 func parseValueType(tokenBytes []byte) (wasm.ValueType, error) {
 	t := string(tokenBytes)
 	switch t {
diff --git a/wasm/wat/types.go b/wasm/wat/types.go
index 436f96d8..f3369d07 100644
--- a/wasm/wat/types.go
+++ b/wasm/wat/types.go
@@ -26,7 +26,7 @@ type module struct {
 	// In the future, other types may be introduced to support features such as module linking.
 	//
 	// See https://www.w3.org/TR/wasm-core-1/#types%E2%91%A0%E2%91%A0
-	types []*typeFunc
+	typeFuncs []*typeFunc
 
 	// importFuncs are imports describing functions added in insertion order. Ex (import... (func...))
 	importFuncs []*importFunc
@@ -59,6 +59,12 @@ type startFunction struct {
 //
 // See https://www.w3.org/TR/wasm-core-1/#text-functype
 type typeFunc struct {
+	// name starts with '$'. For example, "$v_v", and only set when explicitly defined in module.typeFuncs
+	//
+	// name is only used for debugging. At runtime, types are called based on raw numeric index. The type index space
+	// begins those explicitly defined in module.typeFuncs, followed by any inlined ones.
+	name string
+
 	// params are the possibly empty sequence of value types accepted by a function with this signature.
 	//
 	// Note: In WebAssembly 1.0 (MVP), there can be at most one result.
@@ -81,8 +87,15 @@ type importFunc struct {
 	// importIndex is the zero-based index in module.imports. This is needed because imports are not always functions.
 	importIndex uint32
 
-	// typeIndex is the zero-based index in module.types representing this function signature.
-	typeIndex uint32
+	// typeIndex is a importFunc.name, such as "$main", or its equivalent numeric index in module.importFuncs, such as
+	// "2". If typeInlined is also present, the signature in module.importFuncs must exist and match that type.
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#text-typeuse
+	typeIndex []byte
+
+	// typeInlined is set if there are any "param" or "result" fields.
+	// See https://www.w3.org/TR/wasm-core-1/#abbreviations%E2%91%A6
+	typeInlined *typeFunc
 
 	// module is the possibly empty module name to import. Ex. "" or "Math"
 	//
@@ -104,9 +117,6 @@ type importFunc struct {
 	// `wat2wasm --debug-names` will do this.
 	// See https://www.w3.org/TR/wasm-core-1/#binary-namesec
 	funcName string
-
-	// TODO: typeuse https://www.w3.org/TR/wasm-core-1/#text-typeuse
-	// TODO: inlined type https://www.w3.org/TR/wasm-core-1/#abbreviations%E2%91%A6
 }
 
 // FormatError allows control over the format of errors parsing the WebAssembly Text Format.


Test patch:
diff --git a/wasm/wat/names_test.go b/wasm/wat/names_test.go
index 1914d8f5..cd4cca15 100644
--- a/wasm/wat/names_test.go
+++ b/wasm/wat/names_test.go
@@ -33,14 +33,13 @@ func TestEncodeNameSection(t *testing.T) {
 func TestEncodeNameSection_OnlyFuncName(t *testing.T) {
 	func0, func1 := "runtime.args_sizes_get", "runtime.fd_write" // note: no leading '$'
 	i32 := wasm.ValueTypeI32
+	type1 := &typeFunc{params: []wasm.ValueType{i32, i32}, result: i32}
+	type2 := &typeFunc{params: []wasm.ValueType{i32, i32, i32, i32}, result: i32}
 	m := &module{
-		types: []*typeFunc{
-			{params: []wasm.ValueType{i32, i32}, result: i32},
-			{params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
-		},
+		typeFuncs: []*typeFunc{type1, type2},
 		importFuncs: []*importFunc{
-			{importIndex: 0, typeIndex: 0, module: "wasi_snapshot_preview1", name: "args_sizes_get", funcName: "$" + func0},
-			{importIndex: 1, typeIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$" + func1},
+			{importIndex: 0, typeIndex: []byte{'0'}, module: "wasi_snapshot_preview1", name: "args_sizes_get", funcName: "$" + func0},
+			{importIndex: 1, typeIndex: []byte{'1'}, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$" + func1},
 		},
 	}
 
diff --git a/wasm/wat/parser_test.go b/wasm/wat/parser_test.go
index 53259c27..91339b4d 100644
--- a/wasm/wat/parser_test.go
+++ b/wasm/wat/parser_test.go
@@ -9,8 +9,15 @@ import (
 )
 
 func TestParseModule(t *testing.T) {
-	typeFuncEmpty := &typeFunc{}
 	i32, i64 := wasm.ValueTypeI32, wasm.ValueTypeI64
+	paramI32 := &typeFunc{params: []wasm.ValueType{i32}}
+	paramI32I32ResultI32 := &typeFunc{params: []wasm.ValueType{i32, i32}, result: i32}
+	paramI32I32I32I32ResultI32 := &typeFunc{params: []wasm.ValueType{i32, i32, i32, i32}, result: i32}
+	paramI32I32I32I32I32I64I32I32ResultI32 := &typeFunc{
+		params: []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32},
+		result: i32,
+	}
+	resultI32 := &typeFunc{result: i32}
 
 	tests := []struct {
 		name     string
@@ -31,18 +38,18 @@ func TestParseModule(t *testing.T) {
 			name:  "import func empty",
 			input: "(module (import \"foo\" \"bar\" (func)))", // ok empty sig
 			expected: &module{
-				types:       []*typeFunc{typeFuncEmpty},
-				importFuncs: []*importFunc{{module: "foo", name: "bar"}},
+				typeFuncs:   []*typeFunc{typeFuncEmpty},
+				importFuncs: []*importFunc{{module: "foo", name: "bar", typeInlined: typeFuncEmpty}},
 			},
 		},
 		{
 			name:  "import func empty twice",
 			input: "(module (import \"foo\" \"bar\" (func)) (import \"baz\" \"qux\" (func)))", // ok empty sig
 			expected: &module{
-				types: []*typeFunc{typeFuncEmpty},
+				typeFuncs: []*typeFunc{typeFuncEmpty},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "foo", name: "bar"},
-					{importIndex: 1, module: "baz", name: "qux"},
+					{importIndex: 0, module: "foo", name: "bar", typeInlined: typeFuncEmpty},
+					{importIndex: 1, module: "baz", name: "qux", typeInlined: typeFuncEmpty},
 				},
 			},
 		},
@@ -52,9 +59,9 @@ func TestParseModule(t *testing.T) {
 	(import "wasi_snapshot_preview1" "fd_write" (func $runtime.fd_write (param i32) (param i32) (param i32) (param i32) (result i32)))
 )`,
 			expected: &module{
-				types: []*typeFunc{{params: []wasm.ValueType{i32, i32, i32, i32}, result: i32}},
+				typeFuncs: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write"},
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write", typeInlined: paramI32I32I32I32ResultI32},
 				},
 			},
 		},
@@ -64,9 +71,9 @@ func TestParseModule(t *testing.T) {
 	(import "wasi_snapshot_preview1" "fd_write" (func $runtime.fd_write (param i32 i32 i32 i32) (result i32)))
 )`,
 			expected: &module{
-				types: []*typeFunc{{params: []wasm.ValueType{i32, i32, i32, i32}, result: i32}},
+				typeFuncs: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write"},
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write", typeInlined: paramI32I32I32I32ResultI32},
 				},
 			},
 		},
@@ -78,9 +85,9 @@ func TestParseModule(t *testing.T) {
 	(import "wasi_snapshot_preview1" "fd_write" (func $runtime.fd_write (param i32) (param i32 i32) (param i32) (result i32)))
 )`,
 			expected: &module{
-				types: []*typeFunc{{params: []wasm.ValueType{i32, i32, i32, i32}, result: i32}},
+				typeFuncs: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write"},
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write", typeInlined: paramI32I32I32I32ResultI32},
 				},
 			},
 		},
@@ -90,9 +97,9 @@ func TestParseModule(t *testing.T) {
 	(import "wasi_snapshot_preview1" "proc_exit" (func $runtime.proc_exit (param i32)))
 )`,
 			expected: &module{
-				types: []*typeFunc{{params: []wasm.ValueType{i32}}},
+				typeFuncs: []*typeFunc{paramI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "proc_exit", funcName: "$runtime.proc_exit"},
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "proc_exit", funcName: "$runtime.proc_exit", typeInlined: paramI32},
 				},
 			},
 		},
@@ -100,8 +107,8 @@ func TestParseModule(t *testing.T) {
 			name:  "import func inlined type no param",
 			input: `(module (import "" "" (func (result i32))))`,
 			expected: &module{
-				types:       []*typeFunc{{result: i32}},
-				importFuncs: []*importFunc{{}},
+				typeFuncs:   []*typeFunc{resultI32},
+				importFuncs: []*importFunc{{typeInlined: resultI32}},
 			},
 		},
 		{
@@ -110,12 +117,9 @@ func TestParseModule(t *testing.T) {
 	(import "wasi_snapshot_preview1" "path_open" (func $runtime.path_open (param i32) (param i32) (param i32) (param i32) (param i32) (param i64) (param i64) (param i32) (param i32) (result i32)))
 )`,
 			expected: &module{
-				types: []*typeFunc{{
-					params: []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32},
-					result: i32,
-				}},
+				typeFuncs: []*typeFunc{paramI32I32I32I32I32I64I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "$runtime.path_open"},
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "$runtime.path_open", typeInlined: paramI32I32I32I32I32I64I32I32ResultI32},
 				},
 			},
 		},
@@ -125,12 +129,9 @@ func TestParseModule(t *testing.T) {
 	(import "wasi_snapshot_preview1" "path_open" (func $runtime.path_open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)))
 )`,
 			expected: &module{
-				types: []*typeFunc{{
-					params: []wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32},
-					result: i32,
-				}},
+				typeFuncs: []*typeFunc{paramI32I32I32I32I32I64I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "$runtime.path_open"},
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "$runtime.path_open", typeInlined: paramI32I32I32I32I32I64I32I32ResultI32},
 				},
 			},
 		},
@@ -141,13 +142,10 @@ func TestParseModule(t *testing.T) {
 	(import "wasi_snapshot_preview1" "fd_write" (func $runtime.fd_write (param i32 i32 i32 i32) (result i32)))
 )`,
 			expected: &module{
-				types: []*typeFunc{
-					{params: []wasm.ValueType{i32, i32}, result: i32},
-					{params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
-				},
+				typeFuncs: []*typeFunc{paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, typeIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get"},
-					{importIndex: 1, typeIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write"},
+					{importIndex: 0, typeInlined: paramI32I32ResultI32, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get"},
+					{importIndex: 1, typeInlined: paramI32I32I32I32ResultI32, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write"},
 				},
 			},
 		},
@@ -158,12 +156,10 @@ func TestParseModule(t *testing.T) {
 	(import "wasi_snapshot_preview1" "arg_sizes_get" (func $runtime.arg_sizes_get (param i32 i32) (result i32)))
 )`,
 			expected: &module{
-				types: []*typeFunc{
-					{params: []wasm.ValueType{i32, i32}, result: i32},
-				},
+				typeFuncs: []*typeFunc{paramI32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, typeIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get"},
-					{importIndex: 1, typeIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get"},
+					{importIndex: 0, typeInlined: paramI32I32ResultI32, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get"},
+					{importIndex: 1, typeInlined: paramI32I32ResultI32, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get"},
 				},
 			},
 		},
@@ -179,8 +175,8 @@ func TestParseModule(t *testing.T) {
 	(start $hello)
 )`,
 			expected: &module{
-				types:         []*typeFunc{typeFuncEmpty},
-				importFuncs:   []*importFunc{{name: "hello", funcName: "$hello"}},
+				typeFuncs:     []*typeFunc{typeFuncEmpty},
+				importFuncs:   []*importFunc{{name: "hello", funcName: "$hello", typeInlined: typeFuncEmpty}},
 				startFunction: &startFunction{"$hello", 3, 9},
 			},
 		},
@@ -191,8 +187,8 @@ func TestParseModule(t *testing.T) {
 	(start 0)
 )`,
 			expected: &module{
-				types:         []*typeFunc{typeFuncEmpty},
-				importFuncs:   []*importFunc{{name: "hello", importIndex: 0}},
+				typeFuncs:     []*typeFunc{typeFuncEmpty},
+				importFuncs:   []*importFunc{{name: "hello", importIndex: 0, typeInlined: typeFuncEmpty}},
 				startFunction: &startFunction{"0", 3, 9},
 			},
 		},