Repository: tetratelabs/wazero
Pull request #1329 (2023-03-31 14:48:12)

wazeroir: Load Ops, Store Ops, Set, Pick, Select, CallIndirect
Complete _most_ OpKinds <= 29, leave out those using `InclusiveRange`s and `Label`s, and branches

- wazeroir: OperationKindCallIndirect
- wazeroir: Load ops
- wazeroir: OperationSelect
- wazeroir: OperationSet, OperationPick
- wazeroir: Store ops



Patch:
diff --git a/internal/engine/compiler/compiler.go b/internal/engine/compiler/compiler.go
index 9ed519fc..8fc9d31c 100644
--- a/internal/engine/compiler/compiler.go
+++ b/internal/engine/compiler/compiler.go
@@ -28,7 +28,7 @@ type compiler interface {
 	// compileUnreachable adds instruction to perform wazeroir.OperationUnreachable.
 	compileUnreachable() error
 	// compileSet adds instruction to perform wazeroir.OperationSet.
-	compileSet(o wazeroir.OperationSet) error
+	compileSet(o wazeroir.UnionOperation) error
 	// compileGlobalGet adds instructions to perform wazeroir.OperationGlobalGet.
 	compileGlobalGet(o wazeroir.UnionOperation) error
 	// compileGlobalSet adds instructions to perform wazeroir.OperationGlobalSet.
@@ -42,13 +42,13 @@ type compiler interface {
 	// compileCall adds instructions to perform wazeroir.OperationCall.
 	compileCall(o wazeroir.UnionOperation) error
 	// compileCallIndirect adds instructions to perform wazeroir.OperationCallIndirect.
-	compileCallIndirect(o wazeroir.OperationCallIndirect) error
+	compileCallIndirect(o wazeroir.UnionOperation) error
 	// compileDrop adds instructions to perform wazeroir.OperationDrop.
 	compileDrop(o wazeroir.OperationDrop) error
 	// compileSelect adds instructions to perform wazeroir.OperationSelect.
-	compileSelect(o wazeroir.OperationSelect) error
+	compileSelect(o wazeroir.UnionOperation) error
 	// compilePick adds instructions to perform wazeroir.OperationPick.
-	compilePick(o wazeroir.OperationPick) error
+	compilePick(o wazeroir.UnionOperation) error
 	// compileAdd adds instructions to perform wazeroir.OperationAdd.
 	compileAdd(o wazeroir.UnionOperation) error
 	// compileSub adds instructions to perform wazeroir.OperationSub.
@@ -134,21 +134,21 @@ type compiler interface {
 	// compileLe adds instructions to perform wazeroir.OperationGe.
 	compileGe(o wazeroir.UnionOperation) error
 	// compileLoad adds instructions to perform wazeroir.OperationLoad.
-	compileLoad(o wazeroir.OperationLoad) error
+	compileLoad(o wazeroir.UnionOperation) error
 	// compileLoad8 adds instructions to perform wazeroir.OperationLoad8.
-	compileLoad8(o wazeroir.OperationLoad8) error
+	compileLoad8(o wazeroir.UnionOperation) error
 	// compileLoad16 adds instructions to perform wazeroir.OperationLoad16.
-	compileLoad16(o wazeroir.OperationLoad16) error
+	compileLoad16(o wazeroir.UnionOperation) error
 	// compileLoad32 adds instructions to perform wazeroir.OperationLoad32.
-	compileLoad32(o wazeroir.OperationLoad32) error
+	compileLoad32(o wazeroir.UnionOperation) error
 	// compileStore adds instructions to perform wazeroir.OperationStore.
-	compileStore(o wazeroir.OperationStore) error
+	compileStore(o wazeroir.UnionOperation) error
 	// compileStore8 adds instructions to perform wazeroir.OperationStore8.
-	compileStore8(o wazeroir.OperationStore8) error
+	compileStore8(o wazeroir.UnionOperation) error
 	// compileStore16 adds instructions to perform wazeroir.OperationStore16.
-	compileStore16(o wazeroir.OperationStore16) error
+	compileStore16(o wazeroir.UnionOperation) error
 	// compileStore32 adds instructions to perform wazeroir.OperationStore32.
-	compileStore32(o wazeroir.OperationStore32) error
+	compileStore32(o wazeroir.UnionOperation) error
 	// compileMemorySize adds instruction to perform wazeroir.OperationMemoryGrow.
 	compileMemoryGrow() error
 	// compileMemorySize adds instruction to perform wazeroir.OperationMemorySize.
diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go
index ac9b7fea..27245221 100644
--- a/internal/engine/compiler/engine.go
+++ b/internal/engine/compiler/engine.go
@@ -1100,32 +1100,8 @@ func compileWasmFunction(cmp compiler, ir *wazeroir.CompilationResult) (*code, e
 			err = cmp.compileBrIf(o)
 		case wazeroir.OperationBrTable:
 			err = cmp.compileBrTable(o)
-		case wazeroir.OperationCallIndirect:
-			err = cmp.compileCallIndirect(o)
 		case wazeroir.OperationDrop:
 			err = cmp.compileDrop(o)
-		case wazeroir.OperationSelect:
-			err = cmp.compileSelect(o)
-		case wazeroir.OperationPick:
-			err = cmp.compilePick(o)
-		case wazeroir.OperationSet:
-			err = cmp.compileSet(o)
-		case wazeroir.OperationLoad:
-			err = cmp.compileLoad(o)
-		case wazeroir.OperationLoad8:
-			err = cmp.compileLoad8(o)
-		case wazeroir.OperationLoad16:
-			err = cmp.compileLoad16(o)
-		case wazeroir.OperationLoad32:
-			err = cmp.compileLoad32(o)
-		case wazeroir.OperationStore:
-			err = cmp.compileStore(o)
-		case wazeroir.OperationStore8:
-			err = cmp.compileStore8(o)
-		case wazeroir.OperationStore16:
-			err = cmp.compileStore16(o)
-		case wazeroir.OperationStore32:
-			err = cmp.compileStore32(o)
 		case wazeroir.OperationITruncFromF:
 			err = cmp.compileITruncFromF(o)
 		case wazeroir.OperationFConvertFromI:
@@ -1262,11 +1238,35 @@ func compileWasmFunction(cmp compiler, ir *wazeroir.CompilationResult) (*code, e
 				err = cmp.compileUnreachable()
 			case wazeroir.OperationKindCall:
 				err = cmp.compileCall(o)
-
+			case wazeroir.OperationKindCallIndirect:
+				err = cmp.compileCallIndirect(o)
+
+			case wazeroir.OperationKindSelect:
+				err = cmp.compileSelect(o)
+			case wazeroir.OperationKindPick:
+				err = cmp.compilePick(o)
+			case wazeroir.OperationKindSet:
+				err = cmp.compileSet(o)
 			case wazeroir.OperationKindGlobalGet:
 				err = cmp.compileGlobalGet(o)
 			case wazeroir.OperationKindGlobalSet:
 				err = cmp.compileGlobalSet(o)
+			case wazeroir.OperationKindLoad:
+				err = cmp.compileLoad(o)
+			case wazeroir.OperationKindLoad8:
+				err = cmp.compileLoad8(o)
+			case wazeroir.OperationKindLoad16:
+				err = cmp.compileLoad16(o)
+			case wazeroir.OperationKindLoad32:
+				err = cmp.compileLoad32(o)
+			case wazeroir.OperationKindStore:
+				err = cmp.compileStore(o)
+			case wazeroir.OperationKindStore8:
+				err = cmp.compileStore8(o)
+			case wazeroir.OperationKindStore16:
+				err = cmp.compileStore16(o)
+			case wazeroir.OperationKindStore32:
+				err = cmp.compileStore32(o)
 
 			case wazeroir.OperationKindMemorySize:
 				err = cmp.compileMemorySize()
diff --git a/internal/engine/compiler/impl_amd64.go b/internal/engine/compiler/impl_amd64.go
index eb3e8dfb..8a90e083 100644
--- a/internal/engine/compiler/impl_amd64.go
+++ b/internal/engine/compiler/impl_amd64.go
@@ -267,10 +267,13 @@ func (c *amd64Compiler) compileUnreachable() error {
 }
 
 // compileSet implements compiler.compileSet for the amd64 architecture.
-func (c *amd64Compiler) compileSet(o wazeroir.OperationSet) error {
-	setTargetIndex := int(c.locationStack.sp) - 1 - o.Depth
+func (c *amd64Compiler) compileSet(o wazeroir.UnionOperation) error {
+	depth := int(o.U1)
+	isTargetVector := o.B3
 
-	if o.IsTargetVector {
+	setTargetIndex := int(c.locationStack.sp) - 1 - depth
+
+	if isTargetVector {
 		_ = c.locationStack.pop() // ignore the higher 64-bits.
 	}
 	v := c.locationStack.pop()
@@ -287,7 +290,7 @@ func (c *amd64Compiler) compileSet(o wazeroir.OperationSet) error {
 	reg := v.register
 	targetLocation.setRegister(reg)
 	targetLocation.valueType = v.valueType
-	if o.IsTargetVector {
+	if isTargetVector {
 		hi := &c.locationStack.stack[setTargetIndex+1]
 		hi.setRegister(reg)
 	}
@@ -756,11 +759,13 @@ func (c *amd64Compiler) compileCall(o wazeroir.UnionOperation) error {
 }
 
 // compileCallIndirect implements compiler.compileCallIndirect for the amd64 architecture.
-func (c *amd64Compiler) compileCallIndirect(o wazeroir.OperationCallIndirect) error {
+func (c *amd64Compiler) compileCallIndirect(o wazeroir.UnionOperation) error {
 	offset := c.locationStack.pop()
 	if err := c.compileEnsureOnRegister(offset); err != nil {
 		return nil
 	}
+	typeIndex := o.U1
+	tableIndex := o.U2
 
 	tmp, err := c.allocateRegister(registerTypeGeneralPurpose)
 	if err != nil {
@@ -777,7 +782,7 @@ func (c *amd64Compiler) compileCallIndirect(o wazeroir.OperationCallIndirect) er
 	// Load the address of the target table: tmp = &module.Tables[0]
 	c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, tmp)
 	// tmp = &module.Tables[0] + Index*8 = &module.Tables[0] + sizeOf(*TableInstance)*index = module.Tables[o.TableIndex].
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmp, int64(o.TableIndex*8), tmp)
+	c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmp, int64(tableIndex*8), tmp)
 
 	// Then, we need to check if the offset doesn't exceed the length of table.
 	c.assembler.CompileMemoryToRegister(amd64.CMPQ, tmp, tableInstanceTableLenOffset, offset.register)
@@ -819,7 +824,7 @@ func (c *amd64Compiler) compileCallIndirect(o wazeroir.OperationCallIndirect) er
 	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
 		amd64ReservedRegisterForCallEngine, callEngineModuleContextTypeIDsElement0AddressOffset,
 		tmp2)
-	c.assembler.CompileMemoryToRegister(amd64.MOVL, tmp2, int64(o.TypeIndex)*4, tmp2)
+	c.assembler.CompileMemoryToRegister(amd64.MOVL, tmp2, int64(typeIndex)*4, tmp2)
 
 	// Jump if the type matches.
 	c.assembler.CompileMemoryToRegister(amd64.CMPL, offset.register, functionTypeIDOffset, tmp2)
@@ -829,7 +834,7 @@ func (c *amd64Compiler) compileCallIndirect(o wazeroir.OperationCallIndirect) er
 	c.compileExitFromNativeCode(nativeCallStatusCodeTypeMismatchOnIndirectCall)
 
 	c.assembler.SetJumpTargetOnNext(jumpIfTypeMatch)
-	targetFunctionType := &c.ir.Types[o.TypeIndex]
+	targetFunctionType := &c.ir.Types[typeIndex]
 	if err = c.compileCallFunctionImpl(offset.register, targetFunctionType); err != nil {
 		return nil
 	}
@@ -881,13 +886,14 @@ func (c *amd64Compiler) compileSelectV128Impl(selectorReg asm.Register) error {
 //
 // The emitted native code depends on whether the values are on
 // the physical registers or memory stack, or maybe conditional register.
-func (c *amd64Compiler) compileSelect(o wazeroir.OperationSelect) error {
+func (c *amd64Compiler) compileSelect(o wazeroir.UnionOperation) error {
 	cv := c.locationStack.pop()
 	if err := c.compileEnsureOnRegister(cv); err != nil {
 		return err
 	}
 
-	if o.IsTargetVector {
+	isTargetVector := o.B3
+	if isTargetVector {
 		return c.compileSelectV128Impl(cv.register)
 	}
 
@@ -936,15 +942,17 @@ func (c *amd64Compiler) compileSelect(o wazeroir.OperationSelect) error {
 }
 
 // compilePick implements compiler.compilePick for the amd64 architecture.
-func (c *amd64Compiler) compilePick(o wazeroir.OperationPick) error {
+func (c *amd64Compiler) compilePick(o wazeroir.UnionOperation) error {
 	if err := c.maybeCompileMoveTopConditionalToGeneralPurposeRegister(); err != nil {
 		return err
 	}
+	depth := o.U1
+	isTargetVector := o.B3
 
 	// TODO: if we track the type of values on the stack,
 	// we could optimize the instruction according to the bit size of the value.
 	// For now, we just move the entire register i.e. as a quad word (8 bytes).
-	pickTarget := &c.locationStack.stack[c.locationStack.sp-1-uint64(o.Depth)]
+	pickTarget := &c.locationStack.stack[c.locationStack.sp-1-uint64(depth)]
 	reg, err := c.allocateRegister(pickTarget.getRegisterType())
 	if err != nil {
 		return err
@@ -952,7 +960,7 @@ func (c *amd64Compiler) compilePick(o wazeroir.OperationPick) error {
 
 	if pickTarget.onRegister() {
 		var inst asm.Instruction
-		if o.IsTargetVector {
+		if isTargetVector {
 			inst = amd64.MOVDQU
 		} else if pickTarget.valueType == runtimeValueTypeI32 { // amd64 cannot copy single-precisions between registers.
 			inst = amd64.MOVL
@@ -963,7 +971,7 @@ func (c *amd64Compiler) compilePick(o wazeroir.OperationPick) error {
 	} else if pickTarget.onStack() {
 		// Copy the value from the stack.
 		var inst asm.Instruction
-		if o.IsTargetVector {
+		if isTargetVector {
 			inst = amd64.MOVDQU
 		} else if pickTarget.valueType == runtimeValueTypeI32 || pickTarget.valueType == runtimeValueTypeF32 {
 			inst = amd64.MOVL
@@ -976,7 +984,7 @@ func (c *amd64Compiler) compilePick(o wazeroir.OperationPick) error {
 	}
 	// Now we already placed the picked value on the register,
 	// so push the location onto the stack.
-	if o.IsTargetVector {
+	if isTargetVector {
 		c.pushVectorRuntimeValueLocationOnRegister(reg)
 	} else {
 		c.pushRuntimeValueLocationOnRegister(reg, pickTarget.valueType)
@@ -3229,14 +3237,18 @@ func (c *amd64Compiler) compileGe(o wazeroir.UnionOperation) error {
 }
 
 // compileLoad implements compiler.compileLoad for the amd64 architecture.
-func (c *amd64Compiler) compileLoad(o wazeroir.OperationLoad) error {
+func (c *amd64Compiler) compileLoad(o wazeroir.UnionOperation) error {
 	var (
 		isIntType         bool
 		movInst           asm.Instruction
 		targetSizeInBytes int64
 		vt                runtimeValueType
 	)
-	switch o.Type {
+
+	unsignedType := wazeroir.UnsignedType(o.B1)
+	offset := uint32(o.U2)
+
+	switch unsignedType {
 	case wazeroir.UnsignedTypeI32:
 		isIntType = true
 		movInst = amd64.MOVL
@@ -3259,7 +3271,7 @@ func (c *amd64Compiler) compileLoad(o wazeroir.OperationLoad) error {
 		vt = runtimeValueTypeF64
 	}
 
-	reg, err := c.compileMemoryAccessCeilSetup(o.Arg.Offset, targetSizeInBytes)
+	reg, err := c.compileMemoryAccessCeilSetup(offset, targetSizeInBytes)
 	if err != nil {
 		return err
 	}
@@ -3290,9 +3302,10 @@ func (c *amd64Compiler) compileLoad(o wazeroir.OperationLoad) error {
 }
 
 // compileLoad8 implements compiler.compileLoad8 for the amd64 architecture.
-func (c *amd64Compiler) compileLoad8(o wazeroir.OperationLoad8) error {
+func (c *amd64Compiler) compileLoad8(o wazeroir.UnionOperation) error {
 	const targetSizeInBytes = 1
-	reg, err := c.compileMemoryAccessCeilSetup(o.Arg.Offset, targetSizeInBytes)
+	offset := uint32(o.U2)
+	reg, err := c.compileMemoryAccessCeilSetup(offset, targetSizeInBytes)
 	if err != nil {
 		return err
 	}
@@ -3301,7 +3314,8 @@ func (c *amd64Compiler) compileLoad8(o wazeroir.OperationLoad8) error {
 	// Note that Load8 is only for integer types.
 	var inst asm.Instruction
 	var vt runtimeValueType
-	switch o.Type {
+	signedInt := wazeroir.SignedInt(o.B1)
+	switch signedInt {
 	case wazeroir.SignedInt32:
 		inst = amd64.MOVBLSX
 		vt = runtimeValueTypeI32
@@ -3326,9 +3340,10 @@ func (c *amd64Compiler) compileLoad8(o wazeroir.OperationLoad8) error {
 }
 
 // compileLoad16 implements compiler.compileLoad16 for the amd64 architecture.
-func (c *amd64Compiler) compileLoad16(o wazeroir.OperationLoad16) error {
+func (c *amd64Compiler) compileLoad16(o wazeroir.UnionOperation) error {
 	const targetSizeInBytes = 16 / 8
-	reg, err := c.compileMemoryAccessCeilSetup(o.Arg.Offset, targetSizeInBytes)
+	offset := uint32(o.U2)
+	reg, err := c.compileMemoryAccessCeilSetup(offset, targetSizeInBytes)
 	if err != nil {
 		return err
 	}
@@ -3337,7 +3352,8 @@ func (c *amd64Compiler) compileLoad16(o wazeroir.OperationLoad16) error {
 	// Note that Load16 is only for integer types.
 	var inst asm.Instruction
 	var vt runtimeValueType
-	switch o.Type {
+	signedInt := wazeroir.SignedInt(o.B1)
+	switch signedInt {
 	case wazeroir.SignedInt32:
 		inst = amd64.MOVWLSX
 		vt = runtimeValueTypeI32
@@ -3362,16 +3378,18 @@ func (c *amd64Compiler) compileLoad16(o wazeroir.OperationLoad16) error {
 }
 
 // compileLoad32 implements compiler.compileLoad32 for the amd64 architecture.
-func (c *amd64Compiler) compileLoad32(o wazeroir.OperationLoad32) error {
+func (c *amd64Compiler) compileLoad32(o wazeroir.UnionOperation) error {
 	const targetSizeInBytes = 32 / 8
-	reg, err := c.compileMemoryAccessCeilSetup(o.Arg.Offset, targetSizeInBytes)
+	offset := uint32(o.U2)
+	reg, err := c.compileMemoryAccessCeilSetup(offset, targetSizeInBytes)
 	if err != nil {
 		return err
 	}
 
 	// Then move 4 bytes at the offset to the register.
 	var inst asm.Instruction
-	if o.Signed {
+	signed := o.B1 == 1
+	if signed {
 		inst = amd64.MOVLQSX
 	} else {
 		inst = amd64.MOVLQZX
@@ -3435,10 +3453,12 @@ func (c *amd64Compiler) compileMemoryAccessCeilSetup(offsetArg uint32, targetSiz
 }
 
 // compileStore implements compiler.compileStore for the amd64 architecture.
-func (c *amd64Compiler) compileStore(o wazeroir.OperationStore) error {
+func (c *amd64Compiler) compileStore(o wazeroir.UnionOperation) error {
 	var movInst asm.Instruction
 	var targetSizeInByte int64
-	switch o.Type {
+	unsignedType := wazeroir.UnsignedType(o.B1)
+	offset := uint32(o.U2)
+	switch unsignedType {
 	case wazeroir.UnsignedTypeI32, wazeroir.UnsignedTypeF32:
 		movInst = amd64.MOVL
 		targetSizeInByte = 32 / 8
@@ -3446,22 +3466,22 @@ func (c *amd64Compiler) compileStore(o wazeroir.OperationStore) error {
 		movInst = amd64.MOVQ
 		targetSizeInByte = 64 / 8
 	}
-	return c.compileStoreImpl(o.Arg.Offset, movInst, targetSizeInByte)
+	return c.compileStoreImpl(offset, movInst, targetSizeInByte)
 }
 
 // compileStore8 implements compiler.compileStore8 for the amd64 architecture.
-func (c *amd64Compiler) compileStore8(o wazeroir.OperationStore8) error {
-	return c.compileStoreImpl(o.Arg.Offset, amd64.MOVB, 1)
+func (c *amd64Compiler) compileStore8(o wazeroir.UnionOperation) error {
+	return c.compileStoreImpl(uint32(o.U2), amd64.MOVB, 1)
 }
 
 // compileStore32 implements compiler.compileStore32 for the amd64 architecture.
-func (c *amd64Compiler) compileStore16(o wazeroir.OperationStore16) error {
-	return c.compileStoreImpl(o.Arg.Offset, amd64.MOVW, 16/8)
+func (c *amd64Compiler) compileStore16(o wazeroir.UnionOperation) error {
+	return c.compileStoreImpl(uint32(o.U2), amd64.MOVW, 16/8)
 }
 
 // compileStore32 implements compiler.compileStore32 for the amd64 architecture.
-func (c *amd64Compiler) compileStore32(o wazeroir.OperationStore32) error {
-	return c.compileStoreImpl(o.Arg.Offset, amd64.MOVL, 32/8)
+func (c *amd64Compiler) compileStore32(o wazeroir.UnionOperation) error {
+	return c.compileStoreImpl(uint32(o.U2), amd64.MOVL, 32/8)
 }
 
 func (c *amd64Compiler) compileStoreImpl(offsetConst uint32, inst asm.Instruction, targetSizeInBytes int64) error {
diff --git a/internal/engine/compiler/impl_arm64.go b/internal/engine/compiler/impl_arm64.go
index 1329a93d..ce5f31cf 100644
--- a/internal/engine/compiler/impl_arm64.go
+++ b/internal/engine/compiler/impl_arm64.go
@@ -496,10 +496,13 @@ func (c *arm64Compiler) compileUnreachable() error {
 }
 
 // compileSet implements compiler.compileSet for the arm64 architecture.
-func (c *arm64Compiler) compileSet(o wazeroir.OperationSet) error {
-	setTargetIndex := int(c.locationStack.sp) - 1 - o.Depth
+func (c *arm64Compiler) compileSet(o wazeroir.UnionOperation) error {
+	depth := int(o.U1)
+	isTargetVector := o.B3
 
-	if o.IsTargetVector {
+	setTargetIndex := int(c.locationStack.sp) - 1 - depth
+
+	if isTargetVector {
 		_ = c.locationStack.pop()
 	}
 	v := c.locationStack.pop()
@@ -516,7 +519,7 @@ func (c *arm64Compiler) compileSet(o wazeroir.OperationSet) error {
 	reg := v.register
 	targetLocation.setRegister(reg)
 	targetLocation.valueType = v.valueType
-	if o.IsTargetVector {
+	if isTargetVector {
 		hi := &c.locationStack.stack[setTargetIndex+1]
 		hi.setRegister(reg)
 	}
@@ -1080,11 +1083,13 @@ func (c *arm64Compiler) compileCallImpl(targetFunctionAddressRegister asm.Regist
 }
 
 // compileCallIndirect implements compiler.compileCallIndirect for the arm64 architecture.
-func (c *arm64Compiler) compileCallIndirect(o wazeroir.OperationCallIndirect) (err error) {
+func (c *arm64Compiler) compileCallIndirect(o wazeroir.UnionOperation) (err error) {
 	offset := c.locationStack.pop()
 	if err = c.compileEnsureOnRegister(offset); err != nil {
 		return err
 	}
+	typeIndex := o.U1
+	tableIndex := o.U2
 
 	offsetReg := offset.register
 	if isZeroRegister(offsetReg) {
@@ -1118,7 +1123,7 @@ func (c *arm64Compiler) compileCallIndirect(o wazeroir.OperationCallIndirect) (e
 	)
 	// tmp = [tmp + TableIndex*8] = [&Tables[0] + TableIndex*sizeOf(*tableInstance)] = Tables[tableIndex]
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		tmp, int64(o.TableIndex)*8,
+		tmp, int64(tableIndex)*8,
 		tmp,
 	)
 	// tmp2 = [tmp + tableInstanceTableLenOffset] = len(Tables[tableIndex])
@@ -1171,7 +1176,7 @@ func (c *arm64Compiler) compileCallIndirect(o wazeroir.OperationCallIndirect) (e
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
 		arm64ReservedRegisterForCallEngine, callEngineModuleContextTypeIDsElement0AddressOffset,
 		tmp2)
-	c.assembler.CompileMemoryToRegister(arm64.LDRW, tmp2, int64(o.TypeIndex)*4, tmp2)
+	c.assembler.CompileMemoryToRegister(arm64.LDRW, tmp2, int64(typeIndex)*4, tmp2)
 
 	// Compare these two values, and if they equal, we are ready to make function call.
 	c.assembler.CompileTwoRegistersToNone(arm64.CMPW, tmp, tmp2)
@@ -1180,7 +1185,7 @@ func (c *arm64Compiler) compileCallIndirect(o wazeroir.OperationCallIndirect) (e
 
 	c.assembler.SetJumpTargetOnNext(brIfTypeMatched)
 
-	targetFunctionType := &c.ir.Types[o.TypeIndex]
+	targetFunctionType := &c.ir.Types[typeIndex]
 	if err := c.compileCallImpl(offsetReg, targetFunctionType); err != nil {
 		return err
 	}
@@ -1224,13 +1229,14 @@ func (c *arm64Compiler) compileSelectV128Impl(selectorRegister asm.Register) err
 }
 
 // compileSelect implements compiler.compileSelect for the arm64 architecture.
-func (c *arm64Compiler) compileSelect(o wazeroir.OperationSelect) error {
+func (c *arm64Compiler) compileSelect(o wazeroir.UnionOperation) error {
 	cv, err := c.popValueOnRegister()
 	if err != nil {
 		return err
 	}
 
-	if o.IsTargetVector {
+	isTargetVector := o.B3
+	if isTargetVector {
 		return c.compileSelectV128Impl(cv.register)
 	}
 
@@ -1300,12 +1306,14 @@ func (c *arm64Compiler) compileSelect(o wazeroir.OperationSelect) error {
 }
 
 // compilePick implements compiler.compilePick for the arm64 architecture.
-func (c *arm64Compiler) compilePick(o wazeroir.OperationPick) error {
+func (c *arm64Compiler) compilePick(o wazeroir.UnionOperation) error {
 	if err := c.maybeCompileMoveTopConditionalToGeneralPurposeRegister(); err != nil {
 		return err
 	}
+	depth := o.U1
+	isTargetVector := o.B3
 
-	pickTarget := &c.locationStack.stack[c.locationStack.sp-1-uint64(o.Depth)]
+	pickTarget := &c.locationStack.stack[c.locationStack.sp-1-uint64(depth)]
 	pickedRegister, err := c.allocateRegister(pickTarget.getRegisterType())
 	if err != nil {
 		return err
@@ -1334,7 +1342,7 @@ func (c *arm64Compiler) compilePick(o wazeroir.OperationPick) error {
 
 		// After the load, we revert the register assignment to the pick target.
 		pickTarget.setRegister(asm.NilRegister)
-		if o.IsTargetVector {
+		if isTargetVector {
 			hi := &c.locationStack.stack[pickTarget.stackPointer+1]
 			hi.setRegister(asm.NilRegister)
 		}
@@ -1343,7 +1351,7 @@ func (c *arm64Compiler) compilePick(o wazeroir.OperationPick) error {
 	// Now we have the value of the target on the pickedRegister,
 	// so push the location.
 	c.pushRuntimeValueLocationOnRegister(pickedRegister, pickTarget.valueType)
-	if o.IsTargetVector {
+	if isTargetVector {
 		c.pushRuntimeValueLocationOnRegister(pickedRegister, runtimeValueTypeV128Hi)
 	}
 	return nil
@@ -2585,7 +2593,7 @@ func (c *arm64Compiler) compileGe(o wazeroir.UnionOperation) error {
 }
 
 // compileLoad implements compiler.compileLoad for the arm64 architecture.
-func (c *arm64Compiler) compileLoad(o wazeroir.OperationLoad) error {
+func (c *arm64Compiler) compileLoad(o wazeroir.UnionOperation) error {
 	var (
 		isFloat           bool
 		loadInst          asm.Instruction
@@ -2593,7 +2601,10 @@ func (c *arm64Compiler) compileLoad(o wazeroir.OperationLoad) error {
 		vt                runtimeValueType
 	)
 
-	switch o.Type {
+	unsignedType := wazeroir.UnsignedType(o.B1)
+	offset := uint32(o.U2)
+
+	switch unsignedType {
 	case wazeroir.UnsignedTypeI32:
 		loadInst = arm64.LDRW
 		targetSizeInBytes = 32 / 8
@@ -2613,14 +2624,18 @@ func (c *arm64Compiler) compileLoad(o wazeroir.OperationLoad) error {
 		targetSizeInBytes = 64 / 8
 		vt = runtimeValueTypeF64
 	}
-	return c.compileLoadImpl(o.Arg.Offset, loadInst, targetSizeInBytes, isFloat, vt)
+	return c.compileLoadImpl(offset, loadInst, targetSizeInBytes, isFloat, vt)
 }
 
 // compileLoad8 implements compiler.compileLoad8 for the arm64 architecture.
-func (c *arm64Compiler) compileLoad8(o wazeroir.OperationLoad8) error {
+func (c *arm64Compiler) compileLoad8(o wazeroir.UnionOperation) error {
 	var loadInst asm.Instruction
 	var vt runtimeValueType
-	switch o.Type {
+
+	signedInt := wazeroir.SignedInt(o.B1)
+	offset := uint32(o.U2)
+
+	switch signedInt {
 	case wazeroir.SignedInt32:
 		loadInst = arm64.LDRSBW
 		vt = runtimeValueTypeI32
@@ -2634,14 +2649,18 @@ func (c *arm64Compiler) compileLoad8(o wazeroir.OperationLoad8) error {
 		loadInst = arm64.LDRB
 		vt = runtimeValueTypeI64
 	}
-	return c.compileLoadImpl(o.Arg.Offset, loadInst, 1, false, vt)
+	return c.compileLoadImpl(offset, loadInst, 1, false, vt)
 }
 
 // compileLoad16 implements compiler.compileLoad16 for the arm64 architecture.
-func (c *arm64Compiler) compileLoad16(o wazeroir.OperationLoad16) error {
+func (c *arm64Compiler) compileLoad16(o wazeroir.UnionOperation) error {
 	var loadInst asm.Instruction
 	var vt runtimeValueType
-	switch o.Type {
+
+	signedInt := wazeroir.SignedInt(o.B1)
+	offset := uint32(o.U2)
+
+	switch signedInt {
 	case wazeroir.SignedInt32:
 		loadInst = arm64.LDRSHW
 		vt = runtimeValueTypeI32
@@ -2655,18 +2674,21 @@ func (c *arm64Compiler) compileLoad16(o wazeroir.OperationLoad16) error {
 		loadInst = arm64.LDRH
 		vt = runtimeValueTypeI64
 	}
-	return c.compileLoadImpl(o.Arg.Offset, loadInst, 16/8, false, vt)
+	return c.compileLoadImpl(offset, loadInst, 16/8, false, vt)
 }
 
 // compileLoad32 implements compiler.compileLoad32 for the arm64 architecture.
-func (c *arm64Compiler) compileLoad32(o wazeroir.OperationLoad32) error {
+func (c *arm64Compiler) compileLoad32(o wazeroir.UnionOperation) error {
 	var loadInst asm.Instruction
-	if o.Signed {
+	signed := o.B1 == 1
+	offset := uint32(o.U2)
+
+	if signed {
 		loadInst = arm64.LDRSW
 	} else {
 		loadInst = arm64.LDRW
 	}
-	return c.compileLoadImpl(o.Arg.Offset, loadInst, 32/8, false, runtimeValueTypeI64)
+	return c.compileLoadImpl(offset, loadInst, 32/8, false, runtimeValueTypeI64)
 }
 
 // compileLoadImpl implements compileLoadImpl* variants for arm64 architecture.
@@ -2699,10 +2721,12 @@ func (c *arm64Compiler) compileLoadImpl(offsetArg uint32, loadInst asm.Instructi
 }
 
 // compileStore implements compiler.compileStore for the arm64 architecture.
-func (c *arm64Compiler) compileStore(o wazeroir.OperationStore) error {
+func (c *arm64Compiler) compileStore(o wazeroir.UnionOperation) error {
 	var movInst asm.Instruction
 	var targetSizeInBytes int64
-	switch o.Type {
+	unsignedType := wazeroir.UnsignedType(o.B1)
+	offset := uint32(o.U2)
+	switch unsignedType {
 	case wazeroir.UnsignedTypeI32:
 		movInst = arm64.STRW
 		targetSizeInBytes = 32 / 8
@@ -2716,22 +2740,22 @@ func (c *arm64Compiler) compileStore(o wazeroir.OperationStore) error {
 		movInst = arm64.FSTRD
 		targetSizeInBytes = 64 / 8
 	}
-	return c.compileStoreImpl(o.Arg.Offset, movInst, targetSizeInBytes)
+	return c.compileStoreImpl(offset, movInst, targetSizeInBytes)
 }
 
 // compileStore8 implements compiler.compileStore8 for the arm64 architecture.
-func (c *arm64Compiler) compileStore8(o wazeroir.OperationStore8) error {
-	return c.compileStoreImpl(o.Arg.Offset, arm64.STRB, 1)
+func (c *arm64Compiler) compileStore8(o wazeroir.UnionOperation) error {
+	return c.compileStoreImpl(uint32(o.U2), arm64.STRB, 1)
 }
 
 // compileStore16 implements compiler.compileStore16 for the arm64 architecture.
-func (c *arm64Compiler) compileStore16(o wazeroir.OperationStore16) error {
-	return c.compileStoreImpl(o.Arg.Offset, arm64.STRH, 16/8)
+func (c *arm64Compiler) compileStore16(o wazeroir.UnionOperation) error {
+	return c.compileStoreImpl(uint32(o.U2), arm64.STRH, 16/8)
 }
 
 // compileStore32 implements compiler.compileStore32 for the arm64 architecture.
-func (c *arm64Compiler) compileStore32(o wazeroir.OperationStore32) error {
-	return c.compileStoreImpl(o.Arg.Offset, arm64.STRW, 32/8)
+func (c *arm64Compiler) compileStore32(o wazeroir.UnionOperation) error {
+	return c.compileStoreImpl(uint32(o.U2), arm64.STRW, 32/8)
 }
 
 // compileStoreImpl implements compleStore* variants for arm64 architecture.
diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go
index 664283a6..d95b8f79 100644
--- a/internal/engine/interpreter/interpreter.go
+++ b/internal/engine/interpreter/interpreter.go
@@ -291,8 +291,21 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) {
 			// Nullary operations don't need any further processing.
 			switch o.Kind() {
 			case wazeroir.OperationKindCall:
+			case wazeroir.OperationKindCallIndirect:
+
+			case wazeroir.OperationKindSelect:
+			case wazeroir.OperationKindPick:
+			case wazeroir.OperationKindSet:
 			case wazeroir.OperationKindGlobalGet:
 			case wazeroir.OperationKindGlobalSet:
+			case wazeroir.OperationKindLoad:
+			case wazeroir.OperationKindLoad8:
+			case wazeroir.OperationKindLoad16:
+			case wazeroir.OperationKindLoad32:
+			case wazeroir.OperationKindStore:
+			case wazeroir.OperationKindStore8:
+			case wazeroir.OperationKindStore16:
+			case wazeroir.OperationKindStore32:
 
 			case wazeroir.OperationKindConstI32:
 			case wazeroir.OperationKindConstI64:
@@ -422,52 +435,10 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) {
 					}
 				}
 			}
-		case wazeroir.OperationCallIndirect:
-			op.U1 = uint64(o.TypeIndex)
-			op.U2 = uint64(o.TableIndex)
 		case wazeroir.OperationDrop:
 			op.Rs = make([]*wazeroir.InclusiveRange, 1)
 			op.Rs[0] = o.Depth
-		case wazeroir.OperationSelect:
-			op.B3 = o.IsTargetVector
-		case wazeroir.OperationPick:
-			op.U1 = uint64(o.Depth)
-			op.B3 = o.IsTargetVector
-		case wazeroir.OperationSet:
-			op.U1 = uint64(o.Depth)
-			op.B3 = o.IsTargetVector
-		case wazeroir.OperationLoad:
-			op.B1 = byte(o.Type)
-			op.U1 = uint64(o.Arg.Alignment)
-			op.U2 = uint64(o.Arg.Offset)
-		case wazeroir.OperationLoad8:
-			op.B1 = byte(o.Type)
-			op.U1 = uint64(o.Arg.Alignment)
-			op.U2 = uint64(o.Arg.Offset)
-		case wazeroir.OperationLoad16:
-			op.B1 = byte(o.Type)
-			op.U1 = uint64(o.Arg.Alignment)
-			op.U2 = uint64(o.Arg.Offset)
-		case wazeroir.OperationLoad32:
-			if o.Signed {
-				op.B1 = 1
-			}
-			op.U1 = uint64(o.Arg.Alignment)
-			op.U2 = uint64(o.Arg.Offset)
-		case wazeroir.OperationStore:
-			op.B1 = byte(o.Type)
-			op.U1 = uint64(o.Arg.Alignment)
-			op.U2 = uint64(o.Arg.Offset)
-		case wazeroir.OperationStore8:
-			op.U1 = uint64(o.Arg.Alignment)
-			op.U2 = uint64(o.Arg.Offset)
-		case wazeroir.OperationStore16:
-			op.U1 = uint64(o.Arg.Alignment)
-			op.U2 = uint64(o.Arg.Offset)
-		case wazeroir.OperationStore32:
-			op.U1 = uint64(o.Arg.Alignment)
-			op.U2 = uint64(o.Arg.Offset)
-		// const ops...
+
 		case wazeroir.OperationITruncFromF:
 			op.B1 = byte(o.InputType)
 			op.B2 = byte(o.OutputType)
diff --git a/internal/wazeroir/compiler.go b/internal/wazeroir/compiler.go
index 4fea98b4..15ce1d4d 100644
--- a/internal/wazeroir/compiler.go
+++ b/internal/wazeroir/compiler.go
@@ -828,13 +828,14 @@ operatorSwitch:
 			NewOperationCall(index),
 		)
 	case wasm.OpcodeCallIndirect:
+		typeIndex := index
 		tableIndex, n, err := leb128.LoadUint32(c.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},
+			NewOperationCallIndirect(typeIndex, tableIndex),
 		)
 	case wasm.OpcodeDrop:
 		r := &InclusiveRange{Start: 0, End: 0}
@@ -851,8 +852,9 @@ operatorSwitch:
 		if c.unreachableState.on {
 			break operatorSwitch
 		}
+		isTargetVector := c.stackPeek() == UnsignedTypeV128
 		c.emit(
-			OperationSelect{IsTargetVector: c.stackPeek() == UnsignedTypeV128},
+			NewOperationSelect(isTargetVector),
 		)
 	case wasm.OpcodeTypedSelect:
 		// Skips two bytes: vector size fixed to 1, and the value type for select.
@@ -862,8 +864,9 @@ operatorSwitch:
 			break operatorSwitch
 		}
 		// Typed select is semantically equivalent to select at runtime.
+		isTargetVector := c.stackPeek() == UnsignedTypeV128
 		c.emit(
-			OperationSelect{IsTargetVector: c.stackPeek() == UnsignedTypeV128},
+			NewOperationSelect(isTargetVector),
 		)
 	case wasm.OpcodeLocalGet:
 		depth := c.localDepth(index)
@@ -871,13 +874,13 @@ operatorSwitch:
 			c.emit(
 				// -1 because we already manipulated the stack before
 				// called localDepth ^^.
-				OperationPick{Depth: depth - 1, IsTargetVector: isVector},
+				NewOperationPick(depth-1, isVector),
 			)
 		} else {
 			c.emit(
 				// -2 because we already manipulated the stack before
 				// called localDepth ^^.
-				OperationPick{Depth: depth - 2, IsTargetVector: isVector},
+				NewOperationPick(depth-2, isVector),
 			)
 		}
 	case wasm.OpcodeLocalSet:
@@ -888,13 +891,13 @@ operatorSwitch:
 			c.emit(
 				// +2 because we already popped the operands for this operation from the c.stack before
 				// called localDepth ^^,
-				OperationSet{Depth: depth + 2, IsTargetVector: isVector},
+				NewOperationSet(depth+2, isVector),
 			)
 		} else {
 			c.emit(
 				// +1 because we already popped the operands for this operation from the c.stack before
 				// called localDepth ^^,
-				OperationSet{Depth: depth + 1, IsTargetVector: isVector},
+				NewOperationSet(depth+1, isVector),
 			)
 		}
 	case wasm.OpcodeLocalTee:
@@ -902,13 +905,13 @@ operatorSwitch:
 		isVector := c.localType(index) == wasm.ValueTypeV128
 		if isVector {
 			c.emit(
-				OperationPick{Depth: 1, IsTargetVector: isVector},
-				OperationSet{Depth: depth + 2, IsTargetVector: isVector},
+				NewOperationPick(1, isVector),
+				NewOperationSet(depth+2, isVector),
 			)
 		} else {
 			c.emit(
-				OperationPick{Depth: 0, IsTargetVector: isVector},
-				OperationSet{Depth: depth + 1, IsTargetVector: isVector},
+				NewOperationPick(0, isVector),
+				NewOperationSet(depth+1, isVector),
 			)
 		}
 	case wasm.OpcodeGlobalGet:
@@ -924,106 +927,92 @@ operatorSwitch:
 		if err != nil {
 			return err
 		}
-		c.emit(OperationLoad{Type: UnsignedTypeI32, Arg: imm})
+		c.emit(NewOperationLoad(UnsignedTypeI32, imm))
 	case wasm.OpcodeI64Load:
 		imm, err := c.readMemoryArg(wasm.OpcodeI64LoadName)
 		if err != nil {
 			return err
 		}
-		c.emit(OperationLoad{Type: UnsignedTypeI64, Arg: imm})
+		c.emit(NewOperationLoad(UnsignedTypeI64, imm))
 	case wasm.OpcodeF32Load:
 		imm, err := c.readMemoryArg(wasm.OpcodeF32LoadName)
 		if err != nil {
 			return err
 		}
-		c.emit(OperationLoad{Type: UnsignedTypeF32, Arg: imm})
+		c.emit(NewOperationLoad(UnsignedTypeF32, imm))
 	case wasm.OpcodeF64Load:
 		imm, err := c.readMemoryArg(wasm.OpcodeF64LoadName)
 		if err != nil {
 			return err
 		}
-		c.emit(OperationLoad{Type: UnsignedTypeF64, Arg: imm})
+		c.emit(NewOperationLoad(UnsignedTypeF64, imm))
 	case wasm.OpcodeI32Load8S:
 		imm, err := c.readMemoryArg(wasm.OpcodeI32Load8SName)
 		if err != nil {
 			return err
 		}
-		c.emit(OperationLoad8{Type: SignedInt32, Arg: imm})
+		c.emit(NewOperationLoad8(SignedInt32, imm))
 	case wasm.OpcodeI32Load8U:
 		imm, err := c.readMemoryArg(wasm.OpcodeI32Load8UName)
 		if err != nil {
 			return err
 		}
-		c.emit(OperationLoad8{Type: SignedUint32, Arg: imm})
+		c.emit(NewOperationLoad8(SignedUint32, imm))
 	case wasm.OpcodeI32Load16S:
 		imm, err := c.readMemoryArg(wasm.OpcodeI32Load16SName)
 		if err != nil {
 			return err
 		}
-		c.emit(OperationLoad16{Type: SignedInt32, Arg: imm})
+		c.emit(NewOperationLoad16(SignedInt32, imm))
 	case wasm.OpcodeI32Load16U:
 		imm, err := c.readMemoryArg(wasm.OpcodeI32Load16UName)
 		if err != nil {
 			return err
 		}
-		c.emit(
-			OperationLoad16{Type: SignedUint32, Arg: imm},
-		)
+		c.emit(NewOperationLoad16(SignedUint32, imm))
 	case wasm.OpcodeI64Load8S:
 		imm, err := c.readMemoryArg(wasm.OpcodeI64Load8SName)
 		if err != nil {
 			return err
 		}
-		c.emit(
-			OperationLoad8{Type: SignedInt64, Arg: imm},
-		)
+		c.emit(NewOperationLoad8(SignedInt64, imm))
 	case wasm.OpcodeI64Load8U:
 		imm, err := c.readMemoryArg(wasm.OpcodeI64Load8UName)
 		if err != nil {
 			return err
 		}
-		c.emit(
-			OperationLoad8{Type: SignedUint64, Arg: imm},
-		)
+		c.emit(NewOperationLoad8(SignedUint64, imm))
 	case wasm.OpcodeI64Load16S:
 		imm, err := c.readMemoryArg(wasm.OpcodeI64Load16SName)
 		if err != nil {
 			return err
 		}
-		c.emit(
-			OperationLoad16{Type: SignedInt64, Arg: imm},
-		)
+		c.emit(NewOperationLoad16(SignedInt64, imm))
 	case wasm.OpcodeI64Load16U:
 		imm, err := c.readMemoryArg(wasm.OpcodeI64Load16UName)
 		if err != nil {
 			return err
 		}
-		c.emit(
-			OperationLoad16{Type: SignedUint64, Arg: imm},
-		)
+		c.emit(NewOperationLoad16(SignedUint64, imm))
 	case wasm.OpcodeI64Load32S:
 		imm, err := c.readMemoryArg(wasm.OpcodeI64Load32SName)
 		if err != nil {
 			return err
 		}
-		c.emit(
-			OperationLoad32{Signed: true, Arg: imm},
-		)
+		c.emit(NewOperationLoad32(true, imm))
 	case wasm.OpcodeI64Load32U:
 		imm, err := c.readMemoryArg(wasm.OpcodeI64Load32UName)
 		if err != nil {
 			return err
 		}
-		c.emit(
-			OperationLoad32{Signed: false, Arg: imm},
-		)
+		c.emit(NewOperationLoad32(false, imm))
 	case wasm.OpcodeI32Store:
 		imm, err := c.readMemoryArg(wasm.OpcodeI32StoreName)
 		if err != nil {
 			return err
 		}
 		c.emit(
-			OperationStore{Type: UnsignedTypeI32, Arg: imm},
+			NewOperationStore(UnsignedTypeI32, imm),
 		)
 	case wasm.OpcodeI64Store:
 		imm, err := c.readMemoryArg(wasm.OpcodeI64StoreName)
@@ -1031,7 +1020,7 @@ operatorSwitch:
 			return err
 		}
 		c.emit(
-			OperationStore{Type: UnsignedTypeI64, Arg: imm},
+			NewOperationStore(UnsignedTypeI64, imm),
 		)
 	case wasm.OpcodeF32Store:
 		imm, err := c.readMemoryArg(wasm.OpcodeF32StoreName)
@@ -1039,7 +1028,7 @@ operatorSwitch:
 			return err
 		}
 		c.emit(
-			OperationStore{Type: UnsignedTypeF32, Arg: imm},
+			NewOperationStore(UnsignedTypeF32, imm),
 		)
 	case wasm.OpcodeF64Store:
 		imm, err := c.readMemoryArg(wasm.OpcodeF64StoreName)
@@ -1047,7 +1036,7 @@ operatorSwitch:
 			return err
 		}
 		c.emit(
-			OperationStore{Type: UnsignedTypeF64, Arg: imm},
+			NewOperationStore(UnsignedTypeF64, imm),
 		)
 	case wasm.OpcodeI32Store8:
 		imm, err := c.readMemoryArg(wasm.OpcodeI32Store8Name)
@@ -1055,7 +1044,7 @@ operatorSwitch:
 			return err
 		}
 		c.emit(
-			OperationStore8{Arg: imm},
+			NewOperationStore8(imm),
 		)
 	case wasm.OpcodeI32Store16:
 		imm, err := c.readMemoryArg(wasm.OpcodeI32Store16Name)
@@ -1063,7 +1052,7 @@ operatorSwitch:
 			return err
 		}
 		c.emit(
-			OperationStore16{Arg: imm},
+			NewOperationStore16(imm),
 		)
 	case wasm.OpcodeI64Store8:
 		imm, err := c.readMemoryArg(wasm.OpcodeI64Store8Name)
@@ -1071,7 +1060,7 @@ operatorSwitch:
 			return err
 		}
 		c.emit(
-			OperationStore8{Arg: imm},
+			NewOperationStore8(imm),
 		)
 	case wasm.OpcodeI64Store16:
 		imm, err := c.readMemoryArg(wasm.OpcodeI64Store16Name)
@@ -1079,7 +1068,7 @@ operatorSwitch:
 			return err
 		}
 		c.emit(
-			OperationStore16{Arg: imm},
+			NewOperationStore16(imm),
 		)
 	case wasm.OpcodeI64Store32:
 		imm, err := c.readMemoryArg(wasm.OpcodeI64Store32Name)
@@ -1087,7 +1076,7 @@ operatorSwitch:
 			return err
 		}
 		c.emit(
-			OperationStore32{Arg: imm},
+			NewOperationStore32(imm),
 		)
 	case wasm.OpcodeMemorySize:
 		c.result.UsesMemory = true
diff --git a/internal/wazeroir/operations.go b/internal/wazeroir/operations.go
index fe7c03d3..e568bbf2 100644
--- a/internal/wazeroir/operations.go
+++ b/internal/wazeroir/operations.go
@@ -721,19 +721,7 @@ var (
 	_ Operation = OperationBr{}
 	_ Operation = OperationBrIf{}
 	_ Operation = OperationBrTable{}
-	_ Operation = OperationCallIndirect{}
 	_ Operation = OperationDrop{}
-	_ Operation = OperationSelect{}
-	_ Operation = OperationPick{}
-	_ Operation = OperationSet{}
-	_ Operation = OperationLoad{}
-	_ Operation = OperationLoad8{}
-	_ Operation = OperationLoad16{}
-	_ Operation = OperationLoad32{}
-	_ Operation = OperationStore{}
-	_ Operation = OperationStore8{}
-	_ Operation = OperationStore16{}
-	_ Operation = OperationStore32{}
 	_ Operation = OperationITruncFromF{}
 	_ Operation = OperationFConvertFromI{}
 	_ Operation = OperationExtend{}
@@ -919,6 +907,7 @@ type UnionOperation struct {
 func (o UnionOperation) String() string {
 	switch o.OpKind {
 	case OperationKindUnreachable,
+		OperationKindSelect,
 		OperationKindMemorySize,
 		OperationKindMemoryGrow,
 		OperationKindI32WrapFromI64,
@@ -943,6 +932,33 @@ func (o UnionOperation) String() string {
 		OperationKindGlobalSet:
 		return fmt.Sprintf("%s %d", o.Kind(), o.B1)
 
+	case OperationKindCallIndirect:
+		return fmt.Sprintf("%s: type=%d, table=%d", o.Kind(), o.U1, o.U2)
+
+	case OperationKindPick, OperationKindSet:
+		return fmt.Sprintf("%s %d (is_vector=%v)", o.Kind(), o.U1, o.B3)
+
+	case OperationKindLoad, OperationKindStore:
+		return fmt.Sprintf("%s.%s (align=%d, offset=%d)", UnsignedType(o.B1), o.Kind(), o.U1, o.U2)
+
+	case OperationKindLoad8,
+		OperationKindLoad16:
+		return fmt.Sprintf("%s.%s (align=%d, offset=%d)", SignedType(o.B1), o.Kind(), o.U1, o.U2)
+
+	case OperationKindStore8,
+		OperationKindStore16,
+		OperationKindStore32:
+		return fmt.Sprintf("%s (align=%d, offset=%d)", o.Kind(), o.U1, o.U2)
+
+	case OperationKindLoad32:
+		var t string
+		if o.B1 == 1 {
+			t = "i64"
+		} else {
+			t = "u64"
+		}
+		return fmt.Sprintf("%s.%s (align=%d, offset=%d)", t, o.Kind(), o.U1, o.U2)
+
 	case OperationKindEq,
 		OperationKindNe,
 		OperationKindAdd,
@@ -1097,7 +1113,7 @@ func NewOperationCall(functionIndex uint32) UnionOperation {
 	return UnionOperation{OpKind: OperationKindCall, U1: uint64(functionIndex)}
 }
 
-// OperationCallIndirect implements Operation.
+// NewOperationCallIndirect implements Operation.
 //
 // This corresponds to wasm.OpcodeCallIndirectName, and engines are expected to
 // consume the one value from the top of stack (called "offset"),
@@ -1109,18 +1125,8 @@ func NewOperationCall(functionIndex uint32) UnionOperation {
 // Therefore, two checks are performed at runtime before entering the target function:
 // 1) whether "offset" exceeds the length of table Tables[OperationCallIndirect.TableIndex].
 // 2) whether the type of the function table[offset] matches the function type specified by OperationCallIndirect.TypeIndex.
-type OperationCallIndirect struct {
-	TypeIndex, TableIndex uint32
-}
-
-// String implements fmt.Stringer.
-func (o OperationCallIndirect) String() string {
-	return fmt.Sprintf("%s: type=%d, table=%d", o.Kind(), o.TypeIndex, o.TableIndex)
-}
-
-// Kind implements Operation.Kind
-func (OperationCallIndirect) Kind() OperationKind {
-	return OperationKindCallIndirect
+func NewOperationCallIndirect(typeIndex, tableIndex uint32) UnionOperation {
+	return UnionOperation{OpKind: OperationKindCallIndirect, U1: uint64(typeIndex), U2: uint64(tableIndex)}
 }
 
 // InclusiveRange is the range which spans across the value stack starting from the top to the bottom, and
@@ -1148,65 +1154,38 @@ func (OperationDrop) Kind() OperationKind {
 	return OperationKindDrop
 }
 
-// OperationSelect implements Operation.
+// NewOperationSelect is a constructor for UnionOperation with Kind OperationKindSelect.
 //
 // This corresponds to wasm.OpcodeSelect.
 //
 // The engines are expected to pop three values, say [..., x2, x1, c], then if the value "c" equals zero,
 // "x1" is pushed back onto the stack and, otherwise "x2" is pushed back.
-type OperationSelect struct {
-	// IsTargetVector true if the selection target value's type is wasm.ValueTypeV128.
-	IsTargetVector bool
-}
-
-// String implements fmt.Stringer.
-func (o OperationSelect) String() string { return o.Kind().String() }
-
-// Kind implements Operation.Kind
-func (OperationSelect) Kind() OperationKind {
-	return OperationKindSelect
+//
+// isTargetVector true if the selection target value's type is wasm.ValueTypeV128.
+func NewOperationSelect(isTargetVector bool) UnionOperation {
+	return UnionOperation{OpKind: OperationKindSelect, B3: isTargetVector}
 }
 
-// OperationPick implements Operation.
+// NewOperationPick is a constructor for UnionOperation with Kind OperationKindPick.
 //
-// The engines are expected to copy a value pointed by OperationPick.Depth, and push the
+// The engines are expected to copy a value pointed by depth, and push the
 // copied value onto the top of the stack.
-type OperationPick struct {
-	// Depth is the location of the pick target in the uint64 value stack at runtime.
-	// If IsTargetVector=true, this points to the location of the lower 64-bits of the vector.
-	Depth          int
-	IsTargetVector bool
-}
-
-// String implements fmt.Stringer.
-func (o OperationPick) String() string {
-	return fmt.Sprintf("%s %d (is_vector=%v)", o.Kind(), o.Depth, o.IsTargetVector)
-}
-
-// Kind implements Operation.Kind
-func (OperationPick) Kind() OperationKind {
-	return OperationKindPick
+//
+// depth is the location of the pick target in the uint64 value stack at runtime.
+// If isTargetVector=true, this points to the location of the lower 64-bits of the vector.
+func NewOperationPick(depth int, isTargetVector bool) UnionOperation {
+	return UnionOperation{OpKind: OperationKindPick, U1: uint64(depth), B3: isTargetVector}
 }
 
-// OperationSet implements Operation.
+// NewOperationSet is a constructor for UnionOperation with Kind OperationKindSet.
 //
 // The engines are expected to set the top value of the stack to the location specified by
-// OperationSet.Depth.
-type OperationSet struct {
-	// Depth is the location of the set target in the uint64 value stack at runtime.
-	// If IsTargetVector=true, this points the location of the lower 64-bits of the vector.
-	Depth          int
-	IsTargetVector bool
-}
-
-// String implements fmt.Stringer.
-func (o OperationSet) String() string {
-	return fmt.Sprintf("%s %d (is_vector=%v)", o.Kind(), o.Depth, o.IsTargetVector)
-}
-
-// Kind implements Operation.Kind
-func (OperationSet) Kind() OperationKind {
-	return OperationKindSet
+// depth.
+//
+// depth is the location of the set target in the uint64 value stack at runtime.
+// If isTargetVector=true, this points the location of the lower 64-bits of the vector.
+func NewOperationSet(depth int, isTargetVector bool) UnionOperation {
+	return UnionOperation{OpKind: OperationKindSet, U1: uint64(depth), B3: isTargetVector}
 }
 
 // NewOperationGlobalGet is a constructor for UnionOperation with Kind OperationKindGlobalGet.
@@ -1244,175 +1223,88 @@ type MemoryArg struct {
 	Offset uint32
 }
 
-// OperationLoad implements Operation.
+// NewOperationLoad is a constructor for UnionOperation with Kind OperationKindLoad.
 //
 // This corresponds to wasm.OpcodeI32LoadName wasm.OpcodeI64LoadName wasm.OpcodeF32LoadName and wasm.OpcodeF64LoadName.
 //
 // The engines are expected to check the boundary of memory length, and exit the execution if this exceeds the boundary,
 // otherwise load the corresponding value following the semantics of the corresponding WebAssembly instruction.
-type OperationLoad struct {
-	Type UnsignedType
-	Arg  MemoryArg
+func NewOperationLoad(unsignedType UnsignedType, arg MemoryArg) UnionOperation {
+	return UnionOperation{OpKind: OperationKindLoad, B1: byte(unsignedType), U1: uint64(arg.Alignment), U2: uint64(arg.Offset)}
 }
 
-// String implements fmt.Stringer.
-func (o OperationLoad) String() string {
-	return fmt.Sprintf("%s.%s (align=%d, offset=%d)", o.Type, o.Kind(), o.Arg.Alignment, o.Arg.Offset)
-}
-
-// Kind implements Operation.Kind
-func (OperationLoad) Kind() OperationKind {
-	return OperationKindLoad
-}
-
-// OperationLoad8 implements Operation.
+// NewOperationLoad8 is a constructor for UnionOperation with Kind OperationKindLoad8.
 //
 // This corresponds to wasm.OpcodeI32Load8SName wasm.OpcodeI32Load8UName wasm.OpcodeI64Load8SName wasm.OpcodeI64Load8UName.
 //
 // The engines are expected to check the boundary of memory length, and exit the execution if this exceeds the boundary,
 // otherwise load the corresponding value following the semantics of the corresponding WebAssembly instruction.
-type OperationLoad8 struct {
-	Type SignedInt
-	Arg  MemoryArg
-}
-
-// String implements fmt.Stringer.
-func (o OperationLoad8) String() string {
-	return fmt.Sprintf("%s.%s (align=%d, offset=%d)", o.Type, o.Kind(), o.Arg.Alignment, o.Arg.Offset)
-}
-
-// Kind implements Operation.Kind
-func (OperationLoad8) Kind() OperationKind {
-	return OperationKindLoad8
+func NewOperationLoad8(signedInt SignedInt, arg MemoryArg) UnionOperation {
+	return UnionOperation{OpKind: OperationKindLoad8, B1: byte(signedInt), U1: uint64(arg.Alignment), U2: uint64(arg.Offset)}
 }
 
-// OperationLoad16 implements Operation.
+// NewOperationLoad16 is a constructor for UnionOperation with Kind OperationKindLoad16.
 //
 // This corresponds to wasm.OpcodeI32Load16SName wasm.OpcodeI32Load16UName wasm.OpcodeI64Load16SName wasm.OpcodeI64Load16UName.
 //
 // The engines are expected to check the boundary of memory length, and exit the execution if this exceeds the boundary,
 // otherwise load the corresponding value following the semantics of the corresponding WebAssembly instruction.
-type OperationLoad16 struct {
-	Type SignedInt
-	Arg  MemoryArg
-}
-
-// String implements fmt.Stringer.
-func (o OperationLoad16) String() string {
-	return fmt.Sprintf("%s.%s (align=%d, offset=%d)", o.Type, o.Kind(), o.Arg.Alignment, o.Arg.Offset)
+func NewOperationLoad16(signedInt SignedInt, arg MemoryArg) UnionOperation {
+	return UnionOperation{OpKind: OperationKindLoad16, B1: byte(signedInt), U1: uint64(arg.Alignment), U2: uint64(arg.Offset)}
 }
 
-// Kind implements Operation.Kind
-func (OperationLoad16) Kind() OperationKind {
-	return OperationKindLoad16
-}
-
-// OperationLoad32 implements Operation.
+// NewOperationLoad32 is a constructor for UnionOperation with Kind OperationKindLoad32.
 //
 // This corresponds to wasm.OpcodeI64Load32SName wasm.OpcodeI64Load32UName.
 //
 // The engines are expected to check the boundary of memory length, and exit the execution if this exceeds the boundary,
 // otherwise load the corresponding value following the semantics of the corresponding WebAssembly instruction.
-type OperationLoad32 struct {
-	Signed bool
-	Arg    MemoryArg
-}
-
-// String implements fmt.Stringer.
-func (o OperationLoad32) String() string {
-	var t string
-	if o.Signed {
-		t = "i64"
-	} else {
-		t = "u64"
+func NewOperationLoad32(signed bool, arg MemoryArg) UnionOperation {
+	sigB := byte(0)
+	if signed {
+		sigB = 1
 	}
-	return fmt.Sprintf("%s.%s (align=%d, offset=%d)", t, o.Kind(), o.Arg.Alignment, o.Arg.Offset)
-}
-
-// Kind implements Operation.Kind
-func (OperationLoad32) Kind() OperationKind {
-	return OperationKindLoad32
+	return UnionOperation{OpKind: OperationKindLoad32, B1: sigB, U1: uint64(arg.Alignment), U2: uint64(arg.Offset)}
 }
 
-// OperationStore implements Operation.
+// NewOperationStore is a constructor for UnionOperation with Kind OperationKindStore.
 //
 // # This corresponds to wasm.OpcodeI32StoreName wasm.OpcodeI64StoreName wasm.OpcodeF32StoreName wasm.OpcodeF64StoreName
 //
 // The engines are expected to check the boundary of memory length, and exit the execution if this exceeds the boundary,
 // otherwise store the corresponding value following the semantics of the corresponding WebAssembly instruction.
-type OperationStore struct {
-	Type UnsignedType
-	Arg  MemoryArg
-}
-
-// String implements fmt.Stringer.
-func (o OperationStore) String() string {
-	return fmt.Sprintf("%s.%s (align=%d, offset=%d)", o.Type, o.Kind(), o.Arg.Alignment, o.Arg.Offset)
-}
-
-// Kind implements Operation.Kind
-func (OperationStore) Kind() OperationKind {
-	return OperationKindStore
+func NewOperationStore(unsignedType UnsignedType, arg MemoryArg) UnionOperation {
+	return UnionOperation{OpKind: OperationKindStore, B1: byte(unsignedType), U1: uint64(arg.Alignment), U2: uint64(arg.Offset)}
 }
 
-// OperationStore8 implements Operation.
+// NewOperationStore8 is a constructor for UnionOperation with Kind OperationKindStore8.
 //
 // # This corresponds to wasm.OpcodeI32Store8Name wasm.OpcodeI64Store8Name
 //
 // The engines are expected to check the boundary of memory length, and exit the execution if this exceeds the boundary,
 // otherwise store the corresponding value following the semantics of the corresponding WebAssembly instruction.
-type OperationStore8 struct {
-	Arg MemoryArg
-}
-
-// String implements fmt.Stringer.
-func (o OperationStore8) String() string {
-	return fmt.Sprintf("%s (align=%d, offset=%d)", o.Kind(), o.Arg.Alignment, o.Arg.Offset)
+func NewOperationStore8(arg MemoryArg) UnionOperation {
+	return UnionOperation{OpKind: OperationKindStore8, U1: uint64(arg.Alignment), U2: uint64(arg.Offset)}
 }
 
-// Kind implements Operation.Kind
-func (OperationStore8) Kind() OperationKind {
-	return OperationKindStore8
-}
-
-// OperationStore16 implements Operation.
+// NewOperationStore16 is a constructor for UnionOperation with Kind OperationKindStore16.
 //
 // # This corresponds to wasm.OpcodeI32Store16Name wasm.OpcodeI64Store16Name
 //
 // The engines are expected to check the boundary of memory length, and exit the execution if this exceeds the boundary,
 // otherwise store the corresponding value following the semantics of the corresponding WebAssembly instruction.
-type OperationStore16 struct {
-	Arg MemoryArg
+func NewOperationStore16(arg MemoryArg) UnionOperation {
+	return UnionOperation{OpKind: OperationKindStore16, U1: uint64(arg.Alignment), U2: uint64(arg.Offset)}
 }
 
-// String implements fmt.Stringer.
-func (o OperationStore16) String() string {
-	return fmt.Sprintf("%s (align=%d, offset=%d)", o.Kind(), o.Arg.Alignment, o.Arg.Offset)
-}
-
-// Kind implements Operation.Kind
-func (OperationStore16) Kind() OperationKind {
-	return OperationKindStore16
-}
-
-// OperationStore32 implements Operation.
+// NewOperationStore32 is a constructor for UnionOperation with Kind OperationKindStore32.
 //
 // # This corresponds to wasm.OpcodeI64Store32Name
 //
 // The engines are expected to check the boundary of memory length, and exit the execution if this exceeds the boundary,
 // otherwise store the corresponding value following the semantics of the corresponding WebAssembly instruction.
-type OperationStore32 struct {
-	Arg MemoryArg
-}
-
-// String implements fmt.Stringer.
-func (o OperationStore32) String() string {
-	return fmt.Sprintf("%s (align=%d, offset=%d)", o.Kind(), o.Arg.Alignment, o.Arg.Offset)
-}
-
-// Kind implements Operation.Kind.
-func (OperationStore32) Kind() OperationKind {
-	return OperationKindStore32
+func NewOperationStore32(arg MemoryArg) UnionOperation {
+	return UnionOperation{OpKind: OperationKindStore32, U1: uint64(arg.Alignment), U2: uint64(arg.Offset)}
 }
 
 // NewOperationMemorySize is a constructor for UnionOperation with Kind OperationKindMemorySize.


Test patch:
diff --git a/internal/engine/compiler/compiler_conditional_save_test.go b/internal/engine/compiler/compiler_conditional_save_test.go
index 803ecd97..caad721a 100644
--- a/internal/engine/compiler/compiler_conditional_save_test.go
+++ b/internal/engine/compiler/compiler_conditional_save_test.go
@@ -27,10 +27,10 @@ func TestCompiler_conditional_value_saving(t *testing.T) {
 
 	// Pick the f32 floating point local (1.0) twice.
 	// Note that the f32 (function local variable in general) is placed above the call frame.
-	err = compiler.compilePick(wazeroir.OperationPick{Depth: int(compiler.runtimeValueLocationStack().sp - 1 - callFrameDataSizeInUint64)})
+	err = compiler.compilePick(wazeroir.NewOperationPick(int(compiler.runtimeValueLocationStack().sp-1-callFrameDataSizeInUint64), false))
 
 	require.NoError(t, err)
-	err = compiler.compilePick(wazeroir.OperationPick{Depth: int(compiler.runtimeValueLocationStack().sp - 1 - callFrameDataSizeInUint64)})
+	err = compiler.compilePick(wazeroir.NewOperationPick(int(compiler.runtimeValueLocationStack().sp-1-callFrameDataSizeInUint64), false))
 
 	require.NoError(t, err)
 	// Generate conditional flag via floating point comparisons.
diff --git a/internal/engine/compiler/compiler_controlflow_test.go b/internal/engine/compiler/compiler_controlflow_test.go
index 682422d0..1dbb2808 100644
--- a/internal/engine/compiler/compiler_controlflow_test.go
+++ b/internal/engine/compiler/compiler_controlflow_test.go
@@ -547,7 +547,7 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 		err := compiler.compilePreamble()
 		require.NoError(t, err)
 
-		targetOperation := wazeroir.OperationCallIndirect{}
+		targetOperation := wazeroir.NewOperationCallIndirect(0, 0)
 
 		// Place the offset value.
 		err = compiler.compileConstI32(wazeroir.NewOperationConstI32(10))
@@ -577,7 +577,7 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 		err := compiler.compilePreamble()
 		require.NoError(t, err)
 
-		targetOperation := wazeroir.OperationCallIndirect{}
+		targetOperation := wazeroir.NewOperationCallIndirect(0, 0)
 		targetOffset := wazeroir.NewOperationConstI32(uint32(0))
 
 		// and the typeID doesn't match the table[targetOffset]'s type ID.
@@ -613,7 +613,7 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 		err := compiler.compilePreamble()
 		require.NoError(t, err)
 
-		targetOperation := wazeroir.OperationCallIndirect{}
+		targetOperation := wazeroir.NewOperationCallIndirect(0, 0)
 		targetOffset := wazeroir.NewOperationConstI32(uint32(0))
 		env.module().TypeIDs = []wasm.FunctionTypeID{1000}
 		// Ensure that the module instance has the type information for targetOperation.TypeIndex,
@@ -648,8 +648,9 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 			Results:           []wasm.ValueType{wasm.ValueTypeI32},
 			ResultNumInUint64: 1,
 		}
+		const typeIndex = 0
 		targetTypeID := wasm.FunctionTypeID(10)
-		operation := wazeroir.OperationCallIndirect{TypeIndex: 0}
+		operation := wazeroir.NewOperationCallIndirect(typeIndex, 0)
 
 		table := make([]wasm.Reference, 10)
 		env := newCompilerEnvironment()
@@ -658,7 +659,7 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 		// Ensure that the module instance has the type information for targetOperation.TypeIndex,
 		// and the typeID matches the table[targetOffset]'s type ID.
 		env.module().TypeIDs = make([]wasm.FunctionTypeID, 100)
-		env.module().TypeIDs[operation.TypeIndex] = targetTypeID
+		env.module().TypeIDs[typeIndex] = targetTypeID
 		env.module().Engine = &moduleEngine{functions: []function{}}
 
 		me := env.moduleEngine()
@@ -678,7 +679,7 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 
 			requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
 			// The function result value must be set at the bottom of the stack.
-			err = compiler.compileSet(wazeroir.OperationSet{Depth: int(compiler.runtimeValueLocationStack().sp - 1)})
+			err = compiler.compileSet(wazeroir.NewOperationSet(int(compiler.runtimeValueLocationStack().sp-1), false))
 			require.NoError(t, err)
 			err = compiler.compileReturnFunction()
 			require.NoError(t, err)
@@ -749,7 +750,7 @@ func TestCompiler_callIndirect_largeTypeIndex(t *testing.T) {
 	// Ensure that the module instance has the type information for targetOperation.TypeIndex,
 	// and the typeID  matches the table[targetOffset]'s type ID.
 	const typeIndex, typeID = 12345, 0
-	operation := wazeroir.OperationCallIndirect{TypeIndex: typeIndex}
+	operation := wazeroir.NewOperationCallIndirect(typeIndex, 0)
 	env.module().TypeIDs = make([]wasm.FunctionTypeID, typeIndex+1)
 	env.module().TypeIDs[typeIndex] = typeID
 	env.module().Engine = &moduleEngine{functions: []function{}}
@@ -825,13 +826,13 @@ func TestCompiler_compileCall(t *testing.T) {
 		err = compiler.compileConstI32(wazeroir.NewOperationConstI32(addTargetValue))
 		require.NoError(t, err)
 		// Picks the function argument placed at the bottom of the stack.
-		err = compiler.compilePick(wazeroir.OperationPick{Depth: int(compiler.runtimeValueLocationStack().sp - 1)})
+		err = compiler.compilePick(wazeroir.NewOperationPick(int(compiler.runtimeValueLocationStack().sp-1), false))
 		require.NoError(t, err)
 		// Adds the const to the picked value.
 		err = compiler.compileAdd(wazeroir.NewOperationAdd(wazeroir.UnsignedTypeI32))
 		require.NoError(t, err)
 		// Then store the added result into the bottom of the stack (which is treated as the result of the function).
-		err = compiler.compileSet(wazeroir.OperationSet{Depth: int(compiler.runtimeValueLocationStack().sp - 1)})
+		err = compiler.compileSet(wazeroir.NewOperationSet(int(compiler.runtimeValueLocationStack().sp-1), false))
 		require.NoError(t, err)
 
 		err = compiler.compileReturnFunction()
diff --git a/internal/engine/compiler/compiler_memory_test.go b/internal/engine/compiler/compiler_memory_test.go
index 4cd425c2..55ca5d7b 100644
--- a/internal/engine/compiler/compiler_memory_test.go
+++ b/internal/engine/compiler/compiler_memory_test.go
@@ -91,7 +91,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i32.load",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad(wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeI32})
+				err := compiler.compileLoad(wazeroir.NewOperationLoad(wazeroir.UnsignedTypeI32, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -101,7 +101,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i64.load",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad(wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeI64})
+				err := compiler.compileLoad(wazeroir.NewOperationLoad(wazeroir.UnsignedTypeI64, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -111,7 +111,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "f32.load",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad(wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeF32})
+				err := compiler.compileLoad(wazeroir.NewOperationLoad(wazeroir.UnsignedTypeF32, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -122,7 +122,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "f64.load",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad(wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeF64})
+				err := compiler.compileLoad(wazeroir.NewOperationLoad(wazeroir.UnsignedTypeF64, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -133,7 +133,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i32.load8s",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad8(wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedInt32})
+				err := compiler.compileLoad8(wazeroir.NewOperationLoad8(wazeroir.SignedInt32, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -143,7 +143,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i32.load8u",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad8(wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedUint32})
+				err := compiler.compileLoad8(wazeroir.NewOperationLoad8(wazeroir.SignedUint32, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -153,7 +153,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i64.load8s",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad8(wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedInt64})
+				err := compiler.compileLoad8(wazeroir.NewOperationLoad8(wazeroir.SignedInt64, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -163,7 +163,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i64.load8u",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad8(wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedUint64})
+				err := compiler.compileLoad8(wazeroir.NewOperationLoad8(wazeroir.SignedUint64, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -173,7 +173,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i32.load16s",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad16(wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedInt32})
+				err := compiler.compileLoad16(wazeroir.NewOperationLoad16(wazeroir.SignedInt32, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -183,7 +183,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i32.load16u",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad16(wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedUint32})
+				err := compiler.compileLoad16(wazeroir.NewOperationLoad16(wazeroir.SignedUint32, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -193,7 +193,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i64.load16s",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad16(wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedInt64})
+				err := compiler.compileLoad16(wazeroir.NewOperationLoad16(wazeroir.SignedInt64, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -203,7 +203,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i64.load16u",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad16(wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedUint64})
+				err := compiler.compileLoad16(wazeroir.NewOperationLoad16(wazeroir.SignedUint64, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -213,7 +213,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i64.load32s",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad32(wazeroir.OperationLoad32{Arg: arg, Signed: true})
+				err := compiler.compileLoad32(wazeroir.NewOperationLoad32(true, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -223,7 +223,7 @@ func TestCompiler_compileLoad(t *testing.T) {
 		{
 			name: "i64.load32u",
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileLoad32(wazeroir.OperationLoad32{Arg: arg, Signed: false})
+				err := compiler.compileLoad32(wazeroir.NewOperationLoad32(false, arg))
 				require.NoError(t, err)
 			},
 			loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
@@ -292,7 +292,7 @@ func TestCompiler_compileStore(t *testing.T) {
 			name:              "i32.store",
 			targetSizeInBytes: 32 / 8,
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileStore(wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeI32})
+				err := compiler.compileStore(wazeroir.NewOperationStore(wazeroir.UnsignedTypeI32, arg))
 				require.NoError(t, err)
 			},
 			storedValueVerifyFn: func(t *testing.T, mem []byte) {
@@ -304,7 +304,7 @@ func TestCompiler_compileStore(t *testing.T) {
 			isFloatTarget:     true,
 			targetSizeInBytes: 32 / 8,
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileStore(wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeF32})
+				err := compiler.compileStore(wazeroir.NewOperationStore(wazeroir.UnsignedTypeF32, arg))
 				require.NoError(t, err)
 			},
 			storedValueVerifyFn: func(t *testing.T, mem []byte) {
@@ -315,7 +315,7 @@ func TestCompiler_compileStore(t *testing.T) {
 			name:              "i64.store",
 			targetSizeInBytes: 64 / 8,
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileStore(wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeI64})
+				err := compiler.compileStore(wazeroir.NewOperationStore(wazeroir.UnsignedTypeI64, arg))
 				require.NoError(t, err)
 			},
 			storedValueVerifyFn: func(t *testing.T, mem []byte) {
@@ -327,7 +327,7 @@ func TestCompiler_compileStore(t *testing.T) {
 			isFloatTarget:     true,
 			targetSizeInBytes: 64 / 8,
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileStore(wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeF64})
+				err := compiler.compileStore(wazeroir.NewOperationStore(wazeroir.UnsignedTypeF64, arg))
 				require.NoError(t, err)
 			},
 			storedValueVerifyFn: func(t *testing.T, mem []byte) {
@@ -338,7 +338,7 @@ func TestCompiler_compileStore(t *testing.T) {
 			name:              "store8",
 			targetSizeInBytes: 1,
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileStore8(wazeroir.OperationStore8{Arg: arg})
+				err := compiler.compileStore8(wazeroir.NewOperationStore8(arg))
 				require.NoError(t, err)
 			},
 			storedValueVerifyFn: func(t *testing.T, mem []byte) {
@@ -349,7 +349,7 @@ func TestCompiler_compileStore(t *testing.T) {
 			name:              "store16",
 			targetSizeInBytes: 16 / 8,
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileStore16(wazeroir.OperationStore16{Arg: arg})
+				err := compiler.compileStore16(wazeroir.NewOperationStore16(arg))
 				require.NoError(t, err)
 			},
 			storedValueVerifyFn: func(t *testing.T, mem []byte) {
@@ -360,7 +360,7 @@ func TestCompiler_compileStore(t *testing.T) {
 			name:              "store32",
 			targetSizeInBytes: 32 / 8,
 			operationSetupFn: func(t *testing.T, compiler compilerImpl) {
-				err := compiler.compileStore32(wazeroir.OperationStore32{Arg: arg})
+				err := compiler.compileStore32(wazeroir.NewOperationStore32(arg))
 				require.NoError(t, err)
 			},
 			storedValueVerifyFn: func(t *testing.T, mem []byte) {
@@ -450,13 +450,13 @@ func TestCompiler_MemoryOutOfBounds(t *testing.T) {
 
 					switch targetSizeInByte {
 					case 1:
-						err = compiler.compileLoad8(wazeroir.OperationLoad8{Type: wazeroir.SignedInt32, Arg: arg})
+						err = compiler.compileLoad8(wazeroir.NewOperationLoad8(wazeroir.SignedInt32, arg))
 					case 2:
-						err = compiler.compileLoad16(wazeroir.OperationLoad16{Type: wazeroir.SignedInt32, Arg: arg})
+						err = compiler.compileLoad16(wazeroir.NewOperationLoad16(wazeroir.SignedInt32, arg))
 					case 4:
-						err = compiler.compileLoad32(wazeroir.OperationLoad32{Signed: false, Arg: arg})
+						err = compiler.compileLoad32(wazeroir.NewOperationLoad32(false, arg))
 					case 8:
-						err = compiler.compileLoad(wazeroir.OperationLoad{Type: wazeroir.UnsignedTypeF64, Arg: arg})
+						err = compiler.compileLoad(wazeroir.NewOperationLoad(wazeroir.UnsignedTypeF64, arg))
 					default:
 						t.Fail()
 					}
diff --git a/internal/engine/compiler/compiler_stack_test.go b/internal/engine/compiler/compiler_stack_test.go
index 56b0b5d9..2db31654 100644
--- a/internal/engine/compiler/compiler_stack_test.go
+++ b/internal/engine/compiler/compiler_stack_test.go
@@ -160,7 +160,7 @@ func TestCompiler_compileLoadValueOnStackToRegister(t *testing.T) {
 func TestCompiler_compilePick_v128(t *testing.T) {
 	const pickTargetLo, pickTargetHi uint64 = 12345, 6789
 
-	op := wazeroir.OperationPick{Depth: 2, IsTargetVector: true}
+	op := wazeroir.NewOperationPick(2, true)
 	tests := []struct {
 		name                   string
 		isPickTargetOnRegister bool
@@ -231,7 +231,7 @@ func TestCompiler_compilePick_v128(t *testing.T) {
 
 func TestCompiler_compilePick(t *testing.T) {
 	const pickTargetValue uint64 = 12345
-	op := wazeroir.OperationPick{Depth: 1}
+	op := wazeroir.NewOperationPick(1, false)
 	tests := []struct {
 		name                                      string
 		pickTargetSetupFunc                       func(compiler compilerImpl, ce *callEngine) error
@@ -580,7 +580,7 @@ func TestCompiler_compileSelect(t *testing.T) {
 					}
 
 					// Now emit code for select.
-					err = compiler.compileSelect(wazeroir.OperationSelect{})
+					err = compiler.compileSelect(wazeroir.NewOperationSelect(false))
 					require.NoError(t, err)
 
 					// x1 should be top of the stack.
@@ -655,7 +655,7 @@ func TestCompiler_compileSwap_v128(t *testing.T) {
 			}
 
 			// Swap x1 and x2.
-			err = compiler.compileSet(wazeroir.OperationSet{Depth: 4, IsTargetVector: true})
+			err = compiler.compileSet(wazeroir.NewOperationSet(4, true))
 			require.NoError(t, err)
 
 			require.NoError(t, compiler.compileReturnFunction())
@@ -730,7 +730,7 @@ func TestCompiler_compileSet(t *testing.T) {
 			}
 
 			// Set x2 into the x1.
-			err = compiler.compileSet(wazeroir.OperationSet{Depth: 2})
+			err = compiler.compileSet(wazeroir.NewOperationSet(2, false))
 			require.NoError(t, err)
 
 			require.NoError(t, compiler.compileReturnFunction())
diff --git a/internal/engine/compiler/compiler_vec_test.go b/internal/engine/compiler/compiler_vec_test.go
index 5096abc3..418b601c 100644
--- a/internal/engine/compiler/compiler_vec_test.go
+++ b/internal/engine/compiler/compiler_vec_test.go
@@ -7394,7 +7394,7 @@ func TestCompiler_compileSelect_v128(t *testing.T) {
 		err = compiler.compileConstI32(wazeroir.NewOperationConstI32(selector))
 		require.NoError(t, err)
 
-		err = compiler.compileSelect(wazeroir.OperationSelect{IsTargetVector: true})
+		err = compiler.compileSelect(wazeroir.NewOperationSelect(true))
 		require.NoError(t, err)
 
 		requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
diff --git a/internal/engine/compiler/impl_amd64_test.go b/internal/engine/compiler/impl_amd64_test.go
index 437d5b57..fe6e5e1b 100644
--- a/internal/engine/compiler/impl_amd64_test.go
+++ b/internal/engine/compiler/impl_amd64_test.go
@@ -22,7 +22,7 @@ func TestAmd64Compiler_indirectCallWithTargetOnCallingConvReg(t *testing.T) {
 	env.addTable(&wasm.TableInstance{References: table})
 	// Ensure that the module instance has the type information for targetOperation.TypeIndex,
 	// and the typeID  matches the table[targetOffset]'s type ID.
-	operation := wazeroir.OperationCallIndirect{TypeIndex: 0}
+	operation := wazeroir.NewOperationCallIndirect(0, 0)
 	env.module().TypeIDs = []wasm.FunctionTypeID{0}
 	env.module().Engine = &moduleEngine{functions: []function{}}
 
diff --git a/internal/engine/compiler/impl_arm64_test.go b/internal/engine/compiler/impl_arm64_test.go
index 0fc7ac29..72a6fe2b 100644
--- a/internal/engine/compiler/impl_arm64_test.go
+++ b/internal/engine/compiler/impl_arm64_test.go
@@ -19,7 +19,7 @@ func TestArm64Compiler_indirectCallWithTargetOnCallingConvReg(t *testing.T) {
 	env.addTable(&wasm.TableInstance{References: table})
 	// Ensure that the module instance has the type information for targetOperation.TypeIndex,
 	// and the typeID  matches the table[targetOffset]'s type ID.
-	operation := wazeroir.OperationCallIndirect{TypeIndex: 0}
+	operation := wazeroir.NewOperationCallIndirect(0, 0)
 	env.module().TypeIDs = []wasm.FunctionTypeID{0}
 	env.module().Engine = &moduleEngine{functions: []function{}}
 
diff --git a/internal/wazeroir/compiler_test.go b/internal/wazeroir/compiler_test.go
index 80b9bda6..ffd46cd3 100644
--- a/internal/wazeroir/compiler_test.go
+++ b/internal/wazeroir/compiler_test.go
@@ -103,7 +103,7 @@ func TestCompile(t *testing.T) {
 			},
 			expected: &CompilationResult{
 				Operations: []Operation{ // begin with params: [$x]
-					OperationPick{Depth: 0},                                 // [$x, $x]
+					NewOperationPick(0, false),                              // [$x, $x]
 					OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // [$x]
 					OperationBr{Target: Label{Kind: LabelKindReturn}},       // return!
 				},
@@ -139,9 +139,9 @@ func TestCompile(t *testing.T) {
 			expected: &CompilationResult{
 				Operations: []Operation{ // begin with params: []
 					NewOperationConstI32(8), // [8]
-					OperationLoad{Type: UnsignedTypeI32, Arg: MemoryArg{Alignment: 2, Offset: 0}}, // [x]
-					OperationDrop{Depth: &InclusiveRange{}},                                       // []
-					OperationBr{Target: Label{Kind: LabelKindReturn}},                             // return!
+					NewOperationLoad(UnsignedTypeI32, MemoryArg{Alignment: 2, Offset: 0}), // [x]
+					OperationDrop{Depth: &InclusiveRange{}},                               // []
+					OperationBr{Target: Label{Kind: LabelKindReturn}},                     // return!
 				},
 				LabelCallers: map[LabelID]uint32{},
 				Types:        []wasm.FunctionType{v_v},
@@ -166,9 +166,9 @@ func TestCompile(t *testing.T) {
 			expected: &CompilationResult{
 				Operations: []Operation{ // begin with params: []
 					NewOperationConstI32(8), // [8]
-					OperationLoad{Type: UnsignedTypeI32, Arg: MemoryArg{Alignment: 2, Offset: 0}}, // [x]
-					OperationDrop{Depth: &InclusiveRange{}},                                       // []
-					OperationBr{Target: Label{Kind: LabelKindReturn}},                             // return!
+					NewOperationLoad(UnsignedTypeI32, MemoryArg{Alignment: 2, Offset: 0}), // [x]
+					OperationDrop{Depth: &InclusiveRange{}},                               // []
+					OperationBr{Target: Label{Kind: LabelKindReturn}},                     // return!
 				},
 				LabelCallers: map[LabelID]uint32{},
 				Types:        []wasm.FunctionType{v_v},
@@ -189,7 +189,7 @@ func TestCompile(t *testing.T) {
 			},
 			expected: &CompilationResult{
 				Operations: []Operation{ // begin with params: [$delta]
-					OperationPick{Depth: 0},                                 // [$delta, $delta]
+					NewOperationPick(0, false),                              // [$delta, $delta]
 					NewOperationMemoryGrow(),                                // [$delta, $old_size]
 					OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // [$old_size]
 					OperationBr{Target: Label{Kind: LabelKindReturn}},       // return!
@@ -397,8 +397,8 @@ func TestCompile_MultiValue(t *testing.T) {
 			},
 			expected: &CompilationResult{
 				Operations: []Operation{ // begin with params: [$x, $y]
-					OperationPick{Depth: 0},                                 // [$x, $y, $y]
-					OperationPick{Depth: 2},                                 // [$x, $y, $y, $x]
+					NewOperationPick(0, false),                              // [$x, $y, $y]
+					NewOperationPick(2, false),                              // [$x, $y, $y, $x]
 					OperationDrop{Depth: &InclusiveRange{Start: 2, End: 3}}, // [$y, $x]
 					OperationBr{Target: Label{Kind: LabelKindReturn}},       // return!
 				},
@@ -499,8 +499,8 @@ func TestCompile_MultiValue(t *testing.T) {
 			//	)
 			expected: &CompilationResult{
 				Operations: []Operation{ // begin with params: [$0]
-					NewOperationConstI32(1), // [$0, 1]
-					OperationPick{Depth: 1}, // [$0, 1, $0]
+					NewOperationConstI32(1),    // [$0, 1]
+					NewOperationPick(1, false), // [$0, 1, $0]
 					OperationBrIf{ // [$0, 1]
 						Then: BranchTargetDrop{Target: Label{FrameID: 2, Kind: LabelKindHeader}},
 						Else: BranchTargetDrop{Target: Label{FrameID: 2, Kind: LabelKindElse}},
@@ -557,9 +557,9 @@ func TestCompile_MultiValue(t *testing.T) {
 			//	)
 			expected: &CompilationResult{
 				Operations: []Operation{ // begin with params: [$0]
-					NewOperationConstI32(1), // [$0, 1]
-					NewOperationConstI32(2), // [$0, 1, 2]
-					OperationPick{Depth: 2}, // [$0, 1, 2, $0]
+					NewOperationConstI32(1),    // [$0, 1]
+					NewOperationConstI32(2),    // [$0, 1, 2]
+					NewOperationPick(2, false), // [$0, 1, 2, $0]
 					OperationBrIf{ // [$0, 1, 2]
 						Then: BranchTargetDrop{Target: Label{FrameID: 2, Kind: LabelKindHeader}},
 						Else: BranchTargetDrop{Target: Label{FrameID: 2, Kind: LabelKindElse}},
@@ -614,9 +614,9 @@ func TestCompile_MultiValue(t *testing.T) {
 			//	)
 			expected: &CompilationResult{
 				Operations: []Operation{ // begin with params: [$0]
-					NewOperationConstI32(1), // [$0, 1]
-					NewOperationConstI32(2), // [$0, 1, 2]
-					OperationPick{Depth: 2}, // [$0, 1, 2, $0]
+					NewOperationConstI32(1),    // [$0, 1]
+					NewOperationConstI32(2),    // [$0, 1, 2]
+					NewOperationPick(2, false), // [$0, 1, 2, $0]
 					OperationBrIf{ // [$0, 1, 2]
 						Then: BranchTargetDrop{Target: Label{FrameID: 2, Kind: LabelKindHeader}},
 						Else: BranchTargetDrop{Target: Label{FrameID: 2, Kind: LabelKindElse}},
@@ -675,7 +675,7 @@ func TestCompile_NonTrappingFloatToIntConversion(t *testing.T) {
 
 	expected := &CompilationResult{
 		Operations: []Operation{ // begin with params: [$0]
-			OperationPick{Depth: 0}, // [$0, $0]
+			NewOperationPick(0, false), // [$0, $0]
 			OperationITruncFromF{ // [$0, i32.trunc_sat_f32_s($0)]
 				InputType:   Float32,
 				OutputType:  SignedInt32,
@@ -710,7 +710,7 @@ func TestCompile_SignExtensionOps(t *testing.T) {
 
 	expected := &CompilationResult{
 		Operations: []Operation{ // begin with params: [$0]
-			OperationPick{Depth: 0},                                 // [$0, $0]
+			NewOperationPick(0, false),                              // [$0, $0]
 			NewOperationSignExtend32From8(),                         // [$0, i32.extend8_s($0)]
 			OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // [i32.extend8_s($0)]
 			OperationBr{Target: Label{Kind: LabelKindReturn}},       // return!
@@ -762,7 +762,7 @@ func TestCompile_CallIndirectNonZeroTableIndex(t *testing.T) {
 	expected := &CompilationResult{
 		Operations: []Operation{ // begin with params: []
 			NewOperationConstI32(0),
-			OperationCallIndirect{TypeIndex: 2, TableIndex: 5},
+			NewOperationCallIndirect(2, 5),
 			OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 		},
 		HasTable:     true,
@@ -1028,7 +1028,7 @@ func TestCompile_Locals(t *testing.T) {
 				}}},
 			},
 			expected: []Operation{
-				OperationPick{Depth: 1, IsTargetVector: true}, // [param[0].low, param[0].high] -> [param[0].low, param[0].high, param[0].low, param[0].high]
+				NewOperationPick(1, true), // [param[0].low, param[0].high] -> [param[0].low, param[0].high, param[0].low, param[0].high]
 				OperationDrop{Depth: &InclusiveRange{Start: 0, End: 3}},
 				OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 			},
@@ -1044,7 +1044,7 @@ func TestCompile_Locals(t *testing.T) {
 				}}},
 			},
 			expected: []Operation{
-				OperationPick{Depth: 0, IsTargetVector: false}, // [param[0]] -> [param[0], param[0]]
+				NewOperationPick(0, false), // [param[0]] -> [param[0], param[0]]
 				OperationDrop{Depth: &InclusiveRange{Start: 0, End: 1}},
 				OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 			},
@@ -1064,7 +1064,7 @@ func TestCompile_Locals(t *testing.T) {
 			},
 			expected: []Operation{
 				OperationV128Const{Lo: 0, Hi: 0},
-				OperationPick{Depth: 1, IsTargetVector: true}, // [p[0].low, p[0].high] -> [p[0].low, p[0].high, p[0].low, p[0].high]
+				NewOperationPick(1, true), // [p[0].low, p[0].high] -> [p[0].low, p[0].high, p[0].low, p[0].high]
 				OperationDrop{Depth: &InclusiveRange{Start: 0, End: 3}},
 				OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 			},
@@ -1086,7 +1086,7 @@ func TestCompile_Locals(t *testing.T) {
 				// [p[0].lo, p[1].hi] -> [p[0].lo, p[1].hi, 0x01, 0x02]
 				OperationV128Const{Lo: 0x01, Hi: 0x02},
 				// [p[0].lo, p[1].hi, 0x01, 0x02] -> [0x01, 0x02]
-				OperationSet{Depth: 3, IsTargetVector: true},
+				NewOperationSet(3, true),
 				OperationDrop{Depth: &InclusiveRange{Start: 0, End: 1}},
 				OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 			},
@@ -1104,7 +1104,7 @@ func TestCompile_Locals(t *testing.T) {
 			},
 			expected: []Operation{
 				NewOperationConstI32(0x1),
-				OperationSet{Depth: 1, IsTargetVector: false},
+				NewOperationSet(1, false),
 				OperationDrop{Depth: &InclusiveRange{Start: 0, End: 0}},
 				OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 			},
@@ -1130,7 +1130,7 @@ func TestCompile_Locals(t *testing.T) {
 				// [p[0].lo, p[1].hi] -> [p[0].lo, p[1].hi, 0x01, 0x02]
 				OperationV128Const{Lo: 0x01, Hi: 0x02},
 				// [p[0].lo, p[1].hi, 0x01, 0x02] -> [0x01, 0x02]
-				OperationSet{Depth: 3, IsTargetVector: true},
+				NewOperationSet(3, true),
 				OperationDrop{Depth: &InclusiveRange{Start: 0, End: 1}},
 				OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 			},
@@ -1152,9 +1152,9 @@ func TestCompile_Locals(t *testing.T) {
 				// [p[0].lo, p[1].hi] -> [p[0].lo, p[1].hi, 0x01, 0x02]
 				OperationV128Const{Lo: 0x01, Hi: 0x02},
 				// [p[0].lo, p[1].hi, 0x01, 0x02] -> [p[0].lo, p[1].hi, 0x01, 0x02, 0x01, 0x02]
-				OperationPick{Depth: 1, IsTargetVector: true},
+				NewOperationPick(1, true),
 				// [p[0].lo, p[1].hi, 0x01, 0x02, 0x01, 0x02] -> [0x01, 0x02, 0x01, 0x02]
-				OperationSet{Depth: 5, IsTargetVector: true},
+				NewOperationSet(5, true),
 				OperationDrop{Depth: &InclusiveRange{Start: 0, End: 3}},
 				OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 			},
@@ -1172,8 +1172,8 @@ func TestCompile_Locals(t *testing.T) {
 			},
 			expected: []Operation{
 				NewOperationConstF32(math.Float32frombits(1)),
-				OperationPick{Depth: 0, IsTargetVector: false},
-				OperationSet{Depth: 2, IsTargetVector: false},
+				NewOperationPick(0, false),
+				NewOperationSet(2, false),
 				OperationDrop{Depth: &InclusiveRange{Start: 0, End: 1}},
 				OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 			},
@@ -1199,9 +1199,9 @@ func TestCompile_Locals(t *testing.T) {
 				// [p[0].lo, p[1].hi] -> [p[0].lo, p[1].hi, 0x01, 0x02]
 				OperationV128Const{Lo: 0x01, Hi: 0x02},
 				// [p[0].lo, p[1].hi, 0x01, 0x02] -> [p[0].lo, p[1].hi, 0x01, 0x02, 0x01, 0x02]
-				OperationPick{Depth: 1, IsTargetVector: true},
+				NewOperationPick(1, true),
 				// [p[0].lo, p[1].hi, 0x01, 0x02, 0x01, 0x2] -> [0x01, 0x02, 0x01, 0x02]
-				OperationSet{Depth: 5, IsTargetVector: true},
+				NewOperationSet(5, true),
 				OperationDrop{Depth: &InclusiveRange{Start: 0, End: 3}},
 				OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 			},
@@ -2938,7 +2938,7 @@ func TestCompile_select_vectors(t *testing.T) {
 				OperationV128Const{Lo: 0x1, Hi: 0x2},
 				OperationV128Const{Lo: 0x3, Hi: 0x4},
 				NewOperationConstI32(0),
-				OperationSelect{IsTargetVector: true},
+				NewOperationSelect(true),
 				OperationDrop{Depth: &InclusiveRange{Start: 0, End: 1}},
 				OperationBr{Target: Label{Kind: LabelKindReturn}},
 			},
@@ -2964,7 +2964,7 @@ func TestCompile_select_vectors(t *testing.T) {
 				OperationV128Const{Lo: 0x1, Hi: 0x2},
 				OperationV128Const{Lo: 0x3, Hi: 0x4},
 				NewOperationConstI32(0),
-				OperationSelect{IsTargetVector: true},
+				NewOperationSelect(true),
 				OperationDrop{Depth: &InclusiveRange{Start: 0, End: 1}},
 				OperationBr{Target: Label{Kind: LabelKindReturn}},
 			},
diff --git a/internal/wazeroir/operations_test.go b/internal/wazeroir/operations_test.go
index 1e5ae8f4..8fc09d35 100644
--- a/internal/wazeroir/operations_test.go
+++ b/internal/wazeroir/operations_test.go
@@ -16,11 +16,20 @@ func TestOperationKind_String(t *testing.T) {
 // TestUnionOperation_String ensures that UnionOperation's stringer is well-defined for all supported OpKinds.
 func TestUnionOperation_String(t *testing.T) {
 	op := UnionOperation{}
+	// TODO: after done with union refactoring, use
+	// `for k := OperationKind(0); k < operationKindEnd; k++ { ... }`
+	// rather than listing all kinds here manually like TestOperationKind_String.
 	for _, k := range []OperationKind{
 		OperationKindUnreachable,
 		OperationKindCall,
+		OperationKindCallIndirect,
+		OperationKindSelect,
 		OperationKindGlobalGet,
 		OperationKindGlobalSet,
+		OperationKindLoad,
+		OperationKindLoad8,
+		OperationKindLoad16,
+		OperationKindLoad32,
 		OperationKindMemorySize,
 		OperationKindMemoryGrow,
 		OperationKindConstI32,