Repository: tetratelabs/wazero
Pull request #133 (2022-01-14 06:44:20)

Adds CustomNameSection codec
This adds `CustomNameSection` and codec operations so that we can
decouple the text format from the binary one.
    
Note: This does not completely implement name decoding. It stops at
converting the old logic as the change became very big. I will follow-up
later to finish that.

mathetake: LGTM except moving the concepts from Wat to wasm pkg
codefromthecrypt: I think being strict about names is a cure worse than the disease, because that means to access names means serialization each time, or redefining another type that is almost the same as the binary format, just to manifest names. That said, I also don't think it is important enough yet and it can be undone later. So, I'll move the code to be pure binary serialization meaning to compare names == hex editor. If later we find this difficult, I'll do the code again in a different way.
codefromthecrypt: opened https://github.com/tetratelabs/wazero/issues/134 because I don't agree, this is just to voice an opinion. That I've done that, I'll proceed as requested! Thanks for the look
codefromthecrypt: I will rewrite this PR as a serializer since there's no way to verify without binary comparison with the current direction. This will result in a larger PR, but that's also cool as we eventually need to serialize anyway.
codefromthecrypt: I think the best way to decouple is to make a `NamesEncoder` interface with the implementation where all the other binary serialization logic is (in the wasm package which is currently implicitly the same as the binary format). Then, I can make the wat tests implement that just catching names, which avoids the problem we have now where it has to know the binary format.
mathetake: Also I think we also should have not only for names, but also for `Encoder` interface like

```
type WasmEncoder struct {
    // original is either WebAssembly text or WebAssembly binary.
    Encode(original []byte) *wasm.Module
}
```

then wasm package should be agnostic to text or binary encoding :D


mathetake: oh maybe this should be called Decoder
mathetake: Anyway thank you for putting a lot of thoughts into my comments! 
codefromthecrypt: heh since you are the reviewer I think my next PR update will give you enough.. neither of us are in a position to eagerly serialize back into text format, so let's punt that part until we are farther ahead.
codefromthecrypt: ok this was completely rewritten PTAL!
codefromthecrypt: pulled out an issue for codec type refactoring as it is cart before the horse. Highest priority is converting the text format into the binary format, not for the sake of writing it, but because currently all code depends on the binary structure. Once that's done, we can refactor it and probably do it cleaner than now anyway!

https://github.com/tetratelabs/wazero/issues/136

Patch:
diff --git a/wasm/leb128/leb128.go b/wasm/leb128/leb128.go
index 0243a8f5..bbba5d89 100644
--- a/wasm/leb128/leb128.go
+++ b/wasm/leb128/leb128.go
@@ -41,7 +41,7 @@ func EncodeUint32(value uint32) (buf []byte) {
 	}
 }
 
-func DecodeUint32(r io.Reader) (ret uint32, num uint64, err error) {
+func DecodeUint32(r io.Reader) (ret uint32, bytesRead uint64, err error) {
 	// Derived from https://github.com/golang/go/blob/aafad20b617ee63d58fcd4f6e0d98fe27760678c/src/encoding/binary/varint.go
 	// with the modification on the overflow handling tailored for 32-bits.
 	var s uint32
@@ -63,7 +63,7 @@ func DecodeUint32(r io.Reader) (ret uint32, num uint64, err error) {
 	return 0, 0, errOverflow32
 }
 
-func DecodeUint64(r io.Reader) (ret uint64, num uint64, err error) {
+func DecodeUint64(r io.Reader) (ret uint64, bytesRead uint64, err error) {
 	// Derived from https://github.com/golang/go/blob/aafad20b617ee63d58fcd4f6e0d98fe27760678c/src/encoding/binary/varint.go
 	var s uint64
 	for i := 0; i < maxVarintLen64; i++ {
@@ -84,7 +84,7 @@ func DecodeUint64(r io.Reader) (ret uint64, num uint64, err error) {
 	return 0, 0, errOverflow64
 }
 
-func DecodeInt32(r io.Reader) (ret int32, num uint64, err error) {
+func DecodeInt32(r io.Reader) (ret int32, bytesRead uint64, err error) {
 	var shift int
 	var b byte
 	for {
@@ -94,18 +94,18 @@ func DecodeInt32(r io.Reader) (ret int32, num uint64, err error) {
 		}
 		ret |= (int32(b) & 0x7f) << shift
 		shift += 7
-		num++
+		bytesRead++
 		if b&0x80 == 0 {
 			if shift < 32 && (b&0x40) != 0 {
 				ret |= ^0 << shift
 			}
 			// Over flow checks.
 			// fixme: can be optimized.
-			if num > 5 {
+			if bytesRead > 5 {
 				return 0, 0, errOverflow32
-			} else if unused := b & 0b00110000; num == 5 && ret < 0 && unused != 0b00110000 {
+			} else if unused := b & 0b00110000; bytesRead == 5 && ret < 0 && unused != 0b00110000 {
 				return 0, 0, errOverflow32
-			} else if num == 5 && ret >= 0 && unused != 0x00 {
+			} else if bytesRead == 5 && ret >= 0 && unused != 0x00 {
 				return 0, 0, errOverflow32
 			}
 			return
@@ -113,7 +113,11 @@ func DecodeInt32(r io.Reader) (ret int32, num uint64, err error) {
 	}
 }
 
-func DecodeInt33AsInt64(r io.Reader) (ret int64, num uint64, err error) {
+// DecodeInt33AsInt64 is a special cased decoder for wasm.BlockType which is encoded as a positive signed integer, yet
+// still needs to fit the 32-bit range of allowed indices. Hence, this is 33, not 32-bit!
+//
+// See https://webassembly.github.io/spec/core/binary/instructions.html#control-instructions
+func DecodeInt33AsInt64(r io.Reader) (ret int64, bytesRead uint64, err error) {
 	const (
 		int33Mask  int64 = 1 << 7
 		int33Mask2       = ^int33Mask
@@ -133,7 +137,7 @@ func DecodeInt33AsInt64(r io.Reader) (ret int64, num uint64, err error) {
 		b = int64(rb)
 		ret |= (b & int33Mask2) << shift
 		shift += 7
-		num++
+		bytesRead++
 		if b&int33Mask == 0 {
 			break
 		}
@@ -151,17 +155,17 @@ func DecodeInt33AsInt64(r io.Reader) (ret int64, num uint64, err error) {
 	}
 	// Over flow checks.
 	// fixme: can be optimized.
-	if num > 5 {
+	if bytesRead > 5 {
 		return 0, 0, errOverflow33
-	} else if unused := b & 0b00100000; num == 5 && ret < 0 && unused != 0b00100000 {
+	} else if unused := b & 0b00100000; bytesRead == 5 && ret < 0 && unused != 0b00100000 {
 		return 0, 0, errOverflow33
-	} else if num == 5 && ret >= 0 && unused != 0x00 {
+	} else if bytesRead == 5 && ret >= 0 && unused != 0x00 {
 		return 0, 0, errOverflow33
 	}
-	return ret, num, nil
+	return ret, bytesRead, nil
 }
 
-func DecodeInt64(r io.Reader) (ret int64, num uint64, err error) {
+func DecodeInt64(r io.Reader) (ret int64, bytesRead uint64, err error) {
 	const (
 		int64Mask3 = 1 << 6
 		int64Mask4 = ^0
@@ -175,18 +179,18 @@ func DecodeInt64(r io.Reader) (ret int64, num uint64, err error) {
 		}
 		ret |= (int64(b) & 0x7f) << shift
 		shift += 7
-		num++
+		bytesRead++
 		if b&0x80 == 0 {
 			if shift < 64 && (b&int64Mask3) == int64Mask3 {
 				ret |= int64Mask4 << shift
 			}
 			// Over flow checks.
 			// fixme: can be optimized.
-			if num > 10 {
+			if bytesRead > 10 {
 				return 0, 0, errOverflow64
-			} else if unused := b & 0b00111110; num == 10 && ret < 0 && unused != 0b00111110 {
+			} else if unused := b & 0b00111110; bytesRead == 10 && ret < 0 && unused != 0b00111110 {
 				return 0, 0, errOverflow64
-			} else if num == 10 && ret >= 0 && unused != 0x00 {
+			} else if bytesRead == 10 && ret >= 0 && unused != 0x00 {
 				return 0, 0, errOverflow64
 			}
 			return
diff --git a/wasm/module.go b/wasm/module.go
index 5a2e40cd..93252964 100644
--- a/wasm/module.go
+++ b/wasm/module.go
@@ -4,14 +4,15 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-
-	"github.com/tetratelabs/wazero/wasm/leb128"
 )
 
-const (
-	magic   = "\x00asm"
-	version = "\x01\x00\x00\x00"
-)
+// magic is the 4 byte preamble (literally "\0asm") of the binary format
+// See https://www.w3.org/TR/wasm-core-1/#binary-magic
+var magic = []byte{0x00, 0x61, 0x73, 0x6D}
+
+// version is format version and doesn't change between known specification versions
+// See https://www.w3.org/TR/wasm-core-1/#binary-version
+var version = []byte{0x01, 0x00, 0x00, 0x00}
 
 type reader struct {
 	binary []byte
@@ -95,6 +96,11 @@ type Module struct {
 	CustomSections map[string][]byte
 }
 
+// Encode encodes the given module into a byte slice. The result can be used directly or saved as a %.wasm file.
+func (m *Module) Encode() (bytes []byte) {
+	return m.encodeSections(append(magic, version...))
+}
+
 // DecodeModule decodes a `raw` module from io.Reader whose index spaces are yet to be initialized
 func DecodeModule(binary []byte) (*Module, error) {
 	r := &reader{binary: binary, buffer: bytes.NewBuffer(binary)}
@@ -130,61 +136,3 @@ func DecodeModule(binary []byte) (*Module, error) {
 	}
 	return ret, nil
 }
-
-func (m *Module) DecodeCustomNameSection() (map[uint32]string, error) {
-	namesec, ok := m.CustomSections["name"]
-	if !ok {
-		return nil, fmt.Errorf("'name' %w", ErrCustomSectionNotFound)
-	}
-
-	r := bytes.NewReader(namesec)
-	for {
-		id, err := r.ReadByte()
-		if err != nil {
-			return nil, fmt.Errorf("failed to read subsection ID: %w", err)
-		}
-
-		size, _, err := leb128.DecodeUint32(r)
-		if err != nil {
-			return nil, fmt.Errorf("failed to read the size of subsection %d: %w", id, err)
-		}
-
-		if id == 1 {
-			// ID = 1 is the function name subsection.
-			break
-		} else {
-			// Skip other subsections.
-			_, err := r.Seek(int64(size), io.SeekCurrent)
-			if err != nil {
-				return nil, fmt.Errorf("failed to skip subsection %d: %w", id, err)
-			}
-		}
-	}
-
-	nameVectorSize, _, err := leb128.DecodeUint32(r)
-	if err != nil {
-		return nil, fmt.Errorf("failed to read the size of name vector: %w", err)
-	}
-
-	ret := make(map[uint32]string, nameVectorSize)
-	for i := uint32(0); i < nameVectorSize; i++ {
-		functionIndex, _, err := leb128.DecodeUint32(r)
-		if err != nil {
-			return nil, fmt.Errorf("failed to read function index: %w", err)
-		}
-
-		functionNameSize, _, err := leb128.DecodeUint32(r)
-		if err != nil {
-			return nil, fmt.Errorf("failed to read function name size: %w", err)
-		}
-
-		namebuf := make([]byte, functionNameSize)
-		_, err = io.ReadFull(r, namebuf)
-		if err != nil {
-			return nil, fmt.Errorf("failed to read function name: %w", err)
-		}
-		ret[functionIndex] = string(namebuf)
-	}
-
-	return ret, nil
-}
diff --git a/wasm/names.go b/wasm/names.go
new file mode 100644
index 00000000..fbc6a39e
--- /dev/null
+++ b/wasm/names.go
@@ -0,0 +1,194 @@
+package wasm
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"sort"
+
+	"github.com/tetratelabs/wazero/wasm/leb128"
+)
+
+// CustomNameSection represent the known custom name subsections defined in the WebAssembly Binary Format
+// See https://www.w3.org/TR/wasm-core-1/#name-section%E2%91%A0
+// See https://github.com/tetratelabs/wazero/issues/134 about adding this to Module
+type CustomNameSection struct {
+	// ModuleName is the symbolic identifier for a module. Ex. math
+	ModuleName string
+	// FunctionNames is an association of a function index to its symbolic identifier. Ex. add
+	//
+	// * the key (idx) is in the function namespace, where module defined functions are preceded by imported ones.
+	//
+	// Ex. Assuming the below text format is the second import, you would expect FunctionNames[1] = "mul"
+	//	(import "Math" "Mul" (func $mul (param $x f32) (param $y f32) (result f32)))
+	//
+	// Note: FunctionNames are a map because the specification requires function indices to be unique. These are sorted
+	// during EncodeData
+	FunctionNames map[uint32]string
+
+	// LocalNames is an association of a function index to any locals which have a symbolic identifier. Ex. add x
+	//
+	// * the key (funcIndex) is in the function namespace, where module defined functions are preceded by imported ones.
+	// * the second key (idx) is in the local namespace, where parameters precede any function locals.
+	//
+	// Ex. Assuming the below text format is the second import, you would expect LocalNames[1][1] = "y"
+	//	(import "Math" "Mul" (func $mul (param $x f32) (param $y f32) (result f32)))
+	//
+	// Note: LocalNames are a map because the specification requires both function and local indices to be unique. These
+	// are sorted during EncodeData
+	LocalNames map[uint32]map[uint32]string
+}
+
+// EncodeData serializes the data associated with the "name" key in SectionIDCustom according to the standard:
+//
+// * ModuleName encode as subsection 0
+// * FunctionNames encode as subsection 1 in ascending order by function index
+// * LocalNames encode as subsection 2 in ascending order by function and local index
+//
+// Note: The result can be nil because this does not encode empty subsections
+//
+// See https://www.w3.org/TR/wasm-core-1/#binary-namesec
+func (n *CustomNameSection) EncodeData() (data []byte) {
+	if n.ModuleName != "" {
+		data = append(data, encodeNameSubsection(uint8(0), encodeSizePrefixed([]byte(n.ModuleName)))...)
+	}
+	if fd := n.encodeFunctionNameData(); len(fd) > 0 {
+		data = append(data, encodeNameSubsection(uint8(1), fd)...)
+	}
+	if ld := n.encodeLocalNameData(); len(ld) > 0 {
+		data = append(data, encodeNameSubsection(uint8(2), ld)...)
+	}
+	return
+}
+
+// encodeFunctionNameData encodes the data for the function name subsection.
+// See https://www.w3.org/TR/wasm-core-1/#binary-funcnamesec
+func (n *CustomNameSection) encodeFunctionNameData() []byte {
+	if len(n.FunctionNames) == 0 {
+		return nil
+	}
+	return encodeSortedAndSizePrefixed(n.FunctionNames)
+}
+
+func encodeSortedAndSizePrefixed(m map[uint32]string) []byte {
+	count := uint32(len(m))
+	data := leb128.EncodeUint32(count)
+
+	// Sort the keys so that they encode in ascending order
+	keys := make([]uint32, 0, count)
+	for k := range m {
+		keys = append(keys, k)
+	}
+	sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
+
+	for _, i := range keys {
+		data = append(data, encodeNameMapEntry(i, []byte(m[i]))...)
+	}
+	return data
+}
+
+// encodeLocalNameData encodes the data for the local name subsection.
+// See https://www.w3.org/TR/wasm-core-1/#binary-localnamesec
+func (n *CustomNameSection) encodeLocalNameData() []byte {
+	if len(n.LocalNames) == 0 {
+		return nil
+	}
+
+	funcNameCount := uint32(len(n.LocalNames))
+	subsection := leb128.EncodeUint32(funcNameCount)
+
+	// Sort the function indices so that they encode in ascending order
+	funcIndex := make([]uint32, 0, funcNameCount)
+	for k := range n.LocalNames {
+		funcIndex = append(funcIndex, k)
+	}
+	sort.Slice(funcIndex, func(i, j int) bool { return funcIndex[i] < funcIndex[j] })
+
+	for _, i := range funcIndex {
+		locals := encodeSortedAndSizePrefixed(n.LocalNames[i])
+		subsection = append(subsection, append(leb128.EncodeUint32(i), locals...)...)
+	}
+	return subsection
+}
+
+// encodeNameSubsection returns a buffer encoding the given subsection
+// See https://www.w3.org/TR/wasm-core-1/#subsections%E2%91%A0
+func encodeNameSubsection(subsectionID uint8, content []byte) []byte {
+	contentSizeInBytes := leb128.EncodeUint32(uint32(len(content)))
+	result := []byte{subsectionID}
+	result = append(result, contentSizeInBytes...)
+	result = append(result, content...)
+	return result
+}
+
+// encodeNameMapEntry encodes the index and data prefixed by their size.
+// See https://www.w3.org/TR/wasm-core-1/#binary-namemap
+func encodeNameMapEntry(i uint32, data []byte) []byte {
+	return append(leb128.EncodeUint32(i), encodeSizePrefixed(data)...)
+}
+
+// encodeSizePrefixed encodes the data prefixed by their size.
+func encodeSizePrefixed(data []byte) []byte {
+	size := leb128.EncodeUint32(uint32(len(data)))
+	return append(size, data...)
+}
+
+// DecodeCustomNameSection deserializes the data associated with the "name" key in SectionIDCustom according to the
+// standard:
+//
+// * ModuleName decode from subsection 0
+// * FunctionNames decode from subsection 1
+// * LocalNames decode from subsection 2
+//
+// See https://www.w3.org/TR/wasm-core-1/#binary-namesec
+func DecodeCustomNameSection(data []byte) (*CustomNameSection, error) {
+	r := bytes.NewReader(data)
+	for {
+		id, err := r.ReadByte()
+		if err != nil {
+			return nil, fmt.Errorf("failed to read subsection ID: %w", err)
+		}
+
+		size, _, err := leb128.DecodeUint32(r)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read the size of subsection %d: %w", id, err)
+		}
+
+		if id == 1 { // the function name subsection.
+			break
+		} else { // TODO: ModuleName and LocalNames!
+			// Skip other subsections.
+			_, err := r.Seek(int64(size), io.SeekCurrent)
+			if err != nil {
+				return nil, fmt.Errorf("failed to skip subsection %d: %w", id, err)
+			}
+		}
+	}
+
+	nameVectorSize, _, err := leb128.DecodeUint32(r)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read the size of name vector: %w", err)
+	}
+
+	funcNames := make(map[uint32]string, nameVectorSize)
+	for i := uint32(0); i < nameVectorSize; i++ {
+		functionIndex, _, err := leb128.DecodeUint32(r)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read function index: %w", err)
+		}
+
+		functionNameSize, _, err := leb128.DecodeUint32(r)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read function name size: %w", err)
+		}
+
+		namebuf := make([]byte, functionNameSize)
+		_, err = io.ReadFull(r, namebuf)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read function name: %w", err)
+		}
+		funcNames[functionIndex] = string(namebuf)
+	}
+
+	return &CustomNameSection{FunctionNames: funcNames}, nil
+}
diff --git a/wasm/section.go b/wasm/section.go
index 2924c214..c20a56ff 100644
--- a/wasm/section.go
+++ b/wasm/section.go
@@ -25,24 +25,82 @@ const (
 	SectionIDData     SectionID = 11
 )
 
+// writeSections appends this module's sections into the buffer in the order required by the specification, custom
+// section first.
+// See https://www.w3.org/TR/wasm-core-1/#modules%E2%91%A0%E2%93%AA
+func (m *Module) encodeSections(buffer []byte) (bytes []byte) {
+	bytes = buffer
+	for name, data := range m.CustomSections {
+		bytes = append(bytes, encodeCustomSection(name, data)...)
+	}
+	if len(m.TypeSection) > 0 {
+		panic("TODO: TypeSection")
+	}
+	if len(m.ImportSection) > 0 {
+		panic("TODO: ImportSection")
+	}
+	if len(m.FunctionSection) > 0 {
+		panic("TODO: FunctionSection")
+	}
+	if len(m.TableSection) > 0 {
+		panic("TODO: TableSection")
+	}
+	if len(m.MemorySection) > 0 {
+		panic("TODO: MemorySection")
+	}
+	if len(m.GlobalSection) > 0 {
+		panic("TODO: GlobalSection")
+	}
+	if len(m.ExportSection) > 0 {
+		panic("TODO: ExportSection")
+	}
+	if m.StartSection != nil {
+		panic("TODO: StartSection")
+	}
+	if len(m.ElementSection) > 0 {
+		panic("TODO: ElementSection")
+	}
+	if len(m.CodeSection) > 0 {
+		panic("TODO: CodeSection")
+	}
+	if len(m.DataSection) > 0 {
+		panic("TODO: DataSection")
+	}
+	return
+}
+
+// encodeCustomSection encodes the opaque bytes for the given name as a SectionIDCustom
+// See https://www.w3.org/TR/wasm-core-1/#binary-customsec
+func encodeCustomSection(name string, data []byte) []byte {
+	// The contents of a custom section is the non-empty name followed by potentially empty opaque data
+	contents := append(encodeSizePrefixed([]byte(name)), data...)
+	return encodeSection(SectionIDCustom, contents)
+}
+
+// encodeSection encodes the sectionID, the size of its contents in bytes, followed by the contents.
+// See https://www.w3.org/TR/wasm-core-1/#sections%E2%91%A0
+func encodeSection(sectionID SectionID, contents []byte) []byte {
+	return append([]byte{sectionID}, encodeSizePrefixed(contents)...)
+}
+
 func (m *Module) readSections(r *reader) error {
 	for {
-		b := make([]byte, 1)
-		if _, err := io.ReadFull(r, b); err == io.EOF {
+		sectionID := make([]byte, 1)
+		if _, err := io.ReadFull(r, sectionID); err == io.EOF {
 			return nil
 		} else if err != nil {
 			return fmt.Errorf("read section id: %w", err)
 		}
 
-		ss, _, err := leb128.DecodeUint32(r)
+		sectionSize, _, err := leb128.DecodeUint32(r)
 		if err != nil {
-			return fmt.Errorf("get size of section for id=%d: %v", SectionID(b[0]), err)
+			return fmt.Errorf("get size of section for id=%d: %v", SectionID(sectionID[0]), err)
 		}
 
 		sectionContentStart := r.read
-		switch b[0] {
+		switch sectionID[0] {
 		case SectionIDCustom:
-			err = m.readSectionCustom(r, int(ss))
+			err = m.readSectionCustom(r, int(sectionSize))
 		case SectionIDType:
 			err = m.readSectionTypes(r)
 		case SectionIDImport:
@@ -69,18 +127,18 @@ func (m *Module) readSections(r *reader) error {
 			err = ErrInvalidSectionID
 		}
 
-		if err == nil && sectionContentStart+int(ss) != r.read {
-			err = fmt.Errorf("invalid section length: expected to be %d but got %d", ss, r.read-sectionContentStart)
+		if err == nil && sectionContentStart+int(sectionSize) != r.read {
+			err = fmt.Errorf("invalid section length: expected to be %d but got %d", sectionSize, r.read-sectionContentStart)
 		}
 
 		if err != nil {
-			return fmt.Errorf("section ID %d: %v", b[0], err)
+			return fmt.Errorf("section ID %d: %v", sectionID[0], err)
 		}
 	}
 }
 
 func (m *Module) readSectionCustom(r *reader, sectionSize int) error {
-	nameLen, nameLenBytes, err := leb128.DecodeUint32(r)
+	nameLen, nameLenSize, err := leb128.DecodeUint32(r)
 	if err != nil {
 		return fmt.Errorf("cannot read custom section name length")
 	}
@@ -93,16 +151,16 @@ func (m *Module) readSectionCustom(r *reader, sectionSize int) error {
 	if !utf8.Valid(nameBuf) {
 		return fmt.Errorf("custom section name must be valid utf8")
 	}
-	contentLen := int(sectionSize) - int(nameLenBytes) - int(nameLen)
-	if contentLen < 0 {
+	dataSize := sectionSize - int(nameLenSize) - int(nameLen)
+	if dataSize < 0 {
 		return fmt.Errorf("malformed custom section %s", string(nameBuf))
 	}
-	contents := make([]byte, contentLen)
-	_, err = io.ReadFull(r, contents)
+	data := make([]byte, dataSize)
+	_, err = io.ReadFull(r, data)
 	if err != nil {
-		return fmt.Errorf("cannot read custom section contents: %v", err)
+		return fmt.Errorf("cannot read custom section data: %v", err)
 	}
-	m.CustomSections[string(nameBuf)] = contents
+	m.CustomSections[string(nameBuf)] = data
 	return nil
 }
 
diff --git a/wasm/store.go b/wasm/store.go
index 7bb20820..1ba61490 100644
--- a/wasm/store.go
+++ b/wasm/store.go
@@ -433,7 +433,13 @@ func (s *Store) buildFunctionInstances(module *Module, target *ModuleInstance) (
 	memoryDeclarations = append(memoryDeclarations, module.MemorySection...)
 	tableDeclarations = append(tableDeclarations, module.TableSection...)
 
-	functionNames, _ := module.DecodeCustomNameSection()
+	// TODO: decode during Module.Decode per #134
+	var functionNames map[uint32]string
+	if data, ok := module.CustomSections["name"]; ok {
+		if c, decodeErr := DecodeCustomNameSection(data); decodeErr == nil {
+			functionNames = c.FunctionNames
+		}
+	}
 	if functionNames == nil {
 		// We cannot guarantee the existence of "name" custom section
 		// in the binary. That is because the custom section is optional
diff --git a/wasm/value.go b/wasm/value.go
index bf7fa12b..7f5ee45c 100644
--- a/wasm/value.go
+++ b/wasm/value.go
@@ -8,6 +8,10 @@ import (
 	"github.com/tetratelabs/wazero/wasm/leb128"
 )
 
+// ValueType is the binary encoding of a type such as i32
+// See https://www.w3.org/TR/wasm-core-1/#binary-valtype
+//
+// Note: This is a type alias as it is easier to encode and decode in the binary format.
 type ValueType = byte
 
 const (
diff --git a/wasm/wat/bind.go b/wasm/wat/bind.go
index 15556d18..e22030c0 100644
--- a/wasm/wat/bind.go
+++ b/wasm/wat/bind.go
@@ -101,7 +101,7 @@ func bindSymbolicIDToNumericIndex(idToIndex map[string]uint32, idx *index, conte
 		context = fmt.Sprintf(context, contextArg0)
 	}
 	return &FormatError{idx.line, idx.col, context,
-		fmt.Errorf("unknown ID %s", idx.ID),
+		fmt.Errorf("unknown ID $%s", idx.ID), // re-attach '$' as that was in the text format!
 	}
 }
 
diff --git a/wasm/wat/convert.go b/wasm/wat/convert.go
index 4b358bd7..2f75aaaf 100644
--- a/wasm/wat/convert.go
+++ b/wasm/wat/convert.go
@@ -21,6 +21,11 @@ func TextToBinary(source []byte) (result *wasm.Module, err error) {
 	// Next, we need to convert the types from the text format into the binary one. This is easy because the only
 	// difference is that the text format has type names and the binary format does not.
 	result = &wasm.Module{}
+	names := wasm.CustomNameSection{
+		ModuleName:    m.name,
+		FunctionNames: map[uint32]string{},
+		LocalNames:    map[uint32]map[uint32]string{},
+	}
 	for _, t := range m.types {
 		var results []wasm.ValueType
 		if t.result != 0 {
@@ -31,18 +36,39 @@ func TextToBinary(source []byte) (result *wasm.Module, err error) {
 
 	// Now, handle any imported functions. Notably, we retain the same insertion order as defined in the text format in
 	// case a numeric index is used for the start function (or another reason such as the call instruction).
-	for _, f := range m.importFuncs {
+	for i, f := range m.importFuncs {
+		funcidx := uint32(i)
+		if f.funcName != "" {
+			names.FunctionNames[funcidx] = f.funcName
+		}
+		if f.paramNames != nil {
+			locals := map[uint32]string{}
+			names.LocalNames[funcidx] = locals
+			for _, pn := range f.paramNames {
+				locals[pn.index] = string(pn.name)
+			}
+		}
 		result.ImportSection = append(result.ImportSection, &wasm.ImportSegment{
 			Module: f.module, Name: f.name,
-			Desc: &wasm.ImportDesc{Kind: wasm.ImportKindFunction, FuncTypeIndex: f.typeIndex.numeric},
+			Desc: &wasm.ImportDesc{
+				Kind:          wasm.ImportKindFunction,
+				FuncTypeIndex: f.typeIndex.numeric,
+			},
 		})
 	}
 
+	// TODO: module function and local names
+
 	// The start function is called on Module.Instantiate.
 	if m.startFunction != nil {
 		result.StartSection = &m.startFunction.numeric
 	}
 
-	// TODO: encode CustomSection["name"] with module function and local names
+	// Encode the custom name section, if there is any data in it
+	if nameData := names.EncodeData(); nameData != nil {
+		result.CustomSections = map[string][]byte{
+			"name": nameData,
+		}
+	}
 	return
 }
diff --git a/wasm/wat/index_parser.go b/wasm/wat/index_parser.go
index c71e5b0f..6fe5db4b 100644
--- a/wasm/wat/index_parser.go
+++ b/wasm/wat/index_parser.go
@@ -46,7 +46,7 @@ func (p *indexParser) parseIndex(tok tokenType, tokenBytes []byte, line, col uin
 		if p.currentIndex != nil {
 			return errors.New("redundant index")
 		}
-		p.currentIndex = &index{ID: string(tokenBytes), line: line, col: col}
+		p.currentIndex = &index{ID: string(stripDollar(tokenBytes)), line: line, col: col}
 	case tokenRParen: // end of this field
 		if p.currentIndex == nil {
 			return errors.New("missing index")
diff --git a/wasm/wat/names.go b/wasm/wat/names.go
deleted file mode 100644
index ab3e7975..00000000
--- a/wasm/wat/names.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package wat
-
-import (
-	"github.com/tetratelabs/wazero/wasm/leb128"
-)
-
-// encodeNameSection encodes a possibly empty buffer representing the "name" wasm.Module CustomSection.
-// See https://www.w3.org/TR/wasm-core-1/#name-section%E2%91%A0
-func encodeNameSection(m *module) []byte {
-	funcNameCount := uint32(0)
-	var funcNameEntries []byte
-	for _, f := range m.importFuncs {
-		if f.funcName != "" {
-			funcNameCount = funcNameCount + 1
-			funcNameEntries = append(funcNameEntries, encodeNameMapEntry(uint32(f.importIndex), f.funcName)...)
-		}
-	}
-	var nameSection []byte
-	if m.name != "" {
-		// See https://www.w3.org/TR/wasm-core-1/#binary-modulenamesec
-		nameSection = append(nameSection, encodeNameSubsection(uint8(0), encodeName(m.name))...)
-	}
-	if funcNameCount > 0 {
-		// See https://www.w3.org/TR/wasm-core-1/#binary-funcnamesec
-		content := leb128.EncodeUint32(funcNameCount)
-		content = append(content, funcNameEntries...)
-		nameSection = append(nameSection, encodeNameSubsection(uint8(1), content)...)
-	}
-	return nameSection
-}
-
-// This returns a buffer encoding the given subsection
-// See https://www.w3.org/TR/wasm-core-1/#subsections%E2%91%A0
-func encodeNameSubsection(subsectionID uint8, content []byte) []byte {
-	contentSizeInBytes := leb128.EncodeUint32(uint32(len(content)))
-	result := []byte{subsectionID}
-	result = append(result, contentSizeInBytes...)
-	result = append(result, content...)
-	return result
-}
-
-// encodeNameMapEntry encodes the index and name prefixed by their size.
-// See https://www.w3.org/TR/wasm-core-1/#binary-namemap
-func encodeNameMapEntry(i uint32, name string) []byte {
-	return append(leb128.EncodeUint32(i), encodeName(name)...)
-}
-
-// encodeName encodes the name prefixed by its size, stripping any leading '$'
-//
-// The WebAssembly 1.0 specification includes support for naming modules, functions, locals and tables via the custom
-// 'name' section: https://www.w3.org/TR/wasm-core-1/#binary-namesec However, how this round-trips between the text and
-// binary format is not discussed.
-//
-// We know that in the text format names must be dollar-sign prefixed to conform with tokenID conventions. However, we
-// don't know if the user's intent was a dollar-sign or not. For example, a function written in a higher level language,
-// targeting the binary format may end up prefixed with '$' for other reasons.
-//
-// This round-tripping concern materializes when a function written in the text format is transpiled into the binary
-// format (ex via `wat2wasm --debug-names`). For example, if a module name was encoded literally in the binary custom
-// 'name' section as "$Math", wabt tools would prefix it again, resulting in "$$Math".
-// https://github.com/WebAssembly/wabt/blob/e59cf9369004a521814222afbc05ae6b59446cd5/src/binary-reader-ir.cc#L1279
-//
-// Until the standard clarifies round-tripping concerns between the text and binary format, we chop off the leading '$'
-// when reading any names from the text format. This prevents awkwardness while wabt tools are in use.
-func encodeName(name string) []byte {
-	nameBytes := []byte(name[1:])
-	nameSize := leb128.EncodeUint32(uint32(len(nameBytes)))
-	content := append(nameSize, nameBytes...)
-	return content
-}
diff --git a/wasm/wat/parser.go b/wasm/wat/parser.go
index df72f7a0..c8292f7c 100644
--- a/wasm/wat/parser.go
+++ b/wasm/wat/parser.go
@@ -197,14 +197,13 @@ func (p *moduleParser) endField() {
 // next parser to parseModule. If the token isn't a tokenID, this calls parseModule.
 //
 // Ex. A module name is present `(module $math)`
-//                       records $math --^
+//                        records math --^
 //
 // Ex. No module name `(module)`
 //   calls parseModule here --^
 func (p *moduleParser) parseModuleName(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $Math
-		name := string(tokenBytes)
-		p.module.name = name
+		p.module.name = string(stripDollar(tokenBytes))
 		p.tokenParser = p.parseModule
 		return nil
 	}
@@ -230,14 +229,14 @@ func (p *moduleParser) parseModule(tok tokenType, tokenBytes []byte, _, _ uint32
 // if not found.
 //
 // Ex. A type name is present `(type $t0 (func (result i32)))`
-//                     records $t0 --^   ^
+//                      records t0 --^   ^
 //              parseType resumes here --+
 //
 // Ex. No type name `(type (func (result i32)))`
 //       calls parseType --^
 func (p *moduleParser) parseTypeName(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $v_v
-		p.currentValue0 = tokenBytes
+		p.currentValue0 = stripDollar(tokenBytes)
 		p.tokenParser = p.parseType
 		return nil
 	}
@@ -401,7 +400,7 @@ func (p *moduleParser) parseImport(tok tokenType, tokenBytes []byte, _, _ uint32
 // and sets the next parser to parseImportFunc. If the token isn't a tokenID, this calls parseImportFunc.
 //
 // Ex. A function name is present `(import "Math" "PI" (func $math.pi (result f32))`
-//                                   records $math.pi here --^
+//                                    records math.pi here --^
 //                                     parseImportFunc resumes here --^
 //
 // Ex. No function name `(import "Math" "PI" (func (result f32))`
@@ -409,7 +408,7 @@ func (p *moduleParser) parseImport(tok tokenType, tokenBytes []byte, _, _ uint32
 func (p *moduleParser) parseImportFuncName(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $main
 		fn := p.module.importFuncs[len(p.module.importFuncs)-1]
-		fn.funcName = string(tokenBytes)
+		fn.funcName = string(stripDollar(tokenBytes))
 		p.tokenParser = p.parseImportFunc
 		return nil
 	}
diff --git a/wasm/wat/token.go b/wasm/wat/token.go
index 5d9ae11d..f75c0ba5 100644
--- a/wasm/wat/token.go
+++ b/wasm/wat/token.go
@@ -172,3 +172,24 @@ func _idChar(ch byte) bool {
 	}
 	return false
 }
+
+// stripDollar returns the input without a leading '$'
+//
+// The WebAssembly 1.0 specification includes support for naming modules, functions, locals and tables via the custom
+// 'name' section: https://www.w3.org/TR/wasm-core-1/#binary-namesec However, how this round-trips between the text and
+// binary format is not discussed.
+//
+// We know that in the text format names must be dollar-sign prefixed to conform with tokenID conventions. However, we
+// don't know if the user's intent was a dollar-sign or not. For example, a function written in a higher level language,
+// targeting the binary format may end up prefixed with '$' for other reasons.
+//
+// This round-tripping concern materializes when a function written in the text format is transpiled into the binary
+// format (ex via `wat2wasm --debug-names`). For example, if a module name was encoded literally in the binary custom
+// 'name' section as "$Math", wabt tools would prefix it again, resulting in "$$Math".
+// https://github.com/WebAssembly/wabt/blob/e59cf9369004a521814222afbc05ae6b59446cd5/src/binary-reader-ir.cc#L1279
+//
+// Until the standard clarifies round-tripping concerns between the text and binary format, we chop off the leading '$'
+// when reading any names from the text format. This prevents awkwardness while wabt tools are in use.
+func stripDollar(tokenID []byte) []byte {
+	return tokenID[1:] // we don't check for leading '$' because we know the call sites must have one per tokenID
+}
diff --git a/wasm/wat/type_parser.go b/wasm/wat/type_parser.go
index f49ce01d..6760db9d 100644
--- a/wasm/wat/type_parser.go
+++ b/wasm/wat/type_parser.go
@@ -193,14 +193,14 @@ func (p *typeParser) parseMoreParamsOrResult(tok tokenType, tokenBytes []byte, l
 // found.
 //
 // Ex. A param name is present `(param $x i32)`
-//                        records $x --^  ^
+//                         records x --^  ^
 //              parseParam resumes here --+
 //
 // Ex. No param name `(param i32)`
 //        calls parseParam --^
 func (p *typeParser) parseParamName(tok tokenType, tokenBytes []byte, line, col uint32) error {
 	if tok == tokenID { // Ex. $len
-		p.currentParamName = tokenBytes
+		p.currentParamName = stripDollar(tokenBytes)
 		p.m.tokenParser = p.parseParam
 		return nil
 	}
diff --git a/wasm/wat/types.go b/wasm/wat/types.go
index a90cb1b9..9e95c00b 100644
--- a/wasm/wat/types.go
+++ b/wasm/wat/types.go
@@ -14,7 +14,7 @@ import (
 //
 // See https://www.w3.org/TR/wasm-core-1/#functions%E2%91%A7
 type module struct {
-	// name is optional and starts with '$'. For example, "$test".
+	// name is optional. For example, "test".
 	// See https://www.w3.org/TR/wasm-core-1/#modules%E2%91%A0%E2%91%A2
 	//
 	// Note: The name may also be stored in the wasm.Module CustomSection under the key "name" subsection 0.
@@ -91,7 +91,7 @@ type index struct {
 //
 // See https://www.w3.org/TR/wasm-core-1/#text-functype
 type typeFunc struct {
-	// name starts with '$'. For example, "$v_v", and only set when explicitly defined in module.types
+	// name is only set when explicitly defined in module.types. Ex. v_v
 	//
 	// name is only used for debugging. At runtime, types are called based on raw numeric index. The type index space
 	// begins those explicitly defined in module.types, followed by any inlined ones.
@@ -116,7 +116,7 @@ func funcTypeEquals(t *typeFunc, params []wasm.ValueType, result wasm.ValueType)
 }
 
 // paramNames are the possibly empty association of names that correspond with params. The index is to params and
-// the name will never be empty and always begin with '$' (ex. "$len") (tokenID). Ex. $x names (param $x i32)
+// the name will never be empty. Ex. x is the name of (param $x i32)
 //
 // paramNames are only used for debugging. At runtime, parameters are called based on raw numeric index.
 //
@@ -158,15 +158,15 @@ type importFunc struct {
 
 	// module is the possibly empty module name to import. Ex. "" or "Math"
 	//
-	// Note: This is not necessarily the module.name, so it does not need to begin with '$'!
+	// Note: This is not necessarily the module.name
 	module string
 
 	// name is the possibly empty entity name to import. Ex. "" or "PI"
 	//
-	// Note: This is not necessarily the funcName, so it does not need to begin with '$'!
+	// Note: This is not necessarily the funcName
 	name string
 
-	// funcName starts with '$'. For example, "$main".
+	// funcName is optional. Ex. main
 	//
 	// funcName is only used for debugging. At runtime, functions are called based on raw numeric index. The function
 	// index space begins with imported functions, followed by any defined in this module.


Test patch:
diff --git a/wasm/module_test.go b/wasm/module_test.go
index 9be307ee..fe7f4e8e 100644
--- a/wasm/module_test.go
+++ b/wasm/module_test.go
@@ -45,67 +45,35 @@ func TestDecodeModule(t *testing.T) {
 	}
 }
 
-func TestModuleDecodeCustomNameSection(t *testing.T) {
-	m := Module{
-		CustomSections: map[string][]byte{},
+func TestModule_Encode(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    *Module
+		expected []byte
+	}{
+		{
+			name:     "empty",
+			input:    &Module{CustomSections: map[string][]byte{}},
+			expected: append(magic, version...),
+		},
+		{
+			name: "only name section",
+			input: &Module{CustomSections: map[string][]byte{
+				"name": {1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+			}},
+			expected: append(append(magic, version...), SectionIDCustom,
+				byte(1+4+10), // size prefixed "name", followed by the section data
+				0x04, 'n', 'a', 'm', 'e',
+				1, 2, 3, 4, 5, 6, 7, 8, 9, 0),
+		},
 	}
-	// Name section not found.
-	_, err := m.DecodeCustomNameSection()
-	require.Error(t, err)
-
-	// Name section found, but cannot read subsection ID
-	m.CustomSections["name"] = []byte{}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read subsection")
 
-	// Cannot read the subsection size
-	m.CustomSections["name"] = []byte{0x04}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read the size of subsection 4")
-
-	// Function name subsection found, but name vector size not found.
-	m.CustomSections["name"] = []byte{0x01, 0x00}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read the size of name vector: EOF")
-
-	// Function name subsection found with name vector size=1.
-	// But cannot read the vector content with EOF.
-	m.CustomSections["name"] = []byte{0x01, 0x01, 0x01}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read function index: EOF")
-	m.CustomSections["name"] = []byte{0x01, 0x01, 0x01, 0x00}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read function name size: EOF")
-	m.CustomSections["name"] = []byte{0x01, 0x01, 0x01, 0x00, 0x01}
-	_, err = m.DecodeCustomNameSection()
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "failed to read function name: EOF")
-
-	// Valid inputs.
-	m.CustomSections["name"] = []byte{
-		0x01 /* function subsection id */, 0x04, /* subsection size*/
-		0x01, /* size of name map */
-		0x00 /* function index*/, 0x01 /* size of name */, 'a',
-	}
-	names, err := m.DecodeCustomNameSection()
-	require.NoError(t, err)
-	require.Equal(t, "a", names[0])
+	for _, tt := range tests {
+		tc := tt
 
-	m.CustomSections["name"] = []byte{
-		0x00, 0x00, // other subsections.
-		0x03, 0x01, 0x00, // other subsections.
-		0x01 /* function subsection id */, 0x04, /* subsection size*/
-		0x02, /* size of name map */
-		0x00 /* function index*/, 0x01 /* size of name */, 'a',
-		0x01 /* function index*/, 0x02 /* size of name */, 'a', 'b',
+		t.Run(tc.name, func(t *testing.T) {
+			bytes := tc.input.Encode()
+			require.Equal(t, tc.expected, bytes)
+		})
 	}
-	names, err = m.DecodeCustomNameSection()
-	require.NoError(t, err)
-	require.Equal(t, "a", names[0])
-	require.Equal(t, "ab", names[1])
 }
diff --git a/wasm/names_test.go b/wasm/names_test.go
new file mode 100644
index 00000000..d13a2c47
--- /dev/null
+++ b/wasm/names_test.go
@@ -0,0 +1,272 @@
+package wasm
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestCustomNameSection_EncodeData(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    *CustomNameSection
+		expected []byte
+	}{
+		{
+			name:  "empty",
+			input: &CustomNameSection{FunctionNames: map[uint32]string{}, LocalNames: map[uint32]map[uint32]string{}},
+		},
+		{
+			name: "only module",
+			// Ex. (module $simple )
+			input: &CustomNameSection{ModuleName: "simple"},
+			expected: []byte{
+				0x00, // Module subsection ID zero
+				0x07, // 7 bytes to follow
+				0x06, // the Module name simple is 6 bytes long
+				's', 'i', 'm', 'p', 'l', 'e',
+			},
+		},
+		{
+			name: "module and function name",
+			//	(module $simple
+			//		(import "" "Hello" (func $hello))
+			//		(start $hello)
+			//	)
+			input: &CustomNameSection{
+				ModuleName:    "simple",
+				FunctionNames: map[uint32]string{0x00: "hello"},
+			},
+			expected: []byte{
+				0x00, // Module subsection ID zero
+				0x07, // 7 bytes to follow
+				0x06, // the Module name simple is 6 bytes long
+				's', 'i', 'm', 'p', 'l', 'e',
+				0x01, // function subsection ID one
+				0x08, // 8 bytes to follow
+				0x01, // one function name
+				0x00, // the function index is zero
+				0x05, // the function name hello is 5 bytes long
+				'h', 'e', 'l', 'l', 'o',
+			},
+		},
+		{
+			name: "two function names", // Ex. TinyGo which at one point didn't set a module name
+			//	(module
+			//		(import "wasi_snapshot_preview1" "args_sizes_get" (func $runtime.args_sizes_get (param i32, i32) (result i32)))
+			//		(import "wasi_snapshot_preview1" "fd_write" (func $runtime.fd_write (param i32, i32, i32, i32) (result i32)))
+			//	)
+			input: &CustomNameSection{
+				FunctionNames: map[uint32]string{
+					0x00: "runtime.args_sizes_get",
+					0x01: "runtime.fd_write",
+				},
+			},
+			expected: []byte{
+				0x01, // function subsection ID one
+				0x2b, // 43 bytes to follow
+				0x02, // two function names
+				0x00, // the function index is zero
+				0x16, // the function name runtime.args_sizes_get is 22 bytes long
+				'r', 'u', 'n', 't', 'i', 'm', 'e', '.', 'a', 'r', 'g', 's', '_', 's', 'i', 'z', 'e', 's', '_', 'g', 'e', 't',
+				0x01, // the function index is one
+				0x10, // the function name runtime.fd_write is 16 bytes long
+				'r', 'u', 'n', 't', 'i', 'm', 'e', '.', 'f', 'd', '_', 'w', 'r', 'i', 't', 'e',
+			},
+		},
+		{
+			name: "function with local names",
+			//	(module
+			//		(import "Math" "Mul" (func $mul (param $x f32) (param $y f32) (result f32)))
+			//		(import "Math" "Add" (func $add (param $l f32) (param $r f32) (result f32)))
+			//	)
+			input: &CustomNameSection{
+				FunctionNames: map[uint32]string{
+					0x00: "mul",
+					0x01: "add",
+				},
+				LocalNames: map[uint32]map[uint32]string{
+					0x00: {0x00: "x", 0x01: "y"},
+					0x01: {0x00: "l", 0x01: "r"},
+				},
+			},
+			expected: []byte{
+				0x01, 0x0b, // function subsection ID one (7 bytes)
+				0x02,                      // two function names
+				0x00, 0x03, 'm', 'u', 'l', // index 0, size of "mul", "mul"
+				0x01, 0x03, 'a', 'd', 'd', // index 1, size of "add", "add"
+				0x02, 0x11, // local subsection ID two (17 bytes)
+				0x02,       // two functions
+				0x00, 0x02, // index 0 has 2 locals
+				0x00, 0x01, 'x', // index 0, size of "x", "x"
+				0x01, 0x01, 'y', // index 1, size of "y", "y"
+				0x01, 0x02, // index 1 has 2 locals
+				0x00, 0x01, 'l', // index 0, size of "l", "l"
+				0x01, 0x01, 'r', // index 1, size of "r", "r"
+			},
+		},
+		{
+			name: "function with local names - out of order",
+			// Names are associated with functions and parameters which are ordered in the module, but decoupled via
+			// CustomNameSection. The impact is they can become out-of-order, so we have to sort during encode.
+			//
+			// Note: We can't force map iteration out of order. However, reversing the order of the same values across
+			// two tests improves the likelihood of needing to sort!
+			input: &CustomNameSection{
+				FunctionNames: map[uint32]string{
+					0x01: "add",
+					0x00: "mul",
+				},
+				LocalNames: map[uint32]map[uint32]string{
+					0x01: {0x01: "r", 0x00: "l"},
+					0x00: {0x01: "y", 0x00: "x"},
+				},
+			},
+			expected: (&CustomNameSection{
+				FunctionNames: map[uint32]string{
+					0x00: "mul",
+					0x01: "add",
+				},
+				LocalNames: map[uint32]map[uint32]string{
+					0x00: {0x00: "x", 0x01: "y"},
+					0x01: {0x00: "l", 0x01: "r"},
+				},
+			}).EncodeData(),
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			bytes := tc.input.EncodeData()
+			require.Equal(t, tc.expected, bytes)
+		})
+	}
+}
+
+func TestEncodeNameSubsection(t *testing.T) {
+	subsectionID := uint8(1)
+	name := []byte("simple")
+	require.Equal(t, []byte{
+		subsectionID,
+		byte(1 + 6), // 1 is the size of 6 in LEB128 encoding
+		6, 's', 'i', 'm', 'p', 'l', 'e'}, encodeNameSubsection(subsectionID, encodeSizePrefixed(name)))
+}
+
+func TestEncodeNameMapEntry(t *testing.T) {
+	index := uint32(1)
+	name := []byte("hello")
+	require.Equal(t, []byte{byte(index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameMapEntry(index, name))
+}
+
+func TestEncodeSizePrefixed(t *testing.T) {
+	// We expect size in bytes (LEB128 encoded) then the bytes
+	require.Equal(t, []byte{5, 'h', 'e', 'l', 'l', 'o'}, encodeSizePrefixed([]byte("hello")))
+}
+
+func TestDecodeCustomNameSection(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    []byte
+		expected *CustomNameSection
+	}{
+		{
+			name: "one function name",
+			//	(module (import "" "Hello" (func $hello)))
+			input: []byte{
+				0x01, // function subsection ID one
+				0x08, // 8 bytes to follow
+				0x01, // one function name
+				0x00, // the function index is zero
+				0x05, // the function name hello is 5 bytes long
+				'h', 'e', 'l', 'l', 'o',
+			},
+			expected: &CustomNameSection{
+				FunctionNames: map[uint32]string{0x00: "hello"},
+			},
+		},
+		{
+			name: "two function names", // Ex. TinyGo which at one point didn't set a module name
+			//	(module
+			//		(import "wasi_snapshot_preview1" "args_sizes_get" (func $runtime.args_sizes_get (param i32, i32) (result i32)))
+			//		(import "wasi_snapshot_preview1" "fd_write" (func $runtime.fd_write (param i32, i32, i32, i32) (result i32)))
+			//	)
+			input: []byte{
+				0x01, // function subsection ID one
+				0x2b, // 43 bytes to follow
+				0x02, // two function names
+				0x00, // the function index is zero
+				0x16, // the function name runtime.args_sizes_get is 22 bytes long
+				'r', 'u', 'n', 't', 'i', 'm', 'e', '.', 'a', 'r', 'g', 's', '_', 's', 'i', 'z', 'e', 's', '_', 'g', 'e', 't',
+				0x01, // the function index is zero
+				0x10, // the function name runtime.fd_write is 16 bytes long
+				'r', 'u', 'n', 't', 'i', 'm', 'e', '.', 'f', 'd', '_', 'w', 'r', 'i', 't', 'e',
+			},
+			expected: &CustomNameSection{
+				FunctionNames: map[uint32]string{
+					0x00: "runtime.args_sizes_get",
+					0x01: "runtime.fd_write",
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			ns, err := DecodeCustomNameSection(tc.input)
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, ns)
+		})
+	}
+}
+
+func TestDecodeCustomNameSection_Errors(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       []byte
+		expectedErr string
+	}{
+		{
+			name:        "empty",
+			input:       []byte{},
+			expectedErr: "failed to read subsection ID: EOF",
+		},
+		{
+			name:        "EOF after subsection ID",
+			input:       []byte{0x01},
+			expectedErr: "failed to read the size of subsection 1: EOF",
+		},
+		{
+			name:        "EOF after size",
+			input:       []byte{0x01, 0x10},
+			expectedErr: "failed to read the size of name vector: EOF",
+		},
+		{
+			name:        "EOF after function name vector size",
+			input:       []byte{0x01, 0x10, 0x01},
+			expectedErr: "failed to read function index: EOF",
+		},
+		{
+			name:        "EOF after function name index",
+			input:       []byte{0x01, 0x10, 0x01, 0x00},
+			expectedErr: "failed to read function name size: EOF",
+		},
+		{
+			name:        "EOF after function name size",
+			input:       []byte{0x01, 0x10, 0x01, 0x00, 0x01},
+			expectedErr: "failed to read function name: EOF",
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := DecodeCustomNameSection(tc.input)
+			require.EqualError(t, err, tc.expectedErr)
+		})
+	}
+}
diff --git a/wasm/wat/bind_test.go b/wasm/wat/bind_test.go
index 050ce1a9..4bf62918 100644
--- a/wasm/wat/bind_test.go
+++ b/wasm/wat/bind_test.go
@@ -23,14 +23,14 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeInlined: &inlinedTypeFunc{paramI32I32I32I32ResultI32, 0, 0}},
 				},
 			},
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexZero},
 				},
 			},
@@ -40,18 +40,18 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeInlined: &inlinedTypeFunc{paramI32I32ResultI32, 0, 0}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeInlined: &inlinedTypeFunc{paramI32I32I32I32ResultI32, 0, 0}},
 				},
 			},
 			expected: &module{
 				types: []*typeFunc{paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: indexZero},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexOne},
 				},
 			},
@@ -61,18 +61,18 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "runtime.args_get",
 						typeInlined: &inlinedTypeFunc{paramI32I32ResultI32, 0, 0}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeInlined: &inlinedTypeFunc{paramI32I32ResultI32, 0, 0}},
 				},
 			},
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "runtime.args_get",
 						typeIndex: indexOne},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: indexOne},
 				},
 			},
@@ -82,26 +82,26 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{
 					typeFuncEmpty,
-					{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
-					{name: "$i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
+					{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+					{name: "i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
-						typeIndex: &index{ID: "$i32i32_i32", line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
-						typeIndex: &index{ID: "$i32i32i32i32_i32", line: 6, col: 76}},
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
+						typeIndex: &index{ID: "i32i32_i32", line: 5, col: 86}},
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
+						typeIndex: &index{ID: "i32i32i32i32_i32", line: 6, col: 76}},
 				},
 			},
 			expected: &module{
 				types: []*typeFunc{
 					typeFuncEmpty,
-					{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
-					{name: "$i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
+					{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+					{name: "i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 6, col: 76}},
 				},
 			},
@@ -111,18 +111,18 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 6, col: 76}},
 				},
 			},
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 6, col: 76}},
 				},
 			},
@@ -132,16 +132,16 @@ func TestBindIndices(t *testing.T) {
 			input: &module{
 				types: []*typeFunc{typeFuncEmpty},
 				importFuncs: []*importFunc{
-					{funcName: "$one", typeIndex: indexZero},
-					{funcName: "$two", typeIndex: indexZero},
+					{funcName: "one", typeIndex: indexZero},
+					{funcName: "two", typeIndex: indexZero},
 				},
-				startFunction: &index{ID: "$two", line: 3, col: 9},
+				startFunction: &index{ID: "two", line: 3, col: 9},
 			},
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty},
 				importFuncs: []*importFunc{
-					{funcName: "$one", typeIndex: indexZero},
-					{funcName: "$two", typeIndex: indexZero},
+					{funcName: "one", typeIndex: indexZero},
+					{funcName: "two", typeIndex: indexZero},
 				},
 				startFunction: &index{numeric: 1, line: 3, col: 9},
 			},
@@ -192,7 +192,7 @@ func TestBindIndices_Errors(t *testing.T) {
 			name: "function type points nowhere",
 			input: &module{
 				types:       []*typeFunc{typeFuncEmpty},
-				importFuncs: []*importFunc{{name: "hello", typeIndex: &index{ID: "$main", line: 3, col: 9}}},
+				importFuncs: []*importFunc{{name: "hello", typeIndex: &index{ID: "main", line: 3, col: 9}}},
 			},
 			expectedErr: "3:9: unknown ID $main in module.import[0].func.type",
 		},
@@ -208,7 +208,7 @@ func TestBindIndices_Errors(t *testing.T) {
 		{
 			name: "start points nowhere",
 			input: &module{
-				startFunction: &index{ID: "$main", line: 1, col: 16},
+				startFunction: &index{ID: "main", line: 1, col: 16},
 			},
 			expectedErr: "1:16: unknown ID $main in module.start",
 		},
diff --git a/wasm/wat/convert_test.go b/wasm/wat/convert_test.go
index e1e33c19..5538fc34 100644
--- a/wasm/wat/convert_test.go
+++ b/wasm/wat/convert_test.go
@@ -66,6 +66,14 @@ func TestTextToBinary(t *testing.T) {
 						},
 					},
 				},
+				CustomSections: map[string][]byte{
+					"name": (&wasm.CustomNameSection{
+						FunctionNames: map[uint32]string{
+							0: "runtime.path_open",
+							1: "runtime.fd_write",
+						},
+					}).EncodeData(),
+				},
 			},
 		},
 		{
@@ -98,6 +106,14 @@ func TestTextToBinary(t *testing.T) {
 						},
 					},
 				},
+				CustomSections: map[string][]byte{
+					"name": (&wasm.CustomNameSection{
+						FunctionNames: map[uint32]string{
+							0: "runtime.arg_sizes_get",
+							1: "runtime.fd_write",
+						},
+					}).EncodeData(),
+				},
 			},
 		},
 		{
@@ -116,6 +132,13 @@ func TestTextToBinary(t *testing.T) {
 					},
 				}},
 				StartSection: &zero,
+				CustomSections: map[string][]byte{
+					"name": (&wasm.CustomNameSection{
+						FunctionNames: map[uint32]string{
+							0: "hello",
+						},
+					}).EncodeData(),
+				},
 			},
 		},
 		{
@@ -180,6 +203,22 @@ func TestTextToBinary(t *testing.T) {
 					},
 				},
 				StartSection: &four,
+				CustomSections: map[string][]byte{
+					"name": (&wasm.CustomNameSection{
+						ModuleName: "example",
+						FunctionNames: map[uint32]string{
+							0: "runtime.arg_sizes_get",
+							1: "runtime.fd_write",
+							2: "mul",
+							3: "add",
+							4: "hello",
+						},
+						LocalNames: map[uint32]map[uint32]string{
+							2: {0: "x", 1: "y"},
+							3: {0: "l", 1: "r"},
+						},
+					}).EncodeData(),
+				},
 			},
 		},
 	}
diff --git a/wasm/wat/index_parser_test.go b/wasm/wat/index_parser_test.go
index 0b3d9689..e3e35125 100644
--- a/wasm/wat/index_parser_test.go
+++ b/wasm/wat/index_parser_test.go
@@ -12,7 +12,7 @@ func TestParseIndex(t *testing.T) {
 		expected    *index
 	}{
 		{"numeric", "(1)", &index{numeric: 0x1, line: 1, col: 2}},
-		{"symbolic", "($v_v)", &index{ID: "$v_v", line: 1, col: 2}},
+		{"symbolic", "($v_v)", &index{ID: "v_v", line: 1, col: 2}},
 	}
 
 	for _, tt := range tests {
diff --git a/wasm/wat/names_test.go b/wasm/wat/names_test.go
deleted file mode 100644
index 9baf8b69..00000000
--- a/wasm/wat/names_test.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package wat
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/require"
-
-	"github.com/tetratelabs/wazero/wasm"
-)
-
-func TestEncodeNameSection(t *testing.T) {
-	m, err := parseModule([]byte(`(module $simple
-	(import "" "hello" (func $hello))
-	(start $hello)
-)`))
-	require.NoError(t, err)
-
-	// TIP: the below is the binary suffix of `wat2wasm --debug-names --debug-parser -v simple.wat` where simple.wat
-	// contains the same text as the above inlined text format.
-	require.Equal(t, []byte{
-		0x00, // module subsection ID zero
-		0x07, // 7 bytes to follow
-		0x06, // the module name simple is 6 characters long
-		's', 'i', 'm', 'p', 'l', 'e',
-		0x01, // function subsection ID one
-		0x08, // 8 bytes to follow
-		0x01, // one function name
-		0x00, // the function index is zero
-		0x05, // the function name hello is 5 characters long
-		'h', 'e', 'l', 'l', 'o',
-	}, encodeNameSection(m))
-}
-
-// TestEncodeNameSection_OnlyFuncName shows that we don't rely on the module name being present. For example, this isn't
-// encoded in TinyGo.
-func TestEncodeNameSection_OnlyFuncName(t *testing.T) {
-	func0, func1 := "runtime.args_sizes_get", "runtime.fd_write" // note: no leading '$'
-	i32 := wasm.ValueTypeI32
-	type1 := &typeFunc{params: []wasm.ValueType{i32, i32}, result: i32}
-	type2 := &typeFunc{params: []wasm.ValueType{i32, i32, i32, i32}, result: i32}
-	m := &module{
-		types: []*typeFunc{type1, type2},
-		importFuncs: []*importFunc{
-			{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_sizes_get", funcName: "$" + func0,
-				typeIndex: &index{ /* TODO 0*/ }},
-			{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$" + func1,
-				typeIndex: &index{ /* TODO 1*/ }},
-		},
-	}
-
-	expected := append(append([]byte{
-		0x01, // function subsection ID one
-		// length includes overhead for size in bytes of the function name count, plus index + length prefix per name
-		byte(1 + 2 + 2 + len(func0) + len(func1)),
-		0x02, // two function names
-	},
-		append([]byte{0x00 /* funcIndex */, byte(len(func0))}, func0...)...),
-		append([]byte{0x01 /* funcIndex */, byte(len(func1))}, func1...)...)
-
-	require.Equal(t, expected, encodeNameSection(m))
-}
-
-func TestEncodeNameSubsection(t *testing.T) {
-	subsectionID := uint8(1)
-	name := "$simple"
-	require.Equal(t, []byte{
-		subsectionID,
-		byte(1 + 6), // 1 is the size of 6 in LEB128 encoding
-		6, 's', 'i', 'm', 'p', 'l', 'e'}, encodeNameSubsection(subsectionID, encodeName(name)))
-}
-
-func TestEncodeNameMapEntry(t *testing.T) {
-	index := uint32(1)
-	name := "$hello"
-	require.Equal(t, []byte{byte(index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameMapEntry(index, name))
-}
-
-func TestEncodeName(t *testing.T) {
-	// We expect a length (in LEB128) prefixed string encoding
-	require.Equal(t, []byte{5, 'h', 'e', 'l', 'l', 'o'}, encodeName("$hello"))
-}
diff --git a/wasm/wat/parser_test.go b/wasm/wat/parser_test.go
index 33b96caa..7a47f21d 100644
--- a/wasm/wat/parser_test.go
+++ b/wasm/wat/parser_test.go
@@ -53,14 +53,14 @@ func TestParseModule(t *testing.T) {
 		{
 			name:     "only name",
 			input:    "(module $tools)",
-			expected: &module{name: "$tools"},
+			expected: &module{name: "tools"},
 		},
 		{
 			name:  "type func one empty",
 			input: "(module (type $i32i32_i32 (func (param i32 i32) (result i32))) (type (func)))",
 			expected: &module{
 				types: []*typeFunc{
-					{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+					{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
 					typeFuncEmpty,
 				},
 			},
@@ -70,13 +70,13 @@ func TestParseModule(t *testing.T) {
 			input: "(module (type $mul (func (param $x f32) (param $y f32) (result f32))))",
 			expected: &module{
 				types: []*typeFunc{
-					{name: "$mul", params: []wasm.ValueType{f32, f32}, result: f32},
+					{name: "mul", params: []wasm.ValueType{f32, f32}, result: f32},
 				},
 				typeParamNames: []*typeParamNames{{
 					index: uint32(0),
 					paramNames: paramNames{
-						&paramNameIndex{uint32(0), []byte{'$', 'x'}},
-						&paramNameIndex{uint32(1), []byte{'$', 'y'}},
+						&paramNameIndex{uint32(0), []byte{'x'}},
+						&paramNameIndex{uint32(1), []byte{'y'}},
 					},
 				}},
 			},
@@ -91,8 +91,8 @@ func TestParseModule(t *testing.T) {
 				typeParamNames: []*typeParamNames{{
 					index: uint32(0),
 					paramNames: paramNames{
-						&paramNameIndex{uint32(2), []byte{'$', 'v'}},
-						&paramNameIndex{uint32(4), []byte{'$', 't'}},
+						&paramNameIndex{uint32(2), []byte{'v'}},
+						&paramNameIndex{uint32(4), []byte{'t'}},
 					},
 				}},
 			},
@@ -106,23 +106,23 @@ func TestParseModule(t *testing.T) {
 )`,
 			expected: &module{
 				types: []*typeFunc{
-					{name: "$mul", params: []wasm.ValueType{f32, f32}, result: f32},
+					{name: "mul", params: []wasm.ValueType{f32, f32}, result: f32},
 					typeFuncEmpty,
-					{name: "$add", params: []wasm.ValueType{f32, f32}, result: f32},
+					{name: "add", params: []wasm.ValueType{f32, f32}, result: f32},
 				},
 				typeParamNames: []*typeParamNames{
 					{
 						index: uint32(0),
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'x'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'y'}},
+							&paramNameIndex{uint32(0), []byte{'x'}},
+							&paramNameIndex{uint32(1), []byte{'y'}},
 						},
 					},
 					{
 						index: uint32(2),
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'l'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'r'}},
+							&paramNameIndex{uint32(0), []byte{'l'}},
+							&paramNameIndex{uint32(1), []byte{'r'}},
 						},
 					},
 				},
@@ -140,7 +140,7 @@ func TestParseModule(t *testing.T) {
 					paramI32I32I32I32ResultI32,
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexOne},
 				},
 			},
@@ -220,7 +220,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexZero},
 				},
 			},
@@ -233,7 +233,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexZero},
 				},
 			},
@@ -248,7 +248,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexZero},
 				},
 			},
@@ -261,7 +261,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "proc_exit", funcName: "$runtime.proc_exit",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "proc_exit", funcName: "runtime.proc_exit",
 						typeIndex: indexZero},
 				},
 			},
@@ -282,7 +282,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32I32I64I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "$runtime.path_open",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "runtime.path_open",
 						typeIndex: indexZero},
 				},
 			},
@@ -295,7 +295,7 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32I32I32I32I64I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "$runtime.path_open",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "path_open", funcName: "runtime.path_open",
 						typeIndex: indexZero},
 				},
 			},
@@ -309,9 +309,9 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: indexZero},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: indexOne},
 				},
 			},
@@ -328,13 +328,13 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{
 					typeFuncEmpty,
-					{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
-					{name: "$i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
+					{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+					{name: "i32i32i32i32_i32", params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 6, col: 76}},
 				},
 			},
@@ -351,9 +351,9 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32, paramI32I32I32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 6, col: 76}},
 				},
 			},
@@ -368,9 +368,9 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "runtime.args_get",
 						typeIndex: indexOne},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: indexOne},
 				},
 			},
@@ -386,9 +386,9 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "runtime.args_get",
 						typeIndex: &index{numeric: 1, line: 4, col: 76}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 5, col: 86}},
 				},
 			},
@@ -404,9 +404,9 @@ func TestParseModule(t *testing.T) {
 			expected: &module{
 				types: []*typeFunc{typeFuncEmpty, paramI32I32ResultI32},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "$runtime.args_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "args_get", funcName: "runtime.args_get",
 						typeIndex: &index{numeric: 1, line: 3, col: 76}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 1, line: 4, col: 86}},
 				},
 			},
@@ -419,11 +419,11 @@ func TestParseModule(t *testing.T) {
 					{params: []wasm.ValueType{f32, f32}, result: f32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "Math", name: "Mul", funcName: "$mul",
+					{importIndex: 0, module: "Math", name: "Mul", funcName: "mul",
 						typeIndex: indexZero,
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'x'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'y'}},
+							&paramNameIndex{uint32(0), []byte{'x'}},
+							&paramNameIndex{uint32(1), []byte{'y'}},
 						},
 					}},
 			},
@@ -439,18 +439,18 @@ func TestParseModule(t *testing.T) {
 					{params: []wasm.ValueType{f32, f32}, result: f32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "Math", name: "Mul", funcName: "$mul",
+					{importIndex: 0, module: "Math", name: "Mul", funcName: "mul",
 						typeIndex: indexZero,
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'x'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'y'}},
+							&paramNameIndex{uint32(0), []byte{'x'}},
+							&paramNameIndex{uint32(1), []byte{'y'}},
 						},
 					},
-					{importIndex: 1, module: "Math", name: "Add", funcName: "$add",
+					{importIndex: 1, module: "Math", name: "Add", funcName: "add",
 						typeIndex: indexZero,
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'l'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'r'}},
+							&paramNameIndex{uint32(0), []byte{'l'}},
+							&paramNameIndex{uint32(1), []byte{'r'}},
 						},
 					},
 				},
@@ -465,8 +465,8 @@ func TestParseModule(t *testing.T) {
 				},
 				importFuncs: []*importFunc{
 					{importIndex: 0, typeIndex: indexZero, paramNames: paramNames{
-						&paramNameIndex{uint32(2), []byte{'$', 'v'}},
-						&paramNameIndex{uint32(4), []byte{'$', 't'}},
+						&paramNameIndex{uint32(2), []byte{'v'}},
+						&paramNameIndex{uint32(4), []byte{'t'}},
 					}}},
 			},
 		},
@@ -478,7 +478,7 @@ func TestParseModule(t *testing.T) {
 )`,
 			expected: &module{
 				types:         []*typeFunc{typeFuncEmpty},
-				importFuncs:   []*importFunc{{name: "hello", funcName: "$hello", typeIndex: indexZero}},
+				importFuncs:   []*importFunc{{name: "hello", funcName: "hello", typeIndex: indexZero}},
 				startFunction: &index{numeric: 0, line: 3, col: 9},
 			},
 		},
@@ -498,37 +498,37 @@ func TestParseModule(t *testing.T) {
 			name:  "example",
 			input: string(example),
 			expected: &module{
-				name: "$example",
+				name: "example",
 				types: []*typeFunc{
-					{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+					{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
 					typeFuncEmpty, // Note: inlined types come after explicit ones even if the latter are defined later
 					{params: []wasm.ValueType{i32, i32, i32, i32}, result: i32},
 					{params: []wasm.ValueType{f32, f32}, result: f32},
 				},
 				importFuncs: []*importFunc{
-					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "$runtime.arg_sizes_get",
+					{importIndex: 0, module: "wasi_snapshot_preview1", name: "arg_sizes_get", funcName: "runtime.arg_sizes_get",
 						typeIndex: &index{numeric: 0, line: 3, col: 86}},
-					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$runtime.fd_write",
+					{importIndex: 1, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "runtime.fd_write",
 						typeIndex: &index{numeric: 2, line: 3, col: 81}},
-					{importIndex: 2, module: "Math", name: "Mul", funcName: "$mul",
+					{importIndex: 2, module: "Math", name: "Mul", funcName: "mul",
 						typeIndex: &index{numeric: 3, line: 3, col: 81},
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'x'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'y'}},
+							&paramNameIndex{uint32(0), []byte{'x'}},
+							&paramNameIndex{uint32(1), []byte{'y'}},
 						},
 					},
-					{importIndex: 3, module: "Math", name: "Add", funcName: "$add",
+					{importIndex: 3, module: "Math", name: "Add", funcName: "add",
 						typeIndex: &index{numeric: 0, line: 6, col: 40},
 						typeInlined: &inlinedTypeFunc{
-							typeFunc: &typeFunc{name: "$i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
+							typeFunc: &typeFunc{name: "i32i32_i32", params: []wasm.ValueType{i32, i32}, result: i32},
 							line:     6, col: 35,
 						},
 						paramNames: paramNames{
-							&paramNameIndex{uint32(0), []byte{'$', 'l'}},
-							&paramNameIndex{uint32(1), []byte{'$', 'r'}},
+							&paramNameIndex{uint32(0), []byte{'l'}},
+							&paramNameIndex{uint32(1), []byte{'r'}},
 						},
 					},
-					{importIndex: 4, module: "", name: "hello", funcName: "$hello",
+					{importIndex: 4, module: "", name: "hello", funcName: "hello",
 						typeIndex: &index{numeric: 1, line: 8, col: 40}},
 				},
 				startFunction: &index{numeric: 4, line: 9, col: 9},