Repository: tetratelabs/wazero
Pull request #231 (2022-02-13 14:20:47)

Adds Module.SectionSize and constrains text parsing rules
This consolidates more code around SectionID, notably consistently
getting and using section ID length. This also enforces rules in text
parsing such as up to one memory, and imported functions can't be
defined after regular ones.

codefromthecrypt: ps there's another need not addressed here, which is count by second index. Ex.
  * custom sections by subsection ID
  * imports/exports by kind

I didn't address this, yet but I think at some point we can refactor things so that you can for example, tell the function index namespace count w/o a for loop.
mathetake: `SectionItemCounts` would be exact? :D
codefromthecrypt: Thanks for the notes on `CustomSection` @mathetake  it is messy and seems per-spec it is allowed to repeat by name also. I'm inclined to drop support for it and just skip anything not the name section (in another PR)

Patch:
diff --git a/wasm/binary/decoder.go b/wasm/binary/decoder.go
index bc7aee5a..0ae26ec4 100644
--- a/wasm/binary/decoder.go
+++ b/wasm/binary/decoder.go
@@ -27,6 +27,8 @@ func DecodeModule(binary []byte) (*wasm.Module, error) {
 
 	m := &wasm.Module{}
 	for {
+		// TODO: except custom sections, all others are required to be in order, but we aren't checking yet.
+		// See https://www.w3.org/TR/wasm-core-1/#modules%E2%91%A0%E2%93%AA
 		sectionID := make([]byte, 1)
 		if _, err := io.ReadFull(r, sectionID); err == io.EOF {
 			break
@@ -36,7 +38,7 @@ func DecodeModule(binary []byte) (*wasm.Module, error) {
 
 		sectionSize, _, err := leb128.DecodeUint32(r)
 		if err != nil {
-			return nil, fmt.Errorf("get size of section for id=%d: %v", sectionID[0], err)
+			return nil, fmt.Errorf("get size of section %s: %v", wasm.SectionIDName(sectionID[0]), err)
 		}
 
 		sectionContentStart := r.Len()
@@ -104,11 +106,11 @@ func DecodeModule(binary []byte) (*wasm.Module, error) {
 		}
 
 		if err != nil {
-			return nil, fmt.Errorf("section ID %d: %v", sectionID[0], err)
+			return nil, fmt.Errorf("section %s: %v", wasm.SectionIDName(sectionID[0]), err)
 		}
 	}
 
-	functionCount, codeCount := len(m.FunctionSection), len(m.CodeSection)
+	functionCount, codeCount := m.SectionSize(wasm.SectionIDFunction), m.SectionSize(wasm.SectionIDCode)
 	if functionCount != codeCount {
 		return nil, fmt.Errorf("function and code section have inconsistent lengths: %d != %d", functionCount, codeCount)
 	}
diff --git a/wasm/binary/encoder.go b/wasm/binary/encoder.go
index 7f755ba3..54417a35 100644
--- a/wasm/binary/encoder.go
+++ b/wasm/binary/encoder.go
@@ -11,47 +11,50 @@ var sizePrefixedName = []byte{4, 'n', 'a', 'm', 'e'}
 // See https://www.w3.org/TR/wasm-core-1/#binary-format%E2%91%A0
 func EncodeModule(m *wasm.Module) (bytes []byte) {
 	bytes = append(magic, version...)
-	for name, data := range m.CustomSections {
-		bytes = append(bytes, encodeCustomSection(name, data)...)
-	}
-	if len(m.TypeSection) > 0 {
+	if m.SectionSize(wasm.SectionIDType) > 0 {
 		bytes = append(bytes, encodeTypeSection(m.TypeSection)...)
 	}
-	if len(m.ImportSection) > 0 {
+	if m.SectionSize(wasm.SectionIDImport) > 0 {
 		bytes = append(bytes, encodeImportSection(m.ImportSection)...)
 	}
-	if len(m.FunctionSection) > 0 {
+	if m.SectionSize(wasm.SectionIDFunction) > 0 {
 		bytes = append(bytes, encodeFunctionSection(m.FunctionSection)...)
 	}
-	if len(m.TableSection) > 0 {
+	if m.SectionSize(wasm.SectionIDTable) > 0 {
 		panic("TODO: TableSection")
 	}
-	if len(m.MemorySection) > 0 {
+	if m.SectionSize(wasm.SectionIDMemory) > 0 {
 		bytes = append(bytes, encodeMemorySection(m.MemorySection)...)
 	}
-	if len(m.GlobalSection) > 0 {
+	if m.SectionSize(wasm.SectionIDGlobal) > 0 {
 		panic("TODO: GlobalSection")
 	}
-	if len(m.ExportSection) > 0 {
+	if m.SectionSize(wasm.SectionIDExport) > 0 {
 		bytes = append(bytes, encodeExportSection(m.ExportSection)...)
 	}
-	if m.StartSection != nil {
+	if m.SectionSize(wasm.SectionIDStart) > 0 {
 		bytes = append(bytes, encodeStartSection(*m.StartSection)...)
 	}
-	if len(m.ElementSection) > 0 {
+	if m.SectionSize(wasm.SectionIDElement) > 0 {
 		panic("TODO: ElementSection")
 	}
-	if len(m.CodeSection) > 0 {
+	if m.SectionSize(wasm.SectionIDCode) > 0 {
 		bytes = append(bytes, encodeCodeSection(m.CodeSection)...)
 	}
-	if len(m.DataSection) > 0 {
+	if m.SectionSize(wasm.SectionIDData) > 0 {
 		panic("TODO: DataSection")
 	}
-	// >> The name section should appear only once in a module, and only after the data section.
-	// See https://www.w3.org/TR/wasm-core-1/#binary-namesec
-	if m.NameSection != nil {
-		nameSection := append(sizePrefixedName, encodeNameSectionData(m.NameSection)...)
-		bytes = append(bytes, encodeSection(wasm.SectionIDCustom, nameSection)...)
+	if m.SectionSize(wasm.SectionIDCustom) > 0 {
+		for name, data := range m.CustomSections {
+			bytes = append(bytes, encodeCustomSection(name, data)...)
+		}
+
+		// >> The name section should appear only once in a module, and only after the data section.
+		// See https://www.w3.org/TR/wasm-core-1/#binary-namesec
+		if m.NameSection != nil {
+			nameSection := append(sizePrefixedName, encodeNameSectionData(m.NameSection)...)
+			bytes = append(bytes, encodeSection(wasm.SectionIDCustom, nameSection)...)
+		}
 	}
 	return
 }
diff --git a/wasm/binary/section.go b/wasm/binary/section.go
index ddc00f3f..e117a6b6 100644
--- a/wasm/binary/section.go
+++ b/wasm/binary/section.go
@@ -165,7 +165,7 @@ func decodeExportSection(r *bytes.Reader) (map[string]*wasm.Export, error) {
 	return exportSection, nil
 }
 
-func decodeStartSection(r *bytes.Reader) (*uint32, error) {
+func decodeStartSection(r *bytes.Reader) (*wasm.Index, error) {
 	vs, _, err := leb128.DecodeUint32(r)
 	if err != nil {
 		return nil, fmt.Errorf("get size of vector: %w", err)
@@ -260,10 +260,10 @@ func encodeImportSection(imports []*wasm.Import) []byte {
 // WebAssembly 1.0 (MVP) Binary Format.
 //
 // See https://www.w3.org/TR/wasm-core-1/#function-section%E2%91%A0
-func encodeFunctionSection(functions []wasm.Index) []byte {
-	contents := leb128.EncodeUint32(uint32(len(functions)))
-	for _, typeIndex := range functions {
-		contents = append(contents, leb128.EncodeUint32(typeIndex)...)
+func encodeFunctionSection(typeIndices []wasm.Index) []byte {
+	contents := leb128.EncodeUint32(uint32(len(typeIndices)))
+	for _, index := range typeIndices {
+		contents = append(contents, leb128.EncodeUint32(index)...)
 	}
 	return encodeSection(wasm.SectionIDFunction, contents)
 }
@@ -307,6 +307,6 @@ func encodeExportSection(exports map[string]*wasm.Export) []byte {
 // encodeStartSection encodes a SectionIDStart for the given function index in WebAssembly 1.0 (MVP) Binary Format.
 //
 // See https://www.w3.org/TR/wasm-core-1/#start-section%E2%91%A0
-func encodeStartSection(funcidx uint32) []byte {
+func encodeStartSection(funcidx wasm.Index) []byte {
 	return encodeSection(wasm.SectionIDStart, leb128.EncodeUint32(funcidx))
 }
diff --git a/wasm/module.go b/wasm/module.go
index 6009dfe3..33e5953c 100644
--- a/wasm/module.go
+++ b/wasm/module.go
@@ -1,6 +1,8 @@
 package wasm
 
-// DecodeModule parses the configured source into a wasm.Module. This function returns when the source is exhausted or
+import "fmt"
+
+// DecodeModule parses the configured source into a Module. This function returns when the source is exhausted or
 // an error occurs. The result can be initialized for use via Store.Instantiate.
 //
 // Here's a description of the return values:
@@ -115,7 +117,8 @@ type Module struct {
 	// this module at TableSection[0].
 	//
 	// Note: Version 1.0 (MVP) of the WebAssembly spec allows at most one table definition per module, so the length of
-	// the TableSection can be zero or one.
+	// the TableSection can be zero or one, and can only be one if there is no ImportKindTable.
+	//
 	// Note: In the Binary Format, this is SectionIDTable.
 	//
 	// See https://www.w3.org/TR/wasm-core-1/#table-section%E2%91%A0
@@ -128,7 +131,8 @@ type Module struct {
 	// this module at TableSection[0].
 	//
 	// Note: Version 1.0 (MVP) of the WebAssembly spec allows at most one memory definition per module, so the length of
-	// the MemorySection can be zero or one.
+	// the MemorySection can be zero or one, and can only be one if there is no ImportKindMemory.
+	//
 	// Note: In the Binary Format, this is SectionIDMemory.
 	//
 	// See https://www.w3.org/TR/wasm-core-1/#memory-section%E2%91%A0
@@ -154,6 +158,7 @@ type Module struct {
 	//
 	// Note: The index here is not the position in the FunctionSection, rather in the function index namespace, which
 	// begins with imported functions.
+	//
 	// Note: In the Binary Format, this is SectionIDStart.
 	//
 	// See https://www.w3.org/TR/wasm-core-1/#start-section%E2%91%A0
@@ -226,7 +231,7 @@ const (
 	ValueTypeF64 ValueType = 0x7c
 )
 
-// ValuTypeName returns the type name of the given ValueType as a string.
+// ValueTypeName returns the type name of the given ValueType as a string.
 // These type names match the names used in the WebAssembly text format.
 // Note that ValueTypeName returns "unknown", if an undefined ValueType value is passed.
 func ValueTypeName(t ValueType) string {
@@ -461,3 +466,47 @@ func (m *Module) allDeclarations() (functions []Index, globals []*GlobalType, me
 	tables = append(tables, m.TableSection...)
 	return
 }
+
+// SectionSize returns the count of items for a given section ID
+//
+// For example, given...
+// * SectionIDType this returns the count of FunctionType
+// * SectionIDCustom this returns the count of unique section names
+// * SectionIDExport this returns the count of unique export names
+func (m *Module) SectionSize(sectionID SectionID) uint32 {
+	switch sectionID {
+	case SectionIDCustom:
+		count := uint32(len(m.CustomSections))
+		if m.NameSection != nil {
+			return count + 1
+		}
+		return count
+	case SectionIDType:
+		return uint32(len(m.TypeSection))
+	case SectionIDImport:
+		return uint32(len(m.ImportSection))
+	case SectionIDFunction:
+		return uint32(len(m.FunctionSection))
+	case SectionIDTable:
+		return uint32(len(m.TableSection))
+	case SectionIDMemory:
+		return uint32(len(m.MemorySection))
+	case SectionIDGlobal:
+		return uint32(len(m.GlobalSection))
+	case SectionIDExport:
+		return uint32(len(m.ExportSection))
+	case SectionIDStart:
+		if m.StartSection != nil {
+			return 1
+		}
+		return 0
+	case SectionIDElement:
+		return uint32(len(m.ElementSection))
+	case SectionIDCode:
+		return uint32(len(m.CodeSection))
+	case SectionIDData:
+		return uint32(len(m.DataSection))
+	default:
+		panic(fmt.Errorf("BUG: unknown section: %d", sectionID))
+	}
+}
diff --git a/wasm/store.go b/wasm/store.go
index d23a7cde..0732111f 100644
--- a/wasm/store.go
+++ b/wasm/store.go
@@ -288,17 +288,21 @@ func (s *Store) Instantiate(module *Module, name string) error {
 
 	for i, f := range instance.Functions {
 		if err := s.engine.Compile(f); err != nil {
-			return fmt.Errorf("compilation failed at index %d/%d: %v", i, len(module.FunctionSection)-1, err)
+			idx := module.SectionSize(SectionIDFunction) - 1
+			return fmt.Errorf("compilation failed at index %d/%d: %v", i, idx, err)
 		}
 	}
 
 	// Check the start function is valid.
-	if startIndex := module.StartSection; startIndex != nil {
-		index := *startIndex
-		if int(index) >= len(instance.Functions) {
-			return fmt.Errorf("invalid start function index: %d", index)
+	// TODO: this should be verified during decode so that errors have the correct source positions
+	var startFunction *FunctionInstance
+	if module.StartSection != nil {
+		startIndex := *module.StartSection
+		if startIndex >= uint32(len(instance.Functions)) {
+			return fmt.Errorf("invalid start function index: %d", startIndex)
 		}
-		ft := instance.Functions[index].FunctionType
+		startFunction = instance.Functions[startIndex]
+		ft := startFunction.FunctionType
 		if len(ft.Type.Params) != 0 || len(ft.Type.Results) != 0 {
 			return fmt.Errorf("start function must have the empty function type")
 		}
@@ -308,10 +312,8 @@ func (s *Store) Instantiate(module *Module, name string) error {
 	rollbackFuncs = nil
 
 	// Execute the start function.
-	ctx := context.Background()
-	if startIndex := module.StartSection; startIndex != nil {
-		f := instance.Functions[*startIndex]
-		if _, err := s.engine.Call(ctx, f); err != nil {
+	if startFunction != nil {
+		if _, err = s.engine.Call(context.Background(), startFunction); err != nil {
 			return fmt.Errorf("calling start function failed: %v", err)
 		}
 	}
@@ -596,9 +598,9 @@ func (s *Store) buildFunctionInstances(module *Module, target *ModuleInstance) (
 	n, nLen := 0, len(functionNames)
 
 	for codeIndex, typeIndex := range module.FunctionSection {
-		if typeIndex >= uint32(len(module.TypeSection)) {
+		if typeIndex >= module.SectionSize(SectionIDType) {
 			return rollbackFuncs, fmt.Errorf("function type index out of range")
-		} else if codeIndex >= len(module.CodeSection) {
+		} else if uint32(codeIndex) >= module.SectionSize(SectionIDCode) {
 			return rollbackFuncs, fmt.Errorf("code index out of range")
 		}
 
@@ -629,7 +631,8 @@ func (s *Store) buildFunctionInstances(module *Module, target *ModuleInstance) (
 		}
 
 		if err := validateFunctionInstance(f, funcs, globals, mems, tables, module.TypeSection, maximumValuesOnStack); err != nil {
-			return rollbackFuncs, fmt.Errorf("invalid function '%s' (%d/%d): %v", f.Name, codeIndex, len(module.FunctionSection)-1, err)
+			idx := module.SectionSize(SectionIDFunction) - 1
+			return rollbackFuncs, fmt.Errorf("invalid function '%s' (%d/%d): %v", f.Name, codeIndex, idx, err)
 		}
 
 		err = s.addFunctionInstance(f)
@@ -680,8 +683,8 @@ func (s *Store) buildMemoryInstances(module *Module, target *ModuleInstance) (ro
 		}
 
 		size := uint64(offset) + uint64(len(d.Init))
-		maxPage := uint32(MemoryMaxPages)
-		if int(d.MemoryIndex) < len(module.MemorySection) && module.MemorySection[d.MemoryIndex].Max != nil {
+		maxPage := MemoryMaxPages
+		if d.MemoryIndex < module.SectionSize(SectionIDMemory) && module.MemorySection[d.MemoryIndex].Max != nil {
 			maxPage = *module.MemorySection[d.MemoryIndex].Max
 		}
 		if size > memoryPagesToBytesNum(maxPage) {
@@ -734,7 +737,7 @@ func (s *Store) buildTableInstances(module *Module, target *ModuleInstance) (rol
 		size := offset + len(elem.Init)
 
 		max := uint32(math.MaxUint32)
-		if int(elem.TableIndex) < len(module.TableSection) && module.TableSection[elem.TableIndex].Limit.Max != nil {
+		if elem.TableIndex < module.SectionSize(SectionIDTable) && module.TableSection[elem.TableIndex].Limit.Max != nil {
 			max = *module.TableSection[elem.TableIndex].Limit.Max
 		}
 
@@ -772,7 +775,7 @@ func (s *Store) buildTableInstances(module *Module, target *ModuleInstance) (rol
 }
 
 func (s *Store) buildExportInstances(module *Module, target *ModuleInstance) (rollbackFuncs []func(), err error) {
-	target.Exports = make(map[string]*ExportInstance, len(module.ExportSection))
+	target.Exports = make(map[string]*ExportInstance, module.SectionSize(SectionIDExport))
 	for name, exp := range module.ExportSection {
 		index := exp.Index
 		var ei *ExportInstance
diff --git a/wasm/text/decoder.go b/wasm/text/decoder.go
index 521f0fff..7ee66b5e 100644
--- a/wasm/text/decoder.go
+++ b/wasm/text/decoder.go
@@ -18,17 +18,32 @@ const (
 	positionParam
 	positionResult
 	positionModule
-	positionModuleType
-	positionModuleImport
-	positionModuleImportFunc
-	positionModuleFunc
-	positionModuleMemory
-	positionModuleExport
-	positionModuleExportFunc
-	positionModuleExportMemory
-	positionModuleStart
+	positionImport
+	positionImportFunc
+	positionMemory
+	positionExport
+	positionExportFunc
+	positionExportMemory
+	positionStart
 )
 
+type callbackPosition byte
+
+const (
+	// callbackPositionUnhandledToken is set on a token besides a paren.
+	callbackPositionUnhandledToken callbackPosition = iota
+	// callbackPositionUnhandledField is at the field name (tokenKeyword) which isn't "type", "param" or "result"
+	callbackPositionUnhandledField
+	// callbackPositionEndField is at the end (tokenRParen) of the field enclosing the type use.
+	callbackPositionEndField
+)
+
+// moduleParser parses a single wasm.Module from WebAssembly 1.0 (MVP) Text format.
+//
+// Note: The indexNamespace of wasm.SectionIDMemory and wasm.SectionIDTable allow up-to-one item. For example, you
+// cannot define both one import and one module-defined memory, rather one or the other (or none). Even if these rules
+// are also enforced in module instantiation, they are also enforced here, to allow relevant source line/col in errors.
+// See https://www.w3.org/TR/wasm-core-1/#modules%E2%91%A3
 type moduleParser struct {
 	// source is the entire WebAssembly text format source code being parsed.
 	source []byte
@@ -153,27 +168,38 @@ func (p *moduleParser) beginModuleField(tok tokenType, tokenBytes []byte, _, _ u
 	if tok == tokenKeyword {
 		switch string(tokenBytes) {
 		case "type":
-			p.pos = positionModuleType
+			p.pos = positionType
 			return p.typeParser.begin, nil
 		case "import":
-			p.pos = positionModuleImport
+			p.pos = positionImport
 			return p.parseImportModule, nil
 		case "func":
-			p.pos = positionModuleFunc
-			p.funcParser.currentIdx = wasm.Index(len(p.module.FunctionSection))
+			p.pos = positionFunc
+			p.funcParser.currentIdx = p.module.SectionSize(wasm.SectionIDFunction)
 			return p.funcParser.begin, nil
+		case "table":
+			return nil, errors.New("TODO: table")
 		case "memory":
-			p.pos = positionModuleMemory
+			if p.memoryNamespace.count > 0 {
+				return nil, moreThanOneInvalidInSection(wasm.SectionIDMemory)
+			}
+			p.pos = positionMemory
 			return p.memoryParser.begin, nil
+		case "global":
+			return nil, errors.New("TODO: global")
 		case "export":
-			p.pos = positionModuleExport
+			p.pos = positionExport
 			return p.parseExportName, nil
 		case "start":
-			if p.module.StartSection != nil {
-				return nil, errors.New("redundant start")
+			if p.module.SectionSize(wasm.SectionIDStart) > 0 {
+				return nil, moreThanOneInvalid("start")
 			}
-			p.pos = positionModuleStart
+			p.pos = positionStart
 			return p.parseStart, nil
+		case "elem":
+			return nil, errors.New("TODO: elem")
+		case "data":
+			return nil, errors.New("TODO: data")
 		default:
 			return nil, unexpectedFieldName(tokenBytes)
 		}
@@ -243,9 +269,9 @@ func (p *moduleParser) parseImportModule(tok tokenType, tokenBytes []byte, _, _
 // parseImportName returns parseImport after recording the import name, or errs if it couldn't be read.
 //
 // Ex. Import name is present `(import "Math" "PI" (func (result f32)))`
-//                                         starts here --^    ^
-//                                           records PI --^   |
-//                                 parseImport resumes here --+
+//                              starts here --^^   ^
+//                                records PI --+   |
+//                      parseImport resumes here --+
 //
 // Ex. Imported function name is absent `(import "Math" (func (result f32)))`
 //                                          errs here --+
@@ -284,7 +310,10 @@ func (p *moduleParser) beginImportDesc(tok tokenType, tokenBytes []byte, _, _ ui
 
 	switch string(tokenBytes) {
 	case "func":
-		p.pos = positionModuleImportFunc
+		if p.module.SectionSize(wasm.SectionIDFunction) > 0 {
+			return nil, importAfterModuleDefined(wasm.SectionIDFunction)
+		}
+		p.pos = positionImportFunc
 		return p.parseImportFuncID, nil
 	case "table", "memory", "global":
 		return nil, fmt.Errorf("TODO: %s", tokenBytes)
@@ -303,17 +332,18 @@ func (p *moduleParser) beginImportDesc(tok tokenType, tokenBytes []byte, _, _ ui
 //                  calls parseImportFunc here --^
 func (p *moduleParser) parseImportFuncID(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
 	if tok == tokenID { // Ex. $main
-		id, err := p.funcNamespace.setID(tokenBytes)
-		if err != nil {
+		if name, err := p.funcNamespace.setID(tokenBytes); err != nil {
 			return nil, err
+		} else {
+			p.addFunctionName(name)
 		}
-		p.addFunctionName(id)
 		return p.parseImportFunc, nil
 	}
 	return p.parseImportFunc(tok, tokenBytes, line, col)
 }
 
-// addFunctionName appends the current imported or module-defined function name to the wasm.NameSection
+// addFunctionName appends the current imported or module-defined function name to the wasm.NameSection iff it is not
+// empty.
 func (p *moduleParser) addFunctionName(name string) {
 	if name == "" {
 		return // there's no value in an empty name
@@ -331,7 +361,7 @@ func (p *moduleParser) addFunctionName(name string) {
 // Ex. If there is no signature `(import "" "main" (func))`
 //                     calls onImportFunc here ---^
 func (p *moduleParser) parseImportFunc(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
-	idx := wasm.Index(len(p.module.ImportSection))
+	idx := p.module.SectionSize(wasm.SectionIDImport)
 	if tok == tokenID { // Ex. (func $main $main)
 		return nil, fmt.Errorf("redundant ID %s", tokenBytes)
 	}
@@ -342,7 +372,7 @@ func (p *moduleParser) parseImportFunc(tok tokenType, tokenBytes []byte, line, c
 // onImportFunc records the type index and local names of the current imported function, and increments
 // funcNamespace as it is shared across imported and module-defined functions. Finally, this returns parseImportEnd to
 // the current import into the ImportSection.
-func (p *moduleParser) onImportFunc(typeIdx wasm.Index, paramNames wasm.NameMap, pos onTypeUsePosition, tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) {
+func (p *moduleParser) onImportFunc(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) {
 	i := p.currentModuleField.(*wasm.Import)
 	i.Kind = wasm.ImportKindFunc
 	i.DescFunc = typeIdx
@@ -351,12 +381,12 @@ func (p *moduleParser) onImportFunc(typeIdx wasm.Index, paramNames wasm.NameMap,
 	p.funcNamespace.count++
 
 	switch pos {
-	case onTypeUseUnhandledToken:
+	case callbackPositionUnhandledToken:
 		return nil, unexpectedToken(tok, tokenBytes)
-	case onTypeUseUnhandledField:
+	case callbackPositionUnhandledField:
 		return nil, unexpectedFieldName(tokenBytes)
-	case onTypeUseEndField:
-		p.pos = positionModuleImport
+	case callbackPositionEndField:
+		p.pos = positionImport
 		return p.parseImportEnd, nil
 	}
 	return p.parseImportFuncEnd, nil
@@ -368,7 +398,7 @@ func (p *moduleParser) parseImportFuncEnd(tok tokenType, tokenBytes []byte, _, _
 		return nil, unexpectedToken(tok, tokenBytes)
 	}
 
-	p.pos = positionModuleImport
+	p.pos = positionImport
 	return p.parseImportEnd, nil
 }
 
@@ -431,7 +461,7 @@ func (p *moduleParser) parseExportName(tok tokenType, tokenBytes []byte, _, _ ui
 	case tokenString: // Ex. "" or "PI"
 		name := string(tokenBytes[1 : len(tokenBytes)-1]) // strip quotes
 		if _, ok := p.module.ExportSection[name]; ok {
-			return nil, fmt.Errorf("duplicate name %q", name)
+			return nil, fmt.Errorf("%q already exported", name)
 		}
 		p.currentModuleField = &wasm.Export{Name: name}
 		return p.parseExport, nil
@@ -464,10 +494,10 @@ func (p *moduleParser) beginExportDesc(tok tokenType, tokenBytes []byte, _, _ ui
 
 	switch string(tokenBytes) {
 	case "func":
-		p.pos = positionModuleExportFunc
+		p.pos = positionExportFunc
 		return p.parseExportDesc, nil
 	case "memory":
-		p.pos = positionModuleExportMemory
+		p.pos = positionExportMemory
 		return p.parseExportDesc, nil
 	case "table", "global":
 		return nil, fmt.Errorf("TODO: %s", tokenBytes)
@@ -481,16 +511,16 @@ func (p *moduleParser) parseExportDesc(tok tokenType, tokenBytes []byte, line, c
 	var namespace *indexNamespace
 	e := p.currentModuleField.(*wasm.Export)
 	switch p.pos {
-	case positionModuleExportFunc:
+	case positionExportFunc:
 		e.Kind = wasm.ExportKindFunc
 		namespace = p.funcNamespace
-	case positionModuleExportMemory:
+	case positionExportMemory:
 		e.Kind = wasm.ExportKindMemory
 		namespace = p.memoryNamespace
 	default:
 		panic(fmt.Errorf("BUG: unhandled parsing state on parseExportDesc: %v", p.pos))
 	}
-	eIdx := wasm.Index(len(p.module.ExportSection))
+	eIdx := p.module.SectionSize(wasm.SectionIDExport)
 	typeIdx, resolved, err := namespace.parseIndex(wasm.SectionIDExport, eIdx, 0, tok, tokenBytes, line, col)
 	if err != nil {
 		return nil, err
@@ -514,7 +544,7 @@ func (p *moduleParser) parseExportDescEnd(tok tokenType, tokenBytes []byte, _, _
 	case tokenUN, tokenID:
 		return nil, errors.New("redundant index")
 	case tokenRParen:
-		p.pos = positionModuleExport
+		p.pos = positionExport
 		return p.parseExportEnd, nil
 	default:
 		return nil, unexpectedToken(tok, tokenBytes)
@@ -612,10 +642,10 @@ func (p *moduleParser) resolveFunctionIndices(module *wasm.Module) error {
 // This errs if any type index is unresolved, out of range or mismatches an inlined type use signature.
 func (p *moduleParser) resolveTypeUses(module *wasm.Module) error {
 	inlinedToRealIdx := p.addInlinedTypes()
-	return p.resolveInlined(module, inlinedToRealIdx)
+	return p.resolveInlinedTypes(module, inlinedToRealIdx)
 }
 
-func (p *moduleParser) resolveInlined(module *wasm.Module, inlinedToRealIdx map[wasm.Index]wasm.Index) error {
+func (p *moduleParser) resolveInlinedTypes(module *wasm.Module, inlinedToRealIdx map[wasm.Index]wasm.Index) error {
 	// Now look for all the uses of the inlined types and apply the mapping above
 	for _, i := range p.typeUseParser.inlinedTypeIndices {
 		switch i.section {
@@ -699,28 +729,28 @@ func (p *moduleParser) errorContext() string {
 		return ""
 	case positionModule:
 		return "module"
-	case positionModuleType:
-		idx := wasm.Index(len(p.module.TypeSection))
+	case positionType:
+		idx := p.module.SectionSize(wasm.SectionIDType)
 		return fmt.Sprintf("module.type[%d]%s", idx, p.typeParser.errorContext())
-	case positionModuleImport, positionModuleImportFunc: // TODO: table, memory or global
-		idx := wasm.Index(len(p.module.ImportSection))
-		if p.pos == positionModuleImport {
+	case positionImport, positionImportFunc: // TODO: table, memory or global
+		idx := p.module.SectionSize(wasm.SectionIDImport)
+		if p.pos == positionImport {
 			return fmt.Sprintf("module.import[%d]", idx)
 		}
 		return fmt.Sprintf("module.import[%d].func%s", idx, p.typeUseParser.errorContext())
-	case positionModuleFunc:
-		idx := wasm.Index(len(p.module.FunctionSection))
+	case positionFunc:
+		idx := p.module.SectionSize(wasm.SectionIDFunction)
 		return fmt.Sprintf("module.func[%d]%s", idx, p.typeUseParser.errorContext())
-	case positionModuleMemory:
-		idx := wasm.Index(len(p.module.MemorySection))
+	case positionMemory:
+		idx := p.module.SectionSize(wasm.SectionIDMemory)
 		return fmt.Sprintf("module.memory[%d]", idx)
-	case positionModuleExport, positionModuleExportFunc: // TODO: table, memory or global
-		idx := wasm.Index(len(p.module.ExportSection))
-		if p.pos == positionModuleExport {
+	case positionExport, positionExportFunc: // TODO: table, memory or global
+		idx := p.module.SectionSize(wasm.SectionIDExport)
+		if p.pos == positionExport {
 			return fmt.Sprintf("module.export[%d]", idx)
 		}
 		return fmt.Sprintf("module.export[%d].func", idx)
-	case positionModuleStart:
+	case positionStart:
 		return "module.start"
 	default: // parserPosition is an enum, we expect to have handled all cases above. panic if we didn't
 		panic(fmt.Errorf("BUG: unhandled parsing state on errorContext: %v", p.pos))
diff --git a/wasm/text/errors.go b/wasm/text/errors.go
index a5c34df0..987fc807 100644
--- a/wasm/text/errors.go
+++ b/wasm/text/errors.go
@@ -45,6 +45,36 @@ func unexpectedToken(tok tokenType, tokenBytes []byte) error {
 	}
 }
 
+// importAfterModuleDefined is the failure for the condition "all imports must occur before any regular definition",
+// which applies regardless of abbreviation.
+//
+// Ex. Both of these fail because an import can only be declared when SectionIDFunction is empty.
+// `(func) (import "" "" (func))` which is the same as  `(func) (import "" "" (func))`
+//
+// See https://www.w3.org/TR/wasm-core-1/#modules%E2%91%A0%E2%91%A2
+func importAfterModuleDefined(section wasm.SectionID) error {
+	return fmt.Errorf("import after module-defined %s", wasm.SectionIDName(section))
+}
+
+// moreThanOneInvalidInSection allows enforcement of section size limits.
+//
+// Ex. All of these fail because they result in two memories.
+// * `(module (memory 1) (memory 1))`
+// * `(module (memory 1) (import "" "" (memory 1)))`
+//   * Note the latter expands to the same as the former: `(import "" "" (memory 1))`
+// * `(module (import "" "" (memory 1)) (import "" "" (memory 1)))`
+//
+// See https://www.w3.org/TR/wasm-core-1/#tables%E2%91%A0
+// See https://www.w3.org/TR/wasm-core-1/#memories%E2%91%A0
+func moreThanOneInvalidInSection(section wasm.SectionID) error {
+	return moreThanOneInvalid(wasm.SectionIDName(section))
+}
+
+// moreThanOneInvalid is the failure when a declaration that can result in more than one item.
+func moreThanOneInvalid(context string) error {
+	return fmt.Errorf("at most one %s allowed", context)
+}
+
 func unhandledSection(section wasm.SectionID) error {
 	return fmt.Errorf("BUG: unhandled %s", wasm.SectionIDName(section))
 }
diff --git a/wasm/text/func_parser.go b/wasm/text/func_parser.go
index efac06ed..9e907e5c 100644
--- a/wasm/text/func_parser.go
+++ b/wasm/text/func_parser.go
@@ -17,8 +17,8 @@ type onFunc func(typeIdx wasm.Index, code *wasm.Code, name string, localNames wa
 // funcParser parses any instructions and dispatches to onFunc.
 //
 // Ex.  `(module (func (nop)))`
-//       starts here --^    ^
-//      calls onFunc here --+
+//        begin here --^    ^
+//  end calls onFunc here --+
 //
 // Note: funcParser is reusable. The caller resets via begin.
 type funcParser struct {
@@ -73,11 +73,11 @@ func (p *funcParser) begin(tok tokenType, tokenBytes []byte, line, col uint32) (
 // are read. Finally, this finishes via endFunc.
 //
 // Ex. `(module (func $math.pi (result f32))`
-//               starts here --^           ^
+//                begin here --^           ^
 //                  endFunc resumes here --+
 //
 // Ex.    `(module (func $math.pi (result f32) (local i32) )`
-//                  starts here --^            ^           ^
+//                   begin here --^            ^           ^
 //      funcParser.afterTypeUse resumes here --+           |
 //                                  endFunc resumes here --+
 //
@@ -98,11 +98,11 @@ func (p *funcParser) parseFunc(tok tokenType, tokenBytes []byte, line, col uint3
 // Ex. Given the source `(module (func nop))`
 //          afterTypeUse starts here --^  ^
 //                    calls onFunc here --+
-func (p *funcParser) afterTypeUse(typeIdx wasm.Index, paramNames wasm.NameMap, pos onTypeUsePosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
+func (p *funcParser) afterTypeUse(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
 	switch pos {
-	case onTypeUseEndField:
+	case callbackPositionEndField:
 		return p.onFunc(typeIdx, codeEnd, p.currentName, paramNames)
-	case onTypeUseUnhandledField:
+	case callbackPositionUnhandledField:
 		return sExpressionsUnsupported(tok, tokenBytes, line, col)
 	}
 
@@ -120,7 +120,7 @@ func sExpressionsUnsupported(tok tokenType, tokenBytes []byte, _, _ uint32) (tok
 	case "param":
 		return nil, errors.New("param after result")
 	case "result":
-		return nil, errors.New("duplicate result")
+		return nil, moreThanOneInvalid("result")
 	case "local":
 		return nil, errors.New("TODO: local")
 	}
diff --git a/wasm/text/index_namespace.go b/wasm/text/index_namespace.go
index 3bc406ff..f6db68d6 100644
--- a/wasm/text/index_namespace.go
+++ b/wasm/text/index_namespace.go
@@ -38,12 +38,22 @@ type indexNamespace struct {
 // setID ensures the given tokenID is unique within this context and raises an error if not. The resulting mapping is
 // stripped of the leading '$' to match other tools, as described in stripDollar.
 func (i *indexNamespace) setID(idToken []byte) (string, error) {
-	id := string(stripDollar(idToken))
-	if _, ok := i.idToIdx[id]; ok {
-		return id, fmt.Errorf("duplicate ID %s", idToken)
+	name, err := i.requireNoID(idToken)
+	if err != nil {
+		return name, err
+	}
+	i.idToIdx[name] = i.count
+	return name, nil
+}
+
+// hasID checks to see if this tokenID is unique within this context and returns an error. The result string is
+// stripped of the leading '$' to match other tools, as described in stripDollar.
+func (i *indexNamespace) requireNoID(idToken []byte) (string, error) {
+	name := string(stripDollar(idToken))
+	if _, ok := i.idToIdx[name]; ok {
+		return name, fmt.Errorf("duplicate ID %s", idToken)
 	}
-	i.idToIdx[id] = i.count
-	return id, nil
+	return name, nil
 }
 
 // parseIndex is a tokenParser called in a field that can only contain a symbolic identifier or raw numeric index.
diff --git a/wasm/text/typeuse_parser.go b/wasm/text/typeuse_parser.go
index 8c2a6838..87eb4dfa 100644
--- a/wasm/text/typeuse_parser.go
+++ b/wasm/text/typeuse_parser.go
@@ -12,32 +12,21 @@ func newTypeUseParser(module *wasm.Module, typeNamespace *indexNamespace) *typeU
 	return &typeUseParser{module: module, typeNamespace: typeNamespace}
 }
 
-type onTypeUsePosition byte
-
-const (
-	// onTypeUseUnhandledToken is set on a token besides a paren.
-	onTypeUseUnhandledToken onTypeUsePosition = iota
-	// onTypeUseUnhandledField is at the field name (tokenKeyword) which isn't "type", "param" or "result"
-	onTypeUseUnhandledField
-	// onTypeUseEndField is at the end (tokenRParen) of the field enclosing the type use.
-	onTypeUseEndField
-)
-
 // onTypeUse is invoked when the grammar "(param)* (result)?" completes.
 //
-// * typeIdx if unresolved, this is replaced in typeUseParser.resolveTypeUses
+// * typeIdx if unresolved, this is replaced in moduleParser.resolveTypeUses
 // * paramNames is nil unless IDs existed on at least one "param" field.
 // * pos is the context used to determine which tokenParser to return
 //
 // Note: this is called when neither a "param" nor a "result" field are parsed, or on any field following a "param"
 // that is not a "result": pos clarifies this.
-type onTypeUse func(typeIdx wasm.Index, paramNames wasm.NameMap, pos onTypeUsePosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error)
+type onTypeUse func(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error)
 
-// typeUseParser parses an inlined type from a field such "func" and calls to onTypeUse or onUnknownField.
+// typeUseParser parses an inlined type from a field such "func" and calls onTypeUse.
 //
-// Ex. `(import "Math" "PI" (func $math.pi (result f32))`
+// Ex. `(import "Math" "PI" (func $math.pi (result f32)))`
 //                           starts here --^           ^
-//                         onTypeUse resumes here --+
+//                            onTypeUse resumes here --+
 //
 // Note: Unlike normal parsers, this is not used for an entire field (enclosed by parens). Rather, this only handles
 // "type", "param" and "result" inner fields in the correct order.
@@ -113,7 +102,7 @@ type typeUseParser struct {
 //                                onTypeUse resumes here --+
 //
 func (p *typeUseParser) begin(section wasm.SectionID, idx wasm.Index, onTypeUse onTypeUse, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
-	pos := onTypeUseUnhandledToken
+	pos := callbackPositionUnhandledToken
 	p.pos = positionInitial // to ensure errorContext reports properly
 	switch tok {
 	case tokenLParen:
@@ -122,7 +111,7 @@ func (p *typeUseParser) begin(section wasm.SectionID, idx wasm.Index, onTypeUse
 		p.onTypeUse = onTypeUse
 		return p.beginTypeParamOrResult, nil
 	case tokenRParen:
-		pos = onTypeUseEndField
+		pos = callbackPositionEndField
 	}
 	return onTypeUse(p.emptyTypeIndex(section, idx), nil, pos, tok, tokenBytes, line, col)
 }
@@ -222,7 +211,7 @@ func (p *typeUseParser) beginParamOrResult(tok tokenType, tokenBytes []byte, lin
 	case "type":
 		return nil, errors.New("redundant type")
 	default:
-		return p.end(onTypeUseUnhandledField, tok, tokenBytes, line, col)
+		return p.end(callbackPositionUnhandledField, tok, tokenBytes, line, col)
 	}
 }
 
@@ -282,9 +271,10 @@ func (p *typeUseParser) setParamID(idToken []byte) error {
 //                         records i32 --^  ^
 //   parseMoreParamsOrResult resumes here --+
 //
-// Ex. One param type is present `(param i32)`
-//                         records i32 --^  ^
-//   parseMoreParamsOrResult resumes here --+
+// Ex. Multiple param types are present `(param i32 i64)`
+//                                records i32 --^   ^  ^
+//                                    records i32 --+  |
+//              parseMoreParamsOrResult resumes here --+
 //
 // Ex. type is missing `(param)`
 //                errs here --^
@@ -351,9 +341,9 @@ func (p *typeUseParser) parseResult(tok tokenType, tokenBytes []byte, _, _ uint3
 }
 
 func (p *typeUseParser) parseEnd(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
-	pos := onTypeUseUnhandledToken
+	pos := callbackPositionUnhandledToken
 	if tok == tokenRParen {
-		pos = onTypeUseEndField
+		pos = callbackPositionEndField
 	}
 	return p.end(pos, tok, tokenBytes, line, col)
 }
@@ -379,7 +369,7 @@ type lineCol struct {
 }
 
 // end invokes onTypeUse to continue parsing
-func (p *typeUseParser) end(pos onTypeUsePosition, tok tokenType, tokenBytes []byte, line, col uint32) (parser tokenParser, err error) {
+func (p *typeUseParser) end(pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (parser tokenParser, err error) {
 	// Record the potentially inlined type if needed and invoke onTypeUse with the parsed index
 	var typeIdx wasm.Index
 	if p.parsedTypeField {


Test patch:
diff --git a/wasm/binary/decoder_test.go b/wasm/binary/decoder_test.go
index c7ab44ec..813d801c 100644
--- a/wasm/binary/decoder_test.go
+++ b/wasm/binary/decoder_test.go
@@ -134,7 +134,7 @@ func TestDecodeModule_Errors(t *testing.T) {
 				wasm.SectionIDCustom, 0x09, // 9 bytes in this section
 				0x04, 'm', 'e', 'm', 'e',
 				subsectionIDModuleName, 0x03, 0x01, 'y'),
-			expectedErr: "section ID 0: redundant custom section meme",
+			expectedErr: "section custom: redundant custom section meme",
 		},
 		{
 			name: "redundant name section",
@@ -145,7 +145,7 @@ func TestDecodeModule_Errors(t *testing.T) {
 				wasm.SectionIDCustom, 0x09, // 9 bytes in this section
 				0x04, 'n', 'a', 'm', 'e',
 				subsectionIDModuleName, 0x03, 0x01, 'x'),
-			expectedErr: "section ID 0: redundant custom section name",
+			expectedErr: "section custom: redundant custom section name",
 		},
 	}
 
diff --git a/wasm/binary/section_test.go b/wasm/binary/section_test.go
index ee77a53d..78b480e4 100644
--- a/wasm/binary/section_test.go
+++ b/wasm/binary/section_test.go
@@ -111,6 +111,11 @@ func TestDecodeExportSection_Errors(t *testing.T) {
 	}
 }
 
+func TestEncodeFunctionSection(t *testing.T) {
+	require.Equal(t, []byte{wasm.SectionIDFunction, 0x2, 0x01, 0x05}, encodeFunctionSection([]wasm.Index{5}))
+}
+
+// TestEncodeStartSection uses the same index as TestEncodeFunctionSection to highlight the encoding is different.
 func TestEncodeStartSection(t *testing.T) {
 	require.Equal(t, []byte{wasm.SectionIDStart, 0x01, 0x05}, encodeStartSection(5))
 }
diff --git a/wasm/module_test.go b/wasm/module_test.go
index 7789a25e..ad558122 100644
--- a/wasm/module_test.go
+++ b/wasm/module_test.go
@@ -203,3 +203,120 @@ func TestModule_allDeclarations(t *testing.T) {
 		})
 	}
 }
+
+func TestModule_SectionSize(t *testing.T) {
+	i32, f32 := ValueTypeI32, ValueTypeF32
+	zero := uint32(0)
+	empty := &ConstantExpression{Opcode: OpcodeI32Const, Data: []byte{0x00}}
+
+	tests := []struct {
+		name     string
+		input    *Module
+		expected map[string]uint32
+	}{
+		{
+			name:     "empty",
+			input:    &Module{},
+			expected: map[string]uint32{},
+		},
+		{
+			name:     "only name section",
+			input:    &Module{NameSection: &NameSection{ModuleName: "simple"}},
+			expected: map[string]uint32{"custom": 1},
+		},
+		{
+			name: "only custom section",
+			input: &Module{CustomSections: map[string][]byte{
+				"meme": {1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+			}},
+			expected: map[string]uint32{"custom": 1},
+		},
+		{
+			name: "name section and a custom section",
+			input: &Module{
+				NameSection: &NameSection{ModuleName: "simple"},
+				CustomSections: map[string][]byte{
+					"meme": {1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+				},
+			},
+			expected: map[string]uint32{"custom": 2},
+		},
+		{
+			name: "type section",
+			input: &Module{
+				TypeSection: []*FunctionType{
+					{},
+					{Params: []ValueType{i32, i32}, Results: []ValueType{i32}},
+					{Params: []ValueType{i32, i32, i32, i32}, Results: []ValueType{i32}},
+				},
+			},
+			expected: map[string]uint32{"type": 3},
+		},
+		{
+			name: "type and import section",
+			input: &Module{
+				TypeSection: []*FunctionType{
+					{Params: []ValueType{i32, i32}, Results: []ValueType{i32}},
+					{Params: []ValueType{f32, f32}, Results: []ValueType{f32}},
+				},
+				ImportSection: []*Import{
+					{
+						Module: "Math", Name: "Mul",
+						Kind:     ImportKindFunc,
+						DescFunc: 1,
+					}, {
+						Module: "Math", Name: "Add",
+						Kind:     ImportKindFunc,
+						DescFunc: 0,
+					},
+				},
+			},
+			expected: map[string]uint32{"import": 2, "type": 2},
+		},
+		{
+			name: "type function and start section",
+			input: &Module{
+				TypeSection:     []*FunctionType{{}},
+				FunctionSection: []Index{0},
+				CodeSection: []*Code{
+					{Body: []byte{OpcodeLocalGet, 0, OpcodeLocalGet, 1, OpcodeI32Add, OpcodeEnd}},
+				},
+				ExportSection: map[string]*Export{
+					"AddInt": {Name: "AddInt", Kind: ExportKindFunc, Index: Index(0)},
+				},
+				StartSection: &zero,
+			},
+			expected: map[string]uint32{"code": 1, "export": 1, "function": 1, "start": 1, "type": 1},
+		},
+		{
+			name: "memory and data",
+			input: &Module{
+				MemorySection: []*MemoryType{{Min: 1}},
+				DataSection:   []*DataSegment{{MemoryIndex: 0, OffsetExpression: empty}},
+			},
+			expected: map[string]uint32{"data": 1, "memory": 1},
+		},
+		{
+			name: "table and element",
+			input: &Module{
+				TableSection:   []*TableType{{ElemType: 0x70, Limit: &LimitsType{Min: 1}}},
+				ElementSection: []*ElementSegment{{TableIndex: 0, OffsetExpr: empty}},
+			},
+			expected: map[string]uint32{"element": 1, "table": 1},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			actual := map[string]uint32{}
+			for i := SectionID(0); i <= SectionIDData; i++ {
+				if size := tc.input.SectionSize(i); size > 0 {
+					actual[SectionIDName(i)] = size
+				}
+			}
+			require.Equal(t, tc.expected, actual)
+		})
+	}
+}
diff --git a/wasm/text/decoder_test.go b/wasm/text/decoder_test.go
index 43ab766e..5b083703 100644
--- a/wasm/text/decoder_test.go
+++ b/wasm/text/decoder_test.go
@@ -1343,34 +1343,28 @@ func TestDecodeModule(t *testing.T) {
 			},
 		},
 		{
-			name: "export different memory - numeric",
+			name: "export memory - numeric",
 			input: `(module
 	(memory 0)
-	(memory 1)
 	(export "foo" (memory 0))
-	(export "bar" (memory 1))
 )`,
 			expected: &wasm.Module{
-				MemorySection: []*wasm.MemoryType{{Min: 0}, {Min: 1}},
+				MemorySection: []*wasm.MemoryType{{Min: 0}},
 				ExportSection: map[string]*wasm.Export{
 					"foo": {Name: "foo", Kind: wasm.ExportKindMemory, Index: 0},
-					"bar": {Name: "bar", Kind: wasm.ExportKindMemory, Index: 1},
 				},
 			},
 		},
 		{
-			name: "export different memory - numeric - late",
+			name: "export memory - numeric - late",
 			input: `(module
 	(export "foo" (memory 0))
-	(export "bar" (memory 1))
 	(memory 0)
-	(memory 1)
 )`,
 			expected: &wasm.Module{
-				MemorySection: []*wasm.MemoryType{{Min: 0}, {Min: 1}},
+				MemorySection: []*wasm.MemoryType{{Min: 0}},
 				ExportSection: map[string]*wasm.Export{
 					"foo": {Name: "foo", Kind: wasm.ExportKindMemory, Index: 0},
-					"bar": {Name: "bar", Kind: wasm.ExportKindMemory, Index: 1},
 				},
 			},
 		},
@@ -1396,15 +1390,13 @@ func TestDecodeModule(t *testing.T) {
 		{
 			name: "export memory - ID",
 			input: `(module
-    (memory 1)
-    (memory $mem 2)
-    (memory $mam 3)
+    (memory $mem 1)
     (export "memory" (memory $mem))
 )`,
 			expected: &wasm.Module{
-				MemorySection: []*wasm.MemoryType{{Min: 1}, {Min: 2}, {Min: 3}},
+				MemorySection: []*wasm.MemoryType{{Min: 1}},
 				ExportSection: map[string]*wasm.Export{
-					"memory": {Name: "memory", Kind: wasm.ExportKindMemory, Index: 2},
+					"memory": {Name: "memory", Kind: wasm.ExportKindMemory, Index: 0},
 				},
 			},
 		},
@@ -1780,6 +1772,11 @@ func TestParseModule_Errors(t *testing.T) {
 			input:       "(module (import \"foo\" \"bar\" (func (type $v_v))))",
 			expectedErr: "1:41: unknown ID $v_v",
 		},
+		{
+			name:        "import func after func",
+			input:       "(module (func) (import \"\" \"\" (func)))",
+			expectedErr: "1:31: import after module-defined function in module.import[0]",
+		},
 		{
 			name:        "func invalid name",
 			input:       "(module (func baz)))",
@@ -1833,7 +1830,7 @@ func TestParseModule_Errors(t *testing.T) {
 		{
 			name:        "func duplicate result",
 			input:       "(module (func (param i32) (result i32) (result i32)))",
-			expectedErr: "1:41: duplicate result in module.func[0]",
+			expectedErr: "1:41: at most one result allowed in module.func[0]",
 		},
 		{
 			name:        "func double result type",
@@ -1885,11 +1882,6 @@ func TestParseModule_Errors(t *testing.T) {
 			input:       "(module (import \"\" \"\" (func $main)) (func $main)))",
 			expectedErr: "1:43: duplicate ID $main in module.func[0]",
 		},
-		{
-			name:        "import func ID clashes with func ID",
-			input:       "(module (func $main) (import \"\" \"\" (func $main)))",
-			expectedErr: "1:42: duplicate ID $main in module.import[0].func",
-		},
 		{
 			name:        "func ID after result",
 			input:       "(module (func (result i32) $main)))",
@@ -1946,6 +1938,11 @@ func TestParseModule_Errors(t *testing.T) {
 		)`,
 			expectedErr: "3:15: unknown ID $mein in module.code[1].body[1]",
 		},
+		{
+			name:        "second memory",
+			input:       "(module (memory 1) (memory 1))",
+			expectedErr: "1:21: at most one memory allowed in module",
+		},
 		{
 			name: "export duplicates empty name",
 			input: `(module
@@ -1954,7 +1951,7 @@ func TestParseModule_Errors(t *testing.T) {
     (export "" (func 0))
     (export "" (memory 1))
 )`,
-			expectedErr: "5:13: duplicate name \"\" in module.export[1]",
+			expectedErr: `5:13: "" already exported in module.export[1]`,
 		},
 		{
 			name: "export duplicates name",
@@ -1964,7 +1961,7 @@ func TestParseModule_Errors(t *testing.T) {
     (export "a" (func 0))
     (export "a" (memory 1))
 )`,
-			expectedErr: "5:13: duplicate name \"a\" in module.export[1]",
+			expectedErr: `5:13: "a" already exported in module.export[1]`,
 		},
 		{
 			name:        "export double name",
@@ -2057,7 +2054,7 @@ func TestParseModule_Errors(t *testing.T) {
 		{
 			name:        "double start",
 			input:       "(module (start $main) (start $main))",
-			expectedErr: "1:24: redundant start in module",
+			expectedErr: "1:24: at most one start allowed in module",
 		},
 		{
 			name:        "wrong start",
@@ -2098,13 +2095,13 @@ func TestModuleParser_ErrorContext(t *testing.T) {
 	}{
 		{input: "initial", pos: positionInitial, expected: ""},
 		{input: "module", pos: positionModule, expected: "module"},
-		{input: "module import", pos: positionModuleImport, expected: "module.import[0]"},
-		{input: "module import func", pos: positionModuleImportFunc, expected: "module.import[0].func"},
-		{input: "module func", pos: positionModuleFunc, expected: "module.func[0]"},
-		{input: "module memory", pos: positionModuleMemory, expected: "module.memory[0]"},
-		{input: "module export", pos: positionModuleExport, expected: "module.export[0]"},
-		{input: "module export func", pos: positionModuleExportFunc, expected: "module.export[0].func"},
-		{input: "start", pos: positionModuleStart, expected: "module.start"},
+		{input: "module import", pos: positionImport, expected: "module.import[0]"},
+		{input: "module import func", pos: positionImportFunc, expected: "module.import[0].func"},
+		{input: "module func", pos: positionFunc, expected: "module.func[0]"},
+		{input: "module memory", pos: positionMemory, expected: "module.memory[0]"},
+		{input: "module export", pos: positionExport, expected: "module.export[0]"},
+		{input: "module export func", pos: positionExportFunc, expected: "module.export[0].func"},
+		{input: "start", pos: positionStart, expected: "module.start"},
 	}
 
 	for _, tt := range tests {
diff --git a/wasm/text/func_parser_test.go b/wasm/text/func_parser_test.go
index 0c2e2e7c..2294f05a 100644
--- a/wasm/text/func_parser_test.go
+++ b/wasm/text/func_parser_test.go
@@ -213,7 +213,7 @@ func TestFuncParser_Errors(t *testing.T) {
 		{
 			name:        "duplicate result",
 			source:      "(func (result i32) (result i32))",
-			expectedErr: "1:21: duplicate result",
+			expectedErr: "1:21: at most one result allowed",
 		},
 	}
 
diff --git a/wasm/text/memory_parser_test.go b/wasm/text/memory_parser_test.go
index 534bcbb9..9498eb14 100644
--- a/wasm/text/memory_parser_test.go
+++ b/wasm/text/memory_parser_test.go
@@ -60,8 +60,8 @@ func TestMemoryParser(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			namespace := newIndexNamespace()
-			parsed, tp, err := parseMemoryType(namespace, tc.input)
+			memoryNamespace := newIndexNamespace()
+			parsed, tp, err := parseMemoryType(memoryNamespace, tc.input)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, parsed)
 			require.Equal(t, uint32(1), tp.memoryNamespace.count)
@@ -151,13 +151,13 @@ func TestMemoryParser_Errors(t *testing.T) {
 	})
 }
 
-func parseMemoryType(namespace *indexNamespace, input string) (*wasm.MemoryType, *memoryParser, error) {
+func parseMemoryType(memoryNamespace *indexNamespace, input string) (*wasm.MemoryType, *memoryParser, error) {
 	var parsed *wasm.MemoryType
 	var setFunc onMemory = func(min uint32, max *uint32) tokenParser {
 		parsed = &wasm.MemoryType{Min: min, Max: max}
 		return parseErr
 	}
-	tp := newMemoryParser(namespace, setFunc)
+	tp := newMemoryParser(memoryNamespace, setFunc)
 	// memoryParser starts after the '(memory', so we need to eat it first!
 	_, _, err := lex(skipTokens(2, tp.begin), []byte(input))
 	return parsed, tp, err
diff --git a/wasm/text/type_parser_test.go b/wasm/text/type_parser_test.go
index ec07c33c..76f72c2d 100644
--- a/wasm/text/type_parser_test.go
+++ b/wasm/text/type_parser_test.go
@@ -100,8 +100,8 @@ func TestTypeParser(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			namespace := newIndexNamespace()
-			parsed, tp, err := parseFunctionType(namespace, tc.input)
+			typeNamespace := newIndexNamespace()
+			parsed, tp, err := parseFunctionType(typeNamespace, tc.input)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, parsed)
 			require.Equal(t, uint32(1), tp.typeNamespace.count)
@@ -246,13 +246,13 @@ func TestTypeParser_Errors(t *testing.T) {
 	})
 }
 
-func parseFunctionType(namespace *indexNamespace, input string) (*wasm.FunctionType, *typeParser, error) {
+func parseFunctionType(typeNamespace *indexNamespace, input string) (*wasm.FunctionType, *typeParser, error) {
 	var parsed *wasm.FunctionType
 	var setFunc onType = func(ft *wasm.FunctionType) tokenParser {
 		parsed = ft
 		return parseErr
 	}
-	tp := newTypeParser(namespace, setFunc)
+	tp := newTypeParser(typeNamespace, setFunc)
 	// typeParser starts after the '(type', so we need to eat it first!
 	_, _, err := lex(skipTokens(2, tp.begin), []byte(input))
 	return parsed, tp, err
diff --git a/wasm/text/typeuse_parser_test.go b/wasm/text/typeuse_parser_test.go
index 74825ce7..73b3e41d 100644
--- a/wasm/text/typeuse_parser_test.go
+++ b/wasm/text/typeuse_parser_test.go
@@ -18,7 +18,7 @@ type typeUseParserTest struct {
 	expectedTypeIdx     wasm.Index
 	expectedParamNames  wasm.NameMap
 
-	expectedOnTypeUsePosition onTypeUsePosition
+	expectedOnTypeUsePosition callbackPosition
 	expectedOnTypeUseToken    tokenType
 	expectedTrailingTokens    []tokenType
 }
@@ -325,14 +325,14 @@ type typeUseTestFunc func(*typeUseParserTest) (*typeUseParser, func(t *testing.T
 func runTypeUseParserTests(t *testing.T, tests []*typeUseParserTest, tf typeUseTestFunc) {
 	moreTests := make([]*typeUseParserTest, 0, len(tests)*2)
 	for _, tt := range tests {
-		tt.expectedOnTypeUsePosition = onTypeUseEndField
+		tt.expectedOnTypeUsePosition = callbackPositionEndField
 		tt.expectedOnTypeUseToken = tokenRParen // at the end of the field ')'
 		tt.expectedTrailingTokens = nil
 
 		kt := *tt // copy
 		kt.name = fmt.Sprintf("%s - trailing keyword", tt.name)
 		kt.source = fmt.Sprintf("%s nop)", tt.source[:len(tt.source)-1])
-		kt.expectedOnTypeUsePosition = onTypeUseUnhandledToken
+		kt.expectedOnTypeUsePosition = callbackPositionUnhandledToken
 		kt.expectedOnTypeUseToken = tokenKeyword // at 'nop' and ')' remains
 		kt.expectedTrailingTokens = []tokenType{tokenRParen}
 		moreTests = append(moreTests, &kt)
@@ -342,11 +342,11 @@ func runTypeUseParserTests(t *testing.T, tests []*typeUseParserTest, tf typeUseT
 		ft.source = fmt.Sprintf("%s (nop))", tt.source[:len(tt.source)-1])
 		// Two outcomes, we've reached a field not named "type", "param" or "result" or we completed "result"
 		if strings.Contains(tt.source, "result") {
-			ft.expectedOnTypeUsePosition = onTypeUseUnhandledToken
+			ft.expectedOnTypeUsePosition = callbackPositionUnhandledToken
 			ft.expectedOnTypeUseToken = tokenLParen // at '(' and 'nop))' remain
 			ft.expectedTrailingTokens = []tokenType{tokenKeyword, tokenRParen, tokenRParen}
 		} else {
-			ft.expectedOnTypeUsePosition = onTypeUseUnhandledField
+			ft.expectedOnTypeUsePosition = callbackPositionUnhandledField
 			ft.expectedOnTypeUseToken = tokenKeyword // at 'nop' and '))' remain
 			ft.expectedTrailingTokens = []tokenType{tokenRParen, tokenRParen}
 		}
@@ -360,7 +360,7 @@ func runTypeUseParserTests(t *testing.T, tests []*typeUseParserTest, tf typeUseT
 			var parsedTypeIdx wasm.Index
 			var parsedParamNames wasm.NameMap
 			p := &collectTokenTypeParser{}
-			var setTypeUse onTypeUse = func(typeIdx wasm.Index, paramNames wasm.NameMap, pos onTypeUsePosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
+			var setTypeUse onTypeUse = func(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
 				parsedTypeIdx = typeIdx
 				parsedParamNames = paramNames
 				require.Equal(t, tc.expectedOnTypeUsePosition, pos)
@@ -534,11 +534,11 @@ func TestTypeUseParser_FailsMatch(t *testing.T) {
 	}
 }
 
-var ignoreTypeUse onTypeUse = func(typeIdx wasm.Index, paramNames wasm.NameMap, pos onTypeUsePosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
+var ignoreTypeUse onTypeUse = func(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
 	return parseNoop, nil
 }
 
-var failOnTypeUse onTypeUse = func(typeIdx wasm.Index, paramNames wasm.NameMap, pos onTypeUsePosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
+var failOnTypeUse onTypeUse = func(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPosition, tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
 	return nil, errors.New("unexpected to call onTypeUse on error")
 }