Files
wazero/internal/engine/compiler/compiler_memory_test.go
Takeshi Yoneda 0c303258c7 SIMD: implements v128 load, store and lane manipulations. (#588)
This implements various SIMD instructions related to
load, store, and lane manipulations for all engines.

Notablely, now our engines pass the following specification tests:

* simd_address.wast
* simd_const.wast
* simd_align.wast
* simd_laod16_lane.wast
* simd_laod32_lane.wast
* simd_laod64_lane.wast
* simd_laod8_lane.wast
* simd_lane.wast
* simd_load_extend.wast
* simd_load_splat.wast
* simd_load_zero.wast
* simd_store.wast
* simd_store16_lane.wast
* simd_store32_lane.wast
* simd_store64_lane.wast
* simd_store8_lane.wast

part of #484


Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
Co-authored-by: Adrian Cole <adrian@tetrate.io>
2022-06-01 09:30:05 +09:00

482 lines
17 KiB
Go

package compiler
import (
"encoding/binary"
"fmt"
"math"
"testing"
"unsafe"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wazeroir"
)
func TestCompiler_compileMemoryGrow(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, newCompiler, nil)
err := compiler.compilePreamble()
require.NoError(t, err)
err = compiler.compileMemoryGrow()
require.NoError(t, err)
// Emit arbitrary code after MemoryGrow returned so that we can verify
// that the code can set the return address properly.
const expValue uint32 = 100
err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: expValue})
require.NoError(t, err)
err = compiler.compileReturnFunction()
require.NoError(t, err)
// Generate and run the code under test.
code, _, _, err := compiler.compile()
require.NoError(t, err)
env.exec(code)
// After the initial exec, the code must exit with builtin function call status and funcaddress for memory grow.
require.Equal(t, nativeCallStatusCodeCallBuiltInFunction, env.compilerStatus())
require.Equal(t, builtinFunctionIndexMemoryGrow, env.builtinFunctionCallAddress())
// Reenter from the return address.
nativecall(
env.callFrameStackPeek().returnAddress, uintptr(unsafe.Pointer(env.callEngine())),
uintptr(unsafe.Pointer(env.module())),
)
// Check if the code successfully executed the code after builtin function call.
require.Equal(t, expValue, env.stackTopAsUint32())
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
}
func TestCompiler_compileMemorySize(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{HasMemory: true, Signature: &wasm.FunctionType{}})
err := compiler.compilePreamble()
require.NoError(t, err)
// Emit memory.size instructions.
err = compiler.compileMemorySize()
require.NoError(t, err)
// At this point, the size of memory should be pushed onto the stack.
require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
err = compiler.compileReturnFunction()
require.NoError(t, err)
// Generate and run the code under test.
code, _, _, err := compiler.compile()
require.NoError(t, err)
env.exec(code)
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
require.Equal(t, uint32(defaultMemoryPageNumInTest), env.stackTopAsUint32())
}
func TestCompiler_compileLoad(t *testing.T) {
// For testing. Arbitrary number is fine.
loadTargetValue := uint64(0x12_34_56_78_9a_bc_ef_fe)
baseOffset := uint32(100)
arg := &wazeroir.MemoryArg{Offset: 361}
offset := baseOffset + arg.Offset
tests := []struct {
name string
isFloatTarget bool
operationSetupFn func(t *testing.T, compiler compilerImpl)
loadedValueVerifyFn func(t *testing.T, loadedValueAsUint64 uint64)
}{
{
name: "i32.load",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad(&wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeI32})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, uint32(loadTargetValue), uint32(loadedValueAsUint64))
},
},
{
name: "i64.load",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad(&wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeI64})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, loadTargetValue, loadedValueAsUint64)
},
},
{
name: "f32.load",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad(&wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeF32})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, uint32(loadTargetValue), uint32(loadedValueAsUint64))
},
isFloatTarget: true,
},
{
name: "f64.load",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad(&wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeF64})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, loadTargetValue, loadedValueAsUint64)
},
isFloatTarget: true,
},
{
name: "i32.load8s",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad8(&wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedInt32})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, int32(int8(loadedValueAsUint64)), int32(uint32(loadedValueAsUint64)))
},
},
{
name: "i32.load8u",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad8(&wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedUint32})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, uint32(byte(loadedValueAsUint64)), uint32(loadedValueAsUint64))
},
},
{
name: "i64.load8s",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad8(&wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedInt64})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, int64(int8(loadedValueAsUint64)), int64(loadedValueAsUint64))
},
},
{
name: "i64.load8u",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad8(&wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedUint64})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, uint64(byte(loadedValueAsUint64)), loadedValueAsUint64)
},
},
{
name: "i32.load16s",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad16(&wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedInt32})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, int32(int16(loadedValueAsUint64)), int32(uint32(loadedValueAsUint64)))
},
},
{
name: "i32.load16u",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad16(&wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedUint32})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, uint32(loadedValueAsUint64), uint32(loadedValueAsUint64))
},
},
{
name: "i64.load16s",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad16(&wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedInt64})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, int64(int16(loadedValueAsUint64)), int64(loadedValueAsUint64))
},
},
{
name: "i64.load16u",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad16(&wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedUint64})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, uint64(uint16(loadedValueAsUint64)), loadedValueAsUint64)
},
},
{
name: "i64.load32s",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad32(&wazeroir.OperationLoad32{Arg: arg, Signed: true})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, int64(int32(loadedValueAsUint64)), int64(loadedValueAsUint64))
},
},
{
name: "i64.load32u",
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileLoad32(&wazeroir.OperationLoad32{Arg: arg, Signed: false})
require.NoError(t, err)
},
loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) {
require.Equal(t, uint64(uint32(loadedValueAsUint64)), loadedValueAsUint64)
},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{HasMemory: true, Signature: &wasm.FunctionType{}})
err := compiler.compilePreamble()
require.NoError(t, err)
binary.LittleEndian.PutUint64(env.memory()[offset:], loadTargetValue)
// Before load operation, we must push the base offset value.
err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: baseOffset})
require.NoError(t, err)
tc.operationSetupFn(t, compiler)
// At this point, the loaded value must be on top of the stack, and placed on a register.
require.Equal(t, uint64(1), compiler.runtimeValueLocationStack().sp)
require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
loadedLocation := compiler.runtimeValueLocationStack().peek()
require.True(t, loadedLocation.onRegister())
if tc.isFloatTarget {
require.Equal(t, registerTypeVector, loadedLocation.getRegisterType())
} else {
require.Equal(t, registerTypeGeneralPurpose, loadedLocation.getRegisterType())
}
err = compiler.compileReturnFunction()
require.NoError(t, err)
// Generate and run the code under test.
code, _, _, err := compiler.compile()
require.NoError(t, err)
env.exec(code)
// Verify the loaded value.
require.Equal(t, uint64(1), env.stackPointer())
tc.loadedValueVerifyFn(t, env.stackTopAsUint64())
})
}
}
func TestCompiler_compileStore(t *testing.T) {
// For testing. Arbitrary number is fine.
storeTargetValue := uint64(math.MaxUint64)
baseOffset := uint32(100)
arg := &wazeroir.MemoryArg{Offset: 361}
offset := arg.Offset + baseOffset
tests := []struct {
name string
isFloatTarget bool
targetSizeInBytes uint32
operationSetupFn func(t *testing.T, compiler compilerImpl)
storedValueVerifyFn func(t *testing.T, mem []byte)
}{
{
name: "i32.store",
targetSizeInBytes: 32 / 8,
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileStore(&wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeI32})
require.NoError(t, err)
},
storedValueVerifyFn: func(t *testing.T, mem []byte) {
require.Equal(t, uint32(storeTargetValue), binary.LittleEndian.Uint32(mem[offset:]))
},
},
{
name: "f32.store",
isFloatTarget: true,
targetSizeInBytes: 32 / 8,
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileStore(&wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeF32})
require.NoError(t, err)
},
storedValueVerifyFn: func(t *testing.T, mem []byte) {
require.Equal(t, uint32(storeTargetValue), binary.LittleEndian.Uint32(mem[offset:]))
},
},
{
name: "i64.store",
targetSizeInBytes: 64 / 8,
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileStore(&wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeI64})
require.NoError(t, err)
},
storedValueVerifyFn: func(t *testing.T, mem []byte) {
require.Equal(t, storeTargetValue, binary.LittleEndian.Uint64(mem[offset:]))
},
},
{
name: "f64.store",
isFloatTarget: true,
targetSizeInBytes: 64 / 8,
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileStore(&wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeF64})
require.NoError(t, err)
},
storedValueVerifyFn: func(t *testing.T, mem []byte) {
require.Equal(t, storeTargetValue, binary.LittleEndian.Uint64(mem[offset:]))
},
},
{
name: "store8",
targetSizeInBytes: 1,
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileStore8(&wazeroir.OperationStore8{Arg: arg})
require.NoError(t, err)
},
storedValueVerifyFn: func(t *testing.T, mem []byte) {
require.Equal(t, byte(storeTargetValue), mem[offset])
},
},
{
name: "store16",
targetSizeInBytes: 16 / 8,
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileStore16(&wazeroir.OperationStore16{Arg: arg})
require.NoError(t, err)
},
storedValueVerifyFn: func(t *testing.T, mem []byte) {
require.Equal(t, uint16(storeTargetValue), binary.LittleEndian.Uint16(mem[offset:]))
},
},
{
name: "store32",
targetSizeInBytes: 32 / 8,
operationSetupFn: func(t *testing.T, compiler compilerImpl) {
err := compiler.compileStore32(&wazeroir.OperationStore32{Arg: arg})
require.NoError(t, err)
},
storedValueVerifyFn: func(t *testing.T, mem []byte) {
require.Equal(t, uint32(storeTargetValue), binary.LittleEndian.Uint32(mem[offset:]))
},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{HasMemory: true, Signature: &wasm.FunctionType{}})
err := compiler.compilePreamble()
require.NoError(t, err)
// Before store operations, we must push the base offset, and the store target values.
err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: baseOffset})
require.NoError(t, err)
if tc.isFloatTarget {
err = compiler.compileConstF64(&wazeroir.OperationConstF64{Value: math.Float64frombits(storeTargetValue)})
} else {
err = compiler.compileConstI64(&wazeroir.OperationConstI64{Value: storeTargetValue})
}
require.NoError(t, err)
tc.operationSetupFn(t, compiler)
// At this point, no registers must be in use, and no values on the stack since we consumed two values.
require.Zero(t, len(compiler.runtimeValueLocationStack().usedRegisters))
require.Equal(t, uint64(0), compiler.runtimeValueLocationStack().sp)
// Generate the code under test.
err = compiler.compileReturnFunction()
require.NoError(t, err)
code, _, _, err := compiler.compile()
require.NoError(t, err)
// Set the value on the left and right neighboring memoryregion,
// so that we can verify the operation doesn't affect there.
ceil := offset + tc.targetSizeInBytes
mem := env.memory()
expectedNeighbor8Bytes := uint64(0x12_34_56_78_9a_bc_ef_fe)
binary.LittleEndian.PutUint64(mem[offset-8:offset], expectedNeighbor8Bytes)
binary.LittleEndian.PutUint64(mem[ceil:ceil+8], expectedNeighbor8Bytes)
// Run code.
env.exec(code)
tc.storedValueVerifyFn(t, mem)
// The neighboring bytes must be intact.
require.Equal(t, expectedNeighbor8Bytes, binary.LittleEndian.Uint64(mem[offset-8:offset]))
require.Equal(t, expectedNeighbor8Bytes, binary.LittleEndian.Uint64(mem[ceil:ceil+8]))
})
}
}
func TestCompiler_MemoryOutOfBounds(t *testing.T) {
bases := []uint32{0, 1 << 5, 1 << 9, 1 << 10, 1 << 15, math.MaxUint32 - 1, math.MaxUint32}
offsets := []uint32{0,
1 << 10, 1 << 31,
defaultMemoryPageNumInTest*wasm.MemoryPageSize - 1, defaultMemoryPageNumInTest * wasm.MemoryPageSize,
math.MaxInt32 - 1, math.MaxInt32 - 2, math.MaxInt32 - 3, math.MaxInt32 - 4,
math.MaxInt32 - 5, math.MaxInt32 - 8, math.MaxInt32 - 9, math.MaxInt32, math.MaxUint32,
}
targetSizeInBytes := []int64{1, 2, 4, 8}
for _, base := range bases {
base := base
for _, offset := range offsets {
offset := offset
for _, targetSizeInByte := range targetSizeInBytes {
targetSizeInByte := targetSizeInByte
t.Run(fmt.Sprintf("base=%d,offset=%d,targetSizeInBytes=%d", base, offset, targetSizeInByte), func(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, newCompiler, nil)
err := compiler.compilePreamble()
require.NoError(t, err)
err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: base})
require.NoError(t, err)
arg := &wazeroir.MemoryArg{Offset: offset}
switch targetSizeInByte {
case 1:
err = compiler.compileLoad8(&wazeroir.OperationLoad8{Type: wazeroir.SignedInt32, Arg: arg})
case 2:
err = compiler.compileLoad16(&wazeroir.OperationLoad16{Type: wazeroir.SignedInt32, Arg: arg})
case 4:
err = compiler.compileLoad32(&wazeroir.OperationLoad32{Signed: false, Arg: arg})
case 8:
err = compiler.compileLoad(&wazeroir.OperationLoad{Type: wazeroir.UnsignedTypeF64, Arg: arg})
default:
t.Fail()
}
require.NoError(t, err)
require.NoError(t, compiler.compileReturnFunction())
// Generate the code under test and run.
code, _, _, err := compiler.compile()
require.NoError(t, err)
env.exec(code)
mem := env.memory()
if ceil := int64(base) + int64(offset) + int64(targetSizeInByte); int64(len(mem)) < ceil {
// If the targe memory region's ceil exceeds the length of memory, we must exit the function
// with nativeCallStatusCodeMemoryOutOfBounds status code.
require.Equal(t, nativeCallStatusCodeMemoryOutOfBounds, env.compilerStatus())
}
})
}
}
}
}