Repository: tetratelabs/wazero
Pull request #115 (2022-01-04 07:41:56)

Matches how wabt encodes the binary name section in wat2wasm
This PR is the result of hours of trying to figure out how people use
the custom name section in practice. The time loss was largely caused
by it being underspecified and also undocumented in implementations.



Patch:
diff --git a/wasm/jit/compiler.go b/wasm/jit/compiler.go
index df03d693..d13cc9b5 100644
--- a/wasm/jit/compiler.go
+++ b/wasm/jit/compiler.go
@@ -26,6 +26,9 @@ type compiler interface {
 	compileAdd(o *wazeroir.OperationAdd) error
 	compileSub(o *wazeroir.OperationSub) error
 	compileMul(o *wazeroir.OperationMul) error
+	compileClz(o *wazeroir.OperationClz) error
+	compileCtz(o *wazeroir.OperationCtz) error
+	compilePopcnt(o *wazeroir.OperationPopcnt) error
 	compileEq(o *wazeroir.OperationEq) error
 	compileNe(o *wazeroir.OperationNe) error
 	compileEqz(o *wazeroir.OperationEqz) error
diff --git a/wasm/jit/engine.go b/wasm/jit/engine.go
index 8277135a..3d870a87 100644
--- a/wasm/jit/engine.go
+++ b/wasm/jit/engine.go
@@ -539,11 +539,11 @@ func (e *engine) compileWasmFunction(f *wasm.FunctionInstance) (*compiledWasmFun
 		case *wazeroir.OperationMul:
 			err = compiler.compileMul(o)
 		case *wazeroir.OperationClz:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compileClz(o)
 		case *wazeroir.OperationCtz:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compileCtz(o)
 		case *wazeroir.OperationPopcnt:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compilePopcnt(o)
 		case *wazeroir.OperationDiv:
 			err = fmt.Errorf("unsupported operation")
 		case *wazeroir.OperationRem:
diff --git a/wasm/jit/jit_amd64.go b/wasm/jit/jit_amd64.go
index 0667f1de..3b9e7f15 100644
--- a/wasm/jit/jit_amd64.go
+++ b/wasm/jit/jit_amd64.go
@@ -13,6 +13,7 @@ import (
 	"encoding/binary"
 	"fmt"
 	"math"
+	"runtime"
 
 	asm "github.com/twitchyliquid64/golang-asm"
 	"github.com/twitchyliquid64/golang-asm/obj"
@@ -886,7 +887,8 @@ func (c *amd64Compiler) compileMulForInts(is32Bit bool, mulInstruction obj.As) e
 
 	// Now we have the result in the AX register,
 	// so we record it.
-	c.locationStack.pushValueOnRegister(resultRegister)
+	result := c.locationStack.pushValueOnRegister(resultRegister)
+	result.setRegisterType(generalPurposeRegisterTypeInt)
 	return nil
 }
 
@@ -916,6 +918,210 @@ func (c *amd64Compiler) compileMulForFloats(instruction obj.As) error {
 	return nil
 }
 
+// compileClz emits instructions to count up the leading zeros in the
+// current top of the stack, and push the count result.
+// For example, stack of [..., 0x00_ff_ff_ff] results in [..., 8].
+func (c *amd64Compiler) compileClz(o *wazeroir.OperationClz) error {
+	target := c.locationStack.pop()
+	if err := c.ensureOnGeneralPurposeRegister(target); err != nil {
+		return err
+	}
+
+	if runtime.GOOS != "darwin" {
+		countZeros := c.newProg()
+		countZeros.From.Type = obj.TYPE_REG
+		countZeros.From.Reg = target.register
+		countZeros.To.Type = obj.TYPE_REG
+		countZeros.To.Reg = target.register
+		if o.Type == wazeroir.UnsignedInt32 {
+			countZeros.As = x86.ALZCNTL
+		} else {
+			countZeros.As = x86.ALZCNTQ
+		}
+		c.addInstruction(countZeros)
+	} else {
+		// On x86 mac, we cannot use LZCNT as it always results in zero.
+		// Instead we combine BSR (calculating most significant set bit)
+		// with XOR. This logic is described in
+		// "Replace Raw Assembly Code with Builtin Intrinsics" section in:
+		// https://developer.apple.com/documentation/apple-silicon/addressing-architectural-differences-in-your-macos-code.
+
+		// First, we have to check if the target is non-zero as BSR is undefined
+		// on zero. See https://www.felixcloutier.com/x86/bsr.
+		cmpZero := c.newProg()
+		cmpZero.As = x86.ACMPQ
+		cmpZero.From.Type = obj.TYPE_REG
+		cmpZero.From.Reg = target.register
+		cmpZero.To.Type = obj.TYPE_CONST
+		cmpZero.To.Offset = 0
+		c.addInstruction(cmpZero)
+
+		jmpIfNonZero := c.newProg()
+		jmpIfNonZero.As = x86.AJNE
+		jmpIfNonZero.To.Type = obj.TYPE_BRANCH
+		c.addInstruction(jmpIfNonZero)
+
+		// If the value is zero, we just push the const value.
+		if o.Type == wazeroir.UnsignedInt32 {
+			c.emitConstI32(32, target.register)
+		} else {
+			c.emitConstI64(64, target.register)
+		}
+
+		// Emit the jmp instruction to jump to the position right after
+		// the non-zero case.
+		jmpAtEndOfZero := c.newProg()
+		jmpAtEndOfZero.As = obj.AJMP
+		jmpAtEndOfZero.To.Type = obj.TYPE_BRANCH
+		c.addInstruction(jmpAtEndOfZero)
+
+		// Start emitting non-zero case.
+		// First, we calculate the most significant set bit.
+		mostSignificantSetBit := c.newProg()
+		// Set the jump target of the first non-zero check.
+		jmpIfNonZero.To.SetTarget(mostSignificantSetBit)
+		if o.Type == wazeroir.UnsignedInt32 {
+			mostSignificantSetBit.As = x86.ABSRL
+		} else {
+			mostSignificantSetBit.As = x86.ABSRQ
+		}
+		mostSignificantSetBit.From.Type = obj.TYPE_REG
+		mostSignificantSetBit.From.Reg = target.register
+		mostSignificantSetBit.To.Type = obj.TYPE_REG
+		mostSignificantSetBit.To.Reg = target.register
+		c.addInstruction(mostSignificantSetBit)
+
+		// Now we XOR the value with the bit length minus one.
+		xorWithBitLength := c.newProg()
+		xorWithBitLength.To.Type = obj.TYPE_REG
+		xorWithBitLength.To.Reg = target.register
+		xorWithBitLength.From.Type = obj.TYPE_CONST
+		if o.Type == wazeroir.UnsignedInt32 {
+			xorWithBitLength.As = x86.AXORL
+			xorWithBitLength.From.Offset = 31
+		} else {
+			xorWithBitLength.As = x86.AXORQ
+			xorWithBitLength.From.Offset = 63
+		}
+		c.addInstruction(xorWithBitLength)
+
+		// Finally the end jump instruction of zero case must target towards
+		// the next instruction.
+		c.setJmpOrigin = jmpAtEndOfZero
+	}
+
+	// We reused the same register of target for the result.
+	result := c.locationStack.pushValueOnRegister(target.register)
+	result.setRegisterType(generalPurposeRegisterTypeInt)
+	return nil
+}
+
+// compileCtz emits instructions to count up the trailing zeros in the
+// current top of the stack, and push the count result.
+// For example, stack of [..., 0xff_ff_ff_00] results in [..., 8].
+func (c *amd64Compiler) compileCtz(o *wazeroir.OperationCtz) error {
+	target := c.locationStack.pop()
+	if err := c.ensureOnGeneralPurposeRegister(target); err != nil {
+		return err
+	}
+
+	if runtime.GOOS != "darwin" {
+		countZeros := c.newProg()
+		countZeros.From.Type = obj.TYPE_REG
+		countZeros.From.Reg = target.register
+		countZeros.To.Type = obj.TYPE_REG
+		countZeros.To.Reg = target.register
+		if o.Type == wazeroir.UnsignedInt32 {
+			countZeros.As = x86.ATZCNTL
+		} else {
+			countZeros.As = x86.ATZCNTQ
+		}
+		c.addInstruction(countZeros)
+	} else {
+		// Somehow, if the target value is zero, TZCNT always returns zero: this is wrong.
+		// Meanwhile, we need branches for non-zero and zero cases on macos.
+		// TODO: find the reference to this behavior and put the link here.
+
+		// First we compare the target with zero.
+		cmpZero := c.newProg()
+		cmpZero.As = x86.ACMPQ
+		cmpZero.From.Type = obj.TYPE_REG
+		cmpZero.From.Reg = target.register
+		cmpZero.To.Type = obj.TYPE_CONST
+		cmpZero.To.Offset = 0
+		c.addInstruction(cmpZero)
+
+		jmpIfNonZero := c.newProg()
+		jmpIfNonZero.As = x86.AJNE
+		jmpIfNonZero.To.Type = obj.TYPE_BRANCH
+		c.addInstruction(jmpIfNonZero)
+
+		// If the value is zero, we just push the const value.
+		if o.Type == wazeroir.UnsignedInt32 {
+			c.emitConstI32(32, target.register)
+		} else {
+			c.emitConstI64(64, target.register)
+		}
+
+		// Emit the jmp instruction to jump to the position right after
+		// the non-zero case.
+		jmpAtEndOfZero := c.newProg()
+		jmpAtEndOfZero.As = obj.AJMP
+		jmpAtEndOfZero.To.Type = obj.TYPE_BRANCH
+		c.addInstruction(jmpAtEndOfZero)
+
+		// Otherwise, emit the TZCNT.
+		countZeros := c.newProg()
+		jmpIfNonZero.To.SetTarget(countZeros)
+		countZeros.From.Type = obj.TYPE_REG
+		countZeros.From.Reg = target.register
+		countZeros.To.Type = obj.TYPE_REG
+		countZeros.To.Reg = target.register
+		if o.Type == wazeroir.UnsignedInt32 {
+			countZeros.As = x86.ATZCNTL
+		} else {
+			countZeros.As = x86.ATZCNTQ
+		}
+		c.addInstruction(countZeros)
+
+		// Finally the end jump instruction of zero case must target towards
+		// the next instruction.
+		c.setJmpOrigin = jmpAtEndOfZero
+	}
+
+	// We reused the same register of target for the result.
+	result := c.locationStack.pushValueOnRegister(target.register)
+	result.setRegisterType(generalPurposeRegisterTypeInt)
+	return nil
+}
+
+// compilePopcnt emits instructions to count up the number of set bits in the
+// current top of the stack, and push the count result.
+// For example, stack of [..., 0b00_00_00_11] results in [..., 2].
+func (c *amd64Compiler) compilePopcnt(o *wazeroir.OperationPopcnt) error {
+	target := c.locationStack.pop()
+	if err := c.ensureOnGeneralPurposeRegister(target); err != nil {
+		return err
+	}
+
+	countBits := c.newProg()
+	countBits.From.Type = obj.TYPE_REG
+	countBits.From.Reg = target.register
+	countBits.To.Type = obj.TYPE_REG
+	countBits.To.Reg = target.register
+	if o.Type == wazeroir.UnsignedInt32 {
+		countBits.As = x86.APOPCNTL
+	} else {
+		countBits.As = x86.APOPCNTQ
+	}
+	c.addInstruction(countBits)
+
+	// We reused the same register of target for the result.
+	result := c.locationStack.pushValueOnRegister(target.register)
+	result.setRegisterType(generalPurposeRegisterTypeInt)
+	return nil
+}
+
 func (c *amd64Compiler) compileEq(o *wazeroir.OperationEq) error {
 	return c.emitEqOrNe(o.Type, true)
 }
@@ -1542,14 +1748,18 @@ func (c *amd64Compiler) compileConstI32(o *wazeroir.OperationConstI32) error {
 	loc := c.locationStack.pushValueOnRegister(reg)
 	loc.setRegisterType(generalPurposeRegisterTypeInt)
 
+	c.emitConstI32(o.Value, reg)
+	return nil
+}
+
+func (c *amd64Compiler) emitConstI32(val uint32, register int16) {
 	prog := c.newProg()
 	prog.As = x86.AMOVL // Note 32-bit move!
 	prog.From.Type = obj.TYPE_CONST
-	prog.From.Offset = int64(o.Value)
+	prog.From.Offset = int64(val)
 	prog.To.Type = obj.TYPE_REG
-	prog.To.Reg = reg
+	prog.To.Reg = register
 	c.addInstruction(prog)
-	return nil
 }
 
 func (c *amd64Compiler) compileConstI64(o *wazeroir.OperationConstI64) error {
@@ -1560,14 +1770,18 @@ func (c *amd64Compiler) compileConstI64(o *wazeroir.OperationConstI64) error {
 	loc := c.locationStack.pushValueOnRegister(reg)
 	loc.setRegisterType(generalPurposeRegisterTypeInt)
 
+	c.emitConstI64(o.Value, reg)
+	return nil
+}
+
+func (c *amd64Compiler) emitConstI64(val uint64, register int16) {
 	prog := c.newProg()
 	prog.As = x86.AMOVQ
 	prog.From.Type = obj.TYPE_CONST
-	prog.From.Offset = int64(o.Value)
+	prog.From.Offset = int64(val)
 	prog.To.Type = obj.TYPE_REG
-	prog.To.Reg = reg
+	prog.To.Reg = register
 	c.addInstruction(prog)
-	return nil
 }
 
 func (c *amd64Compiler) compileConstF32(o *wazeroir.OperationConstF32) error {
diff --git a/wasm/wat/names.go b/wasm/wat/names.go
index 08dee463..ab3e7975 100644
--- a/wasm/wat/names.go
+++ b/wasm/wat/names.go
@@ -42,18 +42,28 @@ func encodeNameSubsection(subsectionID uint8, content []byte) []byte {
 // 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 {
-	indexBytes := leb128.EncodeUint32(i)
-	nameBytes := []byte(name)
-	nameSize := leb128.EncodeUint32(uint32(len(nameBytes)))
-
-	content := append(indexBytes, nameSize...)
-	content = append(content, nameBytes...)
-	return content
+	return append(leb128.EncodeUint32(i), encodeName(name)...)
 }
 
-// encodeName encodes the name prefixed by its size
+// 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)
+	nameBytes := []byte(name[1:])
 	nameSize := leb128.EncodeUint32(uint32(len(nameBytes)))
 	content := append(nameSize, nameBytes...)
 	return content


Test patch:
diff --git a/wasm/jit/jit_amd64_test.go b/wasm/jit/jit_amd64_test.go
index 8a67b920..6cf6d5b9 100644
--- a/wasm/jit/jit_amd64_test.go
+++ b/wasm/jit/jit_amd64_test.go
@@ -9,6 +9,7 @@ import (
 	"io"
 	"math"
 	"reflect"
+	"runtime"
 	"sync"
 	"testing"
 	"unsafe"
@@ -2384,6 +2385,7 @@ func TestAmd64Compiler_compileMul(t *testing.T) {
 				err := compiler.compileMul(&wazeroir.OperationMul{Type: wazeroir.UnsignedTypeI32})
 				require.NoError(t, err)
 				require.Equal(t, int16(x86.REG_AX), compiler.locationStack.peek().register)
+				require.Equal(t, generalPurposeRegisterTypeInt, compiler.locationStack.peek().regType)
 				require.Equal(t, uint64(2), compiler.locationStack.sp)
 				require.Len(t, compiler.locationStack.usedRegisters, 1)
 				// At this point, the previous value on the DX register is saved to the stack.
@@ -2498,6 +2500,7 @@ func TestAmd64Compiler_compileMul(t *testing.T) {
 				err := compiler.compileMul(&wazeroir.OperationMul{Type: wazeroir.UnsignedTypeI64})
 				require.NoError(t, err)
 				require.Equal(t, int16(x86.REG_AX), compiler.locationStack.peek().register)
+				require.Equal(t, generalPurposeRegisterTypeInt, compiler.locationStack.peek().regType)
 				require.Equal(t, uint64(2), compiler.locationStack.sp)
 				require.Len(t, compiler.locationStack.usedRegisters, 1)
 				// At this point, the previous value on the DX register is saved to the stack.
@@ -2636,6 +2639,315 @@ func TestAmd64Compiler_compileMul(t *testing.T) {
 	})
 }
 
+func TestAmd64Compiler_compilClz(t *testing.T) {
+	t.Run("32bit", func(t *testing.T) {
+		for _, tc := range []struct{ input, expectedLeadingZeros uint32 }{
+			{input: 0xff_ff_ff_ff, expectedLeadingZeros: 0},
+			{input: 0xf0_00_00_00, expectedLeadingZeros: 0},
+			{input: 0x00_ff_ff_ff, expectedLeadingZeros: 8},
+			{input: 0, expectedLeadingZeros: 32},
+		} {
+			tc := tc
+			t.Run(fmt.Sprintf("%032b", tc.input), func(t *testing.T) {
+				compiler := requireNewCompiler(t)
+				compiler.initializeReservedRegisters()
+				// Setup the target value.
+				err := compiler.compileConstI32(&wazeroir.OperationConstI32{Value: tc.input})
+				require.NoError(t, err)
+
+				// Emit the clz instruction.
+				err = compiler.compileClz(&wazeroir.OperationClz{Type: wazeroir.UnsignedInt32})
+				require.NoError(t, err)
+				// Verify that the result is pushed, meaning that
+				// stack pointer must not be changed.
+				require.Equal(t, uint64(1), compiler.locationStack.sp)
+				// Also the location must be register.
+				require.True(t, compiler.locationStack.peek().onRegister())
+				// On darwin, we have two branches and one must jump to the next
+				// instruction after compileClz.
+				require.True(t, runtime.GOOS != "darwin" || compiler.setJmpOrigin != nil)
+
+				// To verify the behavior, we release the value
+				// to the stack.
+				compiler.releaseAllRegistersToStack()
+				compiler.returnFunction()
+
+				// Generate and run the code under test.
+				code, _, err := compiler.generate()
+				require.NoError(t, err)
+				eng := newEngine()
+				mem := newMemoryInst()
+				jitcall(
+					uintptr(unsafe.Pointer(&code[0])),
+					uintptr(unsafe.Pointer(eng)),
+					uintptr(unsafe.Pointer(&mem.Buffer[0])),
+				)
+
+				// Check the stack.
+				require.Equal(t, uint64(1), eng.stackPointer)
+				require.Equal(t, tc.expectedLeadingZeros, uint32(eng.stack[eng.stackPointer-1]))
+			})
+		}
+	})
+	t.Run("64bit", func(t *testing.T) {
+		for _, tc := range []struct{ input, expectedLeadingZeros uint64 }{
+			{input: 0xf0_00_00_00_00_00_00_00, expectedLeadingZeros: 0},
+			{input: 0xff_ff_ff_ff_ff_ff_ff_ff, expectedLeadingZeros: 0},
+			{input: 0x00_ff_ff_ff_ff_ff_ff_ff, expectedLeadingZeros: 8},
+			{input: 0x00_00_00_00_ff_ff_ff_ff, expectedLeadingZeros: 32},
+			{input: 0x00_00_00_00_00_ff_ff_ff, expectedLeadingZeros: 40},
+			{input: 0, expectedLeadingZeros: 64},
+		} {
+			tc := tc
+			t.Run(fmt.Sprintf("%064b", tc.expectedLeadingZeros), func(t *testing.T) {
+				compiler := requireNewCompiler(t)
+				compiler.initializeReservedRegisters()
+				// Setup the target value.
+				err := compiler.compileConstI64(&wazeroir.OperationConstI64{Value: tc.input})
+				require.NoError(t, err)
+
+				// Emit the clz instruction.
+				err = compiler.compileClz(&wazeroir.OperationClz{Type: wazeroir.UnsignedInt64})
+				require.NoError(t, err)
+				// Verify that the result is pushed, meaning that
+				// stack pointer must not be changed.
+				require.Equal(t, uint64(1), compiler.locationStack.sp)
+				// Also the location must be register.
+				require.True(t, compiler.locationStack.peek().onRegister())
+				// On darwin, we have two branches and one must jump to the next
+				// instruction after compileClz.
+				require.True(t, runtime.GOOS != "darwin" || compiler.setJmpOrigin != nil)
+
+				// To verify the behavior, we release the value
+				// to the stack.
+				compiler.releaseAllRegistersToStack()
+				compiler.returnFunction()
+
+				// Generate and run the code under test.
+				code, _, err := compiler.generate()
+				require.NoError(t, err)
+				eng := newEngine()
+				mem := newMemoryInst()
+				jitcall(
+					uintptr(unsafe.Pointer(&code[0])),
+					uintptr(unsafe.Pointer(eng)),
+					uintptr(unsafe.Pointer(&mem.Buffer[0])),
+				)
+
+				// Check the stack.
+				require.Equal(t, uint64(1), eng.stackPointer)
+				require.Equal(t, tc.expectedLeadingZeros, eng.stack[eng.stackPointer-1])
+			})
+		}
+	})
+}
+
+func TestAmd64Compiler_compilCtz(t *testing.T) {
+	t.Run("32bit", func(t *testing.T) {
+		for _, tc := range []struct{ input, expectedTrailingZeros uint32 }{
+			{input: 0xff_ff_ff_ff, expectedTrailingZeros: 0},
+			{input: 0x00_00_00_01, expectedTrailingZeros: 0},
+			{input: 0xff_ff_ff_00, expectedTrailingZeros: 8},
+			{input: 0, expectedTrailingZeros: 32},
+		} {
+			tc := tc
+			t.Run(fmt.Sprintf("%032b", tc.input), func(t *testing.T) {
+				compiler := requireNewCompiler(t)
+				compiler.initializeReservedRegisters()
+				// Setup the target value.
+				err := compiler.compileConstI32(&wazeroir.OperationConstI32{Value: tc.input})
+				require.NoError(t, err)
+
+				// Emit the clz instruction.
+				err = compiler.compileCtz(&wazeroir.OperationCtz{Type: wazeroir.UnsignedInt32})
+				require.NoError(t, err)
+				// Verify that the result is pushed, meaning that
+				// stack pointer must not be changed.
+				require.Equal(t, uint64(1), compiler.locationStack.sp)
+				// Also the location must be register.
+				require.True(t, compiler.locationStack.peek().onRegister())
+				// On darwin, we have two branches and one must jump to the next
+				// instruction after compileCtz.
+				require.True(t, runtime.GOOS != "darwin" || compiler.setJmpOrigin != nil)
+
+				// To verify the behavior, we release the value
+				// to the stack.
+				compiler.releaseAllRegistersToStack()
+				compiler.returnFunction()
+
+				// Generate and run the code under test.
+				code, _, err := compiler.generate()
+				require.NoError(t, err)
+				eng := newEngine()
+				mem := newMemoryInst()
+				jitcall(
+					uintptr(unsafe.Pointer(&code[0])),
+					uintptr(unsafe.Pointer(eng)),
+					uintptr(unsafe.Pointer(&mem.Buffer[0])),
+				)
+
+				// Check the stack.
+				require.Equal(t, uint64(1), eng.stackPointer)
+				require.Equal(t, tc.expectedTrailingZeros, uint32(eng.stack[eng.stackPointer-1]))
+			})
+		}
+	})
+	t.Run("64bit", func(t *testing.T) {
+		for _, tc := range []struct{ input, expectedTrailingZeros uint64 }{
+			{input: 0xff_ff_ff_ff_ff_ff_ff_ff, expectedTrailingZeros: 0},
+			{input: 0x00_00_00_00_00_00_00_01, expectedTrailingZeros: 0},
+			{input: 0xff_ff_ff_ff_ff_ff_ff_00, expectedTrailingZeros: 8},
+			{input: 0xff_ff_ff_ff_00_00_00_00, expectedTrailingZeros: 32},
+			{input: 0xff_ff_ff_00_00_00_00_00, expectedTrailingZeros: 40},
+			{input: 0, expectedTrailingZeros: 64},
+		} {
+			tc := tc
+			t.Run(fmt.Sprintf("%064b", tc.input), func(t *testing.T) {
+				compiler := requireNewCompiler(t)
+				compiler.initializeReservedRegisters()
+				// Setup the target value.
+				err := compiler.compileConstI64(&wazeroir.OperationConstI64{Value: tc.input})
+				require.NoError(t, err)
+
+				// Emit the clz instruction.
+				err = compiler.compileCtz(&wazeroir.OperationCtz{Type: wazeroir.UnsignedInt64})
+				require.NoError(t, err)
+				// Verify that the result is pushed, meaning that
+				// stack pointer must not be changed.
+				require.Equal(t, uint64(1), compiler.locationStack.sp)
+				// Also the location must be register.
+				require.True(t, compiler.locationStack.peek().onRegister())
+				// On darwin, we have two branches and one must jump to the next
+				// instruction after compileCtz.
+				require.True(t, runtime.GOOS != "darwin" || compiler.setJmpOrigin != nil)
+
+				// To verify the behavior, we release the value
+				// to the stack.
+				compiler.releaseAllRegistersToStack()
+				compiler.returnFunction()
+
+				// Generate and run the code under test.
+				code, _, err := compiler.generate()
+				require.NoError(t, err)
+				eng := newEngine()
+				mem := newMemoryInst()
+				jitcall(
+					uintptr(unsafe.Pointer(&code[0])),
+					uintptr(unsafe.Pointer(eng)),
+					uintptr(unsafe.Pointer(&mem.Buffer[0])),
+				)
+
+				// Check the stack.
+				require.Equal(t, uint64(1), eng.stackPointer)
+				require.Equal(t, tc.expectedTrailingZeros, eng.stack[eng.stackPointer-1])
+			})
+		}
+	})
+}
+func TestAmd64Compiler_compilPopcnt(t *testing.T) {
+	t.Run("32bit", func(t *testing.T) {
+		for _, tc := range []struct{ input, expectedSetBits uint32 }{
+			{input: 0xff_ff_ff_ff, expectedSetBits: 32},
+			{input: 0x00_00_00_01, expectedSetBits: 1},
+			{input: 0x10_00_00_00, expectedSetBits: 1},
+			{input: 0x00_00_10_00, expectedSetBits: 1},
+			{input: 0x00_01_00_01, expectedSetBits: 2},
+			{input: 0xff_ff_00_ff, expectedSetBits: 24},
+			{input: 0, expectedSetBits: 0},
+		} {
+			tc := tc
+			t.Run(fmt.Sprintf("%032b", tc.input), func(t *testing.T) {
+				compiler := requireNewCompiler(t)
+				compiler.initializeReservedRegisters()
+				// Setup the target value.
+				err := compiler.compileConstI32(&wazeroir.OperationConstI32{Value: tc.input})
+				require.NoError(t, err)
+
+				// Emit the clz instruction.
+				err = compiler.compilePopcnt(&wazeroir.OperationPopcnt{Type: wazeroir.UnsignedInt32})
+				require.NoError(t, err)
+				// Verify that the result is pushed, meaning that
+				// stack pointer must not be changed.
+				require.Equal(t, uint64(1), compiler.locationStack.sp)
+				// Also the location must be register.
+				require.True(t, compiler.locationStack.peek().onRegister())
+
+				// To verify the behavior, we release the value
+				// to the stack.
+				compiler.releaseAllRegistersToStack()
+				compiler.returnFunction()
+
+				// Generate and run the code under test.
+				code, _, err := compiler.generate()
+				require.NoError(t, err)
+				eng := newEngine()
+				mem := newMemoryInst()
+				jitcall(
+					uintptr(unsafe.Pointer(&code[0])),
+					uintptr(unsafe.Pointer(eng)),
+					uintptr(unsafe.Pointer(&mem.Buffer[0])),
+				)
+
+				// Check the stack.
+				require.Equal(t, uint64(1), eng.stackPointer)
+				require.Equal(t, tc.expectedSetBits, uint32(eng.stack[eng.stackPointer-1]))
+			})
+		}
+	})
+	t.Run("64bit", func(t *testing.T) {
+		for _, tc := range []struct{ in, exp uint64 }{
+			{in: 0xff_ff_ff_ff_ff_ff_ff_ff, exp: 64},
+			{in: 0x00_00_00_00_00_00_00_01, exp: 1},
+			{in: 0x00_00_00_01_00_00_00_00, exp: 1},
+			{in: 0x10_00_00_00_00_00_00_00, exp: 1},
+			{in: 0xf0_00_00_00_00_00_01_00, exp: 5},
+			{in: 0xff_ff_ff_ff_ff_ff_ff_00, exp: 56},
+			{in: 0xff_ff_ff_00_ff_ff_ff_ff, exp: 56},
+			{in: 0xff_ff_ff_ff_00_00_00_00, exp: 32},
+			{in: 0xff_ff_ff_00_00_00_00_00, exp: 24},
+			{in: 0, exp: 0},
+		} {
+			tc := tc
+			t.Run(fmt.Sprintf("%064b", tc.in), func(t *testing.T) {
+				compiler := requireNewCompiler(t)
+				compiler.initializeReservedRegisters()
+				// Setup the target value.
+				err := compiler.compileConstI64(&wazeroir.OperationConstI64{Value: tc.in})
+				require.NoError(t, err)
+
+				// Emit the clz instruction.
+				err = compiler.compilePopcnt(&wazeroir.OperationPopcnt{Type: wazeroir.UnsignedInt64})
+				require.NoError(t, err)
+				// Verify that the result is pushed, meaning that
+				// stack pointer must not be changed.
+				require.Equal(t, uint64(1), compiler.locationStack.sp)
+				// Also the location must be register.
+				require.True(t, compiler.locationStack.peek().onRegister())
+
+				// To verify the behavior, we release the value
+				// to the stack.
+				compiler.releaseAllRegistersToStack()
+				compiler.returnFunction()
+
+				// Generate and run the code under test.
+				code, _, err := compiler.generate()
+				require.NoError(t, err)
+				eng := newEngine()
+				mem := newMemoryInst()
+				jitcall(
+					uintptr(unsafe.Pointer(&code[0])),
+					uintptr(unsafe.Pointer(eng)),
+					uintptr(unsafe.Pointer(&mem.Buffer[0])),
+				)
+
+				// Check the stack.
+				require.Equal(t, uint64(1), eng.stackPointer)
+				require.Equal(t, tc.exp, eng.stack[eng.stackPointer-1])
+			})
+		}
+	})
+}
+
 func TestAmd64Compiler_compileCall(t *testing.T) {
 	t.Run("host function", func(t *testing.T) {
 		const functionIndex = 5
diff --git a/wasm/wat/names_test.go b/wasm/wat/names_test.go
index a2f0daaf..f4419ab9 100644
--- a/wasm/wat/names_test.go
+++ b/wasm/wat/names_test.go
@@ -4,44 +4,74 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/wazero/wasm"
 )
 
 func TestEncodeNameSection(t *testing.T) {
 	m, err := parseModule(simpleExample)
 	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 simpleExample
 	require.Equal(t, []byte{
-		0x0, /* module subsection ID zero */
-		0x8, /* 8 bytes to follow */
-		0x7, /* the module name $simple is 7 characters long */
-		'$', 's', 'i', 'm', 'p', 'l', 'e',
-		0x1, /* function subsection ID one */
-		0x9, /* 9 bytes to follow */
-		0x1, /* one function name */
-		0x0, /* the function index is zero */
-		0x6, /* the function name $hello is 6 characters long */
-		'$', 'h', 'e', 'l', 'l', 'o',
+		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.fd_write", "runtime.args_sizes_get" // note: no leading '$'
+	i32 := wasm.ValueTypeI32
+	m := &module{
+		types: []*typeFunc{
+			{params: []wasm.ValueType{i32, i32, i32, i32}, results: []wasm.ValueType{i32}},
+			{params: []wasm.ValueType{i32, i32}, results: []wasm.ValueType{i32}},
+		},
+		importFuncs: []*importFunc{
+			{importIndex: 0, typeIndex: 0, module: "wasi_snapshot_preview1", name: "fd_write", funcName: "$" + func0},
+			{importIndex: 1, typeIndex: 1, module: "wasi_snapshot_preview1", name: "args_sizes_get", funcName: "$" + func1},
+		},
+	}
+
+	expected := append(append([]byte{
+		0x01, // function subsection ID one
+		// length includes 1 byte overhead for the function name count, and 2 bytes (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, append([]byte{
+	require.Equal(t, []byte{
 		subsectionID,
-		byte(1 + len(name)), // 1 is the size of len(name) in LEB128 encoding
-		byte(len(name))}, []byte(name)...), encodeNameSubsection(subsectionID, encodeName(name)))
+		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, append([]byte{
-		byte(index),
-		byte(len(name))}, []byte(name)...), encodeNameMapEntry(index, name))
+	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{6, '$', 'h', 'e', 'l', 'l', 'o'}, encodeName("$hello"))
+	require.Equal(t, []byte{5, 'h', 'e', 'l', 'l', 'o'}, encodeName("$hello"))
 }