Repository: tetratelabs/wazero
Pull request #41 (2021-11-03 06:42:58)

Introduce wazero's IR and add its compiler/interpreter
This commit introduces the wazero's IR (called `wazeroir` ) as a building block for future JIT compilation engine. 
Notably this adds `wazeroir` package and the implementation of the IR compiler, and also 
its direct interpreter.

The wazeroir is inspired by microwasm format (a.k.a. LightbeamIR), previously used
in the lightbeam JIT compiler in Wasmtime, though it is not specified and only exists
in [the previous codebase of wasmtime](https://github.com/bytecodealliance/wasmtime/blob/v0.29.0/crates/lightbeam/src/microwasm.rs).
The main difference from microwasm is that it doesn't have block operations
in order to simplify the implementation.

The wazeroir direct interpreter passes 100% of Wasm specification tests, and fully compatible just like 
`naivevm` interpreter.

Here's the example output of wazero IR translation from the Wasm function of factorial 

Wast:

```wat
  (func $fac (param i64) (result i64)
    (if (result i64) (i64.eqz (local.get 0))
      (then (i64.const 1))
      (else
        (i64.mul
          (local.get 0)
          (call $fac (i64.sub (local.get 0) (i64.const 1)))
        )
      )
    )
  )
```

wazeroir:

```
.entrypoint:
	pick 0
	i64.eqz
	br_if .L2, .L2_else
.L2:
	i64.const 1
	br .L2_cont
.L2_else:
	pick 0
	pick 1
	i64.const 1
	i64.sub
	call 24
	i64.mul
	br .L2_cont
.L2_cont:
	drop 1..1
	br .return
```


mathetake: progress: 100% done for translation/compilation, 20% for direct interpreter


mathetake: progress: 100% done for translation/compilation, 100% for direct interpreter

now debugging the implementation and the followings tests are not yet passed by the interpreter:
```
align.wast.wast
float_exprs.wast
labels.wast
left-to-right.wast
memory.wast
start.wast
```


mathetake: 6 cases in  `float_exprs.wast` are the only things to burn! 🔥 
mathetake: Passed all the spec tests! 
codefromthecrypt: I pulled some text from your PR desc into a RATIONALE.md. This will help as we make modifications, to have a place to store important "why"s

Patch:
diff --git a/RATIONALE.md b/RATIONALE.md
new file mode 100644
index 00000000..bf3534cf
--- /dev/null
+++ b/RATIONALE.md
@@ -0,0 +1,13 @@
+# Notable rationale of wazero
+
+## wazeroir
+wazero's intermediate representation (IR) is called `wazeroir`. Compiling into an IR provides us a faster interpreter
+and a building block for a future JIT compilation engine. Both of these help answer demands for a more performant
+runtime vs interpreting Wasm directly (the `naivevm` interpreter).
+
+### Intermediate Representation (IR) design
+`wazeroir`'s initial design borrowed heavily from the defunct `microwasm` format (a.k.a. LightbeamIR). Notably,
+`wazeroir` doesn't have block operations: this simplifies the implementation.
+
+Note: `microwasm` was never specified formally, and only exists in a historical codebase of wasmtime:
+https://github.com/bytecodealliance/wasmtime/blob/v0.29.0/crates/lightbeam/src/microwasm.rs
diff --git a/wasm/buildoptions/callstack_overflow_disabled.go b/wasm/buildoptions/callstack_overflow_disabled.go
index 09ae882d..c1040ac2 100644
--- a/wasm/buildoptions/callstack_overflow_disabled.go
+++ b/wasm/buildoptions/callstack_overflow_disabled.go
@@ -1,4 +1,4 @@
-//go:build disablew_callstack_overflow_check
+//go:build disable_callstack_overflow_check
 
 package buildoptions
 
diff --git a/wasm/buildoptions/callstack_overflow_enabled.go b/wasm/buildoptions/callstack_overflow_enabled.go
index 852129f0..f75c7f45 100644
--- a/wasm/buildoptions/callstack_overflow_enabled.go
+++ b/wasm/buildoptions/callstack_overflow_enabled.go
@@ -1,4 +1,4 @@
-//go:build !disablew_callstack_overflow_check
+//go:build !disable_callstack_overflow_check
 
 package buildoptions
 
diff --git a/wasm/buildoptions/debug_enabled.go b/wasm/buildoptions/debug_enabled.go
index 2a276424..2bdde55e 100644
--- a/wasm/buildoptions/debug_enabled.go
+++ b/wasm/buildoptions/debug_enabled.go
@@ -1,12 +1,12 @@
 //go:build debug_mode
 
-package wasm
+package buildoptions
 
 const (
-	isDebugMode = true
+	IsDebugMode = true
 )
 
-var optcodeStrs = [256]string{
+var OptcodeStrs = [256]string{
 	0x00: "Unreachable",
 	0x01: "Nop",
 	0x02: "Block",
diff --git a/wasm/engine.go b/wasm/engine.go
index 7856a88c..f395c89e 100644
--- a/wasm/engine.go
+++ b/wasm/engine.go
@@ -2,7 +2,11 @@ package wasm
 
 const PageSize uint64 = 65536
 
+// Engine is the interface implemented by interpreters.
 type Engine interface {
+	// Call invokes a function instance f with given args.
+	// Returns the values from the function.
 	Call(f *FunctionInstance, args ...uint64) (returns []uint64, err error)
+	// Compile compiles down the function instance.
 	Compile(f *FunctionInstance) error
 }
diff --git a/wasm/naivevm/vm.go b/wasm/naivevm/vm.go
index bcc8afc5..caf9ea7f 100644
--- a/wasm/naivevm/vm.go
+++ b/wasm/naivevm/vm.go
@@ -388,8 +388,8 @@ var virtualMachineInstructions = [256]func(vm *naiveVirtualMachine){
 	wasm.OptCodeF64Converti64s:    f64converti64s,
 	wasm.OptCodeF64Converti64u:    f64converti64u,
 	wasm.OptCodeF64Promotef32:     f64promotef32,
-	wasm.OptCodeI32reinterpretf32: func(vm *naiveVirtualMachine) { vm.activeFrame.pc++ },
-	wasm.OptCodeI64reinterpretf64: func(vm *naiveVirtualMachine) { vm.activeFrame.pc++ },
-	wasm.OptCodeF32reinterpreti32: func(vm *naiveVirtualMachine) { vm.activeFrame.pc++ },
-	wasm.OptCodeF64reinterpreti64: func(vm *naiveVirtualMachine) { vm.activeFrame.pc++ },
+	wasm.OptCodeI32Reinterpretf32: func(vm *naiveVirtualMachine) { vm.activeFrame.pc++ },
+	wasm.OptCodeI64Reinterpretf64: func(vm *naiveVirtualMachine) { vm.activeFrame.pc++ },
+	wasm.OptCodeF32Reinterpreti32: func(vm *naiveVirtualMachine) { vm.activeFrame.pc++ },
+	wasm.OptCodeF64Reinterpreti64: func(vm *naiveVirtualMachine) { vm.activeFrame.pc++ },
 }
diff --git a/wasm/optcode.go b/wasm/optcode.go
index 18e6eb90..d2997b2e 100644
--- a/wasm/optcode.go
+++ b/wasm/optcode.go
@@ -194,8 +194,8 @@ const (
 	OptCodeF64Converti64u OptCode = 0xba
 	OptCodeF64Promotef32  OptCode = 0xbb
 
-	OptCodeI32reinterpretf32 OptCode = 0xbc
-	OptCodeI64reinterpretf64 OptCode = 0xbd
-	OptCodeF32reinterpreti32 OptCode = 0xbe
-	OptCodeF64reinterpreti64 OptCode = 0xbf
+	OptCodeI32Reinterpretf32 OptCode = 0xbc
+	OptCodeI64Reinterpretf64 OptCode = 0xbd
+	OptCodeF32Reinterpreti32 OptCode = 0xbe
+	OptCodeF64Reinterpreti64 OptCode = 0xbf
 )
diff --git a/wasm/store.go b/wasm/store.go
index 39855f2a..7d443395 100644
--- a/wasm/store.go
+++ b/wasm/store.go
@@ -447,20 +447,26 @@ func (s *Store) buildFunctionInstances(module *Module, target *ModuleInstance) (
 				memoryDeclarations, tableDeclarations,
 			)
 			if err != nil {
-				return rollbackFuncs, fmt.Errorf("invalid function at index %d/%d: %v", codeIndex, len(module.FunctionSection), err)
+				return rollbackFuncs, fmt.Errorf("invalid function at index %d/%d: %v", codeIndex, len(module.FunctionSection)-1, err)
 			}
 			analysisCache[codeIndex] = f.Blocks
 		} else {
 			f.Blocks = blocks
 		}
 
-		if err := s.engine.Compile(f); err != nil {
-			return rollbackFuncs, fmt.Errorf("compilation failed at index %d/%d: %v", codeIndex, len(module.FunctionSection), err)
-		}
-
 		target.Functions = append(target.Functions, f)
 		s.Functions = append(s.Functions, f)
 	}
+
+	// We compile functions after successfully finished analyzing all functions.
+	// This is not only because we want to do early feedback on malicious binaries,
+	// but also during the compilation phase, the compilers have to see all the possible
+	// function signatures in the module instance.
+	for i, f := range target.Functions {
+		if err := s.engine.Compile(f); err != nil {
+			return rollbackFuncs, fmt.Errorf("compilation failed at index %d/%d: %v", i, len(module.FunctionSection)-1, err)
+		}
+	}
 	return rollbackFuncs, nil
 }
 
@@ -1219,7 +1225,7 @@ func analyzeFunction(
 			for _, exp := range funcType.ReturnTypes {
 				valueTypeStack.push(exp)
 			}
-		} else if OptCodeI32eqz <= op && op <= OptCodeF64reinterpreti64 {
+		} else if OptCodeI32eqz <= op && op <= OptCodeF64Reinterpreti64 {
 			switch OptCode(op) {
 			case OptCodeI32eqz:
 				if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
@@ -1393,22 +1399,22 @@ func analyzeFunction(
 					return fmt.Errorf("cannot pop the operand for f64.promote_f32: %v", err)
 				}
 				valueTypeStack.push(ValueTypeF64)
-			case OptCodeI32reinterpretf32:
+			case OptCodeI32Reinterpretf32:
 				if err := valueTypeStack.popAndVerifyType(ValueTypeF32); err != nil {
 					return fmt.Errorf("cannot pop the operand for i32.reinterpret_f32: %v", err)
 				}
 				valueTypeStack.push(ValueTypeI32)
-			case OptCodeI64reinterpretf64:
+			case OptCodeI64Reinterpretf64:
 				if err := valueTypeStack.popAndVerifyType(ValueTypeF64); err != nil {
 					return fmt.Errorf("cannot pop the operand for i64.reinterpret_f64: %v", err)
 				}
 				valueTypeStack.push(ValueTypeI64)
-			case OptCodeF32reinterpreti32:
+			case OptCodeF32Reinterpreti32:
 				if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
 					return fmt.Errorf("cannot pop the operand for f32.reinterpret_i32: %v", err)
 				}
 				valueTypeStack.push(ValueTypeF32)
-			case OptCodeF64reinterpreti64:
+			case OptCodeF64Reinterpreti64:
 				if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil {
 					return fmt.Errorf("cannot pop the operand for f64.reinterpret_i64: %v", err)
 				}
@@ -1417,7 +1423,7 @@ func analyzeFunction(
 				return fmt.Errorf("invalid numeric instruction 0x%x", op)
 			}
 		} else if op == OptCodeBlock {
-			bt, num, err := readBlockType(module, bytes.NewBuffer(f.Body[pc+1:]))
+			bt, num, err := ReadBlockType(module.TypeSection, bytes.NewBuffer(f.Body[pc+1:]))
 			if err != nil {
 				return fmt.Errorf("read block: %w", err)
 			}
@@ -1429,7 +1435,7 @@ func analyzeFunction(
 			valueTypeStack.pushStackLimit()
 			pc += num
 		} else if op == OptCodeLoop {
-			bt, num, err := readBlockType(module, bytes.NewBuffer(f.Body[pc+1:]))
+			bt, num, err := ReadBlockType(module.TypeSection, bytes.NewBuffer(f.Body[pc+1:]))
 			if err != nil {
 				return fmt.Errorf("read block: %w", err)
 			}
@@ -1442,7 +1448,7 @@ func analyzeFunction(
 			valueTypeStack.pushStackLimit()
 			pc += num
 		} else if op == OptCodeIf {
-			bt, num, err := readBlockType(module, bytes.NewBuffer(f.Body[pc+1:]))
+			bt, num, err := ReadBlockType(module.TypeSection, bytes.NewBuffer(f.Body[pc+1:]))
 			if err != nil {
 				return fmt.Errorf("read block: %w", err)
 			}
@@ -1474,7 +1480,7 @@ func analyzeFunction(
 			f.Blocks[bl.StartAt] = bl
 			if bl.IsIf && bl.ElseAt <= bl.StartAt {
 				if len(bl.BlockType.ReturnTypes) > 0 {
-					return fmt.Errorf("type mismatch between then and else blocks.")
+					return fmt.Errorf("type mismatch between then and else blocks")
 				}
 				// To handle if block without else properly,
 				// we set ElseAt to EndAt-1 so we can just skip else.
@@ -1543,7 +1549,7 @@ func analyzeFunction(
 	return nil
 }
 
-func readBlockType(module *Module, r io.Reader) (*BlockType, uint64, error) {
+func ReadBlockType(types []*FunctionType, r io.Reader) (*BlockType, uint64, error) {
 	raw, num, err := leb128.DecodeInt33AsInt64(r)
 	if err != nil {
 		return nil, 0, fmt.Errorf("decode int33: %w", err)
@@ -1562,10 +1568,10 @@ func readBlockType(module *Module, r io.Reader) (*BlockType, uint64, error) {
 	case -4: // 0x7c in original byte = f64
 		ret = &BlockType{ReturnTypes: []ValueType{ValueTypeF64}}
 	default:
-		if raw < 0 || (raw >= int64(len(module.TypeSection))) {
+		if raw < 0 || (raw >= int64(len(types))) {
 			return nil, 0, fmt.Errorf("invalid block type: %d", raw)
 		}
-		ret = module.TypeSection[raw]
+		ret = types[raw]
 	}
 	return ret, num, nil
 }
diff --git a/wasm/wazeroir/compiler.go b/wasm/wazeroir/compiler.go
new file mode 100644
index 00000000..b3a9ad10
--- /dev/null
+++ b/wasm/wazeroir/compiler.go
@@ -0,0 +1,1512 @@
+package wazeroir
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+	"math"
+	"os"
+	"strings"
+
+	"github.com/tetratelabs/wazero/wasm"
+	"github.com/tetratelabs/wazero/wasm/buildoptions"
+	"github.com/tetratelabs/wazero/wasm/leb128"
+)
+
+type controlFrameKind byte
+
+const (
+	controlFrameKindBlockWithContinuationLabel controlFrameKind = iota
+	controlFrameKindBlockWithoutContinuationLabel
+	controlFrameKindFunction
+	controlFrameKindLoop
+	controlFrameKindIfWithElse
+	controlFrameKindIfWithoutElse
+)
+
+type (
+	controlFrame struct {
+		frameID          uint32
+		originalStackLen int
+		returns          []SignLessType
+		kind             controlFrameKind
+	}
+	controlFrames struct{ frames []*controlFrame }
+)
+
+func (c *controlFrame) ensureContinuation() {
+	// Make sure that if the frame is block and doesn't have continuation,
+	// change the kind so we can emit the continuation block
+	// later when we reach the end instruction of this frame.
+	if c.kind == controlFrameKindBlockWithoutContinuationLabel {
+		c.kind = controlFrameKindBlockWithContinuationLabel
+	}
+}
+
+func (c *controlFrame) asBranchTarget() *BranchTarget {
+	switch c.kind {
+	case controlFrameKindBlockWithContinuationLabel,
+		controlFrameKindBlockWithoutContinuationLabel:
+		return &BranchTarget{Label: &Label{FrameID: c.frameID, Kind: LabelKindContinuation}}
+	case controlFrameKindLoop:
+		return &BranchTarget{Label: &Label{FrameID: c.frameID, Kind: LabelKindHeader}}
+	case controlFrameKindFunction:
+		// Note nil target is translated as return.
+		return &BranchTarget{Label: nil}
+	case controlFrameKindIfWithElse,
+		controlFrameKindIfWithoutElse:
+		return &BranchTarget{Label: &Label{FrameID: c.frameID, Kind: LabelKindContinuation}}
+	}
+	panic(fmt.Sprintf("unreachable: a bug in wazeroir implementation: %v", c.kind))
+}
+
+func (c *controlFrames) functionFrame() *controlFrame {
+	// No need to check stack bound
+	// as we can assume that all the operations
+	// are valid thanks to analyzeFunction
+	// at module validation phase.
+	return c.frames[0]
+}
+
+func (c *controlFrames) get(n int) *controlFrame {
+	// No need to check stack bound
+	// as we can assume that all the operations
+	// are valid thanks to analyzeFunction
+	// at module validation phase.
+	return c.frames[len(c.frames)-n-1]
+}
+
+func (c *controlFrames) top() *controlFrame {
+	// No need to check stack bound
+	// as we can assume that all the operations
+	// are valid thanks to analyzeFunction
+	// at module validation phase.
+	return c.frames[len(c.frames)-1]
+}
+
+func (c *controlFrames) empty() bool {
+	return len(c.frames) == 0
+}
+
+func (c *controlFrames) pop() (frame *controlFrame) {
+	// No need to check stack bound
+	// as we can assume that all the operations
+	// are valid thanks to analyzeFunction
+	// at module validation phase.
+	frame = c.top()
+	c.frames = c.frames[:len(c.frames)-1]
+	return
+}
+
+func (c *controlFrames) push(frame *controlFrame) {
+	c.frames = append(c.frames, frame)
+}
+
+type compiler struct {
+	stack            []SignLessType
+	currentID        uint32
+	controlFrames    *controlFrames
+	unreachableState struct {
+		on    bool
+		depth int
+	}
+	pc     uint64
+	f      *wasm.FunctionInstance
+	result []Operation
+}
+
+// For debugging only.
+func (c *compiler) stackDump() string {
+	strs := make([]string, 0, len(c.stack))
+	for _, s := range c.stack {
+		strs = append(strs, s.String())
+	}
+	return "[" + strings.Join(strs, ", ") + "]"
+}
+
+func (c *compiler) markUnreachable() {
+	c.unreachableState.on = true
+}
+
+func (c *compiler) resetUnreachable() {
+	c.unreachableState.on = false
+}
+
+// Compile lowers given function instance into wazeroir operations
+// so that the resulting operations can be consumed by the interpreter
+// or the JIT compilation engine.
+func Compile(f *wasm.FunctionInstance) ([]Operation, error) {
+	c := compiler{controlFrames: &controlFrames{}, f: f}
+
+	// Push function arguments.
+	for _, t := range f.Signature.InputTypes {
+		c.stackPush(wasmValueTypeToSignless(t))
+	}
+	// Emit const expressions for locals.
+	// Note that here we don't take function arguments
+	// into account, meaning that callers must push
+	// arguments before entering into the function body.
+	for _, t := range f.LocalTypes {
+		c.emitDefaultValue(t)
+	}
+
+	// Insert the function control frame.
+	returns := make([]SignLessType, 0, len(f.Signature.ReturnTypes))
+	for _, t := range f.Signature.ReturnTypes {
+		returns = append(returns, wasmValueTypeToSignless(t))
+	}
+	c.controlFrames.push(&controlFrame{
+		frameID:          c.nextID(),
+		originalStackLen: len(f.Signature.InputTypes),
+		returns:          returns,
+		kind:             controlFrameKindFunction,
+	})
+
+	// Now enter the function body.
+	for !c.controlFrames.empty() {
+		if err := c.handleInstruction(); err != nil {
+			return nil, fmt.Errorf("handling instruction: %w\ndisassemble: %v", err, Format(c.result))
+		}
+	}
+	return c.result, nil
+}
+
+// Translate the current Wasm instruction to wazeroir's operations,
+// and emit the results into c.results.
+func (c *compiler) handleInstruction() error {
+	op := c.f.Body[c.pc]
+	if buildoptions.IsDebugMode {
+		fmt.Printf("handling %s, unreachable_state(on=%v,depth=%d)\n",
+			buildoptions.OptcodeStrs[op],
+			c.unreachableState.on, c.unreachableState.depth,
+		)
+	}
+
+	// Modify the stack according the current instruction.
+	// Note that some instructions will read "index" in
+	// applyToStack and advance c.pc inside the function.
+	index, err := c.applyToStack(op)
+	if err != nil {
+		return fmt.Errorf("apply stack failed for 0x%x: %w: stack: %s", op, err, c.stackDump())
+	}
+
+	// Now we handle each instruction, and
+	// emit the corresponding wazeroir operations to the results.
+operatorSwitch:
+	switch op {
+	case wasm.OptCodeUnreachable:
+		c.emit(
+			&OperationUnreachable{},
+		)
+		c.markUnreachable()
+	case wasm.OptCodeNop:
+		// Nop is noop!
+	case wasm.OptCodeBlock:
+		bt, num, err := wasm.ReadBlockType(c.f.ModuleInstance.Types,
+			bytes.NewBuffer(c.f.Body[c.pc+1:]))
+		if err != nil {
+			return fmt.Errorf("reading block type for block instruction: %w", err)
+		}
+		c.pc += num
+
+		if c.unreachableState.on {
+			// If it is currently in unreachable,
+			// just remove the entire block.
+			c.unreachableState.depth++
+			break operatorSwitch
+		}
+
+		// Create a new frame -- entering this block.
+		frame := &controlFrame{
+			frameID:          c.nextID(),
+			originalStackLen: len(c.stack),
+			kind:             controlFrameKindBlockWithoutContinuationLabel,
+		}
+		for _, t := range bt.ReturnTypes {
+			frame.returns = append(frame.returns, wasmValueTypeToSignless(t))
+		}
+		c.controlFrames.push(frame)
+
+	case wasm.OptCodeLoop:
+		bt, num, err := wasm.ReadBlockType(c.f.ModuleInstance.Types,
+			bytes.NewBuffer(c.f.Body[c.pc+1:]))
+		if err != nil {
+			return fmt.Errorf("reading block type for loop instruction: %w", err)
+		}
+		c.pc += num
+
+		if c.unreachableState.on {
+			// If it is currently in unreachable,
+			// just remove the entire block.
+			c.unreachableState.depth++
+			break operatorSwitch
+		}
+
+		// Create a new frame -- entering loop.
+		frame := &controlFrame{
+			frameID:          c.nextID(),
+			originalStackLen: len(c.stack),
+			kind:             controlFrameKindLoop,
+		}
+		for _, t := range bt.ReturnTypes {
+			frame.returns = append(frame.returns, wasmValueTypeToSignless(t))
+		}
+		c.controlFrames.push(frame)
+
+		// Prep labels for inside and the continuation of this loop.
+		loopLabel := &Label{FrameID: frame.frameID, Kind: LabelKindHeader}
+
+		// Emit the branch operation to enter inside the loop.
+		c.emit(
+			&OperationBr{
+				Target: loopLabel.asBranchTarget(),
+			},
+			&OperationLabel{Label: loopLabel},
+		)
+
+	case wasm.OptCodeIf:
+		bt, num, err := wasm.ReadBlockType(c.f.ModuleInstance.Types,
+			bytes.NewBuffer(c.f.Body[c.pc+1:]))
+		if err != nil {
+			return fmt.Errorf("reading block type for if instruction: %w", err)
+		}
+		c.pc += num
+
+		if c.unreachableState.on {
+			// If it is currently in unreachable,
+			// just remove the entire block.
+			c.unreachableState.depth++
+			break operatorSwitch
+		}
+
+		// Create a new frame -- entering if.
+		frame := &controlFrame{
+			frameID:          c.nextID(),
+			originalStackLen: len(c.stack),
+			// Note this will be set to controlFrameKindIfWithElse
+			// when else optcode found later.
+			kind: controlFrameKindIfWithoutElse,
+		}
+		for _, t := range bt.ReturnTypes {
+			frame.returns = append(frame.returns, wasmValueTypeToSignless(t))
+		}
+		c.controlFrames.push(frame)
+
+		// Prep labels for if and else of this if.
+		thenLabel := &Label{Kind: LabelKindHeader, FrameID: frame.frameID}
+		elseLabel := &Label{Kind: LabelKindElse, FrameID: frame.frameID}
+
+		// Emit the branch operation to enter the then block.
+		c.emit(
+			&OperationBrIf{
+				Then: thenLabel.asBranchTargetDrop(),
+				Else: elseLabel.asBranchTargetDrop(),
+			},
+			&OperationLabel{
+				Label: thenLabel,
+			},
+		)
+	case wasm.OptCodeElse:
+		frame := c.controlFrames.top()
+		if c.unreachableState.on && c.unreachableState.depth > 0 {
+			// If it is currently in unreachable, and the nested if,
+			// just remove the entire else block.
+			break operatorSwitch
+		} else if c.unreachableState.on {
+			// If it is currently in unreachable, and the non-nested if,
+			// reset the stack so we can correctly handle the else block.
+			top := c.controlFrames.top()
+			c.stack = c.stack[:top.originalStackLen]
+			top.kind = controlFrameKindIfWithElse
+
+			// We are no longer unreachable in else frame,
+			// so emit the correct label, and reset the unreachable state.
+			elseLabel := &Label{FrameID: frame.frameID, Kind: LabelKindElse}
+			c.resetUnreachable()
+			c.emit(
+				&OperationLabel{Label: elseLabel},
+			)
+			break operatorSwitch
+		}
+
+		// Change the kind of this If block, indicating that
+		// the if has else block.
+		frame.kind = controlFrameKindIfWithElse
+
+		// We need to reset the stack so that
+		// the values pushed inside the then block
+		// do not affect the else block.
+		dropOp := &OperationDrop{Range: c.getFrameDropRange(frame)}
+		c.stack = c.stack[:frame.originalStackLen]
+
+		// Prep labels for else and the continueation of this if block.
+		elseLabel := &Label{FrameID: frame.frameID, Kind: LabelKindElse}
+		continuationLabel := &Label{FrameID: frame.frameID, Kind: LabelKindContinuation}
+
+		// Emit the instructions for exiting the if loop,
+		// and then the initiation of else block.
+		c.emit(
+			dropOp,
+			// Jump to the continuation of this block.
+			&OperationBr{Target: continuationLabel.asBranchTarget()},
+			// Initiate the else block.
+			&OperationLabel{Label: elseLabel},
+		)
+	case wasm.OptCodeEnd:
+		if c.unreachableState.on && c.unreachableState.depth > 0 {
+			c.unreachableState.depth--
+			break operatorSwitch
+		} else if c.unreachableState.on {
+			c.resetUnreachable()
+
+			frame := c.controlFrames.pop()
+			if c.controlFrames.empty() {
+				return nil
+			}
+
+			c.stack = c.stack[:frame.originalStackLen]
+			for _, t := range frame.returns {
+				c.stackPush(t)
+			}
+
+			continuationLabel := &Label{FrameID: frame.frameID, Kind: LabelKindContinuation}
+			if frame.kind == controlFrameKindIfWithoutElse {
+				// Emit the else label.
+				elseLabel := &Label{Kind: LabelKindElse, FrameID: frame.frameID}
+				c.emit(
+					&OperationLabel{Label: elseLabel},
+					&OperationBr{Target: continuationLabel.asBranchTarget()},
+					&OperationLabel{Label: continuationLabel},
+				)
+			} else {
+				c.emit(
+					&OperationLabel{Label: continuationLabel},
+				)
+			}
+
+			break operatorSwitch
+		}
+
+		frame := c.controlFrames.pop()
+
+		// We need to reset the stack so that
+		// the values pushed inside the block.
+		dropOp := &OperationDrop{Range: c.getFrameDropRange(frame)}
+		c.stack = c.stack[:frame.originalStackLen]
+
+		// Push the result types onto the stack.
+		for _, t := range frame.returns {
+			c.stackPush(t)
+		}
+
+		// Emit the instructions according to the kind of the current control frame.
+		switch frame.kind {
+		case controlFrameKindFunction:
+			if !c.controlFrames.empty() {
+				// Should never happen. If so, there's a bug in the translation.
+				panic("bug: found more function control frames")
+			}
+			// Return from function.
+			c.emit(
+				dropOp,
+				// Pass empty target instead of nil to avoid misinterpretation as "return"
+				&OperationBr{Target: &BranchTarget{}},
+			)
+		case controlFrameKindIfWithoutElse:
+			// This case we have to emit "empty" else label.
+			elseLabel := &Label{Kind: LabelKindElse, FrameID: frame.frameID}
+			continuationLabel := &Label{Kind: LabelKindContinuation, FrameID: frame.frameID}
+			c.emit(
+				dropOp,
+				&OperationBr{Target: continuationLabel.asBranchTarget()},
+				// Emit the else which soon branches into the continuation.
+				&OperationLabel{Label: elseLabel},
+				&OperationBr{Target: continuationLabel.asBranchTarget()},
+				// Initiate the continuation.
+				&OperationLabel{Label: continuationLabel},
+			)
+		case controlFrameKindBlockWithContinuationLabel,
+			controlFrameKindIfWithElse:
+			continuationLabel := &Label{Kind: LabelKindContinuation, FrameID: frame.frameID}
+			c.emit(
+				dropOp,
+				&OperationBr{Target: continuationLabel.asBranchTarget()},
+				&OperationLabel{Label: continuationLabel},
+			)
+		case controlFrameKindLoop, controlFrameKindBlockWithoutContinuationLabel:
+			c.emit(
+				dropOp,
+			)
+		default:
+			// Should never happen. If so, there's a bug in the translation.
+			panic(fmt.Errorf("bug: invalid control frame kind: 0x%x", frame.kind))
+		}
+
+	case wasm.OptCodeBr:
+		target, n, err := leb128.DecodeUint32(bytes.NewBuffer(c.f.Body[c.pc+1:]))
+		if err != nil {
+			return fmt.Errorf("read the target for br_if: %w", err)
+		}
+		c.pc += n
+
+		targetFrame := c.controlFrames.get(int(target))
+		targetFrame.ensureContinuation()
+		dropOp := &OperationDrop{Range: c.getFrameDropRange(targetFrame)}
+
+		c.emit(
+			dropOp,
+			&OperationBr{Target: targetFrame.asBranchTarget()},
+		)
+		// Br operation is stack-polymorphic, and mark the state as unreachable.
+		// That means subsequent instructions in the current control frame are "unreachable"
+		// and can be safely removed.
+		c.markUnreachable()
+	case wasm.OptCodeBrIf:
+		target, n, err := leb128.DecodeUint32(bytes.NewBuffer(c.f.Body[c.pc+1:]))
+		if err != nil {
+			return fmt.Errorf("read the target for br_if: %w", err)
+		}
+		c.pc += n
+
+		targetFrame := c.controlFrames.get(int(target))
+		targetFrame.ensureContinuation()
+		drop := c.getFrameDropRange(targetFrame)
+
+		continuationLabel := &Label{FrameID: c.nextID(), Kind: LabelKindHeader}
+		c.emit(
+			&OperationBrIf{
+				Then: &BranchTargetDrop{ToDrop: drop, Target: targetFrame.asBranchTarget()},
+				Else: continuationLabel.asBranchTargetDrop(),
+			},
+			// Start emitting then block operations.
+			&OperationLabel{
+				Label: continuationLabel,
+			},
+		)
+	case wasm.OptCodeBrTable:
+		r := bytes.NewBuffer(c.f.Body[c.pc+1:])
+		numTargets, n, err := leb128.DecodeUint32(r)
+		if err != nil {
+			return fmt.Errorf("error reading number of targets in br_table: %w", err)
+		}
+		c.pc += n
+
+		// Read the branch targets.
+		targets := make([]*BranchTargetDrop, numTargets)
+		for i := range targets {
+			l, n, err := leb128.DecodeUint32(r)
+			if err != nil {
+				return fmt.Errorf("error reading target %d in br_table: %w", i, err)
+			}
+			c.pc += n
+			targetFrame := c.controlFrames.get(int(l))
+			targetFrame.ensureContinuation()
+			drop := c.getFrameDropRange(targetFrame)
+			targets[i] = &BranchTargetDrop{ToDrop: drop, Target: targetFrame.asBranchTarget()}
+		}
+
+		// Prep default target control frame.
+		l, n, err := leb128.DecodeUint32(r)
+		if err != nil {
+			return fmt.Errorf("error reading default target of br_table: %w", err)
+		}
+		c.pc += n
+		defaultTargetFrame := c.controlFrames.get(int(l))
+		defaultTargetFrame.ensureContinuation()
+		defaultTargetDrop := c.getFrameDropRange(defaultTargetFrame)
+
+		c.emit(
+			&OperationBrTable{
+				Targets: targets,
+				Default: &BranchTargetDrop{
+					ToDrop: defaultTargetDrop, Target: defaultTargetFrame.asBranchTarget(),
+				},
+			},
+		)
+		// Br operation is stack-polymorphic, and mark the state as unreachable.
+		// That means subsequent instructions in the current control frame are "unreachable"
+		// and can be safely removed.
+		c.markUnreachable()
+	case wasm.OptCodeReturn:
+		functionFrame := c.controlFrames.functionFrame()
+		dropOp := &OperationDrop{Range: c.getFrameDropRange(functionFrame)}
+
+		// Cleanup the stack and then jmp to function frame's continuation (meaning return).
+		c.emit(
+			dropOp,
+			&OperationBr{Target: functionFrame.asBranchTarget()},
+		)
+
+		// Return operation is stack-polymorphic, and mark the state as unreachable.
+		// That means subsequent instructions in the current control frame are "unreachable"
+		// and can be safely removed.
+		c.markUnreachable()
+	case wasm.OptCodeCall:
+		if index == nil {
+			return fmt.Errorf("index does not exist for function call")
+		}
+		c.emit(
+			&OperationCall{FunctionIndex: *index},
+		)
+	case wasm.OptCodeCallIndirect:
+		if index == nil {
+			return fmt.Errorf("index does not exist for indirect function call")
+		}
+		tableIndex, n, err := leb128.DecodeUint32(bytes.NewBuffer(c.f.Body[c.pc+1:]))
+		if err != nil {
+			return fmt.Errorf("read target for br_table: %w", err)
+		}
+		c.pc += n
+		c.emit(
+			&OperationCallIndirect{TypeIndex: *index, TableIndex: tableIndex},
+		)
+	case wasm.OptCodeDrop:
+		c.emit(
+			&OperationDrop{Range: &InclusiveRange{Start: 0, End: 0}},
+		)
+	case wasm.OptCodeSelect:
+		c.emit(
+			&OperationSelect{},
+		)
+	case wasm.OptCodeLocalGet:
+		if index == nil {
+			return fmt.Errorf("index does not exist for local.get")
+		}
+		depth := c.localDepth(*index)
+		c.emit(
+			// -1 because we already manipulated the stack before
+			// called localDepth ^^.
+			&OperationPick{Depth: depth - 1},
+		)
+	case wasm.OptCodeLocalSet:
+		if index == nil {
+			return fmt.Errorf("index does not exist for local.set")
+		}
+		depth := c.localDepth(*index)
+		c.emit(
+			// +1 because we already manipulated the stack before
+			// called localDepth ^^.
+			&OperationSwap{Depth: depth + 1},
+			&OperationDrop{Range: &InclusiveRange{Start: 0, End: 0}},
+		)
+	case wasm.OptCodeLocalTee:
+		if index == nil {
+			return fmt.Errorf("index does not exist for local.tee")
+		}
+		depth := c.localDepth(*index)
+		c.emit(
+			&OperationPick{Depth: 0},
+			&OperationSwap{Depth: depth + 1},
+			&OperationDrop{Range: &InclusiveRange{Start: 0, End: 0}},
+		)
+	case wasm.OptCodeGlobalGet:
+		if index == nil {
+			return fmt.Errorf("index does not exist for global.get")
+		}
+		c.emit(
+			&OperationGlobalGet{Index: *index},
+		)
+	case wasm.OptCodeGlobalSet:
+		if index == nil {
+			return fmt.Errorf("index does not exist for global.set")
+		}
+		c.emit(
+			&OperationGlobalSet{Index: *index},
+		)
+	case wasm.OptCodeI32Load:
+		imm, err := c.readMemoryImmediate("i32.load")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad{Type: SignLessTypeI32, Arg: imm},
+		)
+	case wasm.OptCodeI64Load:
+		imm, err := c.readMemoryImmediate("i64.load")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad{Type: SignLessTypeI64, Arg: imm},
+		)
+	case wasm.OptCodeF32Load:
+		imm, err := c.readMemoryImmediate("f32.load")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad{Type: SignLessTypeF32, Arg: imm},
+		)
+	case wasm.OptCodeF64Load:
+		imm, err := c.readMemoryImmediate("f64.load")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad{Type: SignLessTypeF64, Arg: imm},
+		)
+	case wasm.OptCodeI32Load8s:
+		imm, err := c.readMemoryImmediate("i32.load8_s")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad8{Type: SignFulInt32, Arg: imm},
+		)
+	case wasm.OptCodeI32Load8u:
+		imm, err := c.readMemoryImmediate("i32.load8_u")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad8{Type: SignFulUint32, Arg: imm},
+		)
+	case wasm.OptCodeI32Load16s:
+		imm, err := c.readMemoryImmediate("i32.load16_s")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad16{Type: SignFulInt32, Arg: imm},
+		)
+	case wasm.OptCodeI32Load16u:
+		imm, err := c.readMemoryImmediate("i32.load16_u")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad16{Type: SignFulUint32, Arg: imm},
+		)
+	case wasm.OptCodeI64Load8s:
+		imm, err := c.readMemoryImmediate("i64.load8_s")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad8{Type: SignFulInt64, Arg: imm},
+		)
+	case wasm.OptCodeI64Load8u:
+		imm, err := c.readMemoryImmediate("i64.load8_u")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad8{Type: SignFulUint64, Arg: imm},
+		)
+	case wasm.OptCodeI64Load16s:
+		imm, err := c.readMemoryImmediate("i64.load16_s")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad16{Type: SignFulInt64, Arg: imm},
+		)
+	case wasm.OptCodeI64Load16u:
+		imm, err := c.readMemoryImmediate("i64.load16_u")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad16{Type: SignFulUint64, Arg: imm},
+		)
+	case wasm.OptCodeI64Load32s:
+		imm, err := c.readMemoryImmediate("i64.load32_s")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad32{Signed: true, Arg: imm},
+		)
+	case wasm.OptCodeI64Load32u:
+		imm, err := c.readMemoryImmediate("i64.load32_s")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationLoad32{Signed: false, Arg: imm},
+		)
+	case wasm.OptCodeI32Store:
+		imm, err := c.readMemoryImmediate("i32.store")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationStore{Type: SignLessTypeI32, Arg: imm},
+		)
+	case wasm.OptCodeI64Store:
+		imm, err := c.readMemoryImmediate("i64.store")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationStore{Type: SignLessTypeI64, Arg: imm},
+		)
+	case wasm.OptCodeF32Store:
+		imm, err := c.readMemoryImmediate("f32.store")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationStore{Type: SignLessTypeF32, Arg: imm},
+		)
+	case wasm.OptCodeF64Store:
+		imm, err := c.readMemoryImmediate("f64.store")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationStore{Type: SignLessTypeF64, Arg: imm},
+		)
+	case wasm.OptCodeI32Store8:
+		imm, err := c.readMemoryImmediate("i32.store8")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationStore8{Type: SignLessInt32, Arg: imm},
+		)
+	case wasm.OptCodeI32Store16:
+		imm, err := c.readMemoryImmediate("i32.store16")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationStore16{Type: SignLessInt32, Arg: imm},
+		)
+	case wasm.OptCodeI64Store8:
+		imm, err := c.readMemoryImmediate("i64.store8")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationStore8{Type: SignLessInt64, Arg: imm},
+		)
+	case wasm.OptCodeI64Store16:
+		imm, err := c.readMemoryImmediate("i64.store16")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationStore16{Type: SignLessInt64, Arg: imm},
+		)
+	case wasm.OptCodeI64Store32:
+		imm, err := c.readMemoryImmediate("i64.store32")
+		if err != nil {
+			return err
+		}
+		c.emit(
+			&OperationStore32{Arg: imm},
+		)
+	case wasm.OptCodeMemorySize:
+		c.pc++ // Skip the reserved one byte.
+		c.emit(
+			&OperationMemorySize{},
+		)
+	case wasm.OptCodeMemoryGrow:
+		c.pc++ // Skip the reserved one byte.
+		c.emit(
+			&OperationMemoryGrow{},
+		)
+	case wasm.OptCodeI32Const:
+		val, num, err := leb128.DecodeInt32(bytes.NewBuffer(c.f.Body[c.pc+1:]))
+		if err != nil {
+			return fmt.Errorf("reading i32.const value: %v", err)
+		}
+		c.pc += num
+		c.emit(
+			&OperationConstI32{Value: uint32(val)},
+		)
+	case wasm.OptCodeI64Const:
+		val, num, err := leb128.DecodeInt64(bytes.NewBuffer(c.f.Body[c.pc+1:]))
+		if err != nil {
+			return fmt.Errorf("reading i64.const value: %v", err)
+		}
+		c.pc += num
+		c.emit(
+			&OperationConstI64{Value: uint64(val)},
+		)
+	case wasm.OptCodeF32Const:
+		v := math.Float32frombits(binary.LittleEndian.Uint32(c.f.Body[c.pc+1:]))
+		c.pc += 4
+		c.emit(
+			&OperationConstF32{Value: v},
+		)
+	case wasm.OptCodeF64Const:
+		v := math.Float64frombits(binary.LittleEndian.Uint64(c.f.Body[c.pc+1:]))
+		c.pc += 8
+		c.emit(
+			&OperationConstF64{Value: v},
+		)
+	case wasm.OptCodeI32eqz:
+		c.emit(
+			&OperationEqz{Type: SignLessInt32},
+		)
+	case wasm.OptCodeI32eq:
+		c.emit(
+			&OperationEq{Type: SignLessTypeI32},
+		)
+	case wasm.OptCodeI32ne:
+		c.emit(
+			&OperationNe{Type: SignLessTypeI32},
+		)
+	case wasm.OptCodeI32lts:
+		c.emit(
+			&OperationLt{Type: SignFulTypeInt32},
+		)
+	case wasm.OptCodeI32ltu:
+		c.emit(
+			&OperationLt{Type: SignFulTypeUint32},
+		)
+	case wasm.OptCodeI32gts:
+		c.emit(
+			&OperationGt{Type: SignFulTypeInt32},
+		)
+	case wasm.OptCodeI32gtu:
+		c.emit(
+			&OperationGt{Type: SignFulTypeUint32},
+		)
+	case wasm.OptCodeI32les:
+		c.emit(
+			&OperationLe{Type: SignFulTypeInt32},
+		)
+	case wasm.OptCodeI32leu:
+		c.emit(
+			&OperationLe{Type: SignFulTypeUint32},
+		)
+	case wasm.OptCodeI32ges:
+		c.emit(
+			&OperationGe{Type: SignFulTypeInt32},
+		)
+	case wasm.OptCodeI32geu:
+		c.emit(
+			&OperationGe{Type: SignFulTypeUint32},
+		)
+	case wasm.OptCodeI64eqz:
+		c.emit(
+			&OperationEqz{Type: SignLessInt64},
+		)
+	case wasm.OptCodeI64eq:
+		c.emit(
+			&OperationEq{Type: SignLessTypeI64},
+		)
+	case wasm.OptCodeI64ne:
+		c.emit(
+			&OperationNe{Type: SignLessTypeI64},
+		)
+	case wasm.OptCodeI64lts:
+		c.emit(
+			&OperationLt{Type: SignFulTypeInt64},
+		)
+	case wasm.OptCodeI64ltu:
+		c.emit(
+			&OperationLt{Type: SignFulTypeUint64},
+		)
+	case wasm.OptCodeI64gts:
+		c.emit(
+			&OperationGt{Type: SignFulTypeInt64},
+		)
+	case wasm.OptCodeI64gtu:
+		c.emit(
+			&OperationGt{Type: SignFulTypeUint64},
+		)
+	case wasm.OptCodeI64les:
+		c.emit(
+			&OperationLe{Type: SignFulTypeInt64},
+		)
+	case wasm.OptCodeI64leu:
+		c.emit(
+			&OperationLe{Type: SignFulTypeUint64},
+		)
+	case wasm.OptCodeI64ges:
+		c.emit(
+			&OperationGe{Type: SignFulTypeInt64},
+		)
+	case wasm.OptCodeI64geu:
+		c.emit(
+			&OperationGe{Type: SignFulTypeUint64},
+		)
+	case wasm.OptCodeF32eq:
+		c.emit(
+			&OperationEq{Type: SignLessTypeF32},
+		)
+	case wasm.OptCodeF32ne:
+		c.emit(
+			&OperationNe{Type: SignLessTypeF32},
+		)
+	case wasm.OptCodeF32lt:
+		c.emit(
+			&OperationLt{Type: SignFulTypeFloat32},
+		)
+	case wasm.OptCodeF32gt:
+		c.emit(
+			&OperationGt{Type: SignFulTypeFloat32},
+		)
+	case wasm.OptCodeF32le:
+		c.emit(
+			&OperationLe{Type: SignFulTypeFloat32},
+		)
+	case wasm.OptCodeF32ge:
+		c.emit(
+			&OperationGe{Type: SignFulTypeFloat32},
+		)
+	case wasm.OptCodeF64eq:
+		c.emit(
+			&OperationEq{Type: SignLessTypeF64},
+		)
+	case wasm.OptCodeF64ne:
+		c.emit(
+			&OperationNe{Type: SignLessTypeF64},
+		)
+	case wasm.OptCodeF64lt:
+		c.emit(
+			&OperationLt{Type: SignFulTypeFloat64},
+		)
+	case wasm.OptCodeF64gt:
+		c.emit(
+			&OperationGt{Type: SignFulTypeFloat64},
+		)
+	case wasm.OptCodeF64le:
+		c.emit(
+			&OperationLe{Type: SignFulTypeFloat64},
+		)
+	case wasm.OptCodeF64ge:
+		c.emit(
+			&OperationGe{Type: SignFulTypeFloat64},
+		)
+	case wasm.OptCodeI32clz:
+		c.emit(
+			&OperationClz{Type: SignLessInt32},
+		)
+	case wasm.OptCodeI32ctz:
+		c.emit(
+			&OperationCtz{Type: SignLessInt32},
+		)
+	case wasm.OptCodeI32popcnt:
+		c.emit(
+			&OperationPopcnt{Type: SignLessInt32},
+		)
+	case wasm.OptCodeI32add:
+		c.emit(
+			&OperationAdd{Type: SignLessTypeI32},
+		)
+	case wasm.OptCodeI32sub:
+		c.emit(
+			&OperationSub{Type: SignLessTypeI32},
+		)
+	case wasm.OptCodeI32mul:
+		c.emit(
+			&OperationMul{Type: SignLessTypeI32},
+		)
+	case wasm.OptCodeI32divs:
+		c.emit(
+			&OperationDiv{Type: SignFulTypeInt32},
+		)
+	case wasm.OptCodeI32divu:
+		c.emit(
+			&OperationDiv{Type: SignFulTypeUint32},
+		)
+	case wasm.OptCodeI32rems:
+		c.emit(
+			&OperationRem{Type: SignFulInt32},
+		)
+	case wasm.OptCodeI32remu:
+		c.emit(
+			&OperationRem{Type: SignFulUint32},
+		)
+	case wasm.OptCodeI32and:
+		c.emit(
+			&OperationAnd{Type: SignLessInt32},
+		)
+	case wasm.OptCodeI32or:
+		c.emit(
+			&OperationOr{Type: SignLessInt32},
+		)
+	case wasm.OptCodeI32xor:
+		c.emit(
+			&OperationXor{Type: SignLessInt64},
+		)
+	case wasm.OptCodeI32shl:
+		c.emit(
+			&OperationShl{Type: SignLessInt32},
+		)
+	case wasm.OptCodeI32shrs:
+		c.emit(
+			&OperationShr{Type: SignFulInt32},
+		)
+	case wasm.OptCodeI32shru:
+		c.emit(
+			&OperationShr{Type: SignFulUint32},
+		)
+	case wasm.OptCodeI32rotl:
+		c.emit(
+			&OperationRotl{Type: SignLessInt32},
+		)
+	case wasm.OptCodeI32rotr:
+		c.emit(
+			&OperationRotr{Type: SignLessInt32},
+		)
+	case wasm.OptCodeI64clz:
+		c.emit(
+			&OperationClz{Type: SignLessInt64},
+		)
+	case wasm.OptCodeI64ctz:
+		c.emit(
+			&OperationCtz{Type: SignLessInt64},
+		)
+	case wasm.OptCodeI64popcnt:
+		c.emit(
+			&OperationPopcnt{Type: SignLessInt64},
+		)
+	case wasm.OptCodeI64add:
+		c.emit(
+			&OperationAdd{Type: SignLessTypeI64},
+		)
+	case wasm.OptCodeI64sub:
+		c.emit(
+			&OperationSub{Type: SignLessTypeI64},
+		)
+	case wasm.OptCodeI64mul:
+		c.emit(
+			&OperationMul{Type: SignLessTypeI64},
+		)
+	case wasm.OptCodeI64divs:
+		c.emit(
+			&OperationDiv{Type: SignFulTypeInt64},
+		)
+	case wasm.OptCodeI64divu:
+		c.emit(
+			&OperationDiv{Type: SignFulTypeUint64},
+		)
+	case wasm.OptCodeI64rems:
+		c.emit(
+			&OperationRem{Type: SignFulInt64},
+		)
+	case wasm.OptCodeI64remu:
+		c.emit(
+			&OperationRem{Type: SignFulUint64},
+		)
+	case wasm.OptCodeI64and:
+		c.emit(
+			&OperationAnd{Type: SignLessInt64},
+		)
+	case wasm.OptCodeI64or:
+		c.emit(
+			&OperationOr{Type: SignLessInt64},
+		)
+	case wasm.OptCodeI64xor:
+		c.emit(
+			&OperationXor{Type: SignLessInt64},
+		)
+	case wasm.OptCodeI64shl:
+		c.emit(
+			&OperationShl{Type: SignLessInt64},
+		)
+	case wasm.OptCodeI64shrs:
+		c.emit(
+			&OperationShr{Type: SignFulInt64},
+		)
+	case wasm.OptCodeI64shru:
+		c.emit(
+			&OperationShr{Type: SignFulUint64},
+		)
+	case wasm.OptCodeI64rotl:
+		c.emit(
+			&OperationRotl{Type: SignLessInt64},
+		)
+	case wasm.OptCodeI64rotr:
+		c.emit(
+			&OperationRotr{Type: SignLessInt64},
+		)
+	case wasm.OptCodeF32abs:
+		c.emit(
+			&OperationAbs{Type: Float32},
+		)
+	case wasm.OptCodeF32neg:
+		c.emit(
+			&OperationNeg{Type: Float32},
+		)
+	case wasm.OptCodeF32ceil:
+		c.emit(
+			&OperationCeil{Type: Float32},
+		)
+	case wasm.OptCodeF32floor:
+		c.emit(
+			&OperationFloor{Type: Float32},
+		)
+	case wasm.OptCodeF32trunc:
+		c.emit(
+			&OperationTrunc{Type: Float32},
+		)
+	case wasm.OptCodeF32nearest:
+		c.emit(
+			&OperationNearest{Type: Float32},
+		)
+	case wasm.OptCodeF32sqrt:
+		c.emit(
+			&OperationSqrt{Type: Float32},
+		)
+	case wasm.OptCodeF32add:
+		c.emit(
+			&OperationAdd{Type: SignLessTypeF32},
+		)
+	case wasm.OptCodeF32sub:
+		c.emit(
+			&OperationSub{Type: SignLessTypeF32},
+		)
+	case wasm.OptCodeF32mul:
+		c.emit(
+			&OperationMul{Type: SignLessTypeF32},
+		)
+	case wasm.OptCodeF32div:
+		c.emit(
+			&OperationDiv{Type: SignFulTypeFloat32},
+		)
+	case wasm.OptCodeF32min:
+		c.emit(
+			&OperationMin{Type: Float32},
+		)
+	case wasm.OptCodeF32max:
+		c.emit(
+			&OperationMax{Type: Float32},
+		)
+	case wasm.OptCodeF32copysign:
+		c.emit(
+			&OperationCopysign{Type: Float32},
+		)
+	case wasm.OptCodeF64abs:
+		c.emit(
+			&OperationAbs{Type: Float64},
+		)
+	case wasm.OptCodeF64neg:
+		c.emit(
+			&OperationNeg{Type: Float64},
+		)
+	case wasm.OptCodeF64ceil:
+		c.emit(
+			&OperationCeil{Type: Float64},
+		)
+	case wasm.OptCodeF64floor:
+		c.emit(
+			&OperationFloor{Type: Float64},
+		)
+	case wasm.OptCodeF64trunc:
+		c.emit(
+			&OperationTrunc{Type: Float64},
+		)
+	case wasm.OptCodeF64nearest:
+		c.emit(
+			&OperationNearest{Type: Float64},
+		)
+	case wasm.OptCodeF64sqrt:
+		c.emit(
+			&OperationSqrt{Type: Float64},
+		)
+	case wasm.OptCodeF64add:
+		c.emit(
+			&OperationAdd{Type: SignLessTypeF64},
+		)
+	case wasm.OptCodeF64sub:
+		c.emit(
+			&OperationSub{Type: SignLessTypeF64},
+		)
+	case wasm.OptCodeF64mul:
+		c.emit(
+			&OperationMul{Type: SignLessTypeF64},
+		)
+	case wasm.OptCodeF64div:
+		c.emit(
+			&OperationDiv{Type: SignFulTypeFloat64},
+		)
+	case wasm.OptCodeF64min:
+		c.emit(
+			&OperationMin{Type: Float64},
+		)
+	case wasm.OptCodeF64max:
+		c.emit(
+			&OperationMax{Type: Float64},
+		)
+	case wasm.OptCodeF64copysign:
+		c.emit(
+			&OperationCopysign{Type: Float64},
+		)
+	case wasm.OptCodeI32wrapI64:
+		c.emit(
+			&OperationI32WrapFromI64{},
+		)
+	case wasm.OptCodeI32truncf32s:
+		c.emit(
+			&OperationITruncFromF{InputType: Float32, OutputType: SignFulInt32},
+		)
+	case wasm.OptCodeI32truncf32u:
+		c.emit(
+			&OperationITruncFromF{InputType: Float32, OutputType: SignFulUint32},
+		)
+	case wasm.OptCodeI32truncf64s:
+		c.emit(
+			&OperationITruncFromF{InputType: Float64, OutputType: SignFulInt32},
+		)
+	case wasm.OptCodeI32truncf64u:
+		c.emit(
+			&OperationITruncFromF{InputType: Float64, OutputType: SignFulUint32},
+		)
+	case wasm.OptCodeI64Extendi32s:
+		c.emit(
+			&OperationExtend{Signed: true},
+		)
+	case wasm.OptCodeI64Extendi32u:
+		c.emit(
+			&OperationExtend{Signed: false},
+		)
+	case wasm.OptCodeI64TruncF32s:
+		c.emit(
+			&OperationITruncFromF{InputType: Float32, OutputType: SignFulInt64},
+		)
+	case wasm.OptCodeI64TruncF32u:
+		c.emit(
+			&OperationITruncFromF{InputType: Float32, OutputType: SignFulUint64},
+		)
+	case wasm.OptCodeI64Truncf64s:
+		c.emit(
+			&OperationITruncFromF{InputType: Float64, OutputType: SignFulInt64},
+		)
+	case wasm.OptCodeI64Truncf64u:
+		c.emit(
+			&OperationITruncFromF{InputType: Float64, OutputType: SignFulUint64},
+		)
+	case wasm.OptCodeF32Converti32s:
+		c.emit(
+			&OperationFConvertFromI{InputType: SignFulInt32, OutputType: Float32},
+		)
+	case wasm.OptCodeF32Converti32u:
+		c.emit(
+			&OperationFConvertFromI{InputType: SignFulUint32, OutputType: Float32},
+		)
+	case wasm.OptCodeF32Converti64s:
+		c.emit(
+			&OperationFConvertFromI{InputType: SignFulInt64, OutputType: Float32},
+		)
+	case wasm.OptCodeF32Converti64u:
+		c.emit(
+			&OperationFConvertFromI{InputType: SignFulUint64, OutputType: Float32},
+		)
+	case wasm.OptCodeF32Demotef64:
+		c.emit(
+			&OperationF32DemoteFromF64{},
+		)
+	case wasm.OptCodeF64Converti32s:
+		c.emit(
+			&OperationFConvertFromI{InputType: SignFulInt32, OutputType: Float64},
+		)
+	case wasm.OptCodeF64Converti32u:
+		c.emit(
+			&OperationFConvertFromI{InputType: SignFulUint32, OutputType: Float64},
+		)
+	case wasm.OptCodeF64Converti64s:
+		c.emit(
+			&OperationFConvertFromI{InputType: SignFulInt64, OutputType: Float64},
+		)
+	case wasm.OptCodeF64Converti64u:
+		c.emit(
+			&OperationFConvertFromI{InputType: SignFulUint64, OutputType: Float64},
+		)
+	case wasm.OptCodeF64Promotef32:
+		c.emit(
+			&OperationF64PromoteFromF32{},
+		)
+	case wasm.OptCodeI32Reinterpretf32:
+		c.emit(
+			&OperationI32ReinterpretFromF32{},
+		)
+	case wasm.OptCodeI64Reinterpretf64:
+		c.emit(
+			&OperationI64ReinterpretFromF64{},
+		)
+	case wasm.OptCodeF32Reinterpreti32:
+		c.emit(
+			&OperationF32ReinterpretFromI32{},
+		)
+	case wasm.OptCodeF64Reinterpreti64:
+		c.emit(
+			&OperationF64ReinterpretFromI64{},
+		)
+	default:
+		return fmt.Errorf("unsupported instruction in wazeroir: 0x%x", op)
+	}
+
+	// Move the program counter to point to the next instruction.
+	c.pc++
+	return nil
+}
+
+func (c *compiler) nextID() (id uint32) {
+	id = c.currentID + 1
+	c.currentID++
+	return
+}
+
+func (c *compiler) applyToStack(optCode wasm.OptCode) (*uint32, error) {
+	var index uint32
+	var ptr *uint32
+	switch optCode {
+	case
+		// These are the optcodes that is coupled with "index"　immediate
+		// and it DOES affect the signature of optcode.
+		wasm.OptCodeCall,
+		wasm.OptCodeCallIndirect,
+		wasm.OptCodeLocalGet,
+		wasm.OptCodeLocalSet,
+		wasm.OptCodeLocalTee,
+		wasm.OptCodeGlobalGet,
+		wasm.OptCodeGlobalSet:
+		// Assumes that we are at the optcode now so skip it before read immediates.
+		v, num, err := leb128.DecodeUint32(bytes.NewBuffer(c.f.Body[c.pc+1:]))
+		if err != nil {
+			return nil, fmt.Errorf("reading immediates: %w", err)
+		}
+		c.pc += num
+		index = v
+		ptr = &index
+	default:
+		// Note that other optcodes are free of index
+		// as it doesn't affect the signature of opt code.
+		// In other words, the "index" argument of wasmOptcodeSignature
+		// is ignored there.
+	}
+
+	if c.unreachableState.on {
+		return ptr, nil
+	}
+
+	// Retrieve the signature of the optcode.
+	s, err := wasmOptcodeSignature(c.f, optCode, index)
+	if err != nil {
+		return nil, err
+	}
+
+	// Manipulate the stack according to the signtature.
+	// Note that the following algorithm assumes that
+	// the unknown type is unique in the signature,
+	// and is determined by the actual type on the stack.
+	// The determined type is stored in this typeParam.
+	var typeParam *SignLessType
+	for i := range s.in {
+		want := s.in[len(s.in)-1-i]
+		actual := c.stackPop()
+		if want == SignLessTypeUnknown && typeParam != nil {
+			want = *typeParam
+		} else if want == SignLessTypeUnknown {
+			want = actual
+			typeParam = &actual
+		}
+		if want != actual {
+			return nil, fmt.Errorf("input signature mismatch: want %s but have %s", want, actual)
+		}
+	}
+
+	for _, target := range s.out {
+		if target == SignLessTypeUnknown && typeParam == nil {
+			return nil, fmt.Errorf("cannot determine type of unknown result")
+		} else if target == SignLessTypeUnknown {
+			c.stackPush(*typeParam)
+		} else {
+			c.stackPush(target)
+		}
+	}
+
+	return ptr, nil
+}
+
+func (c *compiler) stackPop() (ret SignLessType) {
+	// No need to check stack bound
+	// as we can assume that all the operations
+	// are valid thanks to analyzeFunction
+	// at module validation phase.
+	ret = c.stack[len(c.stack)-1]
+	c.stack = c.stack[:len(c.stack)-1]
+	return
+}
+
+func (c *compiler) stackPush(t SignLessType) {
+	c.stack = append(c.stack, t)
+}
+
+// Emit the operatiosn into the result.
+func (c *compiler) emit(ops ...Operation) {
+	if !c.unreachableState.on {
+		for _, op := range ops {
+			switch o := op.(type) {
+			case *OperationDrop:
+				// If the drop range is nil,
+				// we could remove such operatinos.
+				// That happens when drop operation is unnecessary.
+				// i.e. when there's no need to adjust stack before jmp.
+				if o.Range == nil {
+					continue
+				}
+			}
+			c.result = append(c.result, op)
+			if buildoptions.IsDebugMode {
+				fmt.Printf("emitting ")
+				formatOperation(os.Stdout, op)
+			}
+		}
+	}
+}
+
+// Emit const expression with default values of the given type.
+func (c *compiler) emitDefaultValue(t wasm.ValueType) {
+	switch t {
+	case wasm.ValueTypeI32:
+		c.stackPush(SignLessTypeI32)
+		c.emit(&OperationConstI32{Value: 0})
+	case wasm.ValueTypeI64:
+		c.stackPush(SignLessTypeI64)
+		c.emit(&OperationConstI64{Value: 0})
+	case wasm.ValueTypeF32:
+		c.stackPush(SignLessTypeF32)
+		c.emit(&OperationConstF32{Value: 0})
+	case wasm.ValueTypeF64:
+		c.stackPush(SignLessTypeF64)
+		c.emit(&OperationConstF64{Value: 0})
+	}
+}
+
+// Returns the "depth" (starting from top of the stack)
+// of the n-th local.
+func (c *compiler) localDepth(n uint32) int {
+	return int(len(c.stack)) - 1 - int(n)
+}
+
+// Returns the range (starting from top of the stack) that spans across
+// the stack. The range is supposed to be dropped from the stack when
+// the given frame exists.
+func (c *compiler) getFrameDropRange(frame *controlFrame) *InclusiveRange {
+	start := len(frame.returns)
+	var end int
+	if frame.kind == controlFrameKindFunction {
+		// On the function return, we eliminate all the contents on the stack
+		// including locals (existing below of frame.originalStackLen)
+		end = len(c.stack) - 1
+	} else {
+		end = len(c.stack) - 1 - frame.originalStackLen
+	}
+	if start <= end {
+		return &InclusiveRange{Start: start, End: end}
+	}
+	return nil
+}
+
+func (c *compiler) readMemoryImmediate(tag string) (*MemoryImmediate, error) {
+	r := bytes.NewBuffer(c.f.Body[c.pc+1:])
+	alignment, num, err := leb128.DecodeUint32(r)
+	if err != nil {
+		return nil, fmt.Errorf("reading alignment for %s: %w", tag, err)
+	}
+	c.pc += num
+	offset, num, err := leb128.DecodeUint32(r)
+	if err != nil {
+		return nil, fmt.Errorf("reading offset for %s: %w", tag, err)
+	}
+	c.pc += num
+	return &MemoryImmediate{Offest: offset, Alignment: alignment}, nil
+}
diff --git a/wasm/wazeroir/engine.go b/wasm/wazeroir/engine.go
new file mode 100644
index 00000000..00a8c24e
--- /dev/null
+++ b/wasm/wazeroir/engine.go
@@ -0,0 +1,14 @@
+package wazeroir
+
+import (
+	"github.com/tetratelabs/wazero/wasm"
+)
+
+func NewEngine() wasm.Engine {
+	// TODO: add option to use JIT instead of interpreter
+	return &interpreter{
+		functions:                  map[*wasm.FunctionInstance]*interpreterFunction{},
+		functionTypeIDs:            map[string]uint64{},
+		onCompilationDoneCallbacks: map[*wasm.FunctionInstance][]func(*interpreterFunction){},
+	}
+}
diff --git a/wasm/wazeroir/format.go b/wasm/wazeroir/format.go
new file mode 100644
index 00000000..f0a8935a
--- /dev/null
+++ b/wasm/wazeroir/format.go
@@ -0,0 +1,192 @@
+package wazeroir
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"strings"
+)
+
+func Format(ops []Operation) string {
+	buf := bytes.NewBuffer(nil)
+
+	_, _ = buf.WriteString(".entrypoint:\n")
+	for _, op := range ops {
+		formatOperation(buf, op)
+	}
+
+	return buf.String()
+}
+
+func formatOperation(w io.StringWriter, b Operation) {
+	var str string
+	var isLabel bool
+	switch o := b.(type) {
+	case *OperationUnreachable:
+		str = "unreachable"
+	case *OperationLabel:
+		isLabel = true
+		str = fmt.Sprintf("%s:", o.Label.asBranchTarget())
+	case *OperationBr:
+		str = fmt.Sprintf("br %s", o.Target.String())
+	case *OperationBrIf:
+		str = fmt.Sprintf("br_if %s, %s", o.Then, o.Else)
+	case *OperationBrTable:
+		targets := make([]string, len(o.Targets))
+		for i, t := range o.Targets {
+			targets[i] = t.String()
+		}
+		str = fmt.Sprintf("br_table [%s] %s", strings.Join(targets, ","), o.Default)
+	case *OperationCall:
+		str = fmt.Sprintf("call %d", o.FunctionIndex)
+	case *OperationCallIndirect:
+		str = fmt.Sprintf("call_indirect: type=%d, table=%d", o.TypeIndex, o.TableIndex)
+	case *OperationDrop:
+		str = fmt.Sprintf("drop %d..%d", o.Range.Start, o.Range.End)
+	case *OperationSelect:
+		str = "select"
+	case *OperationPick:
+		str = fmt.Sprintf("pick %d", o.Depth)
+	case *OperationSwap:
+		str = fmt.Sprintf("swap %d", o.Depth)
+	case *OperationGlobalGet:
+		str = fmt.Sprintf("global.get %d", o.Index)
+	case *OperationGlobalSet:
+		str = fmt.Sprintf("global.set %d", o.Index)
+	case *OperationLoad:
+		str = fmt.Sprintf("%s.load %d, %d", o.Type, o.Arg.Alignment, o.Arg.Offest)
+	case *OperationLoad8:
+		str = fmt.Sprintf("%s.load8 %d, %d", o.Type, o.Arg.Alignment, o.Arg.Offest)
+	case *OperationLoad16:
+		str = fmt.Sprintf("%s.load16 %d, %d", o.Type, o.Arg.Alignment, o.Arg.Offest)
+	case *OperationLoad32:
+		var t string
+		if o.Signed {
+			t = "i64"
+		} else {
+			t = "u64"
+		}
+		str = fmt.Sprintf("%s.load32 %d, %d", t, o.Arg.Alignment, o.Arg.Offest)
+	case *OperationStore:
+		str = fmt.Sprintf("%s.store %d, %d", o.Type, o.Arg.Alignment, o.Arg.Offest)
+	case *OperationStore8:
+		str = fmt.Sprintf("%s.store8 %d, %d", o.Type, o.Arg.Alignment, o.Arg.Offest)
+	case *OperationStore16:
+		str = fmt.Sprintf("%s.store16 %d, %d", o.Type, o.Arg.Alignment, o.Arg.Offest)
+	case *OperationStore32:
+		str = fmt.Sprintf("i64.store32 %d, %d", o.Arg.Alignment, o.Arg.Offest)
+	case *OperationMemorySize:
+		str = "memory.size"
+	case *OperationMemoryGrow:
+		str = "memory.grow"
+	case *OperationConstI32:
+		str = fmt.Sprintf("i32.const %d", o.Value)
+	case *OperationConstI64:
+		str = fmt.Sprintf("i64.const %d", o.Value)
+	case *OperationConstF32:
+		str = fmt.Sprintf("f32.const %f", o.Value)
+	case *OperationConstF64:
+		str = fmt.Sprintf("f64.const %f", o.Value)
+	case *OperationEq:
+		str = fmt.Sprintf("%s.eq", o.Type)
+	case *OperationNe:
+		str = fmt.Sprintf("%s.ne", o.Type)
+	case *OperationEqz:
+		str = fmt.Sprintf("%s.eqz", o.Type)
+	case *OperationLt:
+		str = fmt.Sprintf("%s.lt", o.Type)
+	case *OperationGt:
+		str = fmt.Sprintf("%s.gt", o.Type)
+	case *OperationLe:
+		str = fmt.Sprintf("%s.le", o.Type)
+	case *OperationGe:
+		str = fmt.Sprintf("%s.ge", o.Type)
+	case *OperationAdd:
+		str = fmt.Sprintf("%s.add", o.Type)
+	case *OperationSub:
+		str = fmt.Sprintf("%s.sub", o.Type)
+	case *OperationMul:
+		str = fmt.Sprintf("%s.mul", o.Type)
+	case *OperationClz:
+		str = fmt.Sprintf("%s.clz", o.Type)
+	case *OperationCtz:
+		str = fmt.Sprintf("%s.ctz", o.Type)
+	case *OperationPopcnt:
+		str = fmt.Sprintf("%s.popcnt", o.Type)
+	case *OperationDiv:
+		str = fmt.Sprintf("%s.div", o.Type)
+	case *OperationRem:
+		str = fmt.Sprintf("%s.rem", o.Type)
+	case *OperationAnd:
+		str = fmt.Sprintf("%s.and", o.Type)
+	case *OperationOr:
+		str = fmt.Sprintf("%s.or", o.Type)
+	case *OperationXor:
+		str = fmt.Sprintf("%s.xor", o.Type)
+	case *OperationShl:
+		str = fmt.Sprintf("%s.shl", o.Type)
+	case *OperationShr:
+		str = fmt.Sprintf("%s.shr", o.Type)
+	case *OperationRotl:
+		str = fmt.Sprintf("%s.rotl", o.Type)
+	case *OperationRotr:
+		str = fmt.Sprintf("%s.rotr", o.Type)
+	case *OperationAbs:
+		str = fmt.Sprintf("%s.abs", o.Type)
+	case *OperationNeg:
+		str = fmt.Sprintf("%s.neg", o.Type)
+	case *OperationCeil:
+		str = fmt.Sprintf("%s.ceil", o.Type)
+	case *OperationFloor:
+		str = fmt.Sprintf("%s.floor", o.Type)
+	case *OperationTrunc:
+		str = fmt.Sprintf("%s.trunc", o.Type)
+	case *OperationNearest:
+		str = fmt.Sprintf("%s.nearest", o.Type)
+	case *OperationSqrt:
+		str = fmt.Sprintf("%s.sqrt", o.Type)
+	case *OperationMin:
+		str = fmt.Sprintf("%s.min", o.Type)
+	case *OperationMax:
+		str = fmt.Sprintf("%s.max", o.Type)
+	case *OperationCopysign:
+		str = fmt.Sprintf("%s.copysign", o.Type)
+	case *OperationI32WrapFromI64:
+		str = "i32.wrap_from.i64"
+	case *OperationITruncFromF:
+		str = fmt.Sprintf("%s.truncate_from.%s", o.OutputType, o.InputType)
+	case *OperationFConvertFromI:
+		str = fmt.Sprintf("%s.convert_from.%s", o.OutputType, o.InputType)
+	case *OperationF32DemoteFromF64:
+		str = "f32.demote_from.f64"
+	case *OperationF64PromoteFromF32:
+		str = "f64.promote_from.f32"
+	case *OperationI32ReinterpretFromF32:
+		str = "i32.reinterpret_from.f32"
+	case *OperationI64ReinterpretFromF64:
+		str = "i64.reinterpret_from.f64"
+	case *OperationF32ReinterpretFromI32:
+		str = "f32.reinterpret_from.i32"
+	case *OperationF64ReinterpretFromI64:
+		str = "f64.reinterpret_from.i64"
+	case *OperationExtend:
+		var in, out string
+		if o.Signed {
+			in = "i32"
+			out = "i64"
+		} else {
+			in = "u32"
+			out = "u64"
+		}
+		str = fmt.Sprintf("%s.extend_from.%s", out, in)
+	default:
+		panic("unreachable: a bug in wazeroir implementation")
+	}
+
+	if !isLabel {
+		const indent = "\t"
+		str = indent + str
+	}
+
+	_, _ = w.WriteString(str + "\n")
+}
diff --git a/wasm/wazeroir/interpreter.go b/wasm/wazeroir/interpreter.go
new file mode 100644
index 00000000..ff30d27c
--- /dev/null
+++ b/wasm/wazeroir/interpreter.go
@@ -0,0 +1,1507 @@
+package wazeroir
+
+import (
+	"encoding/binary"
+	"fmt"
+	"math"
+	"math/bits"
+	"reflect"
+	"runtime/debug"
+	"strings"
+	"unsafe"
+
+	"github.com/tetratelabs/wazero/wasm"
+	"github.com/tetratelabs/wazero/wasm/buildoptions"
+)
+
+// interpreter implements wasm.Engine interface.
+// This is the direct interpreter of wazeroir operations.
+type interpreter struct {
+	// Stores compiled functions.
+	functions map[*wasm.FunctionInstance]*interpreterFunction
+	// Cached function type IDs assigned to
+	// function signatures.
+	// Type IDs are used to check the signature of
+	// functions on call_indirect operation at runtime
+	functionTypeIDs map[string]uint64
+	// stack contains the operands.
+	// Note that all the values are represented as uint64.
+	stack []uint64
+	// Function call stack.
+	frames []*interpreterFrame
+	// The callbacks when an function instance is compiled.
+	// See the comment where this is used below for detail.
+	// Not used at runtime, and only in the compilation phase.
+	onCompilationDoneCallbacks map[*wasm.FunctionInstance][]func(*interpreterFunction)
+}
+
+func (it *interpreter) push(v uint64) {
+	it.stack = append(it.stack, v)
+}
+
+func (it *interpreter) pop() (v uint64) {
+	// No need to check stack bound
+	// as we can assume that all the operations
+	// are valid thanks to analyzeFunction
+	// at module validation phase
+	// and wazeroir translation
+	// before compilation.
+	v = it.stack[len(it.stack)-1]
+	it.stack = it.stack[:len(it.stack)-1]
+	return
+}
+
+func (it *interpreter) drop(r *InclusiveRange) {
+	// No need to check stack bound
+	// as we can assume that all the operations
+	// are valid thanks to analyzeFunction
+	// at module validation phase
+	// and wazeroir translation
+	// before compilation.
+	if r == nil {
+		return
+	} else if r.Start == 0 {
+		it.stack = it.stack[:len(it.stack)-1-r.End]
+	} else {
+		newStack := it.stack[:len(it.stack)-1-r.End]
+		newStack = append(newStack, it.stack[len(it.stack)-r.Start:]...)
+		it.stack = newStack
+	}
+}
+
+func (it *interpreter) pushFrame(frame *interpreterFrame) {
+	if buildoptions.CheckCallStackOverflow {
+		if buildoptions.CallStackHeightLimit <= len(it.frames)-1 {
+			panic(wasm.ErrCallStackOverflow)
+		}
+	}
+	it.frames = append(it.frames, frame)
+}
+
+func (it *interpreter) popFrame() (frame *interpreterFrame) {
+	// No need to check stack bound
+	// as we can assume that all the operations
+	// are valid thanks to analyzeFunction
+	// at module validation phase
+	// and wazeroir translation
+	// before compilation.
+	frame = it.frames[len(it.frames)-1]
+	it.frames = it.frames[:len(it.frames)-1]
+	return
+}
+
+type interpreterFrame struct {
+	// Program counter representing the current postion
+	// in the f.body.
+	pc uint64
+	// The compiled function used in this function frame.
+	f *interpreterFunction
+}
+
+type interpreterFunction struct {
+	moduleInstance *wasm.ModuleInstance
+	body           []*interpreterOp
+	hostFn         *reflect.Value
+	signature      *wasm.FunctionType
+	typeID         uint64
+}
+
+// Non-interface union of all the wazeroir operations.
+type interpreterOp struct {
+	kind   OperationKind
+	b1, b2 byte
+	us     []uint64
+	rs     []*InclusiveRange
+	f      *interpreterFunction
+}
+
+// Implements wasm.Engine for interpreter.
+func (it *interpreter) Compile(f *wasm.FunctionInstance) error {
+	if _, ok := it.functions[f]; ok {
+		return nil
+	} else if f.HostFunction != nil {
+		ret := &interpreterFunction{
+			hostFn: f.HostFunction, moduleInstance: f.ModuleInstance,
+			signature: f.Signature,
+		}
+		if id, ok := it.functionTypeIDs[funcTypeString(f.Signature)]; !ok {
+			id = uint64(len(it.functionTypeIDs))
+			it.functionTypeIDs[funcTypeString(f.Signature)] = id
+			ret.typeID = id
+		} else {
+			ret.typeID = id
+		}
+		it.functions[f] = ret
+		return nil
+	}
+
+	irOps, err := Compile(f)
+	if err != nil {
+		return fmt.Errorf("failed to lower Wasm to wazeroir: %w", err)
+	}
+
+	fn, err := it.lowerIROps(f, irOps)
+	if err != nil {
+		return fmt.Errorf("failed to lower wazeroir operations to interpreter operations: %w", err)
+	}
+
+	it.functions[f] = fn
+	for _, cb := range it.onCompilationDoneCallbacks[f] {
+		cb(fn)
+	}
+	delete(it.onCompilationDoneCallbacks, f)
+	return nil
+}
+
+// Lowers the wazeroir operations to interpreter friendly struct.
+func (it *interpreter) lowerIROps(f *wasm.FunctionInstance,
+	ops []Operation) (*interpreterFunction, error) {
+	ret := &interpreterFunction{moduleInstance: f.ModuleInstance, signature: f.Signature}
+	if id, ok := it.functionTypeIDs[funcTypeString(f.Signature)]; !ok {
+		id = uint64(len(it.functionTypeIDs))
+		it.functionTypeIDs[funcTypeString(f.Signature)] = id
+		ret.typeID = id
+	} else {
+		ret.typeID = id
+	}
+	labelAddress := map[string]uint64{}
+	onLabelAddressResolved := map[string][]func(addr uint64){}
+	for _, original := range ops {
+		op := &interpreterOp{kind: original.Kind()}
+		switch o := original.(type) {
+		case *OperationUnreachable:
+		case *OperationLabel:
+			labelKey := o.Label.String()
+			address := uint64(len(ret.body))
+			labelAddress[labelKey] = address
+			for _, cb := range onLabelAddressResolved[labelKey] {
+				cb(address)
+			}
+			delete(onLabelAddressResolved, labelKey)
+			// We just ignore the lable operation
+			// as we translate branch operations to the direct address jmp.
+			continue
+		case *OperationBr:
+			op.us = make([]uint64, 1)
+			if o.Target.IsReturnTarget() {
+				// Jmp to the end of the possible binary.
+				op.us[0] = math.MaxUint64
+			} else {
+				labelKey := o.Target.String()
+				addr, ok := labelAddress[labelKey]
+				if !ok {
+					// If this is the forward jump (e.g. to the continuation of if, etc.),
+					// the target is not emitted yet, so resolve the address later.
+					onLabelAddressResolved[labelKey] = append(onLabelAddressResolved[labelKey],
+						func(addr uint64) {
+							op.us[0] = addr
+						},
+					)
+				} else {
+					op.us[0] = addr
+				}
+			}
+		case *OperationBrIf:
+			op.rs = make([]*InclusiveRange, 2)
+			op.us = make([]uint64, 2)
+			for i, target := range []*BranchTargetDrop{o.Then, o.Else} {
+				op.rs[i] = target.ToDrop
+				if target.Target.IsReturnTarget() {
+					// Jmp to the end of the possible binary.
+					op.us[i] = math.MaxUint64
+				} else {
+					labelKey := target.Target.String()
+					addr, ok := labelAddress[labelKey]
+					if !ok {
+						i := i
+						// If this is the forward jump (e.g. to the continuation of if, etc.),
+						// the target is not emitted yet, so resolve the address later.
+						onLabelAddressResolved[labelKey] = append(onLabelAddressResolved[labelKey],
+							func(addr uint64) {
+								op.us[i] = addr
+							},
+						)
+					} else {
+						op.us[i] = addr
+					}
+				}
+			}
+		case *OperationBrTable:
+			targets := append([]*BranchTargetDrop{o.Default}, o.Targets...)
+			op.rs = make([]*InclusiveRange, len(targets))
+			op.us = make([]uint64, len(targets))
+			for i, target := range targets {
+				op.rs[i] = target.ToDrop
+				if target.Target.IsReturnTarget() {
+					// Jmp to the end of the possible binary.
+					op.us[i] = math.MaxUint64
+				} else {
+					labelKey := target.Target.String()
+					addr, ok := labelAddress[labelKey]
+					if !ok {
+						i := i // pin index for later resolution
+						// If this is the forward jump (e.g. to the continuation of if, etc.),
+						// the target is not emitted yet, so resolve the address later.
+						onLabelAddressResolved[labelKey] = append(onLabelAddressResolved[labelKey],
+							func(addr uint64) {
+								op.us[i] = addr
+							},
+						)
+					} else {
+						op.us[i] = addr
+					}
+				}
+			}
+		case *OperationCall:
+			targetInst := f.ModuleInstance.Functions[o.FunctionIndex]
+			target, ok := it.functions[targetInst]
+			if !ok {
+				// If the target function instance is not compiled,
+				// we set the callback so we can set the pointer to the target when the compilation done.
+				it.onCompilationDoneCallbacks[targetInst] = append(it.onCompilationDoneCallbacks[targetInst],
+					func(compiled *interpreterFunction) {
+						op.f = compiled
+					})
+			} else {
+				op.f = target
+			}
+		case *OperationCallIndirect:
+			op.us = make([]uint64, 2)
+			op.us[0] = uint64(o.TableIndex)
+			key := funcTypeString(f.ModuleInstance.Types[o.TypeIndex])
+			typeid, ok := it.functionTypeIDs[key]
+			if !ok {
+				it.functionTypeIDs[key] = uint64(len(it.functionTypeIDs))
+			}
+			op.us[1] = typeid
+		case *OperationDrop:
+			op.rs = make([]*InclusiveRange, 1)
+			op.rs[0] = o.Range
+		case *OperationSelect:
+		case *OperationPick:
+			op.us = make([]uint64, 1)
+			op.us[0] = uint64(o.Depth)
+		case *OperationSwap:
+			op.us = make([]uint64, 1)
+			op.us[0] = uint64(o.Depth)
+		case *OperationGlobalGet:
+			op.us = make([]uint64, 1)
+			op.us[0] = uint64(o.Index)
+		case *OperationGlobalSet:
+			op.us = make([]uint64, 1)
+			op.us[0] = uint64(o.Index)
+		case *OperationLoad:
+			op.b1 = byte(o.Type)
+			op.us = make([]uint64, 2)
+			op.us[0] = uint64(o.Arg.Alignment)
+			op.us[1] = uint64(o.Arg.Offest)
+		case *OperationLoad8:
+			op.b1 = byte(o.Type)
+			op.us = make([]uint64, 2)
+			op.us[0] = uint64(o.Arg.Alignment)
+			op.us[1] = uint64(o.Arg.Offest)
+		case *OperationLoad16:
+			op.b1 = byte(o.Type)
+			op.us = make([]uint64, 2)
+			op.us[0] = uint64(o.Arg.Alignment)
+			op.us[1] = uint64(o.Arg.Offest)
+		case *OperationLoad32:
+			if o.Signed {
+				op.b1 = 1
+			}
+			op.us = make([]uint64, 2)
+			op.us[0] = uint64(o.Arg.Alignment)
+			op.us[1] = uint64(o.Arg.Offest)
+		case *OperationStore:
+			op.b1 = byte(o.Type)
+			op.us = make([]uint64, 2)
+			op.us[0] = uint64(o.Arg.Alignment)
+			op.us[1] = uint64(o.Arg.Offest)
+		case *OperationStore8:
+			op.b1 = byte(o.Type)
+			op.us = make([]uint64, 2)
+			op.us[0] = uint64(o.Arg.Alignment)
+			op.us[1] = uint64(o.Arg.Offest)
+		case *OperationStore16:
+			op.b1 = byte(o.Type)
+			op.us = make([]uint64, 2)
+			op.us[0] = uint64(o.Arg.Alignment)
+			op.us[1] = uint64(o.Arg.Offest)
+		case *OperationStore32:
+			op.us = make([]uint64, 2)
+			op.us[0] = uint64(o.Arg.Alignment)
+			op.us[1] = uint64(o.Arg.Offest)
+		case *OperationMemorySize:
+		case *OperationMemoryGrow:
+		case *OperationConstI32:
+			op.us = make([]uint64, 1)
+			op.us[0] = uint64(o.Value)
+		case *OperationConstI64:
+			op.us = make([]uint64, 1)
+			op.us[0] = o.Value
+		case *OperationConstF32:
+			op.us = make([]uint64, 1)
+			op.us[0] = uint64(math.Float32bits(o.Value))
+		case *OperationConstF64:
+			op.us = make([]uint64, 1)
+			op.us[0] = math.Float64bits(o.Value)
+		case *OperationEq:
+			op.b1 = byte(o.Type)
+		case *OperationNe:
+			op.b1 = byte(o.Type)
+		case *OperationEqz:
+			op.b1 = byte(o.Type)
+		case *OperationLt:
+			op.b1 = byte(o.Type)
+		case *OperationGt:
+			op.b1 = byte(o.Type)
+		case *OperationLe:
+			op.b1 = byte(o.Type)
+		case *OperationGe:
+			op.b1 = byte(o.Type)
+		case *OperationAdd:
+			op.b1 = byte(o.Type)
+		case *OperationSub:
+			op.b1 = byte(o.Type)
+		case *OperationMul:
+			op.b1 = byte(o.Type)
+		case *OperationClz:
+			op.b1 = byte(o.Type)
+		case *OperationCtz:
+			op.b1 = byte(o.Type)
+		case *OperationPopcnt:
+			op.b1 = byte(o.Type)
+		case *OperationDiv:
+			op.b1 = byte(o.Type)
+		case *OperationRem:
+			op.b1 = byte(o.Type)
+		case *OperationAnd:
+			op.b1 = byte(o.Type)
+		case *OperationOr:
+			op.b1 = byte(o.Type)
+		case *OperationXor:
+			op.b1 = byte(o.Type)
+		case *OperationShl:
+			op.b1 = byte(o.Type)
+		case *OperationShr:
+			op.b1 = byte(o.Type)
+		case *OperationRotl:
+			op.b1 = byte(o.Type)
+		case *OperationRotr:
+			op.b1 = byte(o.Type)
+		case *OperationAbs:
+			op.b1 = byte(o.Type)
+		case *OperationNeg:
+			op.b1 = byte(o.Type)
+		case *OperationCeil:
+			op.b1 = byte(o.Type)
+		case *OperationFloor:
+			op.b1 = byte(o.Type)
+		case *OperationTrunc:
+			op.b1 = byte(o.Type)
+		case *OperationNearest:
+			op.b1 = byte(o.Type)
+		case *OperationSqrt:
+			op.b1 = byte(o.Type)
+		case *OperationMin:
+			op.b1 = byte(o.Type)
+		case *OperationMax:
+			op.b1 = byte(o.Type)
+		case *OperationCopysign:
+			op.b1 = byte(o.Type)
+		case *OperationI32WrapFromI64:
+		case *OperationITruncFromF:
+			op.b1 = byte(o.InputType)
+			op.b2 = byte(o.OutputType)
+		case *OperationFConvertFromI:
+			op.b1 = byte(o.InputType)
+			op.b2 = byte(o.OutputType)
+		case *OperationF32DemoteFromF64:
+		case *OperationF64PromoteFromF32:
+		case *OperationI32ReinterpretFromF32,
+			*OperationI64ReinterpretFromF64,
+			*OperationF32ReinterpretFromI32,
+			*OperationF64ReinterpretFromI64:
+			// Reinterpret ops are essentially nop for interpreter mode
+			// because we treat all values as uint64, and the reinterpret is only used at module
+			// validation phase where we check type soundness of all the operations.
+			// So just eliminate the ops.
+			continue
+		case *OperationExtend:
+			if o.Signed {
+				op.b1 = 1
+			}
+		default:
+			return nil, fmt.Errorf("unreachable: a bug in wazeroir interpreter")
+		}
+		ret.body = append(ret.body, op)
+	}
+
+	if len(onLabelAddressResolved) > 0 {
+		keys := make([]string, 0, len(onLabelAddressResolved))
+		for key := range onLabelAddressResolved {
+			keys = append(keys, key)
+		}
+		return nil, fmt.Errorf("labels are not defined: %s", strings.Join(keys, ","))
+	}
+	return ret, nil
+}
+
+// Implements wasm.Engine for interpreter.
+func (it *interpreter) Call(f *wasm.FunctionInstance, args ...uint64) (returns []uint64, err error) {
+	prevFrameLen := len(it.frames)
+	defer func() {
+		if v := recover(); v != nil {
+			// TODO: include stack trace in the error message.
+			if buildoptions.IsDebugMode {
+				debug.PrintStack()
+			}
+			it.frames = it.frames[:prevFrameLen]
+			err2, ok := v.(error)
+			if ok {
+				err = fmt.Errorf("runtime error: %w", err2)
+			} else {
+				err = fmt.Errorf("runtime error: %v", v)
+			}
+		}
+	}()
+
+	g, ok := it.functions[f]
+	if !ok {
+		err = fmt.Errorf("function not compiled")
+		return
+	}
+
+	if g.hostFn != nil {
+		it.callHostFunc(g, args...)
+	} else {
+		for _, arg := range args {
+			it.push(arg)
+		}
+		it.callNativeFunc(g)
+	}
+	returns = make([]uint64, len(f.Signature.ReturnTypes))
+	for i := range returns {
+		returns[len(returns)-1-i] = it.pop()
+	}
+	return
+}
+
+func (it *interpreter) callHostFunc(f *interpreterFunction, args ...uint64) {
+	tp := f.hostFn.Type()
+	in := make([]reflect.Value, tp.NumIn())
+	for i := len(in) - 1; i >= 1; i-- {
+		val := reflect.New(tp.In(i)).Elem()
+		raw := it.pop()
+		kind := tp.In(i).Kind()
+		switch kind {
+		case reflect.Float64, reflect.Float32:
+			val.SetFloat(math.Float64frombits(raw))
+		case reflect.Uint32, reflect.Uint64:
+			val.SetUint(raw)
+		case reflect.Int32, reflect.Int64:
+			val.SetInt(int64(raw))
+		}
+		in[i] = val
+	}
+
+	val := reflect.New(tp.In(0)).Elem()
+	var memory *wasm.MemoryInstance
+	if len(it.frames) > 0 {
+		memory = it.frames[len(it.frames)-1].f.moduleInstance.Memory
+	}
+	val.Set(reflect.ValueOf(&wasm.HostFunctionCallContext{Memory: memory}))
+	in[0] = val
+
+	frame := &interpreterFrame{f: f}
+	it.pushFrame(frame)
+	for _, ret := range f.hostFn.Call(in) {
+		switch ret.Kind() {
+		case reflect.Float64, reflect.Float32:
+			it.push(math.Float64bits(ret.Float()))
+		case reflect.Uint32, reflect.Uint64:
+			it.push(ret.Uint())
+		case reflect.Int32, reflect.Int64:
+			it.push(uint64(ret.Int()))
+		default:
+			panic("invalid return type")
+		}
+	}
+	it.popFrame()
+}
+
+func (it *interpreter) callNativeFunc(f *interpreterFunction) {
+	frame := &interpreterFrame{f: f}
+	memoryInst := frame.f.moduleInstance.Memory
+	globals := frame.f.moduleInstance.Globals
+	tables := f.moduleInstance.Tables
+	it.pushFrame(frame)
+	bodyLen := uint64(len(frame.f.body))
+	for frame.pc < bodyLen {
+		op := frame.f.body[frame.pc]
+		// TODO: add descriptino of each operation/case
+		// on, for example, how many args are used,
+		// how the stack is modified, etc.
+		switch op.kind {
+		case OperationKindUnreachable:
+			panic("unreachable")
+		case OperationKindBr:
+			{
+				frame.pc = op.us[0]
+			}
+		case OperationKindBrIf:
+			{
+				if it.pop() > 0 {
+					it.drop(op.rs[0])
+					frame.pc = op.us[0]
+				} else {
+					it.drop(op.rs[1])
+					frame.pc = op.us[1]
+				}
+			}
+		case OperationKindBrTable:
+			{
+				if v := int(it.pop()); v < len(op.us)-1 {
+					it.drop(op.rs[v+1])
+					frame.pc = op.us[v+1]
+				} else {
+					// Default branch.
+					it.drop(op.rs[0])
+					frame.pc = op.us[0]
+				}
+			}
+		case OperationKindCall:
+			{
+				if op.f.hostFn != nil {
+					it.callHostFunc(op.f, it.stack[len(it.stack)-len(op.f.signature.InputTypes):]...)
+				} else {
+					it.callNativeFunc(op.f)
+				}
+				frame.pc++
+			}
+		case OperationKindCallIndirect:
+			{
+				index := it.pop()
+				target := it.functions[tables[op.us[0]].Table[index].Function]
+				// Type check.
+				if target.typeID != op.us[1] {
+					panic("function signature mismatch on call_indirect")
+				}
+				// Call in.
+				if target.hostFn != nil {
+					it.callHostFunc(target, it.stack[len(it.stack)-len(target.signature.InputTypes):]...)
+				} else {
+					it.callNativeFunc(target)
+				}
+				frame.pc++
+			}
+		case OperationKindDrop:
+			{
+				it.drop(op.rs[0])
+				frame.pc++
+			}
+		case OperationKindSelect:
+			{
+				c := it.pop()
+				v2 := it.pop()
+				if c == 0 {
+					_ = it.pop()
+					it.push(v2)
+				}
+				frame.pc++
+			}
+		case OperationKindPick:
+			{
+				it.push(it.stack[len(it.stack)-1-int(op.us[0])])
+				frame.pc++
+			}
+		case OperationKindSwap:
+			{
+				index := len(it.stack) - 1 - int(op.us[0])
+				it.stack[len(it.stack)-1], it.stack[index] = it.stack[index], it.stack[len(it.stack)-1]
+				frame.pc++
+			}
+		case OperationKindGlobalGet:
+			{
+				g := globals[op.us[0]]
+				it.push(g.Val)
+				frame.pc++
+			}
+		case OperationKindGlobalSet:
+			{
+				g := globals[op.us[0]]
+				g.Val = it.pop()
+				frame.pc++
+			}
+		case OperationKindLoad:
+			{
+				base := op.us[1] + it.pop()
+				switch SignLessType(op.b1) {
+				case SignLessTypeI32, SignLessTypeF32:
+					it.push(uint64(binary.LittleEndian.Uint32(memoryInst.Buffer[base:])))
+				case SignLessTypeI64, SignLessTypeF64:
+					it.push(binary.LittleEndian.Uint64(memoryInst.Buffer[base:]))
+				}
+				frame.pc++
+			}
+		case OperationKindLoad8:
+			{
+				base := op.us[1] + it.pop()
+				switch SignFulInt(op.b1) {
+				case SignFulInt32, SignFulInt64:
+					it.push(uint64(int8(memoryInst.Buffer[base])))
+				case SignFulUint32, SignFulUint64:
+					it.push(uint64(uint8(memoryInst.Buffer[base])))
+				}
+				frame.pc++
+			}
+		case OperationKindLoad16:
+			{
+				base := op.us[1] + it.pop()
+				switch SignFulInt(op.b1) {
+				case SignFulInt32, SignFulInt64:
+					it.push(uint64(int16(binary.LittleEndian.Uint16(memoryInst.Buffer[base:]))))
+				case SignFulUint32, SignFulUint64:
+					it.push(uint64(binary.LittleEndian.Uint16(memoryInst.Buffer[base:])))
+				}
+				frame.pc++
+			}
+		case OperationKindLoad32:
+			{
+				base := op.us[1] + it.pop()
+				if op.b1 == 1 {
+					it.push(uint64(int32(binary.LittleEndian.Uint32(memoryInst.Buffer[base:]))))
+				} else {
+					it.push(uint64(binary.LittleEndian.Uint32(memoryInst.Buffer[base:])))
+				}
+				frame.pc++
+			}
+		case OperationKindStore:
+			{
+				val := it.pop()
+				base := op.us[1] + it.pop()
+				switch SignLessType(op.b1) {
+				case SignLessTypeI32, SignLessTypeF32:
+					binary.LittleEndian.PutUint32(memoryInst.Buffer[base:], uint32(val))
+				case SignLessTypeI64, SignLessTypeF64:
+					binary.LittleEndian.PutUint64(memoryInst.Buffer[base:], val)
+				}
+				frame.pc++
+			}
+		case OperationKindStore8:
+			{
+				val := byte(it.pop())
+				base := op.us[1] + it.pop()
+				memoryInst.Buffer[base] = val
+				frame.pc++
+			}
+		case OperationKindStore16:
+			{
+				val := uint16(it.pop())
+				base := op.us[1] + it.pop()
+				binary.LittleEndian.PutUint16(memoryInst.Buffer[base:], val)
+				frame.pc++
+			}
+		case OperationKindStore32:
+			{
+				val := uint32(it.pop())
+				base := op.us[1] + it.pop()
+				binary.LittleEndian.PutUint32(memoryInst.Buffer[base:], val)
+				frame.pc++
+			}
+		case OperationKindMemorySize:
+			{
+				v := uint64(len(frame.f.moduleInstance.Memory.Buffer)) / wasm.PageSize
+				it.push(v)
+				frame.pc++
+			}
+		case OperationKindMemoryGrow:
+			{
+				n := it.pop()
+				max := uint64(math.MaxUint32)
+				if memoryInst.Max != nil {
+					max = uint64(*memoryInst.Max) * wasm.PageSize
+				}
+				if uint64(n*wasm.PageSize+uint64(len(memoryInst.Buffer))) > max {
+					v := int32(-1)
+					it.push(uint64(v))
+				} else {
+					it.push(uint64(len(memoryInst.Buffer)) / wasm.PageSize)
+					memoryInst.Buffer = append(memoryInst.Buffer, make([]byte, n*wasm.PageSize)...)
+				}
+				frame.pc++
+			}
+		case OperationKindConstI32, OperationKindConstI64,
+			OperationKindConstF32, OperationKindConstF64:
+			{
+				it.push(op.us[0])
+				frame.pc++
+			}
+		case OperationKindEq:
+			{
+				var b bool
+				switch SignLessType(op.b1) {
+				case SignLessTypeI32, SignLessTypeI64:
+					v2, v1 := it.pop(), it.pop()
+					b = v1 == v2
+				case SignLessTypeF32:
+					v2, v1 := it.pop(), it.pop()
+					b = math.Float32frombits(uint32(v2)) == math.Float32frombits(uint32(v1))
+				case SignLessTypeF64:
+					v2, v1 := it.pop(), it.pop()
+					b = math.Float64frombits(v2) == math.Float64frombits(v1)
+				}
+				if b {
+					it.push(1)
+				} else {
+					it.push(0)
+				}
+				frame.pc++
+			}
+		case OperationKindNe:
+			{
+				var b bool
+				switch SignLessType(op.b1) {
+				case SignLessTypeI32, SignLessTypeI64:
+					v2, v1 := it.pop(), it.pop()
+					b = v1 != v2
+				case SignLessTypeF32:
+					v2, v1 := it.pop(), it.pop()
+					b = math.Float32frombits(uint32(v2)) != math.Float32frombits(uint32(v1))
+				case SignLessTypeF64:
+					v2, v1 := it.pop(), it.pop()
+					b = math.Float64frombits(v2) != math.Float64frombits(v1)
+				}
+				if b {
+					it.push(1)
+				} else {
+					it.push(0)
+				}
+				frame.pc++
+			}
+		case OperationKindEqz:
+			{
+				if it.pop() == 0 {
+					it.push(1)
+				} else {
+					it.push(0)
+				}
+				frame.pc++
+			}
+		case OperationKindLt:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				var b bool
+				switch SignFulType(op.b1) {
+				case SignFulTypeInt32:
+					b = int32(v1) < int32(v2)
+				case SignFulTypeInt64:
+					b = int64(v1) < int64(v2)
+				case SignFulTypeUint32, SignFulTypeUint64:
+					b = v1 < v2
+				case SignFulTypeFloat32:
+					b = math.Float32frombits(uint32(v1)) < math.Float32frombits(uint32(v2))
+				case SignFulTypeFloat64:
+					b = math.Float64frombits(v1) < math.Float64frombits(v2)
+				}
+				if b {
+					it.push(1)
+				} else {
+					it.push(0)
+				}
+				frame.pc++
+			}
+		case OperationKindGt:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				var b bool
+				switch SignFulType(op.b1) {
+				case SignFulTypeInt32:
+					b = int32(v1) > int32(v2)
+				case SignFulTypeInt64:
+					b = int64(v1) > int64(v2)
+				case SignFulTypeUint32, SignFulTypeUint64:
+					b = v1 > v2
+				case SignFulTypeFloat32:
+					b = math.Float32frombits(uint32(v1)) > math.Float32frombits(uint32(v2))
+				case SignFulTypeFloat64:
+					b = math.Float64frombits(v1) > math.Float64frombits(v2)
+				}
+				if b {
+					it.push(1)
+				} else {
+					it.push(0)
+				}
+				frame.pc++
+			}
+		case OperationKindLe:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				var b bool
+				switch SignFulType(op.b1) {
+				case SignFulTypeInt32:
+					b = int32(v1) <= int32(v2)
+				case SignFulTypeInt64:
+					b = int64(v1) <= int64(v2)
+				case SignFulTypeUint32, SignFulTypeUint64:
+					b = v1 <= v2
+				case SignFulTypeFloat32:
+					b = math.Float32frombits(uint32(v1)) <= math.Float32frombits(uint32(v2))
+				case SignFulTypeFloat64:
+					b = math.Float64frombits(v1) <= math.Float64frombits(v2)
+				}
+				if b {
+					it.push(1)
+				} else {
+					it.push(0)
+				}
+				frame.pc++
+			}
+		case OperationKindGe:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				var b bool
+				switch SignFulType(op.b1) {
+				case SignFulTypeInt32:
+					b = int32(v1) >= int32(v2)
+				case SignFulTypeInt64:
+					b = int64(v1) >= int64(v2)
+				case SignFulTypeUint32, SignFulTypeUint64:
+					b = v1 >= v2
+				case SignFulTypeFloat32:
+					b = math.Float32frombits(uint32(v1)) >= math.Float32frombits(uint32(v2))
+				case SignFulTypeFloat64:
+					b = math.Float64frombits(v1) >= math.Float64frombits(v2)
+				}
+				if b {
+					it.push(1)
+				} else {
+					it.push(0)
+				}
+				frame.pc++
+			}
+		case OperationKindAdd:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				switch SignLessType(op.b1) {
+				case SignLessTypeI32:
+					v := uint32(v1) + uint32(v2)
+					it.push(uint64(v))
+				case SignLessTypeI64:
+					it.push(v1 + v2)
+				case SignLessTypeF32:
+					v := math.Float32frombits(uint32(v1)) + math.Float32frombits(uint32(v2))
+					it.push(uint64(math.Float32bits(v)))
+				case SignLessTypeF64:
+					v := math.Float64frombits(v1) + math.Float64frombits(v2)
+					it.push(math.Float64bits(v))
+				}
+				frame.pc++
+			}
+		case OperationKindSub:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				switch SignLessType(op.b1) {
+				case SignLessTypeI32:
+					it.push(uint64(uint32(v1) - uint32(v2)))
+				case SignLessTypeI64:
+					it.push(v1 - v2)
+				case SignLessTypeF32:
+					v := math.Float32frombits(uint32(v1)) - math.Float32frombits(uint32(v2))
+					it.push(uint64(math.Float32bits(v)))
+				case SignLessTypeF64:
+					v := math.Float64frombits(v1) - math.Float64frombits(v2)
+					it.push(math.Float64bits(v))
+				}
+				frame.pc++
+			}
+		case OperationKindMul:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				switch SignLessType(op.b1) {
+				case SignLessTypeI32:
+					it.push(uint64(uint32(v1) * uint32(v2)))
+				case SignLessTypeI64:
+					it.push(v1 * v2)
+				case SignLessTypeF32:
+					v := math.Float32frombits(uint32(v2)) * math.Float32frombits(uint32(v1))
+					it.push(uint64(math.Float32bits(v)))
+				case SignLessTypeF64:
+					v := math.Float64frombits(v2) * math.Float64frombits(v1)
+					it.push(math.Float64bits(v))
+				}
+				frame.pc++
+			}
+		case OperationKindClz:
+			{
+				v := it.pop()
+				if op.b1 == 0 {
+					// SignLessInt32
+					it.push(uint64(bits.LeadingZeros32(uint32(v))))
+				} else {
+					// SignLessInt64
+					it.push(uint64(bits.LeadingZeros64(v)))
+				}
+				frame.pc++
+			}
+		case OperationKindCtz:
+			{
+				v := it.pop()
+				if op.b1 == 0 {
+					// SignLessInt32
+					it.push(uint64(bits.TrailingZeros32(uint32(v))))
+				} else {
+					// SignLessInt64
+					it.push(uint64(bits.TrailingZeros64(v)))
+				}
+				frame.pc++
+			}
+		case OperationKindPopcnt:
+			{
+				v := it.pop()
+				if op.b1 == 0 {
+					// SignLessInt32
+					it.push(uint64(bits.OnesCount32(uint32(v))))
+				} else {
+					// SignLessInt64
+					it.push(uint64(bits.OnesCount64(v)))
+				}
+				frame.pc++
+			}
+		case OperationKindDiv:
+			{
+				switch SignFulType(op.b1) {
+				case SignFulTypeInt32:
+					v2 := int32(it.pop())
+					v1 := int32(it.pop())
+					if v2 == 0 || (v1 == math.MinInt32 && v2 == -1) {
+						panic("undefined")
+					}
+					it.push(uint64(uint32(v1 / v2)))
+				case SignFulTypeInt64:
+					v2 := int64(it.pop())
+					v1 := int64(it.pop())
+					if v2 == 0 || (v1 == math.MinInt64 && v2 == -1) {
+						panic("undefined")
+					}
+					it.push(uint64(v1 / v2))
+				case SignFulTypeUint32:
+					v2 := uint32(it.pop())
+					v1 := uint32(it.pop())
+					it.push(uint64(v1 / v2))
+				case SignFulTypeUint64:
+					v2 := it.pop()
+					v1 := it.pop()
+					it.push(v1 / v2)
+				case SignFulTypeFloat32:
+					v2 := it.pop()
+					v1 := it.pop()
+					v := math.Float32frombits(uint32(v1)) / math.Float32frombits(uint32(v2))
+					it.push(uint64(math.Float32bits(v)))
+				case SignFulTypeFloat64:
+					v2 := it.pop()
+					v1 := it.pop()
+					v := math.Float64frombits(v1) / math.Float64frombits(v2)
+					it.push(uint64(math.Float64bits(v)))
+				}
+				frame.pc++
+			}
+		case OperationKindRem:
+			{
+				switch SignFulInt(op.b1) {
+				case SignFulInt32:
+					v2 := int32(it.pop())
+					v1 := int32(it.pop())
+					it.push(uint64(uint32(v1 % v2)))
+				case SignFulInt64:
+					v2 := int64(it.pop())
+					v1 := int64(it.pop())
+					it.push(uint64(v1 % v2))
+				case SignFulUint32:
+					v2 := uint32(it.pop())
+					v1 := uint32(it.pop())
+					it.push(uint64(v1 % v2))
+				case SignFulUint64:
+					v2 := it.pop()
+					v1 := it.pop()
+					it.push(v1 % v2)
+				}
+				frame.pc++
+			}
+		case OperationKindAnd:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				if op.b1 == 0 {
+					// SignLessInt32
+					it.push(uint64(uint32(v2) & uint32(v1)))
+				} else {
+					// SignLessInt64
+					it.push(uint64(v2 & v1))
+				}
+				frame.pc++
+			}
+		case OperationKindOr:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				if op.b1 == 0 {
+					// SignLessInt32
+					it.push(uint64(uint32(v2) | uint32(v1)))
+				} else {
+					// SignLessInt64
+					it.push(uint64(v2 | v1))
+				}
+				frame.pc++
+			}
+		case OperationKindXor:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				if op.b1 == 0 {
+					// SignLessInt32
+					it.push(uint64(uint32(v2) ^ uint32(v1)))
+				} else {
+					// SignLessInt64
+					it.push(uint64(v2 ^ v1))
+				}
+				frame.pc++
+			}
+		case OperationKindShl:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				if op.b1 == 0 {
+					// SignLessInt32
+					it.push(uint64(uint32(v1) << (uint32(v2) % 32)))
+				} else {
+					// SignLessInt64
+					it.push(v1 << (v2 % 64))
+				}
+				frame.pc++
+			}
+		case OperationKindShr:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				switch SignFulInt(op.b1) {
+				case SignFulInt32:
+					it.push(uint64(int32(v1) >> (uint32(v2) % 32)))
+				case SignFulInt64:
+					it.push(uint64(int64(v1) >> (v2 % 64)))
+				case SignFulUint32:
+					it.push(uint64(uint32(v1) >> (uint32(v2) % 32)))
+				case SignFulUint64:
+					it.push(v1 >> (v2 % 64))
+				}
+				frame.pc++
+			}
+		case OperationKindRotl:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				if op.b1 == 0 {
+					// SignLessInt32
+					it.push(uint64(bits.RotateLeft32(uint32(v1), int(v2))))
+				} else {
+					// SignLessInt64
+					it.push(uint64(bits.RotateLeft64(v1, int(v2))))
+				}
+				frame.pc++
+			}
+		case OperationKindRotr:
+			{
+				v2 := it.pop()
+				v1 := it.pop()
+				if op.b1 == 0 {
+					// SignLessInt32
+					it.push(uint64(bits.RotateLeft32(uint32(v1), -int(v2))))
+				} else {
+					// SignLessInt64
+					it.push(uint64(bits.RotateLeft64(v1, -int(v2))))
+				}
+				frame.pc++
+			}
+		case OperationKindAbs:
+			{
+				if op.b1 == 0 {
+					// Float32
+					const mask uint32 = 1 << 31
+					it.push(uint64(uint32(it.pop()) &^ mask))
+				} else {
+					// Float64
+					const mask uint64 = 1 << 63
+					it.push(uint64(it.pop() &^ mask))
+				}
+				frame.pc++
+			}
+		case OperationKindNeg:
+			{
+				if op.b1 == 0 {
+					// Float32
+					v := -math.Float32frombits(uint32(it.pop()))
+					it.push(uint64(math.Float32bits(v)))
+				} else {
+					// Float64
+					v := -math.Float64frombits(it.pop())
+					it.push(math.Float64bits(v))
+				}
+				frame.pc++
+			}
+		case OperationKindCeil:
+			{
+				if op.b1 == 0 {
+					// Float32
+					v := math.Ceil(float64(math.Float32frombits(uint32(it.pop()))))
+					it.push(uint64(math.Float32bits(float32(v))))
+				} else {
+					// Float64
+					v := math.Ceil(float64(math.Float64frombits(it.pop())))
+					it.push(math.Float64bits(v))
+				}
+				frame.pc++
+			}
+		case OperationKindFloor:
+			{
+				if op.b1 == 0 {
+					// Float32
+					v := math.Floor(float64(math.Float32frombits(uint32(it.pop()))))
+					it.push(uint64(math.Float32bits(float32(v))))
+				} else {
+					// Float64
+					v := math.Floor(float64(math.Float64frombits(it.pop())))
+					it.push(math.Float64bits(v))
+				}
+				frame.pc++
+			}
+		case OperationKindTrunc:
+			{
+				if op.b1 == 0 {
+					// Float32
+					v := math.Trunc(float64(math.Float32frombits(uint32(it.pop()))))
+					it.push(uint64(math.Float32bits(float32(v))))
+				} else {
+					// Float64
+					v := math.Trunc(float64(math.Float64frombits(it.pop())))
+					it.push(math.Float64bits(v))
+				}
+				frame.pc++
+			}
+		case OperationKindNearest:
+			{
+				// Borrowed from https://github.com/wasmerio/wasmer/blob/703bb4ee2ffb17b2929a194fc045a7e351b696e2/lib/vm/src/libcalls.rs#L77
+				if op.b1 == 0 {
+					// Float32
+					f := math.Float32frombits(uint32(it.pop()))
+					f64 := float64(f)
+					if f != -0 && f != 0 {
+						u := float32(math.Ceil(f64))
+						d := float32(math.Floor(f64))
+						um := math.Abs(float64(f - u))
+						dm := math.Abs(float64(f - d))
+						h := u / 2.0
+						if um < dm || float32(math.Floor(float64(h))) == h {
+							f = u
+						} else {
+							f = d
+						}
+					}
+					it.push(uint64(math.Float32bits(f)))
+				} else {
+					// Float64
+					f := math.Float64frombits(it.pop())
+					f64 := float64(f)
+					if f != -0 && f != 0 {
+						u := math.Ceil(f64)
+						d := math.Floor(f64)
+						um := math.Abs(f - u)
+						dm := math.Abs(f - d)
+						h := u / 2.0
+						if um < dm || math.Floor(float64(h)) == h {
+							f = u
+						} else {
+							f = d
+						}
+					}
+					it.push(math.Float64bits(f))
+				}
+				frame.pc++
+			}
+		case OperationKindSqrt:
+			{
+				if op.b1 == 0 {
+					// Float32
+					v := math.Sqrt(float64(math.Float32frombits(uint32(it.pop()))))
+					it.push(uint64(math.Float32bits(float32(v))))
+				} else {
+					// Float64
+					v := math.Sqrt(float64(math.Float64frombits(it.pop())))
+					it.push(math.Float64bits(v))
+				}
+				frame.pc++
+			}
+		case OperationKindMin:
+			{
+				if op.b1 == 0 {
+					// Float32
+					v2 := math.Float32frombits(uint32(it.pop()))
+					v1 := math.Float32frombits(uint32(it.pop()))
+					it.push(uint64(math.Float32bits(float32(min(float64(v1), float64(v2))))))
+				} else {
+					v2 := math.Float64frombits(it.pop())
+					v1 := math.Float64frombits(it.pop())
+					it.push(math.Float64bits(min(v1, v2)))
+				}
+				frame.pc++
+			}
+		case OperationKindMax:
+			{
+
+				if op.b1 == 0 {
+					// Float32
+					v2 := math.Float32frombits(uint32(it.pop()))
+					v1 := math.Float32frombits(uint32(it.pop()))
+					it.push(uint64(math.Float32bits(float32(max(float64(v1), float64(v2))))))
+				} else {
+					// Float64
+					v2 := math.Float64frombits(it.pop())
+					v1 := math.Float64frombits(it.pop())
+					it.push(math.Float64bits(max(v1, v2)))
+				}
+				frame.pc++
+			}
+		case OperationKindCopysign:
+			{
+				if op.b1 == 0 {
+					// Float32
+					v2 := math.Float32frombits(uint32(it.pop()))
+					v1 := math.Float32frombits(uint32(it.pop()))
+					it.push(uint64(math.Float32bits(float32(math.Copysign(float64(v1), float64(v2))))))
+				} else {
+					// Float64
+					v2 := math.Float64frombits(it.pop())
+					v1 := math.Float64frombits(it.pop())
+					it.push(uint64(math.Float64bits(math.Copysign(v1, v2))))
+				}
+				frame.pc++
+			}
+		case OperationKindI32WrapFromI64:
+			{
+				it.push(uint64(uint32(it.pop())))
+				frame.pc++
+			}
+		case OperationKindITruncFromF:
+			{
+				if op.b1 == 0 {
+					// Float32
+					switch SignFulInt(op.b2) {
+					case SignFulInt32:
+						v := math.Trunc(float64(math.Float32frombits(uint32(it.pop()))))
+						if math.IsNaN(v) {
+							panic("invalid conversion")
+						} else if v < math.MinInt32 || v > math.MaxInt32 {
+							panic("integer overflow")
+						}
+						it.push(uint64(int32(v)))
+					case SignFulInt64:
+						v := math.Trunc(float64(math.Float32frombits(uint32(it.pop()))))
+						res := int64(v)
+						if math.IsNaN(v) {
+							panic("invalid conversion")
+						} else if v < math.MinInt64 || v > 0 && res < 0 {
+							panic("integer overflow")
+						}
+						it.push(uint64(res))
+					case SignFulUint32:
+						v := math.Trunc(float64(math.Float32frombits(uint32(it.pop()))))
+						if math.IsNaN(v) {
+							panic("invalid conversion")
+						} else if v < 0 || v > math.MaxUint32 {
+							panic("integer overflow")
+						}
+						it.push(uint64(uint32(v)))
+					case SignFulUint64:
+						v := math.Trunc(float64(math.Float32frombits(uint32(it.pop()))))
+						res := uint64(v)
+						if math.IsNaN(v) {
+							panic("invalid conversion")
+						} else if v < 0 || v > float64(res) {
+							panic("integer overflow")
+						}
+						it.push(res)
+					}
+				} else {
+					// Float64
+					switch SignFulInt(op.b2) {
+					case SignFulInt32:
+						v := math.Trunc(math.Float64frombits(it.pop()))
+						if math.IsNaN(v) {
+							panic("invalid conversion")
+						} else if v < math.MinInt32 || v > math.MaxInt32 {
+							panic("integer overflow")
+						}
+						it.push(uint64(int32(v)))
+					case SignFulInt64:
+						v := math.Trunc(math.Float64frombits(it.pop()))
+						res := int64(v)
+						if math.IsNaN(v) {
+							panic("invalid conversion")
+						} else if v < math.MinInt64 || v > 0 && res < 0 {
+							panic("integer overflow")
+						}
+						it.push(uint64(res))
+					case SignFulUint32:
+						v := math.Trunc(math.Float64frombits(it.pop()))
+						if math.IsNaN(v) {
+							panic("invalid conversion")
+						} else if v < 0 || v > math.MaxUint32 {
+							panic("integer overflow")
+						}
+						it.push(uint64(uint32(v)))
+					case SignFulUint64:
+						v := math.Trunc(math.Float64frombits(it.pop()))
+						res := uint64(v)
+						if math.IsNaN(v) {
+							panic("invalid conversion")
+						} else if v < 0 || v > float64(res) {
+							panic("integer overflow")
+						}
+						it.push(res)
+					}
+				}
+				frame.pc++
+			}
+		case OperationKindFConvertFromI:
+			{
+				switch SignFulInt(op.b1) {
+				case SignFulInt32:
+					if op.b2 == 0 {
+						// Float32
+						v := float32(int32(it.pop()))
+						it.push(uint64(math.Float32bits(v)))
+					} else {
+						// Float64
+						v := float64(int32(it.pop()))
+						it.push(math.Float64bits(v))
+					}
+				case SignFulInt64:
+					if op.b2 == 0 {
+						// Float32
+						v := float32(int64(it.pop()))
+						it.push(uint64(math.Float32bits(v)))
+					} else {
+						// Float64
+						v := float64(int64(it.pop()))
+						it.push(math.Float64bits(v))
+					}
+				case SignFulUint32:
+					if op.b2 == 0 {
+						// Float32
+						v := float32(uint32(it.pop()))
+						it.push(uint64(math.Float32bits(v)))
+					} else {
+						// Float64
+						v := float64(uint32(it.pop()))
+						it.push(math.Float64bits(v))
+					}
+				case SignFulUint64:
+					if op.b2 == 0 {
+						// Float32
+						v := float32(it.pop())
+						it.push(uint64(math.Float32bits(v)))
+					} else {
+						// Float64
+						v := float64(it.pop())
+						it.push(math.Float64bits(v))
+					}
+				}
+				frame.pc++
+			}
+		case OperationKindF32DemoteFromF64:
+			{
+				v := float32(math.Float64frombits(it.pop()))
+				it.push(uint64(math.Float32bits(v)))
+				frame.pc++
+			}
+		case OperationKindF64PromoteFromF32:
+			{
+				v := float64(math.Float32frombits(uint32(it.pop())))
+				it.push(math.Float64bits(v))
+				frame.pc++
+			}
+		case OperationKindExtend:
+			{
+				if op.b1 == 1 {
+					// Signed.
+					v := int64(int32(it.pop()))
+					it.push(uint64(v))
+				} else {
+					v := uint64(uint32(it.pop()))
+					it.push(v)
+				}
+				frame.pc++
+			}
+		}
+	}
+	it.popFrame()
+}
+
+func funcTypeString(t *wasm.FunctionType) string {
+	return fmt.Sprintf("%s-%s",
+		// Fast stringification of byte slice.
+		// This is safe anyway as the results are copied
+		// into the return value string.
+		*(*string)(unsafe.Pointer(&t.InputTypes)),
+		*(*string)(unsafe.Pointer(&t.ReturnTypes)),
+	)
+}
+
+// math.Min doen't comply with the Wasm spec, so we borrow from the original
+// with a change that either one of NaN results in NaN even if another is -Inf.
+// https://github.com/golang/go/blob/1d20a362d0ca4898d77865e314ef6f73582daef0/src/math/dim.go#L74-L91
+func min(x, y float64) float64 {
+	switch {
+	case math.IsNaN(x) || math.IsNaN(y):
+		return math.NaN()
+	case math.IsInf(x, -1) || math.IsInf(y, -1):
+		return math.Inf(-1)
+	case x == 0 && x == y:
+		if math.Signbit(x) {
+			return x
+		}
+		return y
+	}
+	if x < y {
+		return x
+	}
+	return y
+}
+
+// math.Max doen't comply with the Wasm spec, so we borrow from the original
+// with a change that either one of NaN results in NaN even if another is Inf.
+// https://github.com/golang/go/blob/1d20a362d0ca4898d77865e314ef6f73582daef0/src/math/dim.go#L42-L59
+func max(x, y float64) float64 {
+	switch {
+	case math.IsNaN(x) || math.IsNaN(y):
+		return math.NaN()
+	case math.IsInf(x, 1) || math.IsInf(y, 1):
+		return math.Inf(1)
+
+	case x == 0 && x == y:
+		if math.Signbit(x) {
+			return y
+		}
+		return x
+	}
+	if x > y {
+		return x
+	}
+	return y
+}
diff --git a/wasm/wazeroir/operations.go b/wasm/wazeroir/operations.go
new file mode 100644
index 00000000..5e5253f0
--- /dev/null
+++ b/wasm/wazeroir/operations.go
@@ -0,0 +1,866 @@
+package wazeroir
+
+import "fmt"
+
+type SignLessInt byte
+
+const (
+	SignLessInt32 SignLessInt = iota
+	SignLessInt64
+)
+
+func (s SignLessInt) String() (ret string) {
+	switch s {
+	case SignLessInt32:
+		ret = "i32"
+	case SignLessInt64:
+		ret = "i64"
+	}
+	return
+}
+
+type SignFulInt byte
+
+const (
+	SignFulInt32 SignFulInt = iota
+	SignFulInt64
+	SignFulUint32
+	SignFulUint64
+)
+
+func (s SignFulInt) String() (ret string) {
+	switch s {
+	case SignFulUint32:
+		ret = "i32"
+	case SignFulUint64:
+		ret = "i64"
+	case SignFulInt32:
+		ret = "u32"
+	case SignFulInt64:
+		ret = "u64"
+	}
+	return
+}
+
+type Float byte
+
+const (
+	Float32 Float = iota
+	Float64
+)
+
+func (s Float) String() (ret string) {
+	switch s {
+	case Float32:
+		ret = "f32"
+	case Float64:
+		ret = "f64"
+	}
+	return
+}
+
+type SignLessType byte
+
+const (
+	SignLessTypeI32 SignLessType = iota
+	SignLessTypeI64
+	SignLessTypeF32
+	SignLessTypeF64
+	SignLessTypeUnknown
+)
+
+func (s SignLessType) String() (ret string) {
+	switch s {
+	case SignLessTypeI32:
+		ret = "i32"
+	case SignLessTypeI64:
+		ret = "i64"
+	case SignLessTypeF32:
+		ret = "f32"
+	case SignLessTypeF64:
+		ret = "f64"
+	case SignLessTypeUnknown:
+		ret = "unknown"
+	}
+	return
+}
+
+type SignFulType byte
+
+const (
+	SignFulTypeInt32 SignFulType = iota
+	SignFulTypeUint32
+	SignFulTypeInt64
+	SignFulTypeUint64
+	SignFulTypeFloat32
+	SignFulTypeFloat64
+)
+
+func (s SignFulType) String() (ret string) {
+	switch s {
+	case SignFulTypeInt32:
+		ret = "i32"
+	case SignFulTypeUint32:
+		ret = "u32"
+	case SignFulTypeInt64:
+		ret = "i64"
+	case SignFulTypeUint64:
+		ret = "u64"
+	case SignFulTypeFloat32:
+		ret = "f32"
+	case SignFulTypeFloat64:
+		ret = "f64"
+	}
+	return
+}
+
+type Operation interface {
+	Kind() OperationKind
+}
+
+type OperationKind byte
+
+func (o OperationKind) String() (ret string) {
+	switch o {
+	case OperationKindUnreachable:
+		ret = "Unreachable"
+	case OperationKindLabel:
+		ret = "Label"
+	case OperationKindBr:
+		ret = "Br"
+	case OperationKindBrIf:
+		ret = "BrIf"
+	case OperationKindBrTable:
+		ret = "BrTable"
+	case OperationKindCall:
+		ret = "Call"
+	case OperationKindCallIndirect:
+		ret = "CallIndirect"
+	case OperationKindDrop:
+		ret = "Drop"
+	case OperationKindSelect:
+		ret = "Select"
+	case OperationKindPick:
+		ret = "Pick"
+	case OperationKindSwap:
+		ret = "Swap"
+	case OperationKindGlobalGet:
+		ret = "GlobalGet"
+	case OperationKindGlobalSet:
+		ret = "GlobalSet"
+	case OperationKindLoad:
+		ret = "Load"
+	case OperationKindLoad8:
+		ret = "Load8"
+	case OperationKindLoad16:
+		ret = "Load16"
+	case OperationKindLoad32:
+		ret = "Load32"
+	case OperationKindStore:
+		ret = "Store"
+	case OperationKindStore8:
+		ret = "Store8"
+	case OperationKindStore16:
+		ret = "Store16"
+	case OperationKindStore32:
+		ret = "Store32"
+	case OperationKindMemorySize:
+		ret = "MemorySize"
+	case OperationKindMemoryGrow:
+		ret = "MemoryGrow"
+	case OperationKindConstI32:
+		ret = "ConstI32"
+	case OperationKindConstI64:
+		ret = "ConstI64"
+	case OperationKindConstF32:
+		ret = "ConstF32"
+	case OperationKindConstF64:
+		ret = "ConstF64"
+	case OperationKindEq:
+		ret = "Eq"
+	case OperationKindNe:
+		ret = "Ne"
+	case OperationKindEqz:
+		ret = "Eqz"
+	case OperationKindLt:
+		ret = "Lt"
+	case OperationKindGt:
+		ret = "Gt"
+	case OperationKindLe:
+		ret = "Le"
+	case OperationKindGe:
+		ret = "Ge"
+	case OperationKindAdd:
+		ret = "Add"
+	case OperationKindSub:
+		ret = "Sub"
+	case OperationKindMul:
+		ret = "Mul"
+	case OperationKindClz:
+		ret = "Clz"
+	case OperationKindCtz:
+		ret = "Ctz"
+	case OperationKindPopcnt:
+		ret = "Popcnt"
+
+	case OperationKindDiv:
+		ret = "Div"
+	case OperationKindRem:
+		ret = "Rem"
+	case OperationKindAnd:
+		ret = "And"
+	case OperationKindOr:
+		ret = "Or"
+	case OperationKindXor:
+		ret = "Xor"
+	case OperationKindShl:
+		ret = "Shl"
+	case OperationKindShr:
+		ret = "Shr"
+	case OperationKindRotl:
+		ret = "Rotl"
+	case OperationKindRotr:
+		ret = "Rotr"
+	case OperationKindAbs:
+		ret = "Abs"
+	case OperationKindNeg:
+		ret = "Neg"
+	case OperationKindCeil:
+		ret = "Ceil"
+	case OperationKindFloor:
+		ret = "Floor"
+	case OperationKindTrunc:
+		ret = "Trunc"
+	case OperationKindNearest:
+		ret = "Nearest"
+	case OperationKindSqrt:
+		ret = "Sqrt"
+	case OperationKindMin:
+		ret = "Min"
+	case OperationKindMax:
+		ret = "Max"
+	case OperationKindCopysign:
+		ret = "Copysign"
+	case OperationKindI32WrapFromI64:
+		ret = "I32WrapFromI64"
+	case OperationKindITruncFromF:
+		ret = "ITruncFromF"
+	case OperationKindFConvertFromI:
+		ret = "FConvertFromI"
+	case OperationKindF32DemoteFromF64:
+		ret = "F32DemoteFromF64"
+	case OperationKindF64PromoteFromF32:
+		ret = "F64PromoteFromF32"
+	case OperationKindI32ReinterpretFromF32:
+		ret = "I32ReinterpretFromF32"
+	case OperationKindI64ReinterpretFromF64:
+		ret = "I64ReinterpretFromF64"
+	case OperationKindF32ReinterpretFromI32:
+		ret = "F32ReinterpretFromI32"
+	case OperationKindF64ReinterpretFromI64:
+		ret = "F64ReinterpretFromI64"
+	case OperationKindExtend:
+		ret = "Extend"
+	}
+	return
+}
+
+const (
+	OperationKindUnreachable OperationKind = iota
+	OperationKindLabel
+	OperationKindBr
+	OperationKindBrIf
+	OperationKindBrTable
+	OperationKindCall
+	OperationKindCallIndirect
+	OperationKindDrop
+	OperationKindSelect
+	OperationKindPick
+	OperationKindSwap
+	OperationKindGlobalGet
+	OperationKindGlobalSet
+	OperationKindLoad
+	OperationKindLoad8
+	OperationKindLoad16
+	OperationKindLoad32
+	OperationKindStore
+	OperationKindStore8
+	OperationKindStore16
+	OperationKindStore32
+	OperationKindMemorySize
+	OperationKindMemoryGrow
+	OperationKindConstI32
+	OperationKindConstI64
+	OperationKindConstF32
+	OperationKindConstF64
+	OperationKindEq
+	OperationKindNe
+	OperationKindEqz
+	OperationKindLt
+	OperationKindGt
+	OperationKindLe
+	OperationKindGe
+	OperationKindAdd
+	OperationKindSub
+	OperationKindMul
+	OperationKindClz
+	OperationKindCtz
+	OperationKindPopcnt
+	OperationKindDiv
+	OperationKindRem
+	OperationKindAnd
+	OperationKindOr
+	OperationKindXor
+	OperationKindShl
+	OperationKindShr
+	OperationKindRotl
+	OperationKindRotr
+	OperationKindAbs
+	OperationKindNeg
+	OperationKindCeil
+	OperationKindFloor
+	OperationKindTrunc
+	OperationKindNearest
+	OperationKindSqrt
+	OperationKindMin
+	OperationKindMax
+	OperationKindCopysign
+	OperationKindI32WrapFromI64
+	OperationKindITruncFromF
+	OperationKindFConvertFromI
+	OperationKindF32DemoteFromF64
+	OperationKindF64PromoteFromF32
+	OperationKindI32ReinterpretFromF32
+	OperationKindI64ReinterpretFromF64
+	OperationKindF32ReinterpretFromI32
+	OperationKindF64ReinterpretFromI64
+	OperationKindExtend
+)
+
+type Label struct {
+	FrameID uint32
+	Kind    LabelKind
+}
+
+func (l *Label) String() (ret string) {
+	switch l.Kind {
+	case LabelKindHeader:
+		ret = fmt.Sprintf(".L%d", l.FrameID)
+	case LabelKindElse:
+		ret = fmt.Sprintf(".L%d_else", l.FrameID)
+	case LabelKindContinuation:
+		ret = fmt.Sprintf(".L%d_cont", l.FrameID)
+	}
+	return
+}
+
+type LabelKind = byte
+
+const (
+	LabelKindHeader LabelKind = iota
+	LabelKindElse
+	LabelKindContinuation
+)
+
+func (l *Label) asBranchTarget() *BranchTarget {
+	return &BranchTarget{Label: l}
+}
+
+func (l *Label) asBranchTargetDrop() *BranchTargetDrop {
+	return &BranchTargetDrop{Target: l.asBranchTarget()}
+}
+
+type BranchTarget struct {
+	Label *Label
+}
+
+func (b *BranchTarget) IsReturnTarget() bool {
+	return b.Label == nil
+}
+
+func (b *BranchTarget) String() (ret string) {
+	if b.IsReturnTarget() {
+		ret = ".return"
+	} else {
+		ret = b.Label.String()
+	}
+	return
+}
+
+type BranchTargetDrop struct {
+	Target *BranchTarget
+	ToDrop *InclusiveRange
+}
+
+func (b *BranchTargetDrop) String() (ret string) {
+	if b.ToDrop != nil {
+		ret = fmt.Sprintf("%s(drop %d..%d)", b.Target, b.ToDrop.Start, b.ToDrop.End)
+	} else {
+		ret = b.Target.String()
+	}
+	return
+}
+
+type OperationUnreachable struct{}
+
+func (o *OperationUnreachable) Kind() OperationKind {
+	return OperationKindUnreachable
+}
+
+type OperationLabel struct {
+	Label *Label
+}
+
+func (o *OperationLabel) Kind() OperationKind {
+	return OperationKindLabel
+}
+
+type OperationBr struct {
+	Target *BranchTarget
+}
+
+func (o *OperationBr) Kind() OperationKind {
+	return OperationKindBr
+}
+
+type OperationBrIf struct {
+	Then, Else *BranchTargetDrop
+}
+
+func (o *OperationBrIf) Kind() OperationKind {
+	return OperationKindBrIf
+}
+
+type InclusiveRange struct {
+	Start, End int
+}
+
+type OperationBrTable struct {
+	Targets []*BranchTargetDrop
+	Default *BranchTargetDrop
+}
+
+func (o *OperationBrTable) Kind() OperationKind {
+	return OperationKindBrTable
+}
+
+type OperationCall struct {
+	FunctionIndex uint32
+}
+
+func (o *OperationCall) Kind() OperationKind {
+	return OperationKindCall
+}
+
+type OperationCallIndirect struct {
+	TypeIndex, TableIndex uint32
+}
+
+func (o *OperationCallIndirect) Kind() OperationKind {
+	return OperationKindCallIndirect
+}
+
+type OperationDrop struct{ Range *InclusiveRange }
+
+func (o *OperationDrop) Kind() OperationKind {
+	return OperationKindDrop
+}
+
+type OperationSelect struct{}
+
+func (o *OperationSelect) Kind() OperationKind {
+	return OperationKindSelect
+}
+
+type OperationPick struct{ Depth int }
+
+func (o *OperationPick) Kind() OperationKind {
+	return OperationKindPick
+}
+
+type OperationSwap struct{ Depth int }
+
+func (o *OperationSwap) Kind() OperationKind {
+	return OperationKindSwap
+}
+
+type OperationGlobalGet struct{ Index uint32 }
+
+func (o *OperationGlobalGet) Kind() OperationKind {
+	return OperationKindGlobalGet
+}
+
+type OperationGlobalSet struct{ Index uint32 }
+
+func (o *OperationGlobalSet) Kind() OperationKind {
+	return OperationKindGlobalSet
+}
+
+type MemoryImmediate struct {
+	Alignment, Offest uint32
+}
+
+type OperationLoad struct {
+	Type SignLessType
+	Arg  *MemoryImmediate
+}
+
+func (o *OperationLoad) Kind() OperationKind {
+	return OperationKindLoad
+}
+
+type OperationLoad8 struct {
+	Type SignFulInt
+	Arg  *MemoryImmediate
+}
+
+func (o *OperationLoad8) Kind() OperationKind {
+	return OperationKindLoad8
+}
+
+type OperationLoad16 struct {
+	Type SignFulInt
+	Arg  *MemoryImmediate
+}
+
+func (o *OperationLoad16) Kind() OperationKind {
+	return OperationKindLoad16
+}
+
+type OperationLoad32 struct {
+	Signed bool
+	Arg    *MemoryImmediate
+}
+
+func (o *OperationLoad32) Kind() OperationKind {
+	return OperationKindLoad32
+}
+
+type OperationStore struct {
+	Type SignLessType
+	Arg  *MemoryImmediate
+}
+
+func (o *OperationStore) Kind() OperationKind {
+	return OperationKindStore
+}
+
+type OperationStore8 struct {
+	Type SignLessInt
+	Arg  *MemoryImmediate
+}
+
+func (o *OperationStore8) Kind() OperationKind {
+	return OperationKindStore8
+}
+
+type OperationStore16 struct {
+	Type SignLessInt
+	Arg  *MemoryImmediate
+}
+
+func (o *OperationStore16) Kind() OperationKind {
+	return OperationKindStore16
+}
+
+type OperationStore32 struct {
+	Arg *MemoryImmediate
+}
+
+func (o *OperationStore32) Kind() OperationKind {
+	return OperationKindStore32
+}
+
+type OperationMemorySize struct{}
+
+func (o *OperationMemorySize) Kind() OperationKind {
+	return OperationKindMemorySize
+}
+
+type OperationMemoryGrow struct{ Aligment uint64 }
+
+func (o *OperationMemoryGrow) Kind() OperationKind {
+	return OperationKindMemoryGrow
+}
+
+type OperationConstI32 struct{ Value uint32 }
+
+func (o *OperationConstI32) Kind() OperationKind {
+	return OperationKindConstI32
+}
+
+type OperationConstI64 struct{ Value uint64 }
+
+func (o *OperationConstI64) Kind() OperationKind {
+	return OperationKindConstI64
+}
+
+type OperationConstF32 struct{ Value float32 }
+
+func (o *OperationConstF32) Kind() OperationKind {
+	return OperationKindConstF32
+}
+
+type OperationConstF64 struct{ Value float64 }
+
+func (o *OperationConstF64) Kind() OperationKind {
+	return OperationKindConstF64
+}
+
+type OperationEq struct{ Type SignLessType }
+
+func (o *OperationEq) Kind() OperationKind {
+	return OperationKindEq
+}
+
+type OperationNe struct{ Type SignLessType }
+
+func (o *OperationNe) Kind() OperationKind {
+	return OperationKindNe
+}
+
+type OperationEqz struct{ Type SignLessInt }
+
+func (o *OperationEqz) Kind() OperationKind {
+	return OperationKindEqz
+}
+
+type OperationLt struct{ Type SignFulType }
+
+func (o *OperationLt) Kind() OperationKind {
+	return OperationKindLt
+}
+
+type OperationGt struct{ Type SignFulType }
+
+func (o *OperationGt) Kind() OperationKind {
+	return OperationKindGt
+}
+
+type OperationLe struct{ Type SignFulType }
+
+func (o *OperationLe) Kind() OperationKind {
+	return OperationKindLe
+}
+
+type OperationGe struct{ Type SignFulType }
+
+func (o *OperationGe) Kind() OperationKind {
+	return OperationKindGe
+}
+
+type OperationAdd struct{ Type SignLessType }
+
+func (o *OperationAdd) Kind() OperationKind {
+	return OperationKindAdd
+}
+
+type OperationSub struct{ Type SignLessType }
+
+func (o *OperationSub) Kind() OperationKind {
+	return OperationKindSub
+}
+
+type OperationMul struct{ Type SignLessType }
+
+func (o *OperationMul) Kind() OperationKind {
+	return OperationKindMul
+}
+
+type OperationClz struct{ Type SignLessInt }
+
+func (o *OperationClz) Kind() OperationKind {
+	return OperationKindClz
+}
+
+type OperationCtz struct{ Type SignLessInt }
+
+func (o *OperationCtz) Kind() OperationKind {
+	return OperationKindCtz
+}
+
+type OperationPopcnt struct{ Type SignLessInt }
+
+func (o *OperationPopcnt) Kind() OperationKind {
+	return OperationKindPopcnt
+}
+
+type OperationDiv struct{ Type SignFulType }
+
+func (o *OperationDiv) Kind() OperationKind {
+	return OperationKindDiv
+}
+
+type OperationRem struct{ Type SignFulInt }
+
+func (o *OperationRem) Kind() OperationKind {
+	return OperationKindRem
+}
+
+type OperationAnd struct{ Type SignLessInt }
+
+func (o *OperationAnd) Kind() OperationKind {
+	return OperationKindAnd
+}
+
+type OperationOr struct{ Type SignLessInt }
+
+func (o *OperationOr) Kind() OperationKind {
+	return OperationKindOr
+}
+
+type OperationXor struct{ Type SignLessInt }
+
+func (o *OperationXor) Kind() OperationKind {
+	return OperationKindXor
+}
+
+type OperationShl struct{ Type SignLessInt }
+
+func (o *OperationShl) Kind() OperationKind {
+	return OperationKindShl
+}
+
+type OperationShr struct{ Type SignFulInt }
+
+func (o *OperationShr) Kind() OperationKind {
+	return OperationKindShr
+}
+
+type OperationRotl struct{ Type SignLessInt }
+
+func (o *OperationRotl) Kind() OperationKind {
+	return OperationKindRotl
+}
+
+type OperationRotr struct{ Type SignLessInt }
+
+func (o *OperationRotr) Kind() OperationKind {
+	return OperationKindRotr
+}
+
+type OperationAbs struct{ Type Float }
+
+func (o *OperationAbs) Kind() OperationKind {
+	return OperationKindAbs
+}
+
+type OperationNeg struct{ Type Float }
+
+func (o *OperationNeg) Kind() OperationKind {
+	return OperationKindNeg
+}
+
+type OperationCeil struct{ Type Float }
+
+func (o *OperationCeil) Kind() OperationKind {
+	return OperationKindCeil
+}
+
+type OperationFloor struct{ Type Float }
+
+func (o *OperationFloor) Kind() OperationKind {
+	return OperationKindFloor
+}
+
+type OperationTrunc struct{ Type Float }
+
+func (o *OperationTrunc) Kind() OperationKind {
+	return OperationKindTrunc
+}
+
+type OperationNearest struct{ Type Float }
+
+func (o *OperationNearest) Kind() OperationKind {
+	return OperationKindNearest
+}
+
+type OperationSqrt struct{ Type Float }
+
+func (o *OperationSqrt) Kind() OperationKind {
+	return OperationKindSqrt
+}
+
+type OperationMin struct{ Type Float }
+
+func (o *OperationMin) Kind() OperationKind {
+	return OperationKindMin
+}
+
+type OperationMax struct{ Type Float }
+
+func (o *OperationMax) Kind() OperationKind {
+	return OperationKindMax
+}
+
+type OperationCopysign struct{ Type Float }
+
+func (o *OperationCopysign) Kind() OperationKind {
+	return OperationKindCopysign
+}
+
+type OperationI32WrapFromI64 struct{}
+
+func (o *OperationI32WrapFromI64) Kind() OperationKind {
+	return OperationKindI32WrapFromI64
+}
+
+type OperationITruncFromF struct {
+	InputType  Float
+	OutputType SignFulInt
+}
+
+func (o *OperationITruncFromF) Kind() OperationKind {
+	return OperationKindITruncFromF
+}
+
+type OperationFConvertFromI struct {
+	InputType  SignFulInt
+	OutputType Float
+}
+
+func (o *OperationFConvertFromI) Kind() OperationKind {
+	return OperationKindFConvertFromI
+}
+
+type OperationF32DemoteFromF64 struct{}
+
+func (o *OperationF32DemoteFromF64) Kind() OperationKind {
+	return OperationKindF32DemoteFromF64
+}
+
+type OperationF64PromoteFromF32 struct{}
+
+func (o *OperationF64PromoteFromF32) Kind() OperationKind {
+	return OperationKindF64PromoteFromF32
+}
+
+type OperationI32ReinterpretFromF32 struct{}
+
+func (o *OperationI32ReinterpretFromF32) Kind() OperationKind {
+	return OperationKindI32ReinterpretFromF32
+}
+
+type OperationI64ReinterpretFromF64 struct{}
+
+func (o *OperationI64ReinterpretFromF64) Kind() OperationKind {
+	return OperationKindI64ReinterpretFromF64
+}
+
+type OperationF32ReinterpretFromI32 struct{}
+
+func (o *OperationF32ReinterpretFromI32) Kind() OperationKind {
+	return OperationKindF32ReinterpretFromI32
+}
+
+type OperationF64ReinterpretFromI64 struct{}
+
+func (o *OperationF64ReinterpretFromI64) Kind() OperationKind {
+	return OperationKindF64ReinterpretFromI64
+}
+
+type OperationExtend struct{ Signed bool }
+
+func (o *OperationExtend) Kind() OperationKind {
+	return OperationKindExtend
+}
diff --git a/wasm/wazeroir/signature.go b/wasm/wazeroir/signature.go
new file mode 100644
index 00000000..da8230c2
--- /dev/null
+++ b/wasm/wazeroir/signature.go
@@ -0,0 +1,377 @@
+package wazeroir
+
+import (
+	"fmt"
+
+	"github.com/tetratelabs/wazero/wasm"
+)
+
+// signature represents how a Wasm optcode
+// manipulates the value stacks in terms of value types.
+type signature struct {
+	in, out []SignLessType
+}
+
+var (
+	signature_None_None    = &signature{}
+	signature_Unknown_None = &signature{
+		in: []SignLessType{SignLessTypeUnknown},
+	}
+	signature_None_I32 = &signature{
+		out: []SignLessType{SignLessTypeI32},
+	}
+	signature_None_I64 = &signature{
+		out: []SignLessType{SignLessTypeI64},
+	}
+	signature_None_F32 = &signature{
+		out: []SignLessType{SignLessTypeF32},
+	}
+	signature_None_F64 = &signature{
+		out: []SignLessType{SignLessTypeF64},
+	}
+	signature_I32_None = &signature{
+		in: []SignLessType{SignLessTypeI32},
+	}
+	signature_I32_I32 = &signature{
+		in:  []SignLessType{SignLessTypeI32},
+		out: []SignLessType{SignLessTypeI32},
+	}
+	signature_I32_I64 = &signature{
+		in:  []SignLessType{SignLessTypeI32},
+		out: []SignLessType{SignLessTypeI64},
+	}
+	signature_I64_I64 = &signature{
+		in:  []SignLessType{SignLessTypeI64},
+		out: []SignLessType{SignLessTypeI64},
+	}
+	signature_I32_F32 = &signature{
+		in:  []SignLessType{SignLessTypeI32},
+		out: []SignLessType{SignLessTypeF32},
+	}
+	signature_I32_F64 = &signature{
+		in:  []SignLessType{SignLessTypeI32},
+		out: []SignLessType{SignLessTypeF64},
+	}
+	signature_I64_I32 = &signature{
+		in:  []SignLessType{SignLessTypeI64},
+		out: []SignLessType{SignLessTypeI32},
+	}
+	signature_I64_F32 = &signature{
+		in:  []SignLessType{SignLessTypeI64},
+		out: []SignLessType{SignLessTypeF32},
+	}
+	signature_I64_F64 = &signature{
+		in:  []SignLessType{SignLessTypeI64},
+		out: []SignLessType{SignLessTypeF64},
+	}
+	signature_F32_I32 = &signature{
+		in:  []SignLessType{SignLessTypeF32},
+		out: []SignLessType{SignLessTypeI32},
+	}
+	signature_F32_I64 = &signature{
+		in:  []SignLessType{SignLessTypeF32},
+		out: []SignLessType{SignLessTypeI64},
+	}
+	signature_F32_F64 = &signature{
+		in:  []SignLessType{SignLessTypeF32},
+		out: []SignLessType{SignLessTypeF64},
+	}
+	signature_F32_F32 = &signature{
+		in:  []SignLessType{SignLessTypeF32},
+		out: []SignLessType{SignLessTypeF32},
+	}
+	signature_F64_I32 = &signature{
+		in:  []SignLessType{SignLessTypeF64},
+		out: []SignLessType{SignLessTypeI32},
+	}
+	signature_F64_F32 = &signature{
+		in:  []SignLessType{SignLessTypeF64},
+		out: []SignLessType{SignLessTypeF32},
+	}
+	signature_F64_I64 = &signature{
+		in:  []SignLessType{SignLessTypeF64},
+		out: []SignLessType{SignLessTypeI64},
+	}
+	signature_F64_F64 = &signature{
+		in:  []SignLessType{SignLessTypeF64},
+		out: []SignLessType{SignLessTypeF64},
+	}
+	signature_I32I32_None = &signature{
+		in: []SignLessType{SignLessTypeI32, SignLessTypeI32},
+	}
+	signature_I32I32_I32 = &signature{
+		in:  []SignLessType{SignLessTypeI32, SignLessTypeI32},
+		out: []SignLessType{SignLessTypeI32},
+	}
+	signature_I32I64_None = &signature{
+		in: []SignLessType{SignLessTypeI32, SignLessTypeI64},
+	}
+	signature_I32F32_None = &signature{
+		in: []SignLessType{SignLessTypeI32, SignLessTypeF32},
+	}
+	signature_I32F64_None = &signature{
+		in: []SignLessType{SignLessTypeI32, SignLessTypeF64},
+	}
+	signature_I64I64_I32 = &signature{
+		in:  []SignLessType{SignLessTypeI64, SignLessTypeI64},
+		out: []SignLessType{SignLessTypeI32},
+	}
+	signature_I64I64_I64 = &signature{
+		in:  []SignLessType{SignLessTypeI64, SignLessTypeI64},
+		out: []SignLessType{SignLessTypeI64},
+	}
+	signature_F32F32_I32 = &signature{
+		in:  []SignLessType{SignLessTypeF32, SignLessTypeF32},
+		out: []SignLessType{SignLessTypeI32},
+	}
+	signature_F32F32_F32 = &signature{
+		in:  []SignLessType{SignLessTypeF32, SignLessTypeF32},
+		out: []SignLessType{SignLessTypeF32},
+	}
+	signature_F64F64_I32 = &signature{
+		in:  []SignLessType{SignLessTypeF64, SignLessTypeF64},
+		out: []SignLessType{SignLessTypeI32},
+	}
+	signature_F64F64_F64 = &signature{
+		in:  []SignLessType{SignLessTypeF64, SignLessTypeF64},
+		out: []SignLessType{SignLessTypeF64},
+	}
+	signature_UnknownUnkownI32_Unknown = &signature{
+		in:  []SignLessType{SignLessTypeUnknown, SignLessTypeUnknown, SignLessTypeI32},
+		out: []SignLessType{SignLessTypeUnknown},
+	}
+)
+
+// wasmOptcodeSignature returns the signature of given Wasm optcode.
+// Note that some of optcodes' signature vary depending on
+// the function instance (for example, local types).
+// "index" parameter is not used by most of optcodes.
+// The returned signature is used for stack validation when lowering Wasm's optcodes to wazeroir.
+func wasmOptcodeSignature(f *wasm.FunctionInstance, op wasm.OptCode, index uint32) (*signature, error) {
+	switch op {
+	case wasm.OptCodeUnreachable, wasm.OptCodeNop, wasm.OptCodeBlock, wasm.OptCodeLoop:
+		return signature_None_None, nil
+	case wasm.OptCodeIf:
+		return signature_I32_None, nil
+	case wasm.OptCodeElse, wasm.OptCodeEnd, wasm.OptCodeBr:
+		return signature_None_None, nil
+	case wasm.OptCodeBrIf, wasm.OptCodeBrTable:
+		return signature_I32_None, nil
+	case wasm.OptCodeReturn:
+		return signature_None_None, nil
+	case wasm.OptCodeCall:
+		return funcTypeToSignature(f.ModuleInstance.Functions[index].Signature), nil
+	case wasm.OptCodeCallIndirect:
+		ret := funcTypeToSignature(f.ModuleInstance.Types[index])
+		ret.in = append(ret.in, SignLessTypeI32)
+		return ret, nil
+	case wasm.OptCodeDrop:
+		return signature_Unknown_None, nil
+	case wasm.OptCodeSelect:
+		return signature_UnknownUnkownI32_Unknown, nil
+	case wasm.OptCodeLocalGet:
+		inputLen := uint32(len(f.Signature.InputTypes))
+		if l := f.NumLocals + inputLen; index >= l {
+			return nil, fmt.Errorf("invalid local index for local.get %d >= %d", index, l)
+		}
+		var t SignLessType
+		if index < inputLen {
+			t = wasmValueTypeToSignless(f.Signature.InputTypes[index])
+		} else {
+			t = wasmValueTypeToSignless(f.LocalTypes[index-inputLen])
+		}
+		return &signature{out: []SignLessType{t}}, nil
+	case wasm.OptCodeLocalSet:
+		inputLen := uint32(len(f.Signature.InputTypes))
+		if l := f.NumLocals + inputLen; index >= l {
+			return nil, fmt.Errorf("invalid local index for local.get %d >= %d", index, l)
+		}
+		var t SignLessType
+		if index < inputLen {
+			t = wasmValueTypeToSignless(f.Signature.InputTypes[index])
+		} else {
+			t = wasmValueTypeToSignless(f.LocalTypes[index-inputLen])
+		}
+		return &signature{in: []SignLessType{t}}, nil
+	case wasm.OptCodeLocalTee:
+		inputLen := uint32(len(f.Signature.InputTypes))
+		if l := f.NumLocals + inputLen; index >= l {
+			return nil, fmt.Errorf("invalid local index for local.get %d >= %d", index, l)
+		}
+		var t SignLessType
+		if index < inputLen {
+			t = wasmValueTypeToSignless(f.Signature.InputTypes[index])
+		} else {
+			t = wasmValueTypeToSignless(f.LocalTypes[index-inputLen])
+		}
+		return &signature{in: []SignLessType{t}, out: []SignLessType{t}}, nil
+	case wasm.OptCodeGlobalGet:
+		if len(f.ModuleInstance.Globals) <= int(index) {
+			return nil, fmt.Errorf("invalid global index for global.get %d >= %d", index, len(f.ModuleInstance.Globals))
+		}
+		return &signature{
+			out: []SignLessType{wasmValueTypeToSignless(f.ModuleInstance.Globals[index].Type.ValType)},
+		}, nil
+	case wasm.OptCodeGlobalSet:
+		if len(f.ModuleInstance.Globals) <= int(index) {
+			return nil, fmt.Errorf("invalid global index for global.get %d >= %d", index, len(f.ModuleInstance.Globals))
+		}
+		return &signature{
+			in: []SignLessType{wasmValueTypeToSignless(f.ModuleInstance.Globals[index].Type.ValType)},
+		}, nil
+	case wasm.OptCodeI32Load:
+		return signature_I32_I32, nil
+	case wasm.OptCodeI64Load:
+		return signature_I32_I64, nil
+	case wasm.OptCodeF32Load:
+		return signature_I32_F32, nil
+	case wasm.OptCodeF64Load:
+		return signature_I32_F64, nil
+	case wasm.OptCodeI32Load8s, wasm.OptCodeI32Load8u, wasm.OptCodeI32Load16s, wasm.OptCodeI32Load16u:
+		return signature_I32_I32, nil
+	case wasm.OptCodeI64Load8s, wasm.OptCodeI64Load8u, wasm.OptCodeI64Load16s, wasm.OptCodeI64Load16u,
+		wasm.OptCodeI64Load32s, wasm.OptCodeI64Load32u:
+		return signature_I32_I64, nil
+	case wasm.OptCodeI32Store:
+		return signature_I32I32_None, nil
+	case wasm.OptCodeI64Store:
+		return signature_I32I64_None, nil
+	case wasm.OptCodeF32Store:
+		return signature_I32F32_None, nil
+	case wasm.OptCodeF64Store:
+		return signature_I32F64_None, nil
+	case wasm.OptCodeI32Store8:
+		return signature_I32I32_None, nil
+	case wasm.OptCodeI32Store16:
+		return signature_I32I32_None, nil
+	case wasm.OptCodeI64Store8:
+		return signature_I32I64_None, nil
+	case wasm.OptCodeI64Store16:
+		return signature_I32I64_None, nil
+	case wasm.OptCodeI64Store32:
+		return signature_I32I64_None, nil
+	case wasm.OptCodeMemorySize:
+		return signature_None_I32, nil
+	case wasm.OptCodeMemoryGrow:
+		return signature_I32_I32, nil
+	case wasm.OptCodeI32Const:
+		return signature_None_I32, nil
+	case wasm.OptCodeI64Const:
+		return signature_None_I64, nil
+	case wasm.OptCodeF32Const:
+		return signature_None_F32, nil
+	case wasm.OptCodeF64Const:
+		return signature_None_F64, nil
+	case wasm.OptCodeI32eqz:
+		return signature_I32_I32, nil
+	case wasm.OptCodeI32eq, wasm.OptCodeI32ne, wasm.OptCodeI32lts,
+		wasm.OptCodeI32ltu, wasm.OptCodeI32gts, wasm.OptCodeI32gtu,
+		wasm.OptCodeI32les, wasm.OptCodeI32leu, wasm.OptCodeI32ges,
+		wasm.OptCodeI32geu:
+		return signature_I32I32_I32, nil
+	case wasm.OptCodeI64eqz:
+		return signature_I64_I32, nil
+	case wasm.OptCodeI64eq, wasm.OptCodeI64ne, wasm.OptCodeI64lts,
+		wasm.OptCodeI64ltu, wasm.OptCodeI64gts, wasm.OptCodeI64gtu,
+		wasm.OptCodeI64les, wasm.OptCodeI64leu, wasm.OptCodeI64ges,
+		wasm.OptCodeI64geu:
+		return signature_I64I64_I32, nil
+	case wasm.OptCodeF32eq, wasm.OptCodeF32ne, wasm.OptCodeF32lt,
+		wasm.OptCodeF32gt, wasm.OptCodeF32le, wasm.OptCodeF32ge:
+		return signature_F32F32_I32, nil
+	case wasm.OptCodeF64eq, wasm.OptCodeF64ne, wasm.OptCodeF64lt,
+		wasm.OptCodeF64gt, wasm.OptCodeF64le, wasm.OptCodeF64ge:
+		return signature_F64F64_I32, nil
+	case wasm.OptCodeI32clz, wasm.OptCodeI32ctz, wasm.OptCodeI32popcnt:
+		return signature_I32_I32, nil
+	case wasm.OptCodeI32add, wasm.OptCodeI32sub, wasm.OptCodeI32mul,
+		wasm.OptCodeI32divs, wasm.OptCodeI32divu, wasm.OptCodeI32rems,
+		wasm.OptCodeI32remu, wasm.OptCodeI32and, wasm.OptCodeI32or,
+		wasm.OptCodeI32xor, wasm.OptCodeI32shl, wasm.OptCodeI32shrs,
+		wasm.OptCodeI32shru, wasm.OptCodeI32rotl, wasm.OptCodeI32rotr:
+		return signature_I32I32_I32, nil
+	case wasm.OptCodeI64clz, wasm.OptCodeI64ctz, wasm.OptCodeI64popcnt:
+		return signature_I64_I64, nil
+	case wasm.OptCodeI64add, wasm.OptCodeI64sub, wasm.OptCodeI64mul,
+		wasm.OptCodeI64divs, wasm.OptCodeI64divu, wasm.OptCodeI64rems,
+		wasm.OptCodeI64remu, wasm.OptCodeI64and, wasm.OptCodeI64or,
+		wasm.OptCodeI64xor, wasm.OptCodeI64shl, wasm.OptCodeI64shrs,
+		wasm.OptCodeI64shru, wasm.OptCodeI64rotl, wasm.OptCodeI64rotr:
+		return signature_I64I64_I64, nil
+	case wasm.OptCodeF32abs, wasm.OptCodeF32neg, wasm.OptCodeF32ceil,
+		wasm.OptCodeF32floor, wasm.OptCodeF32trunc, wasm.OptCodeF32nearest,
+		wasm.OptCodeF32sqrt:
+		return signature_F32_F32, nil
+	case wasm.OptCodeF32add, wasm.OptCodeF32sub, wasm.OptCodeF32mul,
+		wasm.OptCodeF32div, wasm.OptCodeF32min, wasm.OptCodeF32max,
+		wasm.OptCodeF32copysign:
+		return signature_F32F32_F32, nil
+	case wasm.OptCodeF64abs, wasm.OptCodeF64neg, wasm.OptCodeF64ceil,
+		wasm.OptCodeF64floor, wasm.OptCodeF64trunc, wasm.OptCodeF64nearest,
+		wasm.OptCodeF64sqrt:
+		return signature_F64_F64, nil
+	case wasm.OptCodeF64add, wasm.OptCodeF64sub, wasm.OptCodeF64mul,
+		wasm.OptCodeF64div, wasm.OptCodeF64min, wasm.OptCodeF64max,
+		wasm.OptCodeF64copysign:
+		return signature_F64F64_F64, nil
+	case wasm.OptCodeI32wrapI64:
+		return signature_I64_I32, nil
+	case wasm.OptCodeI32truncf32s, wasm.OptCodeI32truncf32u:
+		return signature_F32_I32, nil
+	case wasm.OptCodeI32truncf64s, wasm.OptCodeI32truncf64u:
+		return signature_F64_I32, nil
+	case wasm.OptCodeI64Extendi32s, wasm.OptCodeI64Extendi32u:
+		return signature_I32_I64, nil
+	case wasm.OptCodeI64TruncF32s, wasm.OptCodeI64TruncF32u:
+		return signature_F32_I64, nil
+	case wasm.OptCodeI64Truncf64s, wasm.OptCodeI64Truncf64u:
+		return signature_F64_I64, nil
+	case wasm.OptCodeF32Converti32s, wasm.OptCodeF32Converti32u:
+		return signature_I32_F32, nil
+	case wasm.OptCodeF32Converti64s, wasm.OptCodeF32Converti64u:
+		return signature_I64_F32, nil
+	case wasm.OptCodeF32Demotef64:
+		return signature_F64_F32, nil
+	case wasm.OptCodeF64Converti32s, wasm.OptCodeF64Converti32u:
+		return signature_I32_F64, nil
+	case wasm.OptCodeF64Converti64s, wasm.OptCodeF64Converti64u:
+		return signature_I64_F64, nil
+	case wasm.OptCodeF64Promotef32:
+		return signature_F32_F64, nil
+	case wasm.OptCodeI32Reinterpretf32:
+		return signature_F32_I32, nil
+	case wasm.OptCodeI64Reinterpretf64:
+		return signature_F64_I64, nil
+	case wasm.OptCodeF32Reinterpreti32:
+		return signature_I32_F32, nil
+	case wasm.OptCodeF64Reinterpreti64:
+		return signature_I64_F64, nil
+	default:
+		return nil, fmt.Errorf("unsupported instruction in wazeroir: 0x%x", op)
+	}
+}
+
+func funcTypeToSignature(tps *wasm.FunctionType) *signature {
+	ret := &signature{}
+	for _, vt := range tps.InputTypes {
+		ret.in = append(ret.in, wasmValueTypeToSignless(vt))
+	}
+	for _, vt := range tps.ReturnTypes {
+		ret.out = append(ret.out, wasmValueTypeToSignless(vt))
+	}
+	return ret
+}
+
+func wasmValueTypeToSignless(vt wasm.ValueType) SignLessType {
+	switch vt {
+	case wasm.ValueTypeI32:
+		return SignLessTypeI32
+	case wasm.ValueTypeI64:
+		return SignLessTypeI64
+	case wasm.ValueTypeF32:
+		return SignLessTypeF32
+	case wasm.ValueTypeF64:
+		return SignLessTypeF64
+	}
+	panic("unreachable")
+}
diff --git a/wasm/wazeroir/wazeroir.go b/wasm/wazeroir/wazeroir.go
new file mode 100644
index 00000000..8b32b9cd
--- /dev/null
+++ b/wasm/wazeroir/wazeroir.go
@@ -0,0 +1,8 @@
+// A pkg to compile down the standard Wasm binary to wazero's specific IR (wazeroir).
+// The wazeroir is inspired by microwasm format (a.k.a. LightbeamIR), previously used
+// in the lightbeam JIT compiler in Wasmtime, though it is not specified and only exists
+// in the previous codebase of wasmtime
+// e.g. https://github.com/bytecodealliance/wasmtime/blob/v0.29.0/crates/lightbeam/src/microwasm.rs
+//
+// See RATIONALE.md for detail.
+package wazeroir


Test patch:
diff --git a/wasm/spectests/spec_test.go b/wasm/spectests/spec_test.go
index 2258eaaa..3b184fa3 100644
--- a/wasm/spectests/spec_test.go
+++ b/wasm/spectests/spec_test.go
@@ -1,4 +1,4 @@
-package spec
+package spectests
 
 import (
 	"encoding/json"
@@ -17,6 +17,7 @@ import (
 
 	"github.com/tetratelabs/wazero/wasm"
 	"github.com/tetratelabs/wazero/wasm/naivevm"
+	"github.com/tetratelabs/wazero/wasm/wazeroir"
 )
 
 type (
@@ -28,20 +29,20 @@ type (
 		CommandType string `json:"type"`
 		Line        int    `json:"line"`
 
-		// type == "module" || "register"
+		// Set when type == "module" || "register"
 		Name string `json:"name,omitempty"`
 
-		// type == "module" || "assert_uninstantiable" || "assert_malformed"
+		// Set when type == "module" || "assert_uninstantiable" || "assert_malformed"
 		Filename string `json:"filename,omitempty"`
 
-		// type == "register"
+		// Set when type == "register"
 		As string `json:"as,omitempty"`
 
-		// type == "assert_return" || "action"
+		// Set when type == "assert_return" || "action"
 		Action commandAction      `json:"action,omitempty"`
 		Exps   []commandActionVal `json:"expected"`
 
-		// type == "assert_malformed"
+		// Set when type == "assert_malformed"
 		ModuleType string `json:"module_type"`
 	}
 
@@ -49,7 +50,7 @@ type (
 		ActionType string             `json:"type"`
 		Args       []commandActionVal `json:"args"`
 
-		// ActionType == "invoke"
+		// Set when ActionType == "invoke"
 		Field  string `json:"field,omitempty"`
 		Module string `json:"module,omitempty"`
 	}
@@ -111,26 +112,26 @@ func (c command) String() string {
 	return "{" + msg + "}"
 }
 
-func (c command) getAssertReturnArgs(t *testing.T) []uint64 {
+func (c command) getAssertReturnArgs() []uint64 {
 	var args []uint64
 	for _, arg := range c.Action.Args {
-		args = append(args, arg.ToUint64(t))
+		args = append(args, arg.toUint64())
 	}
 	return args
 }
 
-func (c command) getAssertReturnArgsExps(t *testing.T) ([]uint64, []uint64) {
+func (c command) getAssertReturnArgsExps() ([]uint64, []uint64) {
 	var args, exps []uint64
 	for _, arg := range c.Action.Args {
-		args = append(args, arg.ToUint64(t))
+		args = append(args, arg.toUint64())
 	}
 	for _, exp := range c.Exps {
-		exps = append(exps, exp.ToUint64(t))
+		exps = append(exps, exp.toUint64())
 	}
 	return args, exps
 }
 
-func (v commandActionVal) ToUint64(t *testing.T) uint64 {
+func (v commandActionVal) toUint64() uint64 {
 	if strings.Contains(v.Value, "nan") {
 		if v.ValType == "f32" {
 			return uint64(math.Float32bits(float32(math.NaN())))
@@ -139,33 +140,27 @@ func (v commandActionVal) ToUint64(t *testing.T) uint64 {
 	}
 
 	if strings.Contains(v.ValType, "32") {
-		ret, err := strconv.ParseUint(v.Value, 10, 32)
-		require.NoError(t, err)
+		ret, _ := strconv.ParseUint(v.Value, 10, 32)
 		return ret
 	} else {
-		ret, err := strconv.ParseUint(v.Value, 10, 64)
-		require.NoError(t, err)
+		ret, _ := strconv.ParseUint(v.Value, 10, 64)
 		return ret
 	}
 }
 
-func requireAddSpectestModule(t *testing.T, store *wasm.Store) {
-	// Add functions
-	err := store.AddHostFunction("spectest", "print", reflect.ValueOf(func(*wasm.HostFunctionCallContext) {}))
-	require.NoError(t, err)
-	err = store.AddHostFunction("spectest", "print_i32", reflect.ValueOf(func(*wasm.HostFunctionCallContext, uint32) {}))
-	require.NoError(t, err)
-	err = store.AddHostFunction("spectest", "print_f32", reflect.ValueOf(func(*wasm.HostFunctionCallContext, float32) {}))
-	require.NoError(t, err)
-	err = store.AddHostFunction("spectest", "print_i64", reflect.ValueOf(func(*wasm.HostFunctionCallContext, uint64) {}))
-	require.NoError(t, err)
-	err = store.AddHostFunction("spectest", "print_f64", reflect.ValueOf(func(*wasm.HostFunctionCallContext, float64) {}))
-	require.NoError(t, err)
-	err = store.AddHostFunction("spectest", "print_i32_f32", reflect.ValueOf(func(*wasm.HostFunctionCallContext, uint32, float32) {}))
-	require.NoError(t, err)
-	err = store.AddHostFunction("spectest", "print_f64_f64", reflect.ValueOf(func(*wasm.HostFunctionCallContext, float64, float64) {}))
-	require.NoError(t, err)
-	// Register globals.
+func addSpectestModule(t *testing.T, store *wasm.Store) {
+	for n, v := range map[string]reflect.Value{
+		"print":         reflect.ValueOf(func(*wasm.HostFunctionCallContext) {}),
+		"print_i32":     reflect.ValueOf(func(*wasm.HostFunctionCallContext, uint32) {}),
+		"print_f32":     reflect.ValueOf(func(*wasm.HostFunctionCallContext, float32) {}),
+		"print_i64":     reflect.ValueOf(func(*wasm.HostFunctionCallContext, uint64) {}),
+		"print_f64":     reflect.ValueOf(func(*wasm.HostFunctionCallContext, float64) {}),
+		"print_i32_f32": reflect.ValueOf(func(*wasm.HostFunctionCallContext, uint32, float32) {}),
+		"print_f64_f64": reflect.ValueOf(func(*wasm.HostFunctionCallContext, float64, float64) {}),
+	} {
+		require.NoError(t, store.AddHostFunction("spectest", n, v), "AddHostFunction(%s)", n)
+	}
+
 	for _, g := range []struct {
 		name      string
 		valueType wasm.ValueType
@@ -176,17 +171,14 @@ func requireAddSpectestModule(t *testing.T, store *wasm.Store) {
 		{name: "global_f32", valueType: wasm.ValueTypeF32, value: uint64(uint32(0x44268000))},
 		{name: "global_f64", valueType: wasm.ValueTypeF64, value: uint64(0x4084d00000000000)},
 	} {
-		err = store.AddGlobal("spectest", g.name, g.value, g.valueType, false)
-		require.NoError(t, err)
+		require.NoError(t, store.AddGlobal("spectest", g.name, g.value, g.valueType, false), "AddGlobal(%s)", g.name)
 	}
-	// Register table export.
+
 	tableLimitMax := uint32(20)
-	err = store.AddTableInstance("spectest", "table", 10, &tableLimitMax)
-	require.NoError(t, err)
-	// Register table export.
+	require.NoError(t, store.AddTableInstance("spectest", "table", 10, &tableLimitMax))
+
 	memoryLimitMax := uint32(2)
-	err = store.AddMemoryInstance("spectest", "memory", 1, &memoryLimitMax)
-	require.NoError(t, err)
+	require.NoError(t, store.AddMemoryInstance("spectest", "memory", 1, &memoryLimitMax))
 }
 
 func TestSpecification(t *testing.T) {
@@ -204,171 +196,185 @@ func TestSpecification(t *testing.T) {
 
 		var base testbase
 		require.NoError(t, json.Unmarshal(raw, &base))
+
 		wastName := filepath.Base(base.SourceFile)
 		t.Run(wastName, func(t *testing.T) {
-			store := wasm.NewStore(naivevm.NewEngine())
-			requireAddSpectestModule(t, store)
-
-			var lastInstanceName string
-			for _, c := range base.Commands {
-				t.Run(fmt.Sprintf("%s/line:%d", c.CommandType, c.Line), func(t *testing.T) {
-					msg := fmt.Sprintf("%s:%d", wastName, c.Line)
-					switch c.CommandType {
-					case "module":
-						buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
-						require.NoError(t, err, msg)
-
-						mod, err := wasm.DecodeModule(buf)
-						require.NoError(t, err, msg)
-
-						lastInstanceName = c.Name
-						if lastInstanceName == "" {
-							lastInstanceName = c.Filename
-						}
-						err = store.Instantiate(mod, lastInstanceName)
-						require.NoError(t, err)
-					case "register":
-						name := lastInstanceName
-						if c.Name != "" {
-							name = c.Name
-						}
-						store.ModuleInstances[c.As] = store.ModuleInstances[name]
-					case "assert_return", "action":
-						moduleName := lastInstanceName
-						if c.Action.Module != "" {
-							moduleName = c.Action.Module
-						}
-						switch c.Action.ActionType {
-						case "invoke":
-							args, exps := c.getAssertReturnArgsExps(t)
-							msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
-							if c.Action.Module != "" {
-								msg += " in module " + c.Action.Module
-							}
-							vals, types, err := store.CallFunction(moduleName, c.Action.Field, args...)
-							if assert.NoError(t, err, msg) &&
-								assert.Equal(t, len(exps), len(vals), msg) &&
-								assert.Equal(t, len(exps), len(types), msg) {
-								for i, exp := range exps {
-									assertValueEq(t, vals[i], exp, types[i], msg)
+			for _, tc := range []struct {
+				name   string
+				engine wasm.Engine
+			}{
+				{engine: naivevm.NewEngine(), name: "naivevm"},
+				{engine: wazeroir.NewEngine(), name: "wazeroir_interpreter"},
+			} {
+				tc := tc
+				t.Run(tc.name, func(t *testing.T) {
+					t.Parallel()
+					store := wasm.NewStore(tc.engine)
+					addSpectestModule(t, store)
+
+					var lastInstanceName string
+					for _, c := range base.Commands {
+						t.Run(fmt.Sprintf("%s/line:%d", c.CommandType, c.Line), func(t *testing.T) {
+							msg := fmt.Sprintf("%s:%d", wastName, c.Line)
+							switch c.CommandType {
+							case "module":
+								buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
+								require.NoError(t, err, msg)
+
+								mod, err := wasm.DecodeModule(buf)
+								require.NoError(t, err, msg)
+
+								lastInstanceName = c.Name
+								if lastInstanceName == "" {
+									lastInstanceName = c.Filename
 								}
+								err = store.Instantiate(mod, lastInstanceName)
+								require.NoError(t, err)
+							case "register":
+								name := lastInstanceName
+								if c.Name != "" {
+									name = c.Name
+								}
+								store.ModuleInstances[c.As] = store.ModuleInstances[name]
+							case "assert_return", "action":
+								moduleName := lastInstanceName
+								if c.Action.Module != "" {
+									moduleName = c.Action.Module
+								}
+								switch c.Action.ActionType {
+								case "invoke":
+									args, exps := c.getAssertReturnArgsExps()
+									msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
+									if c.Action.Module != "" {
+										msg += " in module " + c.Action.Module
+									}
+									vals, types, err := store.CallFunction(moduleName, c.Action.Field, args...)
+									if assert.NoError(t, err, msg) &&
+										assert.Equal(t, len(exps), len(vals), msg) &&
+										assert.Equal(t, len(exps), len(types), msg) {
+										for i, exp := range exps {
+											assertValueEq(t, vals[i], exp, types[i], msg)
+										}
+									}
+								case "get":
+									_, exps := c.getAssertReturnArgsExps()
+									require.Len(t, exps, 1)
+									msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
+									if c.Action.Module != "" {
+										msg += " in module " + c.Action.Module
+									}
+									inst, ok := store.ModuleInstances[moduleName]
+									require.True(t, ok, msg)
+									addr := inst.Exports[c.Action.Field]
+									if addr.Kind != wasm.ExportKindGlobal {
+										t.Fatal()
+									}
+									actual := addr.Global
+									var expType wasm.ValueType
+									switch c.Exps[0].ValType {
+									case "i32":
+										expType = wasm.ValueTypeI32
+									case "i64":
+										expType = wasm.ValueTypeI64
+									case "f32":
+										expType = wasm.ValueTypeF32
+									case "f64":
+										expType = wasm.ValueTypeF64
+									}
+									require.NotNil(t, actual, msg)
+									assert.Equal(t, expType, actual.Type.ValType, msg)
+									assert.Equal(t, exps[0], actual.Val, expType, msg)
+								default:
+									t.Fatalf("unsupported action type type: %v", c)
+								}
+							case "assert_malformed":
+								if c.ModuleType == "text" {
+									// We don't support direct loading of wast yet.
+									t.Skip()
+								}
+								buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
+								require.NoError(t, err, msg)
+								mod, err := wasm.DecodeModule(buf)
+								if err == nil {
+									err = store.Instantiate(mod, "")
+								}
+								require.Error(t, err, msg)
+							case "assert_trap":
+								moduleName := lastInstanceName
+								if c.Action.Module != "" {
+									moduleName = c.Action.Module
+								}
+								switch c.Action.ActionType {
+								case "invoke":
+									args := c.getAssertReturnArgs()
+									msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
+									if c.Action.Module != "" {
+										msg += " in module " + c.Action.Module
+									}
+									_, _, err := store.CallFunction(moduleName, c.Action.Field, args...)
+									assert.Error(t, err, msg)
+								default:
+									t.Fatalf("unsupported action type type: %v", c)
+								}
+							case "assert_invalid":
+								if c.ModuleType == "text" {
+									// We don't support direct loading of wast yet.
+									t.Skip()
+								}
+								buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
+								require.NoError(t, err, msg)
+								mod, err := wasm.DecodeModule(buf)
+								if err == nil {
+									err = store.Instantiate(mod, "")
+								}
+								require.Error(t, err, msg)
+							case "assert_exhaustion":
+								moduleName := lastInstanceName
+								if c.Action.Module != "" {
+									moduleName = c.Action.Module
+								}
+								switch c.Action.ActionType {
+								case "invoke":
+									args := c.getAssertReturnArgs()
+									msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
+									if c.Action.Module != "" {
+										msg += " in module " + c.Action.Module
+									}
+									_, _, err := store.CallFunction(moduleName, c.Action.Field, args...)
+									assert.Error(t, err, msg)
+									assert.True(t, errors.Is(err, wasm.ErrCallStackOverflow), msg)
+								default:
+									t.Fatalf("unsupported action type type: %v", c)
+								}
+							case "assert_unlinkable":
+								if c.ModuleType == "text" {
+									// We don't support direct loading of wast yet.
+									t.Skip()
+								}
+								buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
+								require.NoError(t, err, msg)
+								mod, err := wasm.DecodeModule(buf)
+								if err == nil {
+									err = store.Instantiate(mod, "")
+								}
+								require.Error(t, err, msg)
+							case "assert_uninstantiable":
+								buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
+								require.NoError(t, err, msg)
+
+								mod, err := wasm.DecodeModule(buf)
+								require.NoError(t, err, msg)
+
+								err = store.Instantiate(mod, "")
+								require.Error(t, err, msg)
+							default:
+								t.Fatalf("unsupported command type: %s", c)
 							}
-						case "get":
-							_, exps := c.getAssertReturnArgsExps(t)
-							require.Len(t, exps, 1)
-							msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
-							if c.Action.Module != "" {
-								msg += " in module " + c.Action.Module
-							}
-							inst, ok := store.ModuleInstances[moduleName]
-							require.True(t, ok, msg)
-							addr := inst.Exports[c.Action.Field]
-							if addr.Kind != wasm.ExportKindGlobal {
-								t.Fatal()
-							}
-							actual := addr.Global
-							var expType wasm.ValueType
-							switch c.Exps[0].ValType {
-							case "i32":
-								expType = wasm.ValueTypeI32
-							case "i64":
-								expType = wasm.ValueTypeI64
-							case "f32":
-								expType = wasm.ValueTypeF32
-							case "f64":
-								expType = wasm.ValueTypeF64
-							}
-							require.NotNil(t, actual, msg)
-							assert.Equal(t, expType, actual.Type.ValType, msg)
-							assert.Equal(t, exps[0], actual.Val, expType, msg)
-						default:
-							t.Fatalf("unsupported action type type: %v", c)
-						}
-					case "assert_malformed":
-						if c.ModuleType == "text" {
-							// We don't support direct loading of wast yet.
-							t.Skip()
-						}
-						buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
-						require.NoError(t, err, msg)
-						mod, err := wasm.DecodeModule(buf)
-						if err == nil {
-							err = store.Instantiate(mod, "")
-						}
-						require.Error(t, err, msg)
-					case "assert_trap":
-						moduleName := lastInstanceName
-						if c.Action.Module != "" {
-							moduleName = c.Action.Module
-						}
-						switch c.Action.ActionType {
-						case "invoke":
-							args := c.getAssertReturnArgs(t)
-							msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
-							if c.Action.Module != "" {
-								msg += " in module " + c.Action.Module
-							}
-							_, _, err := store.CallFunction(moduleName, c.Action.Field, args...)
-							assert.Error(t, err, msg)
-						default:
-							t.Fatalf("unsupported action type type: %v", c)
-						}
-					case "assert_invalid":
-						if c.ModuleType == "text" {
-							// We don't support direct loading of wast yet.
-							t.Skip()
-						}
-						buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
-						require.NoError(t, err, msg)
-						mod, err := wasm.DecodeModule(buf)
-						if err == nil {
-							err = store.Instantiate(mod, "")
-						}
-						require.Error(t, err, msg)
-					case "assert_exhaustion":
-						moduleName := lastInstanceName
-						if c.Action.Module != "" {
-							moduleName = c.Action.Module
-						}
-						switch c.Action.ActionType {
-						case "invoke":
-							args := c.getAssertReturnArgs(t)
-							msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
-							if c.Action.Module != "" {
-								msg += " in module " + c.Action.Module
-							}
-							_, _, err := store.CallFunction(moduleName, c.Action.Field, args...)
-							assert.Error(t, err, msg)
-							assert.True(t, errors.Is(err, wasm.ErrCallStackOverflow), msg)
-						default:
-							t.Fatalf("unsupported action type type: %v", c)
-						}
-					case "assert_unlinkable":
-						if c.ModuleType == "text" {
-							// We don't support direct loading of wast yet.
-							t.Skip()
-						}
-						buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
-						require.NoError(t, err, msg)
-						mod, err := wasm.DecodeModule(buf)
-						if err == nil {
-							err = store.Instantiate(mod, "")
-						}
-						require.Error(t, err, msg)
-					case "assert_uninstantiable":
-						buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
-						require.NoError(t, err, msg)
-
-						mod, err := wasm.DecodeModule(buf)
-						require.NoError(t, err, msg)
-
-						err = store.Instantiate(mod, "")
-						require.Error(t, err, msg)
-					default:
-						t.Fatalf("unsupported command type: %s", c)
+						})
 					}
 				})
 			}
+
 		})
 	}
 }