Repository: tetratelabs/wazero
Pull request #1633 (2023-08-16 23:30:50)

wazevo: adds support for store instructions
#1496 


Patch:
diff --git a/internal/engine/wazevo/backend/isa/arm64/instr.go b/internal/engine/wazevo/backend/isa/arm64/instr.go
index c318a442..4f4a60ea 100644
--- a/internal/engine/wazevo/backend/isa/arm64/instr.go
+++ b/internal/engine/wazevo/backend/isa/arm64/instr.go
@@ -61,6 +61,8 @@ var defKinds = [numInstructionKinds]defKind{
 	call:            defKindCall,
 	callInd:         defKindCall,
 	ret:             defKindNone,
+	store8:          defKindNone,
+	store16:         defKindNone,
 	store32:         defKindNone,
 	store64:         defKindNone,
 	exitSequence:    defKindNone,
@@ -149,6 +151,8 @@ var useKinds = [numInstructionKinds]useKind{
 	call:            useKindCall,
 	callInd:         useKindCallInd,
 	ret:             useKindRet,
+	store8:          useKindRNAMode,
+	store16:         useKindRNAMode,
 	store32:         useKindRNAMode,
 	store64:         useKindRNAMode,
 	exitSequence:    useKindRN,
@@ -705,9 +709,9 @@ func (i *instruction) String() (str string) {
 	case uLoad64:
 		str = fmt.Sprintf("ldr %s, %s", formatVRegSized(i.rd.nr(), 64), i.amode.format(64))
 	case store8:
-		panic("TODO")
+		str = fmt.Sprintf("strb %s, %s", formatVRegSized(i.rn.nr(), 32), i.amode.format(8))
 	case store16:
-		panic("TODO")
+		str = fmt.Sprintf("strh %s, %s", formatVRegSized(i.rn.nr(), 32), i.amode.format(16))
 	case store32:
 		str = fmt.Sprintf("str %s, %s", formatVRegSized(i.rn.nr(), 32), i.amode.format(32))
 	case store64:
diff --git a/internal/engine/wazevo/call_engine.go b/internal/engine/wazevo/call_engine.go
index 62a3ff71..e546d0ea 100644
--- a/internal/engine/wazevo/call_engine.go
+++ b/internal/engine/wazevo/call_engine.go
@@ -33,7 +33,8 @@ type (
 		// execCtx holds various information to be read/written by assembly functions.
 		execCtx executionContext
 		// execCtxPtr holds the pointer to the executionContext which doesn't change after callEngine is created.
-		execCtxPtr uintptr
+		execCtxPtr      uintptr
+		numberOfResults int
 	}
 
 	// executionContext is the struct to be read/written by assembly functions.
@@ -103,7 +104,7 @@ func (c *callEngine) Call(ctx context.Context, params ...uint64) ([]uint64, erro
 	if err := c.CallWithStack(ctx, paramResultSlice); err != nil {
 		return nil, err
 	}
-	return paramResultSlice, nil
+	return paramResultSlice[:c.numberOfResults], nil
 }
 
 // CallWithStack implements api.Function.
diff --git a/internal/engine/wazevo/frontend/lower.go b/internal/engine/wazevo/frontend/lower.go
index b1284910..f867ab42 100644
--- a/internal/engine/wazevo/frontend/lower.go
+++ b/internal/engine/wazevo/frontend/lower.go
@@ -509,6 +509,52 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 		// After the memory grow, reload the cached memory base and len.
 		c.reloadMemoryBaseLen()
+
+	case wasm.OpcodeI32Store,
+		wasm.OpcodeI64Store,
+		wasm.OpcodeF32Store,
+		wasm.OpcodeF64Store,
+		wasm.OpcodeI32Store8,
+		wasm.OpcodeI32Store16,
+		wasm.OpcodeI64Store8,
+		wasm.OpcodeI64Store16,
+		wasm.OpcodeI64Store32:
+
+		_, offset := c.readMemArg()
+		if state.unreachable {
+			return
+		}
+
+		var opSize uint64
+		var opcode ssa.Opcode
+		switch op {
+		case wasm.OpcodeI32Store, wasm.OpcodeF32Store:
+			opcode = ssa.OpcodeStore
+			opSize = 4
+		case wasm.OpcodeI64Store, wasm.OpcodeF64Store:
+			opcode = ssa.OpcodeStore
+			opSize = 8
+		case wasm.OpcodeI32Store8, wasm.OpcodeI64Store8:
+			opcode = ssa.OpcodeIstore8
+			opSize = 1
+		case wasm.OpcodeI32Store16, wasm.OpcodeI64Store16:
+			opcode = ssa.OpcodeIstore16
+			opSize = 2
+		case wasm.OpcodeI64Store32:
+			opcode = ssa.OpcodeIstore32
+			opSize = 4
+		default:
+			panic("BUG")
+		}
+
+		value := state.pop()
+		baseAddr := state.pop()
+		addr := c.memOpSetup(baseAddr, uint64(offset), opSize)
+
+		store := builder.AllocateInstruction()
+		store.AsStore(opcode, value, addr, offset)
+		builder.InsertInstruction(store)
+
 	case wasm.OpcodeI32Load,
 		wasm.OpcodeI64Load,
 		wasm.OpcodeF32Load,
@@ -528,59 +574,28 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 			return
 		}
 
-		ceil := offset
+		var opSize uint64
 		switch op {
 		case wasm.OpcodeI32Load, wasm.OpcodeF32Load:
-			ceil += 4
+			opSize = 4
 		case wasm.OpcodeI64Load, wasm.OpcodeF64Load:
-			ceil += 8
+			opSize = 8
 		case wasm.OpcodeI32Load8S, wasm.OpcodeI32Load8U:
-			ceil += 1
+			opSize = 1
 		case wasm.OpcodeI32Load16S, wasm.OpcodeI32Load16U:
-			ceil += 2
+			opSize = 2
 		case wasm.OpcodeI64Load8S, wasm.OpcodeI64Load8U:
-			ceil += 1
+			opSize = 1
 		case wasm.OpcodeI64Load16S, wasm.OpcodeI64Load16U:
-			ceil += 2
+			opSize = 2
 		case wasm.OpcodeI64Load32S, wasm.OpcodeI64Load32U:
-			ceil += 4
+			opSize = 4
 		default:
 			panic("BUG")
 		}
 
-		ceilConst := builder.AllocateInstruction()
-		ceilConst.AsIconst64(uint64(ceil))
-		builder.InsertInstruction(ceilConst)
-
-		// We calculate the offset in 64-bit space.
 		baseAddr := state.pop()
-		extBaseAddr := builder.AllocateInstruction()
-		extBaseAddr.AsUExtend(baseAddr, 32, 64)
-		builder.InsertInstruction(extBaseAddr)
-
-		// Note: memLen is already zero extended to 64-bit space at the load time.
-		memLen := c.getMemoryLenValue(false)
-
-		// baseAddrPlusCeil = baseAddr + ceil
-		baseAddrPlusCeil := builder.AllocateInstruction()
-		baseAddrPlusCeil.AsIadd(extBaseAddr.Return(), ceilConst.Return())
-		builder.InsertInstruction(baseAddrPlusCeil)
-
-		// Check for out of bounds memory access: `memLen >= baseAddrPlusCeil`.
-		cmp := builder.AllocateInstruction()
-		cmp.AsIcmp(memLen, baseAddrPlusCeil.Return(), ssa.IntegerCmpCondUnsignedLessThan)
-		builder.InsertInstruction(cmp)
-		exitIfNZ := builder.AllocateInstruction()
-		exitIfNZ.AsExitIfTrueWithCode(c.execCtxPtrValue, cmp.Return(), wazevoapi.ExitCodeMemoryOutOfBounds)
-		builder.InsertInstruction(exitIfNZ)
-
-		// Load the value from memBase + extBaseAddr.
-		memBase := c.getMemoryBaseValue(false)
-		addrCalc := builder.AllocateInstruction()
-		addrCalc.AsIadd(memBase, extBaseAddr.Return())
-		builder.InsertInstruction(addrCalc)
-
-		addr := addrCalc.Return()
+		addr := c.memOpSetup(baseAddr, uint64(offset), opSize)
 		load := builder.AllocateInstruction()
 		switch op {
 		case wasm.OpcodeI32Load:
@@ -1059,6 +1074,44 @@ func (c *Compiler) lowerCallIndirect(typeIndex, tableIndex uint32) {
 	c.reloadAfterCall()
 }
 
+// memOpSetup inserts the bounds check and calculates the address of the memory operation (loads/stores).
+func (c *Compiler) memOpSetup(baseAddr ssa.Value, constOffset, operationSizeInBytes uint64) (address ssa.Value) {
+	builder := c.ssaBuilder
+
+	ceil := constOffset + operationSizeInBytes
+	ceilConst := builder.AllocateInstruction()
+	ceilConst.AsIconst64(ceil)
+	builder.InsertInstruction(ceilConst)
+
+	// We calculate the offset in 64-bit space.
+	extBaseAddr := builder.AllocateInstruction()
+	extBaseAddr.AsUExtend(baseAddr, 32, 64)
+	builder.InsertInstruction(extBaseAddr)
+
+	// Note: memLen is already zero extended to 64-bit space at the load time.
+	memLen := c.getMemoryLenValue(false)
+
+	// baseAddrPlusCeil = baseAddr + ceil
+	baseAddrPlusCeil := builder.AllocateInstruction()
+	baseAddrPlusCeil.AsIadd(extBaseAddr.Return(), ceilConst.Return())
+	builder.InsertInstruction(baseAddrPlusCeil)
+
+	// Check for out of bounds memory access: `memLen >= baseAddrPlusCeil`.
+	cmp := builder.AllocateInstruction()
+	cmp.AsIcmp(memLen, baseAddrPlusCeil.Return(), ssa.IntegerCmpCondUnsignedLessThan)
+	builder.InsertInstruction(cmp)
+	exitIfNZ := builder.AllocateInstruction()
+	exitIfNZ.AsExitIfTrueWithCode(c.execCtxPtrValue, cmp.Return(), wazevoapi.ExitCodeMemoryOutOfBounds)
+	builder.InsertInstruction(exitIfNZ)
+
+	// Load the value from memBase + extBaseAddr.
+	memBase := c.getMemoryBaseValue(false)
+	addrCalc := builder.AllocateInstruction()
+	addrCalc.AsIadd(memBase, extBaseAddr.Return())
+	builder.InsertInstruction(addrCalc)
+	return addrCalc.Return()
+}
+
 func (c *Compiler) reloadAfterCall() {
 	// Note that when these are not used in the following instructions, they will be optimized out.
 	// So in any ways, we define them!
@@ -1092,7 +1145,7 @@ func (c *Compiler) setWasmGlobalValue(index wasm.Index, v ssa.Value) {
 	builder.InsertInstruction(loadGlobalInstPtr)
 
 	store := builder.AllocateInstruction()
-	store.AsStore(v, loadGlobalInstPtr.Return(), uint32(globalInstanceValueOffset))
+	store.AsStore(ssa.OpcodeStore, v, loadGlobalInstPtr.Return(), uint32(globalInstanceValueOffset))
 	builder.InsertInstruction(store)
 
 	// The value has changed to `v`, so we record it.
@@ -1216,7 +1269,8 @@ func (c *Compiler) storeCallerModuleContext() {
 	builder := c.ssaBuilder
 	execCtx := c.execCtxPtrValue
 	store := builder.AllocateInstruction()
-	store.AsStore(c.moduleCtxPtrValue, execCtx, wazevoapi.ExecutionContextOffsets.CallerModuleContextPtr.U32())
+	store.AsStore(ssa.OpcodeStore,
+		c.moduleCtxPtrValue, execCtx, wazevoapi.ExecutionContextOffsets.CallerModuleContextPtr.U32())
 	builder.InsertInstruction(store)
 }
 
diff --git a/internal/engine/wazevo/module_engine.go b/internal/engine/wazevo/module_engine.go
index 1c4fc622..0bf801f1 100644
--- a/internal/engine/wazevo/module_engine.go
+++ b/internal/engine/wazevo/module_engine.go
@@ -117,6 +117,7 @@ func (m *moduleEngine) NewFunction(index wasm.Index) api.Function {
 		executable:             &p.executable[offset.offset],
 		parent:                 m,
 		sizeOfParamResultSlice: sizeOfParamResultSlice,
+		numberOfResults:        typ.ResultNumInUint64,
 	}
 
 	ce.execCtx.memoryGrowTrampolineAddress = &m.parent.builtinFunctions.memoryGrowExecutable[0]
@@ -151,7 +152,8 @@ func getTypeIDOf(funcIndex wasm.Index, m *wasm.ModuleInstance) wasm.FunctionType
 		for i := range source.ImportSection {
 			if source.ImportSection[i].Type == wasm.ExternTypeFunc {
 				if cnt == funcIndex {
-					return m.TypeIDs[source.ImportSection[i].DescFunc]
+					typeIndex = source.ImportSection[i].DescFunc
+					break
 				}
 				cnt++
 			}
diff --git a/internal/engine/wazevo/ssa/instructions.go b/internal/engine/wazevo/ssa/instructions.go
index 0ab771b6..6f58b974 100644
--- a/internal/engine/wazevo/ssa/instructions.go
+++ b/internal/engine/wazevo/ssa/instructions.go
@@ -857,6 +857,9 @@ var instructionSideEffects = [opcodeEnd]sideEffect{
 	OpcodeSshr:               sideEffectFalse,
 	OpcodeUshr:               sideEffectFalse,
 	OpcodeStore:              sideEffectTrue,
+	OpcodeIstore8:            sideEffectTrue,
+	OpcodeIstore16:           sideEffectTrue,
+	OpcodeIstore32:           sideEffectTrue,
 	OpcodeExitWithCode:       sideEffectTrue,
 	OpcodeExitIfTrueWithCode: sideEffectTrue,
 	OpcodeReturn:             sideEffectTrue,
@@ -934,6 +937,9 @@ var instructionReturnTypes = [opcodeEnd]returnTypesFn{
 	OpcodeF32const:           returnTypesFnF32,
 	OpcodeF64const:           returnTypesFnF64,
 	OpcodeStore:              returnTypesFnNoReturns,
+	OpcodeIstore8:            returnTypesFnNoReturns,
+	OpcodeIstore16:           returnTypesFnNoReturns,
+	OpcodeIstore32:           returnTypesFnNoReturns,
 	OpcodeExitWithCode:       returnTypesFnNoReturns,
 	OpcodeExitIfTrueWithCode: returnTypesFnNoReturns,
 	OpcodeReturn:             returnTypesFnNoReturns,
@@ -973,11 +979,25 @@ func (i *Instruction) LoadData() (ptr Value, offset uint32, typ Type) {
 }
 
 // AsStore initializes this instruction as a store instruction with OpcodeStore.
-func (i *Instruction) AsStore(value, ptr Value, offset uint32) {
-	i.opcode = OpcodeStore
+func (i *Instruction) AsStore(storeOp Opcode, value, ptr Value, offset uint32) {
+	i.opcode = storeOp
 	i.v = value
 	i.v2 = ptr
-	i.u64 = uint64(offset) | uint64(value.Type().Bits())<<32
+
+	var dstSize uint64
+	switch storeOp {
+	case OpcodeStore:
+		dstSize = uint64(value.Type().Bits())
+	case OpcodeIstore8:
+		dstSize = 8
+	case OpcodeIstore16:
+		dstSize = 16
+	case OpcodeIstore32:
+		dstSize = 32
+	default:
+		panic("invalid store opcode" + storeOp.String())
+	}
+	i.u64 = uint64(offset) | dstSize<<32
 }
 
 // StoreData returns the operands for a store instruction.
@@ -1353,7 +1373,7 @@ func (i *Instruction) Format(b Builder) string {
 		} else {
 			instSuffix = fmt.Sprintf(" %s:%s, %s", FuncRef(i.u64), SignatureID(i.v), strings.Join(vs, ", "))
 		}
-	case OpcodeStore:
+	case OpcodeStore, OpcodeIstore8, OpcodeIstore16, OpcodeIstore32:
 		instSuffix = fmt.Sprintf(" %s, %s, %#x", i.v.Format(b), i.v2.Format(b), int32(i.u64))
 	case OpcodeLoad:
 		instSuffix = fmt.Sprintf(" %s, %#x", i.v.Format(b), int32(i.u64))
@@ -1401,6 +1421,7 @@ func (i *Instruction) Format(b Builder) string {
 		instSuffix = strings.Join(vs, ", ")
 	case OpcodeIshl, OpcodeSshr, OpcodeUshr:
 		instSuffix = fmt.Sprintf(" %s, %s", i.v.Format(b), i.v2.Format(b))
+	case OpcodeUndefined:
 	default:
 		panic(fmt.Sprintf("TODO: format for %s", i.opcode))
 	}
@@ -1460,7 +1481,7 @@ func (o Opcode) String() (ret string) {
 	case OpcodeInvalid:
 		return "invalid"
 	case OpcodeUndefined:
-		return "undefined"
+		return "Undefined"
 	case OpcodeJump:
 		return "Jump"
 	case OpcodeBrz:


Test patch:
diff --git a/internal/engine/wazevo/backend/backend_test.go b/internal/engine/wazevo/backend/backend_test.go
index 71ffa0d5..e1f20f25 100644
--- a/internal/engine/wazevo/backend/backend_test.go
+++ b/internal/engine/wazevo/backend/backend_test.go
@@ -1914,6 +1914,107 @@ L1 (SSA Block: blk0):
 	ldr w0, [x8]
 	ldr x30, [sp], #0x10
 	ret
+`,
+		},
+		{
+			name: "memory_stores", m: testcases.MemoryStores.Module,
+			afterFinalizeARM64: `
+L1 (SSA Block: blk0):
+	str x30, [sp, #-0x10]!
+	mov x8, xzr
+	uxtw x10, w8
+	ldr w8, [x1, #0x10]
+	add x9, x10, #0x4
+	subs xzr, x8, x9
+	b.hs #0x20
+	movz x27, #0x4, LSL 0
+	str w27, [x0]
+	exit_sequence x0
+	ldr x9, [x1, #0x8]
+	add x10, x9, x10
+	str w2, [x10]
+	orr w10, wzr, #0x8
+	uxtw x11, w10
+	add x10, x11, #0x8
+	subs xzr, x8, x10
+	b.hs #0x20
+	movz x27, #0x4, LSL 0
+	str w27, [x0]
+	exit_sequence x0
+	add x10, x9, x11
+	str x3, [x10]
+	orr w10, wzr, #0x10
+	uxtw x11, w10
+	add x10, x11, #0x4
+	subs xzr, x8, x10
+	b.hs #0x20
+	movz x27, #0x4, LSL 0
+	str w27, [x0]
+	exit_sequence x0
+	add x10, x9, x11
+	str s0, [x10]
+	orr w10, wzr, #0x18
+	uxtw x11, w10
+	add x10, x11, #0x8
+	subs xzr, x8, x10
+	b.hs #0x20
+	movz x27, #0x4, LSL 0
+	str w27, [x0]
+	exit_sequence x0
+	add x10, x9, x11
+	str d1, [x10]
+	orr w10, wzr, #0x20
+	uxtw x11, w10
+	add x10, x11, #0x1
+	subs xzr, x8, x10
+	b.hs #0x20
+	movz x27, #0x4, LSL 0
+	str w27, [x0]
+	exit_sequence x0
+	add x10, x9, x11
+	strb w2, [x10]
+	movz w10, #0x28, LSL 0
+	uxtw x11, w10
+	add x10, x11, #0x2
+	subs xzr, x8, x10
+	b.hs #0x20
+	movz x27, #0x4, LSL 0
+	str w27, [x0]
+	exit_sequence x0
+	add x10, x9, x11
+	strh w2, [x10]
+	orr w10, wzr, #0x30
+	uxtw x11, w10
+	add x10, x11, #0x1
+	subs xzr, x8, x10
+	b.hs #0x20
+	movz x27, #0x4, LSL 0
+	str w27, [x0]
+	exit_sequence x0
+	add x10, x9, x11
+	strb w3, [x10]
+	orr w10, wzr, #0x38
+	uxtw x11, w10
+	add x10, x11, #0x2
+	subs xzr, x8, x10
+	b.hs #0x20
+	movz x27, #0x4, LSL 0
+	str w27, [x0]
+	exit_sequence x0
+	add x10, x9, x11
+	strh w3, [x10]
+	orr w10, wzr, #0x40
+	uxtw x11, w10
+	add x10, x11, #0x4
+	subs xzr, x8, x10
+	b.hs #0x20
+	movz x27, #0x4, LSL 0
+	str w27, [x0]
+	exit_sequence x0
+	add x8, x9, x11
+	str w3, [x8]
+	ldr x30, [sp], #0x10
+	ret
 `,
 		},
 		{
@@ -2667,7 +2768,9 @@ L1 (SSA Block: blk0):
 
 			switch runtime.GOARCH {
 			case "arm64":
-				require.Equal(t, tc.afterLoweringARM64, be.Format())
+				if tc.afterLoweringARM64 != "" {
+					require.Equal(t, tc.afterLoweringARM64, be.Format())
+				}
 			default:
 				t.Fail()
 			}
diff --git a/internal/engine/wazevo/e2e_test.go b/internal/engine/wazevo/e2e_test.go
index 0f933997..00994c29 100644
--- a/internal/engine/wazevo/e2e_test.go
+++ b/internal/engine/wazevo/e2e_test.go
@@ -2,6 +2,7 @@ package wazevo_test
 
 import (
 	"context"
+	"encoding/binary"
 	"fmt"
 	"math"
 	"testing"
@@ -30,10 +31,9 @@ func TestE2E(t *testing.T) {
 		expErr             string
 	}
 	for _, tc := range []struct {
-		name           string
-		imported, m    *wasm.Module
-		needHostModule bool
-		calls          []callCase
+		name        string
+		imported, m *wasm.Module
+		calls       []callCase
 	}{
 		{
 			name: "swap", m: testcases.SwapParamAndReturn.Module,
@@ -87,6 +87,18 @@ func TestE2E(t *testing.T) {
 				{params: []uint64{100}, expResults: []uint64{100 * 100}},
 			},
 		},
+		{
+			name: "memory_store_basic",
+			m:    testcases.MemoryStoreBasic.Module,
+			calls: []callCase{
+				{params: []uint64{0, 0xf}, expResults: []uint64{0xf}},
+				{params: []uint64{256, 0xff}, expResults: []uint64{0xff}},
+				{params: []uint64{100, 0xffffffff}, expResults: []uint64{0xffffffff}},
+				// We load I32, so we can't load from the last 3 bytes.
+				{params: []uint64{uint64(wasm.MemoryPageSize) - 3}, expErr: "out of bounds memory access"},
+			},
+		},
+
 		{
 			name: "memory_load_basic",
 			m:    testcases.MemoryLoadBasic.Module,
@@ -320,3 +332,73 @@ func TestE2E_host_functions(t *testing.T) {
 		math.Float64bits(math.Sqrt(35.0)),
 	}, res)
 }
+
+func TestE2E_stores(t *testing.T) {
+	config := wazero.NewRuntimeConfigCompiler()
+
+	// Configure the new optimizing backend!
+	configureWazevo(config)
+
+	ctx := context.Background()
+	r := wazero.NewRuntimeWithConfig(ctx, config)
+	defer func() {
+		require.NoError(t, r.Close(ctx))
+	}()
+
+	compiled, err := r.CompileModule(ctx, binaryencoding.EncodeModule(testcases.MemoryStores.Module))
+	require.NoError(t, err)
+
+	inst, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig())
+	require.NoError(t, err)
+
+	f := inst.ExportedFunction("f")
+
+	mem, ok := inst.Memory().Read(0, wasm.MemoryPageSize)
+	require.True(t, ok)
+	for _, tc := range []struct {
+		i32 uint32
+		i64 uint64
+		f32 float32
+		f64 float64
+	}{
+		{0, 0, 0, 0},
+		{1, 2, 3.0, 4.0},
+		{math.MaxUint32, math.MaxUint64, float32(math.NaN()), math.NaN()},
+		{1 << 31, 1 << 63, 3.0, 4.0},
+	} {
+		t.Run(fmt.Sprintf("i32=%#x,i64=%#x,f32=%#x,f64=%#x", tc.i32, tc.i64, tc.f32, tc.f64), func(t *testing.T) {
+			_, err = f.Call(ctx, []uint64{uint64(tc.i32), tc.i64, uint64(math.Float32bits(tc.f32)), math.Float64bits(tc.f64)}...)
+			require.NoError(t, err)
+
+			offset := 0
+			require.Equal(t, binary.LittleEndian.Uint32(mem[offset:]), tc.i32)
+			offset += 8
+			require.Equal(t, binary.LittleEndian.Uint64(mem[offset:]), tc.i64)
+			offset += 8
+			require.Equal(t, math.Float32bits(tc.f32), binary.LittleEndian.Uint32(mem[offset:]))
+			offset += 8
+			require.Equal(t, math.Float64bits(tc.f64), binary.LittleEndian.Uint64(mem[offset:]))
+			offset += 8
+
+			// i32.store_8
+			view := binary.LittleEndian.Uint64(mem[offset:])
+			require.Equal(t, uint64(tc.i32)&0xff, view)
+			offset += 8
+			// i32.store_16
+			view = binary.LittleEndian.Uint64(mem[offset:])
+			require.Equal(t, uint64(tc.i32)&0xffff, view)
+			offset += 8
+			// i64.store_8
+			view = binary.LittleEndian.Uint64(mem[offset:])
+			require.Equal(t, tc.i64&0xff, view)
+			offset += 8
+			// i64.store_16
+			view = binary.LittleEndian.Uint64(mem[offset:])
+			require.Equal(t, tc.i64&0xffff, view)
+			offset += 8
+			// i64.store_32
+			view = binary.LittleEndian.Uint64(mem[offset:])
+			require.Equal(t, tc.i64&0xffffffff, view)
+		})
+	}
+}
diff --git a/internal/engine/wazevo/frontend/frontend_test.go b/internal/engine/wazevo/frontend/frontend_test.go
index 37f990f0..8260b133 100644
--- a/internal/engine/wazevo/frontend/frontend_test.go
+++ b/internal/engine/wazevo/frontend/frontend_test.go
@@ -859,6 +859,29 @@ blk3: () <-- (blk2)
 	v10:i32 = Call f0:sig0, exec_ctx, module_ctx, v9
 	v11:i32 = Iadd v7, v10
 	Jump blk_ret, v11
+`,
+		},
+		{
+			name: "memory_store_basic", m: testcases.MemoryStoreBasic.Module,
+			exp: `
+blk0: (exec_ctx:i64, module_ctx:i64, v2:i32, v3:i32)
+	v4:i64 = Iconst_64 0x4
+	v5:i64 = UExtend v2, 32->64
+	v6:i64 = Uload32 module_ctx, 0x10
+	v7:i64 = Iadd v5, v4
+	v8:i32 = Icmp lt_u, v6, v7
+	ExitIfTrue v8, exec_ctx, memory_out_of_bounds
+	v9:i64 = Load module_ctx, 0x8
+	v10:i64 = Iadd v9, v5
+	Store v3, v10, 0x0
+	v11:i64 = Iconst_64 0x4
+	v12:i64 = UExtend v2, 32->64
+	v13:i64 = Iadd v12, v11
+	v14:i32 = Icmp lt_u, v6, v13
+	ExitIfTrue v14, exec_ctx, memory_out_of_bounds
+	v15:i64 = Iadd v9, v12
+	v16:i32 = Load v15, 0x0
+	Jump blk_ret, v16
 `,
 		},
 		{
diff --git a/internal/engine/wazevo/ssa/pass_test.go b/internal/engine/wazevo/ssa/pass_test.go
index 572d6ddf..f05aff66 100644
--- a/internal/engine/wazevo/ssa/pass_test.go
+++ b/internal/engine/wazevo/ssa/pass_test.go
@@ -200,7 +200,7 @@ blk2: () <-- (blk1)
 
 				// This has side effect.
 				store := b.AllocateInstruction()
-				store.AsStore(refThriceVal, refThriceVal, 0)
+				store.AsStore(OpcodeStore, refThriceVal, refThriceVal, 0)
 				b.InsertInstruction(store)
 
 				iconstDeadInst := b.AllocateInstruction()
diff --git a/internal/engine/wazevo/testcases/testcases.go b/internal/engine/wazevo/testcases/testcases.go
index e6728358..05c4d08a 100644
--- a/internal/engine/wazevo/testcases/testcases.go
+++ b/internal/engine/wazevo/testcases/testcases.go
@@ -779,6 +779,75 @@ var (
 			}}},
 		},
 	}
+
+	MemoryStoreBasic = TestCase{
+		Name: "memory_load_basic",
+		Module: &wasm.Module{
+			TypeSection:     []wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}},
+			ExportSection:   []wasm.Export{{Name: ExportName, Type: wasm.ExternTypeFunc, Index: 0}},
+			MemorySection:   &wasm.Memory{Min: 1},
+			FunctionSection: []wasm.Index{0},
+			CodeSection: []wasm.Code{{Body: []byte{
+				wasm.OpcodeLocalGet, 0, // offset
+				wasm.OpcodeLocalGet, 1, // value
+				wasm.OpcodeI32Store, 0x2, 0x0, // alignment=2 (natural alignment) staticOffset=0
+				// Read back.
+				wasm.OpcodeLocalGet, 0, // offset
+				wasm.OpcodeI32Load, 0x2, 0x0, // alignment=2 (natural alignment) staticOffset=0
+				wasm.OpcodeEnd,
+			}}},
+		},
+	}
+
+	MemoryStores = TestCase{
+		Name: "memory_load_basic",
+		Module: &wasm.Module{
+			TypeSection:     []wasm.FunctionType{{Params: []wasm.ValueType{i32, i64, f32, f64}}},
+			ExportSection:   []wasm.Export{{Name: ExportName, Type: wasm.ExternTypeFunc, Index: 0}},
+			MemorySection:   &wasm.Memory{Min: 1},
+			FunctionSection: []wasm.Index{0},
+			CodeSection: []wasm.Code{{Body: []byte{
+				wasm.OpcodeI32Const, 0, // offset
+				wasm.OpcodeLocalGet, 0, // value
+				wasm.OpcodeI32Store, 0x2, 0x0,
+
+				wasm.OpcodeI32Const, 8, // offset
+				wasm.OpcodeLocalGet, 1, // value
+				wasm.OpcodeI64Store, 0x3, 0x0,
+
+				wasm.OpcodeI32Const, 16, // offset
+				wasm.OpcodeLocalGet, 2, // value
+				wasm.OpcodeF32Store, 0x2, 0x0,
+
+				wasm.OpcodeI32Const, 24, // offset
+				wasm.OpcodeLocalGet, 3, // value
+				wasm.OpcodeF64Store, 0x3, 0x0,
+
+				wasm.OpcodeI32Const, 32,
+				wasm.OpcodeLocalGet, 0, // value
+				wasm.OpcodeI32Store8, 0x0, 0,
+
+				wasm.OpcodeI32Const, 40,
+				wasm.OpcodeLocalGet, 0, // value
+				wasm.OpcodeI32Store16, 0x1, 0,
+
+				wasm.OpcodeI32Const, 48,
+				wasm.OpcodeLocalGet, 1, // value
+				wasm.OpcodeI64Store8, 0x0, 0,
+
+				wasm.OpcodeI32Const, 56,
+				wasm.OpcodeLocalGet, 1, // value
+				wasm.OpcodeI64Store16, 0x1, 0,
+
+				wasm.OpcodeI32Const, 0xc0, 0, // 64 in leb128.
+				wasm.OpcodeLocalGet, 1, // value
+				wasm.OpcodeI64Store32, 0x2, 0,
+
+				wasm.OpcodeEnd,
+			}}},
+		},
+	}
+
 	MemoryLoadBasic = TestCase{
 		Name: "memory_load_basic",
 		Module: &wasm.Module{