Repository: tetratelabs/wazero
Pull request #163 (2022-01-25 07:54:26)

Enforces uniqueness within symbolic indices
> An identifier context is well-formed if no index space contains duplicate identifiers.

We weren't checking this, and now we are!. This comes at a cost, though,
but I don't see a good alternative to initializing maps wrt uniqueness.

```
BenchmarkCodecExample/text.DecodeModule
BenchmarkCodecExample/text.DecodeModule-16           	   78700	     14460 ns/op	    7328 B/op	     323 allocs/op
```

After
```
BenchmarkCodecExample/text.DecodeModule
BenchmarkCodecExample/text.DecodeModule-16           	   74214	     16921 ns/op	    8288 B/op	     333 allocs/op
```

See https://www.w3.org/TR/wasm-core-1/#text-context

codefromthecrypt: ok I reduced the change which also shaved off a couple allocations and backfilled a missing test

Patch:
diff --git a/wasm/text/bind.go b/wasm/text/bind.go
index 8d90e027..49314ef4 100644
--- a/wasm/text/bind.go
+++ b/wasm/text/bind.go
@@ -158,7 +158,8 @@ func mergeLocalNames(m *module, typeParamNames map[wasm.Index]wasm.NameMap) wasm
 		}
 
 		// Use any inlined names or default to any on the type
-		typeNames, hasType := typeParamNames[m.typeUses[i].typeIndex.numeric]
+		typeIndex := m.typeUses[i].typeIndex.numeric
+		typeNames, hasType := typeParamNames[typeIndex]
 		var localNames wasm.NameMap
 		if inlinedNames == nil && !hasType {
 			continue
diff --git a/wasm/text/parser.go b/wasm/text/parser.go
index 41402987..ec4e032c 100644
--- a/wasm/text/parser.go
+++ b/wasm/text/parser.go
@@ -83,19 +83,44 @@ type moduleParser struct {
 	indexParser *indexParser
 	funcParser  *funcParser
 
-	// typeNameToIndex is only used for resolving symbolic index names to numeric ones.
+	// typeIDContext resolves symbolic identifiers, such as "v_v" to a numeric index in wasm.Module TypeSection, such
+	// as '2'. Duplicate identifiers are not allowed by specification.
 	//
 	// Note: This is not encoded in the wasm.NameSection as there is no type name section in WebAssembly 1.0 (MVP)
-	typeNameToIndex map[string]wasm.Index
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#text-context
+	typeIDContext idContext
 
-	// funcNameToIndex is only used for resolving symbolic index names to numeric ones. It contains any imported or
-	// module-defined function names and points to the function index which is preceded by imports.
+	// funcIDContext resolves symbolic identifiers, such as "main" to a numeric index in function index namespace, such
+	// as '2'. Duplicate identifiers are not allowed by specification.
+	//
+	// Note: the function index namespace starts with any wasm.ImportKindFunc in the wasm.Module TypeSection followed by
+	// the wasm.Module FunctionSection.
 	//
 	// Note: this should be updated alongside module.names FunctionNames
-	funcNameToIndex map[string]wasm.Index
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#text-context
+	funcIDContext idContext
 
-	// typeParamNames are nil when no types had named (param) fields.
+	// typeParamIDContext resolves symbolic identifiers, such as "x" to a numeric index in locals namespace for each
+	// function that uses this type, yet doesn't override the symbolic IDs of the parameters.
+	//
+	// For example, given `(module (type) (type (func (param $x i32) (param i64) (param $y i32))`, the mapping would be
+	// 1 -> [{0, "x"}, {2, "y"}].
+	//
+	// Given the above mapping, if a function refers to the type alone, like `(func (type 1))`, the parameter IDs from
+	// the type become that function's locals context: [{0, "x"}, {2, "y"}]
+	//
+	// However, a type use can override IDS like this: `(func (type 1) (param $1 i32) (param $2 i64) (param $3 i32))`.
+	// In this case, the parameter IDs from the type are ignored. Ex. [{0, "1"}, {1, "2"}, {2, "3"}]
+	//
 	// Note: This is a map not a wasm.IndirectNameMap as late lookup is needed for after parsing
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#text-functype
+	// See https://www.w3.org/TR/wasm-core-1/#type-uses%E2%91%A0
+	typeParamIDContext map[wasm.Index]idContext
+
+	// typeParamNames are the typeParamIDContext formatted for the wasm.NameSection LocalNames
 	typeParamNames map[wasm.Index]wasm.NameMap
 }
 
@@ -114,8 +139,16 @@ func (p *moduleParser) parse(tok tokenType, tokenBytes []byte, line, col uint32)
 // * module is the result of parsing or nil on error
 // * err is a FormatError invoking the parser, dangling block comments or unexpected characters.
 func parseModule(source []byte) (*module, error) {
-	p := moduleParser{source: source, module: &module{names: &wasm.NameSection{}}, indexParser: &indexParser{}}
-	p.typeParser = &typeParser{m: &p}
+	p := moduleParser{
+		source:             source,
+		module:             &module{names: &wasm.NameSection{}},
+		indexParser:        &indexParser{},
+		typeIDContext:      idContext{}, // initialize contexts to reduce the amount of runtime nil checks
+		typeParamIDContext: map[wasm.Index]idContext{},
+		typeParamNames:     map[wasm.Index]wasm.NameMap{},
+		funcIDContext:      idContext{},
+	}
+	p.typeParser = &typeParser{m: &p, paramIDContext: idContext{}}
 	p.funcParser = &funcParser{m: &p, onBodyEnd: p.parseFuncEnd}
 
 	// A valid source must begin with the token '(', but it could be preceded by whitespace or comments. For this
@@ -130,7 +163,7 @@ func parseModule(source []byte) (*module, error) {
 	p.module.types = append(p.module.types, p.typeParser.inlinedTypes...)
 
 	// Ensure indices only point to numeric values
-	if err = bindIndices(p.module, p.typeNameToIndex, p.funcNameToIndex); err != nil {
+	if err = bindIndices(p.module, p.typeIDContext, p.funcIDContext); err != nil {
 		return nil, err
 	}
 
@@ -178,14 +211,13 @@ func (p *moduleParser) beginField(tok tokenType, fieldName []byte, _, _ uint32)
 		switch string(fieldName) {
 		case "type":
 			p.currentField = fieldModuleType
-			p.tokenParser = p.parseTypeName
+			p.tokenParser = p.parseTypeID
 		case "import":
 			p.currentField = fieldModuleImport
 			p.tokenParser = p.parseImportModule
 		case "func":
 			p.currentField = fieldModuleFunc
-			p.module.code = append(p.module.code, &wasm.Code{})
-			p.tokenParser = p.parseFuncName
+			p.tokenParser = p.parseFuncID
 		case "export":
 			p.currentField = fieldModuleExport
 			p.tokenParser = p.parseExportName
@@ -210,7 +242,7 @@ func (p *moduleParser) beginField(tok tokenType, fieldName []byte, _, _ uint32)
 			})
 
 			p.currentField = fieldModuleImportFunc
-			p.tokenParser = p.parseImportFuncName
+			p.tokenParser = p.parseImportFuncID
 		} // TODO: table, memory or global
 	case fieldModuleExport:
 		// Add the next export func object and ready for parsing it.
@@ -268,7 +300,7 @@ func (p *moduleParser) parseModuleName(tok tokenType, tokenBytes []byte, line, c
 func (p *moduleParser) parseModule(tok tokenType, tokenBytes []byte, _, _ uint32) error {
 	switch tok {
 	case tokenID:
-		return fmt.Errorf("redundant name: %s", string(tokenBytes))
+		return fmt.Errorf("redundant ID %s", tokenBytes)
 	case tokenLParen:
 		p.tokenParser = p.beginField // after this look for a field name
 		return nil
@@ -280,20 +312,19 @@ func (p *moduleParser) parseModule(tok tokenType, tokenBytes []byte, _, _ uint32
 	return nil
 }
 
-// parseTypeName is the first parser inside a type field. This records the typeFunc.name if present or calls parseType
+// parseTypeID is the first parser inside a type field. This records the symbolic ID, if present, or calls parseType
 // if not found.
 //
-// Ex. A type name is present `(type $t0 (func (result i32)))`
-//                      records t0 --^   ^
-//              parseType resumes here --+
+// Ex. A type ID is present `(type $t0 (func (result i32)))`
+//                    records t0 --^   ^
+//            parseType resumes here --+
 //
-// Ex. No type name `(type (func (result i32)))`
-//       calls parseType --^
-func (p *moduleParser) parseTypeName(tok tokenType, tokenBytes []byte, line, col uint32) error {
+// Ex. No type ID `(type (func (result i32)))`
+//     calls parseType --^
+func (p *moduleParser) parseTypeID(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $v_v
-		p.currentValue0 = stripDollar(tokenBytes)
 		p.tokenParser = p.parseType
-		return nil
+		return p.setTypeID(tokenBytes)
 	}
 	return p.parseType(tok, tokenBytes, line, col)
 }
@@ -310,7 +341,7 @@ func (p *moduleParser) parseTypeName(tok tokenType, tokenBytes []byte, line, col
 func (p *moduleParser) parseType(tok tokenType, tokenBytes []byte, _, _ uint32) error {
 	switch tok {
 	case tokenID:
-		return errors.New("redundant name")
+		return fmt.Errorf("redundant ID %s", tokenBytes)
 	case tokenLParen: // start fields, ex. (func
 		// Err if there's a second func. Ex. (type (func) (func))
 		if uint32(len(p.module.types)) > p.currentTypeIndex {
@@ -355,26 +386,11 @@ func (p *moduleParser) parseTypeFunc(tok tokenType, tokenBytes []byte, line, col
 // the token is tokenRParen and sets the next parser to parseType on tokenRParen.
 func (p *moduleParser) parseTypeFuncEnd(tok tokenType, tokenBytes []byte, _, _ uint32) error {
 	if tok == tokenRParen {
-		sig, localNames := p.typeParser.getType()
-		idx := wasm.Index(len(p.module.types))
-
-		typeName := string(p.currentValue0)
-		if typeName != "" {
-			if p.typeNameToIndex == nil {
-				p.typeNameToIndex = map[string]wasm.Index{typeName: idx}
-			} else {
-				p.typeNameToIndex[typeName] = idx
-			}
+		sig, paramIDs, paramNames := p.typeParser.getType()
+		if paramIDs != nil {
+			p.typeParamIDContext[p.currentTypeIndex] = paramIDs
+			p.typeParamNames[p.currentTypeIndex] = paramNames
 		}
-
-		if localNames != nil {
-			if p.typeParamNames == nil {
-				p.typeParamNames = map[wasm.Index]wasm.NameMap{idx: localNames}
-			} else {
-				p.typeParamNames[idx] = localNames
-			}
-		}
-
 		p.module.types = append(p.module.types, sig)
 		p.currentValue0 = nil
 		p.currentField = fieldModuleType
@@ -467,33 +483,39 @@ func (p *moduleParser) parseImport(tok tokenType, tokenBytes []byte, _, _ uint32
 	return nil
 }
 
-// parseImportFuncName is the first parser inside an imported function field. This records the typeFunc.name if present
+// parseImportFuncID is the first parser inside an imported function field. This records the symbolic ID, 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. A function ID 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 {
+// Ex. No function ID `(import "Math" "PI" (func (result f32))`
+//                  calls parseImportFunc here --^
+func (p *moduleParser) parseImportFuncID(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $main
-		p.addFuncName(tokenBytes)
 		p.tokenParser = p.parseImportFunc
-		return nil
+		return p.setFuncID(tokenBytes)
 	}
 	return p.parseImportFunc(tok, tokenBytes, line, col)
 }
 
-// addFuncName adds the normalized ('$' stripped) function name to the parser cache and the wasm.NameSection.
-func (p *moduleParser) addFuncName(idToken []byte) {
-	na := &wasm.NameAssoc{Index: p.currentFuncIndex, Name: string(stripDollar(idToken))}
-	p.module.names.FunctionNames = append(p.module.names.FunctionNames, na)
-	if p.funcNameToIndex == nil {
-		p.funcNameToIndex = map[string]wasm.Index{na.Name: na.Index}
-	} else {
-		p.funcNameToIndex[na.Name] = na.Index
+// setTypeID adds the normalized ('$' stripped) type ID to the typeIDContext.
+func (p *moduleParser) setTypeID(idToken []byte) error {
+	idx := p.currentTypeIndex
+	_, err := p.typeIDContext.setID(idToken, idx)
+	return err
+}
+
+// setFuncID adds the normalized ('$' stripped) function ID to the funcIDContext and the wasm.NameSection.
+func (p *moduleParser) setFuncID(idToken []byte) error {
+	idx := p.currentFuncIndex
+	id, err := p.funcIDContext.setID(idToken, idx)
+	if err != nil {
+		return err
 	}
+	p.module.names.FunctionNames = append(p.module.names.FunctionNames, &wasm.NameAssoc{Index: idx, Name: id})
+	return nil
 }
 
 // parseImportFunc is the second parser inside the imported function field. This passes control to the typeParser until
@@ -501,15 +523,15 @@ func (p *moduleParser) addFuncName(idToken []byte) {
 //
 // Ex. `(import "Math" "PI" (func $math.pi (result f32)))`
 //                           starts here --^           ^
-//             parseImportFuncEnd resumes here --+
+//                   parseImportFuncEnd resumes here --+
 //
 // Ex. If there is no signature `(import "" "main" (func))`
-//               calls parseImportFuncEnd here ---^
+//                     calls parseImportFuncEnd here ---^
 func (p *moduleParser) parseImportFunc(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	p.typeParser.reset() // reset now in case there is never a tokenLParen
 	switch tok {
 	case tokenID: // Ex. (func $main $main)
-		return fmt.Errorf("redundant name: %s", tokenBytes)
+		return fmt.Errorf("redundant ID %s", tokenBytes)
 	case tokenLParen:
 		p.typeParser.beginTypeUse(p.parseImportFuncEnd, p.parseImportFuncEnd) // start fields, ex. (param or (result
 		return nil
@@ -521,11 +543,11 @@ func (p *moduleParser) parseImportFunc(tok tokenType, tokenBytes []byte, line, c
 // and/or importFunc.typeInlined and sets the next parser to parseImport.
 func (p *moduleParser) parseImportFuncEnd(tok tokenType, tokenBytes []byte, _, _ uint32) error {
 	if tok == tokenRParen {
-		tu, paramNames := p.typeParser.getTypeUse()
+		tu, _, paramNames := p.typeParser.getTypeUse()
 		p.module.typeUses = append(p.module.typeUses, tu)
 		if paramNames != nil {
-			p.module.names.LocalNames =
-				append(p.module.names.LocalNames, &wasm.NameMapAssoc{Index: p.currentFuncIndex, NameMap: paramNames})
+			na := &wasm.NameMapAssoc{Index: p.currentFuncIndex, NameMap: paramNames}
+			p.module.names.LocalNames = append(p.module.names.LocalNames, na)
 		}
 		p.currentField = fieldModuleImport
 		p.tokenParser = p.parseImport
@@ -534,20 +556,19 @@ func (p *moduleParser) parseImportFuncEnd(tok tokenType, tokenBytes []byte, _, _
 	return unexpectedToken(tok, tokenBytes)
 }
 
-// parseFuncName is the first parser inside a function field. This records the function name if present and sets the
-// next parser to parseFunc. If the token isn't a tokenID, this calls parseFunc.
+// parseFuncID is the first parser inside a function field. This records the function ID, if present, and sets the next
+// parser to parseFunc. If the token isn't a tokenID, this calls parseFunc.
 //
-// Ex. A function name is present `(module (func $math.pi (result f32))`
-//                        records math.pi here --^
-//                               parseFunc resumes here --^
+// Ex. A function ID is present `(module (func $math.pi (result f32))`
+//                      records math.pi here --^
+//                             parseFunc resumes here --^
 //
-// Ex. No function name `(module (func (result f32))`
-//              calls parseFunc here --^
-func (p *moduleParser) parseFuncName(tok tokenType, tokenBytes []byte, line, col uint32) error {
+// Ex. No function ID `(module (func (result f32))`
+//            calls parseFunc here --^
+func (p *moduleParser) parseFuncID(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $main
-		p.addFuncName(tokenBytes)
 		p.tokenParser = p.parseFunc
-		return nil
+		return p.setFuncID(tokenBytes)
 	}
 	return p.parseFunc(tok, tokenBytes, line, col)
 }
@@ -570,7 +591,7 @@ func (p *moduleParser) parseFunc(tok tokenType, tokenBytes []byte, line, col uin
 	p.typeParser.reset() // reset now in case there is never a tokenLParen
 	switch tok {
 	case tokenID: // Ex. (func $main $main)
-		return fmt.Errorf("redundant name: %s", tokenBytes)
+		return fmt.Errorf("redundant ID %s", tokenBytes)
 	case tokenLParen: // start fields, ex. (local or (i32.const
 		p.typeParser.beginTypeUse(p.parseFuncBody, p.parseFuncBodyField)
 		return nil
@@ -590,15 +611,16 @@ func (p *moduleParser) parseFuncBodyField(tok tokenType, tokenBytes []byte, line
 // and/or Func.typeInlined and sets the next parser to parse.
 func (p *moduleParser) parseFuncEnd(tok tokenType, tokenBytes []byte, _, _ uint32) error {
 	if tok == tokenRParen {
-		tu, paramNames := p.typeParser.getTypeUse()
+		tu, _, paramNames := p.typeParser.getTypeUse()
 		p.module.typeUses = append(p.module.typeUses, tu)
+		p.module.code = append(p.module.code, &wasm.Code{Body: p.funcParser.getBody()})
+
+		// TODO: locals and also check they don't conflict with paramIDs returned from the type use
+		// Note: locals may be unverifiable wrt ID collision if the type isn't known, yet (ex func before type)
 		if paramNames != nil {
-			p.module.names.LocalNames =
-				append(p.module.names.LocalNames, &wasm.NameMapAssoc{Index: p.currentFuncIndex, NameMap: paramNames})
+			na := &wasm.NameMapAssoc{Index: p.currentFuncIndex, NameMap: paramNames}
+			p.module.names.LocalNames = append(p.module.names.LocalNames, na)
 		}
-		code := p.module.code[p.currentFuncIndex-uint32(len(p.module.importFuncs))]
-		// TODO: localTypes
-		code.Body = p.funcParser.getBody()
 
 		// Multiple funcs are allowed, so advance in case there's a next.
 		p.currentFuncIndex++
diff --git a/wasm/text/type_parser.go b/wasm/text/type_parser.go
index 992bbdd6..3e77a74f 100644
--- a/wasm/text/type_parser.go
+++ b/wasm/text/type_parser.go
@@ -67,29 +67,33 @@ type typeParser struct {
 	// See https://www.w3.org/TR/wasm-core-1/#type-uses%E2%91%A0
 	currentTypeIndex *index
 
-	// currentParams allow us to accumulate typeFunc.params across multiple fields, as well support abbreviated
+	// currentParams allow us to accumulate wasm.FunctionType 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
 	currentParams []wasm.ValueType
 
-	// currentParamNames accumulates any typeFunc.params for the current type. Ex. $x names (param $x i32)
+	// paramIDContext accumulates any symbolic identifier to numeric index mappings for the currentParams.
+	// Ex. x is the symbolic ID, and name, of the parameter (param $x i32)
 	//
-	// Note: names can be missing because they were never assigned, ex. (param i32), or due to abbreviated format which
-	// does not support names. Ex. (param i32 i32)
+	// Note: IDs can be missing because they were never assigned, ex. (param i32), or due to abbreviated format which
+	// does not support it. Ex. (param i32 i32)
 	// See https://www.w3.org/TR/wasm-core-1/#abbreviations%E2%91%A2
-	currentParamNames wasm.NameMap
+	paramIDContext idContext
+
+	// paramNames are the paramIDContext formatted for the wasm.NameSection LocalNames
+	paramNames wasm.NameMap
 
 	// currentParamField is a field index and used to give an appropriate errorContext. Due to abbreviation it may be
 	// unrelated to the length of currentParams
-	currentParamField uint32
-
-	// currentLocalName is the name of the currentParamField, added on tokenRParen to currentParamNames
-	currentLocalName []byte
+	currentParamField wasm.Index
 
 	// foundParam allows us to check if we found a type in a "param" field. We can't use currentParamField because when
 	// parameters are abbreviated, ex. (param i32 i32), the currentParamField will be less than the type count.
 	foundParam bool
 
+	// foundID is true when the field at currentParamField had an ID. Ex. (param $x i32)
+	foundID bool
+
 	// currentResults is empty until set, and only set once as WebAssembly 1.0 only supports up to one result.
 	currentResults []wasm.ValueType
 
@@ -158,7 +162,10 @@ func (p *typeParser) beginType(onTypeEnd tokenParser) {
 func (p *typeParser) reset() {
 	p.currentTypeIndex = nil
 	p.currentParams = nil
-	p.currentParamNames = nil
+	if len(p.paramIDContext) > 0 {
+		p.paramIDContext = idContext{}
+		p.paramNames = nil
+	}
 	p.currentParamField = 0
 	p.currentResults = nil
 }
@@ -176,9 +183,8 @@ func (p *typeParser) beginParamOrResult(tok tokenType, tokenBytes []byte, line,
 		switch string(tokenBytes) {
 		case "param":
 			p.state = parsingParam
-			p.foundParam = false
-			p.currentLocalName = nil
-			p.m.tokenParser = p.parseParamName
+			p.foundParam, p.foundID = false, false
+			p.m.tokenParser = p.parseParamID
 			return nil
 		case "result":
 			p.state = parsingResult
@@ -204,24 +210,37 @@ func (p *typeParser) parseMoreParamsOrResult(tok tokenType, tokenBytes []byte, l
 	return p.onTypeEnd(tok, tokenBytes, line, col)
 }
 
-// parseParamName is the first parser inside a param field. This records the name if present or calls parseParam if not
+// parseParamID is the first parser inside a param field. This records the ID if present or calls parseParam if not
 // found.
 //
-// Ex. A param name is present `(param $x i32)`
-//                         records x --^  ^
-//              parseParam resumes here --+
+// Ex. A param ID is present `(param $x i32)`
+//                       records x --^  ^
+//            parseParam resumes here --+
 //
-// Ex. No param name `(param i32)`
-//        calls parseParam --^
-func (p *typeParser) parseParamName(tok tokenType, tokenBytes []byte, line, col uint32) error {
+// Ex. No param ID `(param i32)`
+//      calls parseParam --^
+func (p *typeParser) parseParamID(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $len
-		p.currentLocalName = stripDollar(tokenBytes)
+		p.foundID = true
 		p.m.tokenParser = p.parseParam
-		return nil
+		return p.setParamID(tokenBytes)
 	}
 	return p.parseParam(tok, tokenBytes, line, col)
 }
 
+// setParamID adds the normalized ('$' stripped) parameter ID to the paramIDContext and the wasm.NameSection.
+func (p *typeParser) setParamID(idToken []byte) error {
+	// Note: currentParamField is the index of the param field, but due to mixing and matching of abbreviated params
+	// it can be less than the param index. Ex. (param i32 i32) (param $v i32) is param field 2, but the 3rd param.
+	idx := wasm.Index(len(p.currentParams))
+	id, err := p.paramIDContext.setID(idToken, idx)
+	if err != nil {
+		return err
+	}
+	p.paramNames = append(p.paramNames, &wasm.NameAssoc{Index: idx, Name: id})
+	return nil
+}
+
 // parseParam is the last parser inside the param field. This records value type and continues if it is an abbreviated
 // form with multiple value types. When complete, this sets the next parser to parseMoreParamsOrResult.
 //
@@ -238,14 +257,14 @@ func (p *typeParser) parseParamName(tok tokenType, tokenBytes []byte, line, col
 func (p *typeParser) parseParam(tok tokenType, tokenBytes []byte, _, _ uint32) error {
 	switch tok {
 	case tokenID: // Ex. $len
-		return errors.New("redundant name")
+		return fmt.Errorf("redundant ID %s", tokenBytes)
 	case tokenKeyword: // Ex. i32
 		vt, err := parseValueType(tokenBytes)
 		if err != nil {
 			return err
 		}
-		if p.foundParam && p.currentLocalName != nil {
-			return errors.New("cannot name parameters in abbreviated form")
+		if p.foundParam && p.foundID {
+			return errors.New("cannot assign IDs to parameters in abbreviated form")
 		}
 		p.currentParams = append(p.currentParams, vt)
 		p.foundParam = true
@@ -253,16 +272,6 @@ func (p *typeParser) parseParam(tok tokenType, tokenBytes []byte, _, _ uint32) e
 		if !p.foundParam {
 			return errors.New("expected a type")
 		}
-
-		// Note: currentParamField is the index of the param field, but due to mixing and matching of abbreviated params
-		// it can be less than the param index. Ex. (param i32 i32) (param $v i32) is param field 2, but the 3rd param.
-		if p.currentLocalName != nil {
-			p.currentParamNames = append(p.currentParamNames, &wasm.NameAssoc{
-				Index: uint32(len(p.currentParams) - 1),
-				Name:  string(p.currentLocalName),
-			})
-		}
-
 		// since multiple param fields are valid, ex `(func (param i32) (param i64))`, prepare for any next.
 		p.currentParamField++
 		p.state = parsingParamOrResult
@@ -314,9 +323,12 @@ var typeFuncEmpty = &wasm.FunctionType{}
 
 // getTypeUse finalizes any current params or result and returns the current typeIndex and/or type. localNames are only
 // returned if defined inline.
-func (p *typeParser) getTypeUse() (ty *typeUse, paramNames wasm.NameMap) {
+func (p *typeParser) getTypeUse() (ty *typeUse, paramIDs map[string]wasm.Index, paramNames wasm.NameMap) {
 	ty = &typeUse{typeIndex: p.currentTypeIndex}
-	paramNames = p.currentParamNames
+	if len(p.paramIDContext) > 0 {
+		paramIDs = p.paramIDContext
+		paramNames = p.paramNames
+	}
 
 	// Don't conflate lack of verification type with nullary
 	if ty.typeIndex != nil && funcTypeEquals(typeFuncEmpty, p.currentParams, p.currentResults) {
@@ -360,8 +372,11 @@ func funcTypeEquals(f *wasm.FunctionType, params []wasm.ValueType, results []was
 // getType finalizes any current params or result and returns the current type and any paramNames for it.
 //
 // If the current type is in typeParser.inlinedTypes, it is removed prior to returning.
-func (p *typeParser) getType() (sig *wasm.FunctionType, paramNames wasm.NameMap) {
-	paramNames = p.currentParamNames
+func (p *typeParser) getType() (sig *wasm.FunctionType, paramIDs map[string]wasm.Index, paramNames wasm.NameMap) {
+	if len(p.paramIDContext) > 0 {
+		paramIDs = p.paramIDContext
+		paramNames = p.paramNames
+	}
 
 	// Search inlined types in case a matching type was found after its type use.
 	for i, t := range p.inlinedTypes {
diff --git a/wasm/text/types.go b/wasm/text/types.go
index 8fafd1f9..08fa5063 100644
--- a/wasm/text/types.go
+++ b/wasm/text/types.go
@@ -87,6 +87,28 @@ type index struct {
 	col uint32
 }
 
+// idContext is an association of symbolic IDs to numeric indices.
+//
+// The Web Assembly Text Format allows use of symbolic identifiers, ex "$main", instead of numeric indices for most
+// sections, notably types, functions and parameters. For example, if a function is defined with the tokenID "$main",
+// the start section can use that symbolic ID instead of its numeric offset in the code section. These IDs require two
+// features, uniqueness and lookup, as implemented with a map. The key is stripped of the leading '$' to match other
+// tools, as described in stripDollar
+//
+// See https://www.w3.org/TR/wasm-core-1/#text-context
+type idContext map[string]wasm.Index
+
+// 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 (ctx idContext) setID(idToken []byte, idx uint32) (string, error) {
+	id := string(stripDollar(idToken))
+	if _, ok := ctx[id]; ok {
+		return id, fmt.Errorf("duplicate ID %s", idToken)
+	}
+	ctx[id] = idx
+	return id, nil
+}
+
 // importFunc corresponds to the text format of a WebAssembly function import.
 //
 // Note: the type usage of this import is at module.typeUses the same index as module.importFuncs


Test patch:
diff --git a/wasm/text/bind_test.go b/wasm/text/bind_test.go
index ba5b79af..ce86ca5f 100644
--- a/wasm/text/bind_test.go
+++ b/wasm/text/bind_test.go
@@ -421,10 +421,10 @@ func TestMergeLocalNames(t *testing.T) {
 				names:       &wasm.NameSection{},
 			},
 			inputTypeParamNames: map[wasm.Index]wasm.NameMap{
-				wasm.Index(1): {{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}},
+				wasm.Index(1): {{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}},
 			},
 			expected: wasm.IndirectNameMap{
-				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 			},
 		},
 		{
@@ -435,12 +435,12 @@ func TestMergeLocalNames(t *testing.T) {
 				importFuncs: []*importFunc{{module: "wasi_snapshot_preview1", name: "args_get"}},
 				names: &wasm.NameSection{
 					LocalNames: wasm.IndirectNameMap{
-						{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+						{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 					},
 				},
 			},
 			expected: wasm.IndirectNameMap{
-				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 			},
 		},
 		{
@@ -452,10 +452,10 @@ func TestMergeLocalNames(t *testing.T) {
 				names:       &wasm.NameSection{},
 			},
 			inputTypeParamNames: map[wasm.Index]wasm.NameMap{
-				wasm.Index(1): {{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}},
+				wasm.Index(1): {{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}},
 			},
 			expected: wasm.IndirectNameMap{
-				{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+				{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 			},
 		},
 		{
@@ -466,12 +466,12 @@ func TestMergeLocalNames(t *testing.T) {
 				importFuncs: []*importFunc{{module: "", name: ""}, {module: "wasi_snapshot_preview1", name: "args_get"}},
 				names: &wasm.NameSection{
 					LocalNames: wasm.IndirectNameMap{
-						{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+						{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 					},
 				},
 			},
 			expected: wasm.IndirectNameMap{
-				{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+				{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 			},
 		},
 		{
@@ -482,7 +482,7 @@ func TestMergeLocalNames(t *testing.T) {
 				importFuncs: []*importFunc{{module: "wasi_snapshot_preview1", name: "args_get"}},
 				names: &wasm.NameSection{
 					LocalNames: wasm.IndirectNameMap{
-						{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+						{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 					},
 				},
 			},
@@ -490,7 +490,7 @@ func TestMergeLocalNames(t *testing.T) {
 				wasm.Index(1): {{Index: wasm.Index(0), Name: "x"}, {Index: wasm.Index(0), Name: "y"}},
 			},
 			expected: wasm.IndirectNameMap{
-				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 			},
 		},
 		{
@@ -502,10 +502,10 @@ func TestMergeLocalNames(t *testing.T) {
 				names:    &wasm.NameSection{},
 			},
 			inputTypeParamNames: map[wasm.Index]wasm.NameMap{
-				wasm.Index(1): {{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}},
+				wasm.Index(1): {{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}},
 			},
 			expected: wasm.IndirectNameMap{
-				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 			},
 		},
 		{
@@ -516,12 +516,12 @@ func TestMergeLocalNames(t *testing.T) {
 				code:     []*wasm.Code{{Body: localGet0End}},
 				names: &wasm.NameSection{
 					LocalNames: wasm.IndirectNameMap{
-						{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+						{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 					},
 				},
 			},
 			expected: wasm.IndirectNameMap{
-				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 			},
 		},
 		{
@@ -533,10 +533,10 @@ func TestMergeLocalNames(t *testing.T) {
 				names:    &wasm.NameSection{},
 			},
 			inputTypeParamNames: map[wasm.Index]wasm.NameMap{
-				wasm.Index(1): {{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}},
+				wasm.Index(1): {{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}},
 			},
 			expected: wasm.IndirectNameMap{
-				{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+				{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 			},
 		},
 		{
@@ -547,12 +547,12 @@ func TestMergeLocalNames(t *testing.T) {
 				code:     []*wasm.Code{{Body: end}, {Body: localGet0End}},
 				names: &wasm.NameSection{
 					LocalNames: wasm.IndirectNameMap{
-						{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+						{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 					},
 				},
 			},
 			expected: wasm.IndirectNameMap{
-				{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+				{Index: wasm.Index(1), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 			},
 		},
 		{
@@ -563,7 +563,7 @@ func TestMergeLocalNames(t *testing.T) {
 				code:     []*wasm.Code{{Body: localGet0End}},
 				names: &wasm.NameSection{
 					LocalNames: wasm.IndirectNameMap{
-						{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+						{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 					},
 				},
 			},
@@ -571,7 +571,7 @@ func TestMergeLocalNames(t *testing.T) {
 				wasm.Index(1): {{Index: wasm.Index(0), Name: "x"}, {Index: wasm.Index(0), Name: "y"}},
 			},
 			expected: wasm.IndirectNameMap{
-				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(0), Name: "argv_buf"}}},
+				{Index: wasm.Index(0), NameMap: wasm.NameMap{{Index: wasm.Index(0), Name: "argv"}, {Index: wasm.Index(1), Name: "argv_buf"}}},
 			},
 		},
 		{
diff --git a/wasm/text/parser_test.go b/wasm/text/parser_test.go
index ec4f8e35..80a63a34 100644
--- a/wasm/text/parser_test.go
+++ b/wasm/text/parser_test.go
@@ -48,14 +48,14 @@ func TestParseModule(t *testing.T) {
 			},
 		},
 		{
-			name:  "type func param names",
+			name:  "type func param IDs",
 			input: "(module (type $mul (func (param $x f32) (param $y f32) (result f32))))",
 			expected: &module{
 				types: []*wasm.FunctionType{{Params: []wasm.ValueType{f32, f32}, Results: []wasm.ValueType{f32}}},
 			},
 		},
 		{
-			name:  "type func mixed param names", // Verifies we can handle less param fields than Params
+			name:  "type func mixed param IDs", // Verifies we can handle less param fields than Params
 			input: "(module (type (func (param i32 i32) (param $v i32) (param i64) (param $t f32))))",
 			expected: &module{
 				types: []*wasm.FunctionType{
@@ -412,7 +412,7 @@ func TestParseModule(t *testing.T) {
 			},
 		},
 		{
-			name:  "import func param names",
+			name:  "import func param IDs",
 			input: "(module (import \"Math\" \"Mul\" (func $mul (param $x f32) (param $y f32) (result f32))))",
 			expected: &module{
 				types: []*wasm.FunctionType{
@@ -450,7 +450,7 @@ func TestParseModule(t *testing.T) {
 			},
 		},
 		{
-			name:  "import func mixed param names", // Verifies we can handle less param fields than Params
+			name:  "import func mixed param IDs", // Verifies we can handle less param fields than Params
 			input: "(module (import \"\" \"\" (func (param i32 i32) (param $v i32) (param i64) (param $t f32))))",
 			expected: &module{
 				types: []*wasm.FunctionType{
@@ -766,7 +766,7 @@ func TestParseModule(t *testing.T) {
 			},
 		},
 		{
-			name:  "func param names",
+			name:  "func param IDs",
 			input: "(module (func $one (param $x i32) (param $y i32) (result i32) local.get 0))",
 			expected: &module{
 				types:    []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}},
@@ -799,7 +799,7 @@ func TestParseModule(t *testing.T) {
 			},
 		},
 		{
-			name:  "func mixed param names", // Verifies we can handle less param fields than Params
+			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: &module{
 				types:    []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32, i32, i64, f32}}},
@@ -1008,9 +1008,9 @@ func TestParseModule_Errors(t *testing.T) {
 			expectedErr: "1:10: unexpected field: test in module",
 		},
 		{
-			name:        "module double name",
+			name:        "module double ID",
 			input:       "(module $foo $bar)",
-			expectedErr: "1:14: redundant name: $bar in module",
+			expectedErr: "1:14: redundant ID $bar in module",
 		},
 		{
 			name:        "module empty field",
@@ -1038,22 +1038,32 @@ func TestParseModule_Errors(t *testing.T) {
 			expectedErr: "1:22: unexpected '(' in module.type[0]",
 		},
 		{
-			name:        "type func second name",
+			name:        "type func second ID",
 			input:       "(module (type $v_v $v_v func()))",
-			expectedErr: "1:20: redundant name in module.type[0]",
+			expectedErr: "1:20: redundant ID $v_v in module.type[0]",
 		},
 		{
-			name:        "type func param second name",
+			name:        "type ID clash",
+			input:       "(module (type $1 (func)) (type $1 (func (param i32))))",
+			expectedErr: "1:32: duplicate ID $1 in module.type[1]",
+		},
+		{
+			name:        "type func param second ID",
 			input:       "(module (type (func (param $x $y i32) ))",
-			expectedErr: "1:31: redundant name in module.type[0].func.param[0]",
+			expectedErr: "1:31: redundant ID $y in module.type[0].func.param[0]",
+		},
+		{
+			name:        "type func param ID clash",
+			input:       "(module (type (func (param $x i32) (param i32) (param $x i32)))",
+			expectedErr: "1:55: duplicate ID $x in module.type[0].func.param[2]",
 		},
 		{
-			name:        "type func param name in abbreviation",
+			name:        "type func param ID in abbreviation",
 			input:       "(module (type (func (param $x i32 i64) ))",
-			expectedErr: "1:35: cannot name parameters in abbreviated form in module.type[0].func.param[0]",
+			expectedErr: "1:35: cannot assign IDs to parameters in abbreviated form in module.type[0].func.param[0]",
 		},
 		{
-			name:        "type func name wrong place",
+			name:        "type func ID wrong place",
 			input:       "(module (type (func $v_v )))",
 			expectedErr: "1:21: unexpected id: $v_v in module.type[0].func",
 		},
@@ -1123,19 +1133,29 @@ func TestParseModule_Errors(t *testing.T) {
 			expectedErr: "1:36: unexpected id: $param in module.import[0].func",
 		},
 		{
-			name:        "import func double name",
+			name:        "import func double ID",
 			input:       "(module (import \"foo\" \"bar\" (func $baz $qux)))",
-			expectedErr: "1:40: redundant name: $qux in module.import[0].func",
+			expectedErr: "1:40: redundant ID $qux in module.import[0].func",
 		},
 		{
-			name:        "import func param second name",
+			name:        "import func clash ID",
+			input:       "(module (import \"\" \"\" (func $main)) (import \"\" \"\" (func $main)))",
+			expectedErr: "1:57: duplicate ID $main in module.import[1].func",
+		},
+		{
+			name:        "import func param second ID",
 			input:       "(module (import \"\" \"\" (func (param $x $y i32) ))",
-			expectedErr: "1:39: redundant name in module.import[0].func.param[0]",
+			expectedErr: "1:39: redundant ID $y in module.import[0].func.param[0]",
 		},
 		{
-			name:        "import func param name in abbreviation",
+			name:        "import func param ID in abbreviation",
 			input:       "(module (import \"\" \"\" (func (param $x i32 i64) ))",
-			expectedErr: "1:43: cannot name parameters in abbreviated form in module.import[0].func.param[0]",
+			expectedErr: "1:43: cannot assign IDs to parameters in abbreviated form in module.import[0].func.param[0]",
+		},
+		{
+			name:        "import func param ID clash",
+			input:       "(module (import \"\" \"\" (func (param $x i32) (param i32) (param $x i32)))_",
+			expectedErr: "1:63: duplicate ID $x in module.import[0].func.param[2]",
 		},
 		{
 			name:        "import func missing param0 type",
@@ -1193,12 +1213,12 @@ func TestParseModule_Errors(t *testing.T) {
 			expectedErr: "1:37: unexpected '(' in module.import[0].func.result",
 		},
 		{
-			name:        "import func name after param",
+			name:        "import func ID after param",
 			input:       "(module (import \"\" \"\" (func (param i32) $main)))",
 			expectedErr: "1:41: unexpected id: $main in module.import[0].func",
 		},
 		{
-			name:        "import func name after result",
+			name:        "import func ID after result",
 			input:       "(module (import \"\" \"\" (func (result i32) $main)))",
 			expectedErr: "1:42: unexpected id: $main in module.import[0].func",
 		},
@@ -1238,19 +1258,24 @@ func TestParseModule_Errors(t *testing.T) {
 			expectedErr: "1:16: unexpected id: $param in module.func[0]",
 		},
 		{
-			name:        "func double name",
+			name:        "func double param ID",
 			input:       "(module (func $baz $qux)))",
-			expectedErr: "1:20: redundant name: $qux in module.func[0]",
+			expectedErr: "1:20: redundant ID $qux in module.func[0]",
 		},
 		{
-			name:        "func param second name",
+			name:        "func param ID clash",
+			input:       "(module (func (param $x i32) (param i32) (param $x i32)))",
+			expectedErr: "1:49: duplicate ID $x in module.func[0].param[2]",
+		},
+		{
+			name:        "func param second ID",
 			input:       "(module (func (param $x $y i32) ))",
-			expectedErr: "1:25: redundant name in module.func[0].param[0]",
+			expectedErr: "1:25: redundant ID $y in module.func[0].param[0]",
 		},
 		{
-			name:        "func param name in abbreviation",
+			name:        "func param ID in abbreviation",
 			input:       "(module (func (param $x i32 i64) ))",
-			expectedErr: "1:29: cannot name parameters in abbreviated form in module.func[0].param[0]",
+			expectedErr: "1:29: cannot assign IDs to parameters in abbreviated form in module.func[0].param[0]",
 		},
 		{
 			name:        "func missing param0 type",
@@ -1308,12 +1333,27 @@ func TestParseModule_Errors(t *testing.T) {
 			expectedErr: "1:23: unexpected '(' in module.func[0].result",
 		},
 		{
-			name:        "func name after param",
+			name:        "func ID after param",
 			input:       "(module (func (param i32) $main)))",
 			expectedErr: "1:27: unexpected id: $main in module.func[0]",
 		},
 		{
-			name:        "func name after result",
+			name:        "clash on func ID",
+			input:       "(module (func $main) (func $main)))",
+			expectedErr: "1:28: duplicate ID $main in module.func[1]",
+		},
+		{
+			name:        "func ID clashes with import func ID",
+			input:       "(module (import \"\" \"\" (func $main)) (func $main)))",
+			expectedErr: "1:43: duplicate ID $main in module.func[1]",
+		},
+		{
+			name:        "import func ID clashes with func ID",
+			input:       "(module (func $main) (import \"\" \"\" (func $main)))",
+			expectedErr: "1:42: duplicate ID $main in module.import[1].func",
+		},
+		{
+			name:        "func ID after result",
 			input:       "(module (func (result i32) $main)))",
 			expectedErr: "1:28: unexpected id: $main in module.func[0]",
 		},
diff --git a/wasm/text/types_test.go b/wasm/text/types_test.go
index bab6daf1..1e03db08 100644
--- a/wasm/text/types_test.go
+++ b/wasm/text/types_test.go
@@ -5,8 +5,25 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/wazero/wasm"
 )
 
+func TestIdContext_SetId(t *testing.T) {
+	ctx := idContext{}
+	t.Run("set when empty", func(t *testing.T) {
+		id, err := ctx.setID([]byte("$x"), wasm.Index(0))
+		require.NoError(t, err)
+		require.Equal(t, "x", id) // strips "$" to be like the name section
+		require.Equal(t, idContext{"x": wasm.Index(0)}, ctx)
+	})
+	t.Run("set when exists fails", func(t *testing.T) {
+		_, err := ctx.setID([]byte("$x"), wasm.Index(0))
+		require.EqualError(t, err, "duplicate ID $x")        // keeps the original $ prefix
+		require.Equal(t, idContext{"x": wasm.Index(0)}, ctx) // no change
+	})
+}
+
 func TestFormatError_Error(t *testing.T) {
 	t.Run("with context", func(t *testing.T) {
 		require.EqualError(t, &FormatError{