Repository: tetratelabs/wazero
Pull request #77 (2021-12-20 03:58:29)

Centralizes error handling of line and column numbers 
This changes the lex API to return the line and column number it stopped
at. This removes a lot of redundant formatting code and allow users to
control error formatting centrally.

codefromthecrypt: marked as draft until I have representative usage example
codefromthecrypt: one design I tried and dismissed was having the parser return a result of the next parser. While neat, this slowed benchmarks 5x!

Next step will be to show how to use this API to parse a module iteratively.
codefromthecrypt: https://github.com/tetratelabs/wazero/pull/78 should go before this, and show that this mostly just helps with error handling. we may not actually need to iteratively parse afterall so I may repurpose this solely to centralize error formatting with line/col

Patch:
diff --git a/wasm/jit/jit_amd64.go b/wasm/jit/jit_amd64.go
index a4e2983a..cb19f266 100644
--- a/wasm/jit/jit_amd64.go
+++ b/wasm/jit/jit_amd64.go
@@ -6,6 +6,7 @@ package jit
 import (
 	"encoding/binary"
 	"fmt"
+	"math"
 	"unsafe"
 
 	asm "github.com/twitchyliquid64/golang-asm"
@@ -16,6 +17,11 @@ import (
 	"github.com/tetratelabs/wazero/wasm/wazeroir"
 )
 
+// jitcall is implemented in jit_amd64.s as a Go Assembler function.
+// This is used by engine.exec and the entrypoint to enter the JITed native code.
+// codeSegment is the pointer to the initial instruction of the compiled native code.
+// engine is the pointer to the "*engine" as uintptr.
+// memory is the pointer to the first byte of memoryInstance.Buffer slice to be used by the target function.
 func jitcall(codeSegment, engine, memory uintptr)
 
 // Reserved registers.
@@ -124,15 +130,21 @@ func (e *engine) compileWasmFunction(f *wasm.FunctionInstance) (*compiledWasmFun
 		case *wazeroir.OperationMemoryGrow:
 			return nil, fmt.Errorf("unsupported operation in JIT compiler: %v", o)
 		case *wazeroir.OperationConstI32:
-			return nil, fmt.Errorf("unsupported operation in JIT compiler: %v", o)
+			if err := builder.handleConstI32(o); err != nil {
+				return nil, fmt.Errorf("error handling i32.const operation: %w", err)
+			}
 		case *wazeroir.OperationConstI64:
 			if err := builder.handleConstI64(o); err != nil {
 				return nil, fmt.Errorf("error handling i64.const operation: %w", err)
 			}
 		case *wazeroir.OperationConstF32:
-			return nil, fmt.Errorf("unsupported operation in JIT compiler: %v", o)
+			if err := builder.handleConstF32(o); err != nil {
+				return nil, fmt.Errorf("error handling f32.const operation: %w", err)
+			}
 		case *wazeroir.OperationConstF64:
-			return nil, fmt.Errorf("unsupported operation in JIT compiler: %v", o)
+			if err := builder.handleConstF64(o); err != nil {
+				return nil, fmt.Errorf("error handling f64.const operation: %w", err)
+			}
 		case *wazeroir.OperationEq:
 			return nil, fmt.Errorf("unsupported operation in JIT compiler: %v", o)
 		case *wazeroir.OperationNe:
@@ -1096,6 +1108,24 @@ func (b *amd64Builder) handleLe(o *wazeroir.OperationLe) error {
 	return nil
 }
 
+func (b *amd64Builder) handleConstI32(o *wazeroir.OperationConstI32) error {
+	reg, err := b.allocateRegister(gpTypeInt)
+	if err != nil {
+		return err
+	}
+	loc := b.locationStack.pushValueOnRegister(reg)
+	loc.setValueType(wazeroir.SignLessTypeI32)
+
+	prog := b.newProg()
+	prog.As = x86.AMOVL // Note 32-bit move!
+	prog.From.Type = obj.TYPE_CONST
+	prog.From.Offset = int64(o.Value)
+	prog.To.Type = obj.TYPE_REG
+	prog.To.Reg = reg
+	b.addInstruction(prog)
+	return nil
+}
+
 func (b *amd64Builder) handleConstI64(o *wazeroir.OperationConstI64) error {
 	reg, err := b.allocateRegister(gpTypeInt)
 	if err != nil {
@@ -1103,7 +1133,82 @@ func (b *amd64Builder) handleConstI64(o *wazeroir.OperationConstI64) error {
 	}
 	loc := b.locationStack.pushValueOnRegister(reg)
 	loc.setValueType(wazeroir.SignLessTypeI64)
-	b.movConstToRegister(int64(o.Value), reg)
+
+	prog := b.newProg()
+	prog.As = x86.AMOVQ
+	prog.From.Type = obj.TYPE_CONST
+	prog.From.Offset = int64(o.Value)
+	prog.To.Type = obj.TYPE_REG
+	prog.To.Reg = reg
+	b.addInstruction(prog)
+	return nil
+}
+
+func (b *amd64Builder) handleConstF32(o *wazeroir.OperationConstF32) error {
+	reg, err := b.allocateRegister(gpTypeFloat)
+	if err != nil {
+		return err
+	}
+	loc := b.locationStack.pushValueOnRegister(reg)
+	loc.setValueType(wazeroir.SignLessTypeF32)
+
+	// We cannot directly load the value from memory to float regs,
+	// so we move it to int reg temporarily.
+	tmpReg, err := b.allocateRegister(gpTypeInt)
+	if err != nil {
+		return err
+	}
+	moveToTmpReg := b.newProg()
+	moveToTmpReg.As = x86.AMOVL // Note 32-bit mov!
+	moveToTmpReg.From.Type = obj.TYPE_CONST
+	moveToTmpReg.From.Offset = int64(uint64(math.Float32bits(o.Value)))
+	moveToTmpReg.To.Type = obj.TYPE_REG
+	moveToTmpReg.To.Reg = tmpReg
+	b.addInstruction(moveToTmpReg)
+
+	prog := b.newProg()
+	prog.As = x86.AMOVL // Note 32-bit mov!
+	prog.From.Type = obj.TYPE_REG
+	prog.From.Reg = tmpReg
+	prog.To.Type = obj.TYPE_REG
+	prog.To.Reg = reg
+	b.addInstruction(prog)
+	// We don't need to explicitly release tmpReg here
+	// as allocateRegister doesn't mark it used.
+	return nil
+}
+
+func (b *amd64Builder) handleConstF64(o *wazeroir.OperationConstF64) error {
+	reg, err := b.allocateRegister(gpTypeFloat)
+	if err != nil {
+		return err
+	}
+	loc := b.locationStack.pushValueOnRegister(reg)
+	loc.setValueType(wazeroir.SignLessTypeF64)
+
+	// We cannot directly load the value from memory to float regs,
+	// so we move it to int reg temporarily.
+	tmpReg, err := b.allocateRegister(gpTypeInt)
+	if err != nil {
+		return err
+	}
+	moveToTmpReg := b.newProg()
+	moveToTmpReg.As = x86.AMOVQ
+	moveToTmpReg.From.Type = obj.TYPE_CONST
+	moveToTmpReg.From.Offset = int64(math.Float64bits(o.Value))
+	moveToTmpReg.To.Type = obj.TYPE_REG
+	moveToTmpReg.To.Reg = tmpReg
+	b.addInstruction(moveToTmpReg)
+
+	prog := b.newProg()
+	prog.As = x86.AMOVQ
+	prog.From.Type = obj.TYPE_REG
+	prog.From.Reg = tmpReg
+	prog.To.Type = obj.TYPE_REG
+	prog.To.Reg = reg
+	b.addInstruction(prog)
+	// We don't need to explicitly release tmpReg here
+	// as allocateRegister doesn't mark it used.
 	return nil
 }
 
@@ -1197,6 +1302,10 @@ func (b *amd64Builder) moveConditionalToGPRegister(loc *valueLocation) error {
 	return nil
 }
 
+// allocateRegister returns an unused register of the given type. The register will be taken
+// either from the free register pool or by stealing an used register.
+// Note that resulting registers are NOT marked as used so the call site should
+// mark it used if necessary.
 func (b *amd64Builder) allocateRegister(t generalPurposeRegisterType) (reg int16, err error) {
 	var ok bool
 	// Try to get the unused register.
@@ -1358,17 +1467,6 @@ func (b *amd64Builder) setFunctionCallIndexFromConst(index int64) {
 	b.addInstruction(prog)
 }
 
-func (b *amd64Builder) movConstToRegister(val int64, targetRegister int16) *obj.Prog {
-	prog := b.newProg()
-	prog.As = x86.AMOVQ
-	prog.From.Type = obj.TYPE_CONST
-	prog.From.Offset = val
-	prog.To.Type = obj.TYPE_REG
-	prog.To.Reg = targetRegister
-	b.addInstruction(prog)
-	return prog
-}
-
 func (b *amd64Builder) releaseRegisterToStack(loc *valueLocation) {
 	// Push value.
 	prog := b.newProg()
diff --git a/wasm/wat/lexer.go b/wasm/wat/lexer.go
index bf94f862..8b6aeffa 100644
--- a/wasm/wat/lexer.go
+++ b/wasm/wat/lexer.go
@@ -1,17 +1,22 @@
 package wat
 
 import (
+	"errors"
 	"fmt"
 	"unicode/utf8"
 )
 
 // parseToken allows a parser to inspect a token without necessarily allocating strings
+//
 // * tokenType is the token type
 // * tokenBytes are the UTF-8 bytes representing the token. Do not modify this.
 // * line is the source line number determined by unescaped '\n' characters.
 // * col is the UTF-8 column number.
 //
 // Returning an error will short-circuit any future invocations.
+//
+// Note: Do not include the line and column number in a parsing error as that will be attached automatically. Line and
+// column are here for storing the source location, such as for use in runtime stack traces.
 type parseToken func(tok tokenType, tokenBytes []byte, line, col int) error
 
 var (
@@ -19,19 +24,20 @@ var (
 	constantRParen = []byte{')'}
 )
 
-// lex invokes the parser function for the given source.
+// lex invokes the parser function for the given source. This function returns when the source is exhausted or an error
+// occurs.
 //
-// This function returns when the source is exhausted or an error occurs. Example errors include an error invoking the
-// parser, dangling block comments or unexpected characters.
-func lex(parser parseToken, fileName string, source []byte) error {
+// Here's a description of the return values:
+// * line is the source line number determined by unescaped '\n' characters of the error or EOF
+// * col is the UTF-8 column number of the error or EOF
+// * err is an error invoking the parser, dangling block comments or unexpected characters.
+func lex(parser parseToken, source []byte) (line int, col int, err error) {
 	// i is the source index to begin reading, inclusive.
 	i := 0
 	// end is the source index to stop reading, exclusive.
 	end := len(source)
-	// line is the source line number determined by unescaped '\n' characters.
-	line := 1
-	// col is the UTF-8 column number.
-	col := 1
+	line = 1
+	col = 1
 
 	// Web assembly expressions are grouped by parenthesis, even the minimal example "(module)". We track nesting level
 	// to help report problems instead of bubbling to the parser layer.
@@ -49,7 +55,7 @@ func lex(parser parseToken, fileName string, source []byte) error {
 		// See https://www.w3.org/TR/wasm-core-1/#text-comment
 		if b1 == '\n' {
 			line = line + 1
-			col = 0
+			col = 0  // for loop will + 1
 			continue // next line
 		}
 
@@ -62,7 +68,7 @@ func lex(parser parseToken, fileName string, source []byte) error {
 		case '(':
 			peek := i + 1
 			if peek == end { // invalid regardless of block comment or not. nothing opens at EOF!
-				return fmt.Errorf("%s:%d:%d: found '(' at end of input", fileName, line, col)
+				return line, col, errors.New("found '(' at end of input")
 			}
 			if source[peek] == ';' { // next block comment
 				i = peek // continue after "(;"
@@ -71,7 +77,7 @@ func lex(parser parseToken, fileName string, source []byte) error {
 				continue
 			} else if blockCommentDepth == 0 { // Fast path left paren token at the expense of code duplication.
 				if e := parser(tokenLParen, constantLParen, line, col); e != nil {
-					return e
+					return line, col, e
 				}
 				parenDepth = parenDepth + 1
 				continue
@@ -79,10 +85,10 @@ func lex(parser parseToken, fileName string, source []byte) error {
 		case ')':
 			if blockCommentDepth == 0 { // Fast path right paren token at the expense of code duplication.
 				if parenDepth == 0 {
-					return fmt.Errorf("%s:%d:%d: found ')' before '('", fileName, line, col)
+					return line, col, errors.New("found ')' before '('")
 				}
 				if e := parser(tokenRParen, constantRParen, line, col); e != nil {
-					return e
+					return line, col, e
 				}
 				parenDepth = parenDepth - 1
 				continue
@@ -113,7 +119,7 @@ func lex(parser parseToken, fileName string, source []byte) error {
 						col = col + 1
 						s := utf8Size[peeked] // While unlikely, it is possible the byte peeked is invalid unicode
 						if s == 0 {
-							return fmt.Errorf("%s:%d:%d: found an invalid byte in line comment: 0x%x", fileName, line, col, peeked)
+							return line, col, fmt.Errorf("found an invalid byte in line comment: 0x%x", peeked)
 						}
 						peek = peek + s
 					}
@@ -129,7 +135,7 @@ func lex(parser parseToken, fileName string, source []byte) error {
 		if blockCommentDepth > 0 {
 			s := utf8Size[b1] // While unlikely, it is possible the current byte is invalid unicode
 			if s == 0 {
-				return fmt.Errorf("%s:%d:%d: found an invalid byte in block comment: 0x%x", fileName, line, col, b1)
+				return line, col, fmt.Errorf("found an invalid byte in block comment: 0x%x", b1)
 			}
 			i = i + s - 1 // -1 because for loop will + 1: This optimizes speed of tokenization over block comments.
 			continue
@@ -150,13 +156,14 @@ func lex(parser parseToken, fileName string, source []byte) error {
 		c := col // the start column of the token (fixed)
 
 		switch tok {
+		//case tokenLParen, tokenRParen: // min/max 1 byte
 		case tokenSN: // min 2 bytes for sign and number; ambiguous: could be tokenFN
-			return fmt.Errorf("%s:%d:%d: TODO: signed", fileName, line, col)
+			return line, c, errors.New("TODO: signed")
 		case tokenUN: // min 1 byte; ambiguous when >=3 bytes as could be tokenFN
 			if peek < end {
 				peeked := source[peek]
 				if peeked == 'x' {
-					return fmt.Errorf("%s:%d:%d: TODO: hex", fileName, line, col)
+					return line, col, errors.New("TODO: hex")
 				}
 			Number:
 				// Start after the number and run until the end. Note all allowed characters are single byte.
@@ -184,13 +191,13 @@ func lex(parser parseToken, fileName string, source []byte) error {
 				col = col + 1
 				s := utf8Size[peeked] // While unlikely, it is possible the current byte is invalid unicode
 				if s == 0 {
-					return fmt.Errorf("%s:%d:%d: found an invalid byte in string token: 0x%x", fileName, line, col, peeked)
+					return line, col, fmt.Errorf("found an invalid byte in string token: 0x%x", peeked)
 				}
 				peek = peek + s
 			}
 
 			if !hitQuote {
-				return fmt.Errorf("%s:%d:%d: expected end quote", fileName, line, col)
+				return line, col, errors.New("expected end quote")
 			}
 
 			i = peek
@@ -210,9 +217,9 @@ func lex(parser parseToken, fileName string, source []byte) error {
 		default:
 			if b1 > 0x7F { // non-ASCII
 				r, _ := utf8.DecodeRune(source[line:])
-				return fmt.Errorf("%s:%d:%d: expected an ASCII character, not %s", fileName, line, col, string(r))
+				return line, col, fmt.Errorf("expected an ASCII character, not %s", string(r))
 			}
-			return fmt.Errorf("%s:%d:%d: unexpected character %s", fileName, line, col, string(b1))
+			return line, col, fmt.Errorf("unexpected character %s", string(b1))
 		}
 
 		// Unsigned floating-point constants for infinity or canonical NaN (not a number) clash with keyword
@@ -222,17 +229,17 @@ func lex(parser parseToken, fileName string, source []byte) error {
 		// TODO: Ex. inf nan nan:0xfffffffffffff or nan:0x400000
 
 		if e := parser(tok, source[b:peek], line, c); e != nil {
-			return e
+			return line, c, e
 		}
 	}
 
 	if blockCommentDepth > 0 {
-		return fmt.Errorf("%s:%d:%d: expected block comment end ';)', but reached end of input", fileName, line, col)
+		return line, col, errors.New("expected block comment end ';)', but reached end of input")
 	}
 	if parenDepth > 0 {
-		return fmt.Errorf("%s:%d:%d: expected ')', but reached end of input", fileName, line, col)
+		return line, col, errors.New("expected ')', but reached end of input")
 	}
-	return nil // EOF
+	return line, col, nil
 }
 
 // utf8Size returns the size of the UTF-8 rune based on its first byte, or zero.


Test patch:
diff --git a/wasm/jit/jit_amd64_test.go b/wasm/jit/jit_amd64_test.go
index 0e55b074..2738a88e 100644
--- a/wasm/jit/jit_amd64_test.go
+++ b/wasm/jit/jit_amd64_test.go
@@ -8,6 +8,7 @@ import (
 	"encoding/hex"
 	"fmt"
 	"io"
+	"math"
 	"reflect"
 	"sync"
 	"testing"
@@ -36,6 +37,17 @@ func requireNewBuilder(t *testing.T) *amd64Builder {
 	}
 }
 
+func (b *amd64Builder) movIntConstToRegister(val int64, targetRegister int16) *obj.Prog {
+	prog := b.newProg()
+	prog.As = x86.AMOVQ
+	prog.From.Type = obj.TYPE_CONST
+	prog.From.Offset = val
+	prog.To.Type = obj.TYPE_REG
+	prog.To.Reg = targetRegister
+	b.addInstruction(prog)
+	return prog
+}
+
 func TestAmd64Builder_pushFunctionInputs(t *testing.T) {
 	f := &wasm.FunctionInstance{Signature: &wasm.FunctionType{
 		InputTypes: []wasm.ValueType{wasm.ValueTypeF64, wasm.ValueTypeI32},
@@ -90,7 +102,7 @@ func TestRecursiveFunctionCalls(t *testing.T) {
 	// ::End
 	// If zero, we return from this function after pushing 5.
 	builder.assignRegisterToValue(loc, tmpReg)
-	prog = builder.movConstToRegister(5, loc.register)
+	prog = builder.movIntConstToRegister(5, loc.register)
 	jmp.To.SetTarget(prog) // the above mov instruction is the jump target of the JEQ.
 	builder.releaseRegisterToStack(loc)
 	builder.setJITStatus(jitCallStatusCodeReturned)
@@ -155,7 +167,7 @@ func TestPushValueWithGoroutines(t *testing.T) {
 			// Push consts to pushTargetRegister.
 			_ = builder.locationStack.pushValueOnStack() // dummy value, not actually used!
 			loc := builder.locationStack.pushValueOnRegister(pushTargetRegister)
-			builder.movConstToRegister(int64(targetValue), pushTargetRegister)
+			builder.movIntConstToRegister(int64(targetValue), pushTargetRegister)
 			// Push pushTargetRegister into the engine.stack[engine.sp].
 			builder.releaseRegisterToStack(loc)
 			// Finally increment the stack pointer and write it back to the eng.sp
@@ -250,7 +262,7 @@ func Test_setFunctionCallIndexFromRegister(t *testing.T) {
 		// Build codes.
 		builder := requireNewBuilder(t)
 		builder.initializeReservedRegisters()
-		builder.movConstToRegister(index, reg)
+		builder.movIntConstToRegister(index, reg)
 		builder.setFunctionCallIndexFromRegister(reg)
 		builder.returnFunction()
 		// Compile.
@@ -281,7 +293,7 @@ func Test_setContinuationAtNextInstruction(t *testing.T) {
 	// The continuation after function calls.
 	_ = builder.locationStack.pushValueOnStack() // dummy value, not actually used!
 	loc := builder.locationStack.pushValueOnRegister(tmpReg)
-	builder.movConstToRegister(int64(50), tmpReg)
+	builder.movIntConstToRegister(int64(50), tmpReg)
 	builder.releaseRegisterToStack(loc)
 	require.NotContains(t, builder.locationStack.usedRegisters, tmpReg)
 	builder.setJITStatus(jitCallStatusCodeCallWasmFunction)
@@ -325,7 +337,7 @@ func Test_callFunction(t *testing.T) {
 		// We push the value onto stack
 		_ = builder.locationStack.pushValueOnStack() // dummy value, not actually used!
 		loc := builder.locationStack.pushValueOnRegister(tmpReg)
-		builder.movConstToRegister(int64(50), tmpReg)
+		builder.movIntConstToRegister(int64(50), tmpReg)
 		builder.releaseRegisterToStack(loc)
 		require.NotContains(t, builder.locationStack.usedRegisters, tmpReg)
 		builder.returnFunction()
@@ -359,13 +371,13 @@ func Test_callFunction(t *testing.T) {
 		// Build codes.
 		builder := requireNewBuilder(t)
 		builder.initializeReservedRegisters()
-		builder.movConstToRegister(functionIndex, tmpReg)
+		builder.movIntConstToRegister(functionIndex, tmpReg)
 		builder.callFunctionFromRegisterIndex(tmpReg)
 		// On the continuation after function call,
 		// We push the value onto stack
 		_ = builder.locationStack.pushValueOnStack() // dummy value, not actually used!
 		loc := builder.locationStack.pushValueOnRegister(tmpReg)
-		builder.movConstToRegister(int64(50), tmpReg)
+		builder.movIntConstToRegister(int64(50), tmpReg)
 		builder.releaseRegisterToStack(loc)
 		require.NotContains(t, builder.locationStack.usedRegisters, tmpReg)
 		builder.returnFunction()
@@ -408,7 +420,7 @@ func TestEngine_exec_callHostFunction(t *testing.T) {
 		// Push the value onto stack.
 		_ = builder.locationStack.pushValueOnStack() // dummy value, not actually used!
 		loc := builder.locationStack.pushValueOnRegister(tmpReg)
-		builder.movConstToRegister(int64(50), tmpReg)
+		builder.movIntConstToRegister(int64(50), tmpReg)
 		builder.releaseRegisterToStack(loc)
 		require.NotContains(t, builder.locationStack.usedRegisters, tmpReg)
 		builder.callHostFunctionFromConstIndex(functionIndex)
@@ -448,11 +460,11 @@ func TestEngine_exec_callHostFunction(t *testing.T) {
 		// Push the value onto stack.
 		_ = builder.locationStack.pushValueOnStack() // dummy value, not actually used!
 		loc := builder.locationStack.pushValueOnRegister(x86.REG_AX)
-		builder.movConstToRegister(int64(50), tmpReg)
+		builder.movIntConstToRegister(int64(50), tmpReg)
 		builder.releaseRegisterToStack(loc)
 		require.NotContains(t, builder.locationStack.usedRegisters, tmpReg)
 		// Set the function index
-		builder.movConstToRegister(int64(1), tmpReg)
+		builder.movIntConstToRegister(int64(1), tmpReg)
 		builder.callHostFunctionFromRegisterIndex(tmpReg)
 		// On the continuation after function call,
 		// We push the value onto stack
@@ -579,7 +591,7 @@ func TestAmd64Builder_allocateRegister(t *testing.T) {
 			builder.locationStack.markRegisterUsed(r)
 		}
 		stealTargetLocation := builder.locationStack.pushValueOnRegister(stealTarget)
-		builder.movConstToRegister(int64(50), stealTargetLocation.register)
+		builder.movIntConstToRegister(int64(50), stealTargetLocation.register)
 		require.Equal(t, int16(stealTarget), stealTargetLocation.register)
 		require.True(t, stealTargetLocation.onRegister())
 		reg, err := builder.allocateRegister(gpTypeInt)
@@ -589,7 +601,7 @@ func TestAmd64Builder_allocateRegister(t *testing.T) {
 
 		// Create new value using the stolen register.
 		loc := builder.locationStack.pushValueOnRegister(reg)
-		builder.movConstToRegister(int64(2000), loc.register)
+		builder.movIntConstToRegister(int64(2000), loc.register)
 		builder.releaseRegisterToStack(loc)
 		builder.returnFunction()
 
@@ -653,7 +665,7 @@ func TestAmd64Builder_handlePick(t *testing.T) {
 		pickTargetLocation := builder.locationStack.pushValueOnRegister(int16(x86.REG_R10))
 		pickTargetLocation.setValueType(wazeroir.SignLessTypeI32)
 		builder.locationStack.pushValueOnStack() // Dummy value!
-		builder.movConstToRegister(100, pickTargetLocation.register)
+		builder.movIntConstToRegister(100, pickTargetLocation.register)
 		// Now insert pick code.
 		err := builder.handlePick(o)
 		require.NoError(t, err)
@@ -735,16 +747,56 @@ func TestAmd64Builder_handlePick(t *testing.T) {
 	})
 }
 
+func TestAmd64Builder_handleConstI32(t *testing.T) {
+	for _, v := range []uint32{1, 1 << 5, 1 << 31} {
+		t.Run(fmt.Sprintf("%d", v), func(t *testing.T) {
+			builder := requireNewBuilder(t)
+			builder.initializeReservedRegisters()
+
+			// Now emit the const instruction.
+			o := &wazeroir.OperationConstI32{Value: v}
+			err := builder.handleConstI32(o)
+			require.NoError(t, err)
+
+			// To verify the behavior, we increment and push the const value
+			// to the stack.
+			loc := builder.locationStack.peek()
+			require.Equal(t, wazeroir.SignLessTypeI32, loc.valueType)
+			prog := builder.newProg()
+			prog.As = x86.AINCQ
+			prog.To.Type = obj.TYPE_REG
+			prog.To.Reg = loc.register
+			builder.addInstruction(prog)
+			builder.releaseRegisterToStack(loc)
+			builder.returnFunction()
+
+			// Assemble.
+			code, err := builder.assemble()
+			require.NoError(t, err)
+			// Run code.
+			eng := newEngine()
+			mem := newMemoryInst()
+			jitcall(
+				uintptr(unsafe.Pointer(&code[0])),
+				uintptr(unsafe.Pointer(eng)),
+				uintptr(unsafe.Pointer(&mem.Buffer[0])),
+			)
+			// As we push the constant to the stack, the stack pointer must be incremented.
+			require.Equal(t, uint64(1), eng.currentStackPointer)
+			// Check the value of the top on the stack equals the const plus one.
+			require.Equal(t, uint64(o.Value)+1, eng.stack[eng.currentStackPointer-1])
+		})
+	}
+}
+
 func TestAmd64Builder_handleConstI64(t *testing.T) {
 	for _, v := range []uint64{1, 1 << 5, 1 << 35, 1 << 63} {
 		t.Run(fmt.Sprintf("%d", v), func(t *testing.T) {
-			o := &wazeroir.OperationConstI64{Value: v}
 			builder := requireNewBuilder(t)
 			builder.initializeReservedRegisters()
-			// Dummy value not used!
-			_ = builder.locationStack.pushValueOnStack()
 
 			// Now emit the const instruction.
+			o := &wazeroir.OperationConstI64{Value: v}
 			err := builder.handleConstI64(o)
 			require.NoError(t, err)
 
@@ -762,7 +814,6 @@ func TestAmd64Builder_handleConstI64(t *testing.T) {
 
 			// Assemble.
 			code, err := builder.assemble()
-			fmt.Println(hex.EncodeToString(code))
 			require.NoError(t, err)
 			// Run code.
 			eng := newEngine()
@@ -772,13 +823,102 @@ func TestAmd64Builder_handleConstI64(t *testing.T) {
 				uintptr(unsafe.Pointer(eng)),
 				uintptr(unsafe.Pointer(&mem.Buffer[0])),
 			)
-			// Check the stack.
-			require.Equal(t, uint64(2), eng.currentStackPointer)
+			// As we push the constant to the stack, the stack pointer must be incremented.
+			require.Equal(t, uint64(1), eng.currentStackPointer)
+			// Check the value of the top on the stack equals the const plus one.
 			require.Equal(t, o.Value+1, eng.stack[eng.currentStackPointer-1])
 		})
 	}
 }
 
+func TestAmd64Builder_handleConstF32(t *testing.T) {
+	for _, v := range []float32{1, -3.23, 100.123} {
+		t.Run(fmt.Sprintf("%f", v), func(t *testing.T) {
+			builder := requireNewBuilder(t)
+			builder.initializeReservedRegisters()
+
+			// Now emit the const instruction.
+			o := &wazeroir.OperationConstF32{Value: v}
+			err := builder.handleConstF32(o)
+			require.NoError(t, err)
+
+			// To verify the behavior, we double and push the const value
+			// to the stack.
+			loc := builder.locationStack.peek()
+			require.Equal(t, wazeroir.SignLessTypeF32, loc.valueType)
+			prog := builder.newProg()
+			prog.As = x86.AADDSS
+			prog.To.Type = obj.TYPE_REG
+			prog.To.Reg = loc.register
+			prog.From.Type = obj.TYPE_REG
+			prog.From.Reg = loc.register
+			builder.addInstruction(prog)
+			builder.releaseRegisterToStack(loc)
+			builder.returnFunction()
+
+			// Assemble.
+			code, err := builder.assemble()
+			require.NoError(t, err)
+			// Run code.
+			eng := newEngine()
+			mem := newMemoryInst()
+			jitcall(
+				uintptr(unsafe.Pointer(&code[0])),
+				uintptr(unsafe.Pointer(eng)),
+				uintptr(unsafe.Pointer(&mem.Buffer[0])),
+			)
+			// As we push the constant to the stack, the stack pointer must be incremented.
+			require.Equal(t, uint64(1), eng.currentStackPointer)
+			// Check the value of the top on the stack equals the squared const.
+			require.Equal(t, o.Value*2, math.Float32frombits(uint32(eng.stack[eng.currentStackPointer-1])))
+		})
+	}
+}
+
+func TestAmd64Builder_handleConstF64(t *testing.T) {
+	for _, v := range []float64{1, -3.23, 100.123} {
+		t.Run(fmt.Sprintf("%f", v), func(t *testing.T) {
+			builder := requireNewBuilder(t)
+			builder.initializeReservedRegisters()
+
+			// Now emit the const instruction.
+			o := &wazeroir.OperationConstF64{Value: v}
+			err := builder.handleConstF64(o)
+			require.NoError(t, err)
+
+			// To verify the behavior, we double and push the const value
+			// to the stack.
+			loc := builder.locationStack.peek()
+			require.Equal(t, wazeroir.SignLessTypeF64, loc.valueType)
+			prog := builder.newProg()
+			prog.As = x86.AADDSD
+			prog.To.Type = obj.TYPE_REG
+			prog.To.Reg = loc.register
+			prog.From.Type = obj.TYPE_REG
+			prog.From.Reg = loc.register
+			builder.addInstruction(prog)
+			builder.releaseRegisterToStack(loc)
+			builder.returnFunction()
+
+			// Assemble.
+			code, err := builder.assemble()
+			require.NoError(t, err)
+			// Run code.
+			eng := newEngine()
+			mem := newMemoryInst()
+			jitcall(
+				uintptr(unsafe.Pointer(&code[0])),
+				uintptr(unsafe.Pointer(eng)),
+				uintptr(unsafe.Pointer(&mem.Buffer[0])),
+			)
+			// As we push the constant to the stack, the stack pointer must be incremented.
+			require.Equal(t, uint64(1), eng.currentStackPointer)
+			// Check the value of the top on the stack equals the squared const.
+			require.Equal(t, o.Value*2, math.Float64frombits(eng.stack[eng.currentStackPointer-1]))
+		})
+	}
+}
+
 func TestAmd64Builder_handleAdd(t *testing.T) {
 	t.Run("int64", func(t *testing.T) {
 		o := &wazeroir.OperationAdd{Type: wazeroir.SignLessTypeI64}
@@ -789,8 +929,8 @@ func TestAmd64Builder_handleAdd(t *testing.T) {
 			x2Reg := int16(x86.REG_R10)
 			x1Location := builder.locationStack.pushValueOnRegister(x1Reg)
 			builder.locationStack.pushValueOnRegister(x2Reg)
-			builder.movConstToRegister(100, x1Reg)
-			builder.movConstToRegister(300, x2Reg)
+			builder.movIntConstToRegister(100, x1Reg)
+			builder.movIntConstToRegister(300, x2Reg)
 			err := builder.handleAdd(o)
 			require.NoError(t, err)
 			require.Contains(t, builder.locationStack.usedRegisters, x1Reg)
@@ -824,7 +964,7 @@ func TestAmd64Builder_handleAdd(t *testing.T) {
 			x1Location := builder.locationStack.pushValueOnStack()
 			x2Location := builder.locationStack.pushValueOnRegister(x86.REG_R10)
 			eng.stack[x1Location.stackPointer] = 5000
-			builder.movConstToRegister(300, x2Location.register)
+			builder.movIntConstToRegister(300, x2Location.register)
 			err := builder.handleAdd(o)
 			require.NoError(t, err)
 
@@ -889,7 +1029,7 @@ func TestAmd64Builder_handleAdd(t *testing.T) {
 			x1Location := builder.locationStack.pushValueOnRegister(x86.REG_R10)
 			x2Location := builder.locationStack.pushValueOnStack()
 			eng.stack[x2Location.stackPointer] = 5000
-			builder.movConstToRegister(132, x1Location.register)
+			builder.movIntConstToRegister(132, x1Location.register)
 			err := builder.handleAdd(o)
 			require.NoError(t, err)
 			require.True(t, x1Location.onRegister())
@@ -943,8 +1083,8 @@ func TestAmd64Builder_handleLe(t *testing.T) {
 			x2Reg := int16(x86.REG_R10)
 			builder.locationStack.pushValueOnRegister(x1Reg)
 			builder.locationStack.pushValueOnRegister(x2Reg)
-			builder.movConstToRegister(int64(tc.x1), x1Reg)
-			builder.movConstToRegister(int64(tc.x2), x2Reg)
+			builder.movIntConstToRegister(int64(tc.x1), x1Reg)
+			builder.movIntConstToRegister(int64(tc.x2), x2Reg)
 			err := builder.handleLe(o)
 			require.NoError(t, err)
 
@@ -1002,8 +1142,8 @@ func TestAmd64Builder_handleLe(t *testing.T) {
 			x2Reg := int16(x86.REG_R10)
 			builder.locationStack.pushValueOnRegister(x1Reg)
 			builder.locationStack.pushValueOnRegister(x2Reg)
-			builder.movConstToRegister(tc.x1, x1Reg)
-			builder.movConstToRegister(tc.x2, x2Reg)
+			builder.movIntConstToRegister(tc.x1, x1Reg)
+			builder.movIntConstToRegister(tc.x2, x2Reg)
 			err := builder.handleLe(o)
 			require.NoError(t, err)
 			require.NotContains(t, builder.locationStack.usedRegisters, x1Reg)
@@ -1050,8 +1190,8 @@ func TestAmd64Builder_handleSub(t *testing.T) {
 			x2Reg := int16(x86.REG_R10)
 			x1Location := builder.locationStack.pushValueOnRegister(x1Reg)
 			builder.locationStack.pushValueOnRegister(x2Reg)
-			builder.movConstToRegister(300, x1Reg)
-			builder.movConstToRegister(51, x2Reg)
+			builder.movIntConstToRegister(300, x1Reg)
+			builder.movIntConstToRegister(51, x2Reg)
 			err := builder.handleSub(o)
 			require.NoError(t, err)
 			require.Contains(t, builder.locationStack.usedRegisters, x1Reg)
@@ -1085,7 +1225,7 @@ func TestAmd64Builder_handleSub(t *testing.T) {
 			x1Location := builder.locationStack.pushValueOnStack()
 			x2Location := builder.locationStack.pushValueOnRegister(x86.REG_R10)
 			eng.stack[x1Location.stackPointer] = 5000
-			builder.movConstToRegister(300, x2Location.register)
+			builder.movIntConstToRegister(300, x2Location.register)
 			err := builder.handleSub(o)
 			require.NoError(t, err)
 
@@ -1150,7 +1290,7 @@ func TestAmd64Builder_handleSub(t *testing.T) {
 			x1Location := builder.locationStack.pushValueOnRegister(x86.REG_R10)
 			x2Location := builder.locationStack.pushValueOnStack()
 			eng.stack[x2Location.stackPointer] = 132
-			builder.movConstToRegister(5000, x1Location.register)
+			builder.movIntConstToRegister(5000, x1Location.register)
 			err := builder.handleSub(o)
 			require.NoError(t, err)
 			require.True(t, x1Location.onRegister())
@@ -1199,7 +1339,7 @@ func TestAmd64Builder_handleCall(t *testing.T) {
 		// Push the value onto stack.
 		_ = builder.locationStack.pushValueOnStack() // dummy value, not actually used!
 		loc := builder.locationStack.pushValueOnRegister(x86.REG_AX)
-		builder.movConstToRegister(int64(50), loc.register)
+		builder.movIntConstToRegister(int64(50), loc.register)
 		err := builder.handleCall(&wazeroir.OperationCall{FunctionIndex: functionIndex})
 		require.NoError(t, err)
 
@@ -1240,7 +1380,7 @@ func TestAmd64Builder_handleCall(t *testing.T) {
 		_ = builder.locationStack.pushValueOnStack() // dummy value, not actually used!
 		_ = builder.locationStack.pushValueOnStack() // dummy value, not actually used!
 		loc := builder.locationStack.pushValueOnRegister(x86.REG_AX)
-		builder.movConstToRegister(int64(50), loc.register)
+		builder.movIntConstToRegister(int64(50), loc.register)
 		err := builder.handleCall(&wazeroir.OperationCall{FunctionIndex: functionIndex})
 		require.NoError(t, err)
 
@@ -1370,7 +1510,7 @@ func TestAmd64Builder_handleDrop(t *testing.T) {
 			builder.locationStack.pushValueOnRegister(x86.REG_R9)
 			top := builder.locationStack.pushValueOnStack()
 			eng.stack[top.stackPointer] = 5000
-			builder.movConstToRegister(300, bottom.register)
+			builder.movIntConstToRegister(300, bottom.register)
 			err := builder.handleDrop(&wazeroir.OperationDrop{
 				Range: &wazeroir.InclusiveRange{Start: 1, End: 1},
 			})
@@ -1414,8 +1554,8 @@ func TestAmd64Builder_releaseAllRegistersToStack(t *testing.T) {
 	require.Len(t, builder.locationStack.usedRegisters, 2)
 
 	// Set the values supposed to be released to stack memory space.
-	builder.movConstToRegister(300, x1Reg)
-	builder.movConstToRegister(51, x2Reg)
+	builder.movIntConstToRegister(300, x1Reg)
+	builder.movIntConstToRegister(51, x2Reg)
 	builder.releaseAllRegistersToStack()
 	require.Len(t, builder.locationStack.usedRegisters, 0)
 	builder.returnFunction()
@@ -1459,8 +1599,8 @@ func TestAmd64Builder_handleUnreachable(t *testing.T) {
 	x2Reg := int16(x86.REG_R10)
 	builder.locationStack.pushValueOnRegister(x1Reg)
 	builder.locationStack.pushValueOnRegister(x2Reg)
-	builder.movConstToRegister(300, x1Reg)
-	builder.movConstToRegister(51, x2Reg)
+	builder.movIntConstToRegister(300, x1Reg)
+	builder.movIntConstToRegister(51, x2Reg)
 	builder.handleUnreachable()
 
 	// Assemble.
@@ -1536,14 +1676,14 @@ func TestAmd64Builder_handleSelect(t *testing.T) {
 			var x1, x2, c *valueLocation
 			if tc.x1OnRegister {
 				x1 = builder.locationStack.pushValueOnRegister(x86.REG_AX)
-				builder.movConstToRegister(x1Value, x1.register)
+				builder.movIntConstToRegister(x1Value, x1.register)
 			} else {
 				x1 = builder.locationStack.pushValueOnStack()
 				eng.stack[x1.stackPointer] = x1Value
 			}
 			if tc.x2OnRegister {
 				x2 = builder.locationStack.pushValueOnRegister(x86.REG_R10)
-				builder.movConstToRegister(x2Value, x2.register)
+				builder.movIntConstToRegister(x2Value, x2.register)
 			} else {
 				x2 = builder.locationStack.pushValueOnStack()
 				eng.stack[x2.stackPointer] = x2Value
@@ -1558,12 +1698,12 @@ func TestAmd64Builder_handleSelect(t *testing.T) {
 			} else if tc.condValueOnGPRegister {
 				c = builder.locationStack.pushValueOnRegister(x86.REG_R9)
 				if tc.selectX1 {
-					builder.movConstToRegister(1, c.register)
+					builder.movIntConstToRegister(1, c.register)
 				} else {
-					builder.movConstToRegister(0, c.register)
+					builder.movIntConstToRegister(0, c.register)
 				}
 			} else if tc.condValueOnCondRegister {
-				builder.movConstToRegister(0, x86.REG_CX)
+				builder.movIntConstToRegister(0, x86.REG_CX)
 				cmp := builder.newProg()
 				cmp.As = x86.ACMPQ
 				cmp.From.Type = obj.TYPE_REG
@@ -1632,7 +1772,7 @@ func TestAmd64Builder_handleSwap(t *testing.T) {
 			builder.initializeReservedRegisters()
 			if tc.x2OnRegister {
 				x2 := builder.locationStack.pushValueOnRegister(x86.REG_R10)
-				builder.movConstToRegister(x2Value, x2.register)
+				builder.movIntConstToRegister(x2Value, x2.register)
 			} else {
 				x2 := builder.locationStack.pushValueOnStack()
 				eng.stack[x2.stackPointer] = uint64(x2Value)
@@ -1640,12 +1780,12 @@ func TestAmd64Builder_handleSwap(t *testing.T) {
 			_ = builder.locationStack.pushValueOnStack() // Dummy value!
 			if tc.x1OnRegister && !tc.x1OnConditionalRegister {
 				x1 := builder.locationStack.pushValueOnRegister(x86.REG_AX)
-				builder.movConstToRegister(x1Value, x1.register)
+				builder.movIntConstToRegister(x1Value, x1.register)
 			} else if !tc.x1OnConditionalRegister {
 				x1 := builder.locationStack.pushValueOnStack()
 				eng.stack[x1.stackPointer] = uint64(x1Value)
 			} else {
-				builder.movConstToRegister(0, x86.REG_AX)
+				builder.movIntConstToRegister(0, x86.REG_AX)
 				cmp := builder.newProg()
 				cmp.As = x86.ACMPQ
 				cmp.From.Type = obj.TYPE_REG
diff --git a/wasm/wat/lexer_test.go b/wasm/wat/lexer_test.go
index bde8ae15..d7efabc7 100644
--- a/wasm/wat/lexer_test.go
+++ b/wasm/wat/lexer_test.go
@@ -1,6 +1,7 @@
 package wat
 
 import (
+	"errors"
 	"fmt"
 	"testing"
 	"unicode/utf8"
@@ -36,8 +37,6 @@ const exampleWat = `(module
 
 // TestLex_Example is intentionally verbose to catch line/column/position bugs
 func TestLex_Example(t *testing.T) {
-	tokens, e := lexTokens(exampleWat)
-	require.NoError(t, e)
 	require.Equal(t, []*token{
 		{tokenLParen, 1, 1, "("},
 		{tokenKeyword, 1, 2, "module"},
@@ -183,7 +182,7 @@ func TestLex_Example(t *testing.T) {
 		{tokenString, 22, 23, "\"0000\""},
 		{tokenRParen, 22, 29, ")"},
 		{tokenRParen, 23, 1, ")"},
-	}, tokens)
+	}, lexTokens(t, exampleWat))
 }
 
 func TestLex(t *testing.T) {
@@ -433,109 +432,186 @@ func TestLex(t *testing.T) {
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
-			tokens, e := lexTokens(tc.input)
-			require.NoError(t, e)
-			require.Equal(t, tc.expected, tokens)
+			require.Equal(t, tc.expected, lexTokens(t, tc.input))
 		})
 	}
 }
 
 func TestLex_Errors(t *testing.T) {
 	tests := []struct {
-		name        string
-		input       []byte
-		expectedErr string
+		name         string
+		parser       parseToken
+		input        []byte
+		expectedLine int
+		expectedCol  int
+		expectedErr  string
 	}{
 		{
-			name:        "close paren before open paren",
-			input:       []byte(")("),
-			expectedErr: "test.wasm:1:1: found ')' before '('",
+			name:         "close paren before open paren",
+			input:        []byte(")("),
+			expectedLine: 1,
+			expectedCol:  1,
+			expectedErr:  "found ')' before '('",
 		},
 		{
-			name:        "unbalanced nesting",
-			input:       []byte("(()"),
-			expectedErr: "test.wasm:1:4: expected ')', but reached end of input",
+			name:         "unbalanced nesting",
+			input:        []byte("(()"),
+			expectedLine: 1,
+			expectedCol:  4,
+			expectedErr:  "expected ')', but reached end of input",
 		},
 		{
-			name:        "open paren at end of input",
-			input:       []byte("("),
-			expectedErr: "test.wasm:1:1: found '(' at end of input",
+			name:         "open paren at end of input",
+			input:        []byte("("),
+			expectedLine: 1,
+			expectedCol:  1,
+			expectedErr:  "found '(' at end of input",
 		},
 		{
-			name:        "begin block comment at end of input",
-			input:       []byte("(;"),
-			expectedErr: "test.wasm:1:3: expected block comment end ';)', but reached end of input",
+			name:         "begin block comment at end of input",
+			input:        []byte("(;"),
+			expectedLine: 1,
+			expectedCol:  3,
+			expectedErr:  "expected block comment end ';)', but reached end of input",
 		},
 		{
-			name:        "half line comment",
-			input:       []byte("; TODO"),
-			expectedErr: "test.wasm:1:1: unexpected character ;",
+			name:         "half line comment",
+			input:        []byte("; TODO"),
+			expectedLine: 1,
+			expectedCol:  1,
+			expectedErr:  "unexpected character ;",
 		},
 		{
-			name:        "open block comment",
-			input:       []byte("(; TODO"),
-			expectedErr: "test.wasm:1:8: expected block comment end ';)', but reached end of input",
+			name:         "open block comment",
+			input:        []byte("(; TODO"),
+			expectedLine: 1,
+			expectedCol:  8,
+			expectedErr:  "expected block comment end ';)', but reached end of input",
 		},
 		{
-			name:        "close block comment",
-			input:       []byte(";) TODO"),
-			expectedErr: "test.wasm:1:1: unexpected character ;",
+			name:         "close block comment",
+			input:        []byte(";) TODO"),
+			expectedLine: 1,
+			expectedCol:  1,
+
+			expectedErr: "unexpected character ;",
+		},
+		{
+			name:         "unbalanced nested block comment",
+			input:        []byte("(; TODO (; (YOLO) ;)"),
+			expectedLine: 1,
+			expectedCol:  21,
+			expectedErr:  "expected block comment end ';)', but reached end of input",
 		},
 		{
-			name:        "unbalanced nested block comment",
-			input:       []byte("(; TODO (; (YOLO) ;)"),
-			expectedErr: "test.wasm:1:21: expected block comment end ';)', but reached end of input",
+			name:         "dangling unicode",
+			input:        []byte(" 私"),
+			expectedLine: 1,
+			expectedCol:  2,
+			expectedErr:  "expected an ASCII character, not 私",
 		},
 		{
-			name:        "dangling unicode",
-			input:       []byte(" 私"),
-			expectedErr: "test.wasm:1:2: expected an ASCII character, not 私",
+			name:         "0x80 in block comment",
+			input:        []byte("(; \200)"),
+			expectedLine: 1,
+			expectedCol:  4,
+			expectedErr:  "found an invalid byte in block comment: 0x80",
 		},
 		{
-			name:        "0x80 in block comment",
-			input:       []byte("(; \200)"),
-			expectedErr: "test.wasm:1:4: found an invalid byte in block comment: 0x80",
+			name:         "0x80 in block comment unicode",
+			input:        []byte("(; 私\200)"),
+			expectedLine: 1,
+			expectedCol:  5,
+			expectedErr:  "found an invalid byte in block comment: 0x80",
 		},
 		{
-			name:        "0x80 in block comment unicode",
-			input:       []byte("(; 私\200)"),
-			expectedErr: "test.wasm:1:5: found an invalid byte in block comment: 0x80",
+			name:         "0x80 in line comment",
+			input:        []byte(";; \200"),
+			expectedLine: 1,
+			expectedCol:  4,
+			expectedErr:  "found an invalid byte in line comment: 0x80",
 		},
 		{
-			name:        "0x80 in line comment",
-			input:       []byte(";; \200"),
-			expectedErr: "test.wasm:1:4: found an invalid byte in line comment: 0x80",
+			name:         "0x80 in line comment unicode",
+			input:        []byte(";; 私\200"),
+			expectedLine: 1,
+			expectedCol:  5,
+			expectedErr:  "found an invalid byte in line comment: 0x80",
 		},
 		{
-			name:        "0x80 in line comment unicode",
-			input:       []byte(";; 私\200"),
-			expectedErr: "test.wasm:1:5: found an invalid byte in line comment: 0x80",
+			name:         "0x80 in string",
+			input:        []byte("\" \200\""),
+			expectedLine: 1,
+			expectedCol:  3,
+			expectedErr:  "found an invalid byte in string token: 0x80",
 		},
 		{
-			name:        "0x80 in string",
-			input:       []byte("\" \200\""),
-			expectedErr: "test.wasm:1:3: found an invalid byte in string token: 0x80",
+			name:  "parser error: lParen",
+			input: []byte(" (module)"),
+			parser: func(tok tokenType, tokenBytes []byte, _, _ int) error {
+				if tok != tokenLParen {
+					return nil
+				}
+				return errors.New("error on " + string(tokenBytes))
+			},
+			expectedLine: 1,
+			expectedCol:  2,
+			expectedErr:  "error on (",
+		},
+		{
+			name:  "parser error: keyword",
+			input: []byte(" (module)"),
+			parser: func(tok tokenType, tokenBytes []byte, _, _ int) error {
+				if tok != tokenKeyword {
+					return nil
+				}
+				return errors.New("error on " + string(tokenBytes))
+			},
+			expectedLine: 1,
+			expectedCol:  3,
+			expectedErr:  "error on module",
+		},
+		{
+			name:  "parser error: rParen",
+			input: []byte(" (module)"),
+			parser: func(tok tokenType, tokenBytes []byte, _, _ int) error {
+				if tok != tokenRParen {
+					return nil
+				}
+				return errors.New("error on " + string(tokenBytes))
+			},
+			expectedLine: 1,
+			expectedCol:  9,
+			expectedErr:  "error on )",
 		},
 	}
 
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
-			require.EqualError(t, lex(noopParseToken, "test.wasm", tc.input), tc.expectedErr)
+			parser := tc.parser
+			if parser == nil {
+				parser = noopParseToken
+			}
+			line, col, err := lex(parser, tc.input)
+			require.Equal(t, tc.expectedLine, line)
+			require.Equal(t, tc.expectedCol, col)
+			require.EqualError(t, err, tc.expectedErr)
 		})
 	}
 }
 
-func lexTokens(input string) ([]*token, error) {
+func lexTokens(t *testing.T, input string) []*token {
 	var tokens []*token
-	e := lex(func(tok tokenType, tokenUTF8 []byte, line, col int) (err error) {
+	line, col, err := lex(func(tok tokenType, tokenUTF8 []byte, line, col int) error {
 		tokens = append(tokens, &token{tok, line, col, string(tokenUTF8)})
-		return
-	}, "test.wasm", []byte(input))
-	return tokens, e
+		return nil
+	}, []byte(input))
+	require.NoError(t, err, "%d:%d: %s", line, col, err)
+	return tokens
 }
 
-var noopParseToken parseToken = func(tok tokenType, tokenUTF8 []byte, line, col int) error {
+var noopParseToken parseToken = func(_ tokenType, _ []byte, _, _ int) error {
 	return nil
 }
 
@@ -571,9 +647,9 @@ func BenchmarkLex(b *testing.B) {
 		b.Run(bm.name, func(b *testing.B) {
 			b.ReportAllocs()
 			for i := 0; i < b.N; i++ {
-				err := lex(noopParseToken, "test.wasm", bm.data)
+				line, col, err := lex(noopParseToken, bm.data)
 				if err != nil {
-					panic(err)
+					panic(fmt.Errorf("%d:%d: %w", line, col, err))
 				}
 			}
 		})