Files
wazero/internal/engine/compiler/impl_vec_amd64_test.go
2023-05-19 07:06:30 +02:00

325 lines
11 KiB
Go

package compiler
import (
"encoding/binary"
"math"
"testing"
"github.com/tetratelabs/wazero/internal/asm"
"github.com/tetratelabs/wazero/internal/asm/amd64"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wazeroir"
)
// TestAmd64Compiler_V128Shuffle_ConstTable_MiddleOfFunction ensures that flushing constant table in the middle of
// function works well by intentionally setting amd64.AssemblerImpl MaxDisplacementForConstantPool = 0.
func TestAmd64Compiler_V128Shuffle_ConstTable_MiddleOfFunction(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler,
&wazeroir.CompilationResult{HasMemory: true})
err := compiler.compilePreamble()
require.NoError(t, err)
lanes := []uint64{1, 1, 1, 1, 0, 0, 0, 0, 10, 10, 10, 10, 0, 0, 0, 0}
v := [16]byte{0: 0xa, 1: 0xb, 10: 0xc}
w := [16]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
exp := [16]byte{
0xb, 0xb, 0xb, 0xb,
0xa, 0xa, 0xa, 0xa,
0xc, 0xc, 0xc, 0xc,
0xa, 0xa, 0xa, 0xa,
}
err = compiler.compileV128Const(operationPtr(wazeroir.NewOperationV128Const(binary.LittleEndian.Uint64(v[:8]), binary.LittleEndian.Uint64(v[8:]))))
require.NoError(t, err)
err = compiler.compileV128Const(operationPtr(wazeroir.NewOperationV128Const(binary.LittleEndian.Uint64(w[:8]), binary.LittleEndian.Uint64(w[8:]))))
require.NoError(t, err)
err = compiler.compileV128Shuffle(operationPtr(wazeroir.NewOperationV128Shuffle(lanes)))
require.NoError(t, err)
assembler := compiler.(*amd64Compiler).assembler.(*amd64.AssemblerImpl)
assembler.MaxDisplacementForConstantPool = 0 // Ensures that constant table for shuffle will be flushed immediately.
err = compiler.compileReturnFunction()
require.NoError(t, err)
code := asm.CodeSegment{}
defer func() { require.NoError(t, code.Unmap()) }()
// Generate and run the code under test.
_, err = compiler.compile(code.NextCodeSection())
require.NoError(t, err)
env.exec(code.Bytes())
lo, hi := env.stackTopAsV128()
var actual [16]byte
binary.LittleEndian.PutUint64(actual[:8], lo)
binary.LittleEndian.PutUint64(actual[8:], hi)
require.Equal(t, exp, actual)
}
func TestAmd64Compiler_compileV128ShrI64x2SignedImpl(t *testing.T) {
x := [16]byte{
0, 0, 0, 0x80, 0, 0, 0, 0x80,
0, 0, 0, 0x80, 0, 0, 0, 0x80,
}
exp := [16]byte{
0, 0, 0, 0x40, 0, 0, 0, 0x80 | 0x80>>1,
0, 0, 0, 0x40, 0, 0, 0, 0x80 | 0x80>>1,
}
shiftAmount := uint32(1)
tests := []struct {
name string
shiftAmountSetupFn func(t *testing.T, c *amd64Compiler)
verifyFn func(t *testing.T, env *compilerEnv)
}{
{
name: "RegR10/CX not in use",
shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
// Move the shift amount to R10.
loc := c.locationStack.peek()
oldReg, newReg := loc.register, amd64.RegR10
c.assembler.CompileRegisterToRegister(amd64.MOVQ, oldReg, newReg)
loc.setRegister(newReg)
c.locationStack.markRegisterUnused(oldReg)
c.locationStack.markRegisterUsed(newReg)
},
verifyFn: func(t *testing.T, env *compilerEnv) {},
},
{
name: "RegR10/CX not in use and CX is next free register",
shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
// Move the shift amount to R10.
loc := c.locationStack.peek()
oldReg, newReg := loc.register, amd64.RegR10
c.assembler.CompileRegisterToRegister(amd64.MOVQ, oldReg, newReg)
loc.setRegister(newReg)
c.locationStack.markRegisterUnused(oldReg)
c.locationStack.markRegisterUsed(newReg)
// Ensures that the next free becomes CX.
newUnreservedRegs := make([]asm.Register, len(c.locationStack.unreservedVectorRegisters))
copy(newUnreservedRegs, c.locationStack.unreservedGeneralPurposeRegisters)
for i, r := range newUnreservedRegs {
// If CX register is found, we swap it with the first register in the list.
// This forces runtimeLocationStack to take CX as a first free register.
if r == amd64.RegCX {
newUnreservedRegs[0], newUnreservedRegs[i] = newUnreservedRegs[i], newUnreservedRegs[0]
}
}
c.locationStack.unreservedGeneralPurposeRegisters = newUnreservedRegs
},
verifyFn: func(t *testing.T, env *compilerEnv) {},
},
{
name: "RegR10/CX in use",
shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
// Pop the shift amount and vector values temporarily.
shiftAmountLocation := c.locationStack.pop()
vecReg := c.locationStack.popV128().register
// Move the shift amount to R10.
oldReg, newReg := shiftAmountLocation.register, amd64.RegR10
c.assembler.CompileRegisterToRegister(amd64.MOVQ, oldReg, newReg)
c.locationStack.markRegisterUnused(oldReg)
c.locationStack.markRegisterUsed(newReg)
// Create the previous usage of CX register.
c.pushRuntimeValueLocationOnRegister(amd64.RegCX, runtimeValueTypeI32)
c.assembler.CompileConstToRegister(amd64.MOVQ, 100, amd64.RegCX)
// push the operands back to the location registers.
c.pushVectorRuntimeValueLocationOnRegister(vecReg)
c.pushRuntimeValueLocationOnRegister(newReg, runtimeValueTypeI32)
},
verifyFn: func(t *testing.T, env *compilerEnv) {
// at the bottom of stack, the previous value on the CX register must be saved.
actual := env.stack()[callFrameDataSizeInUint64]
require.Equal(t, uint64(100), actual)
},
},
{
name: "Stack/CX not in use",
shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
// Release the shift amount value to the stack.
loc := c.locationStack.peek()
c.compileReleaseRegisterToStack(loc)
},
verifyFn: func(t *testing.T, env *compilerEnv) {},
},
{
name: "Stack/CX in use",
shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
// Pop the shift amount and vector values temporarily.
shiftAmountReg := c.locationStack.pop().register
require.NotEqual(t, amd64.RegCX, shiftAmountReg)
vecReg := c.locationStack.popV128().register
// Create the previous usage of CX register.
c.pushRuntimeValueLocationOnRegister(amd64.RegCX, runtimeValueTypeI32)
c.assembler.CompileConstToRegister(amd64.MOVQ, 100, amd64.RegCX)
// push the operands back to the location registers.
c.pushVectorRuntimeValueLocationOnRegister(vecReg)
// Release the shift amount value to the stack.
loc := c.pushRuntimeValueLocationOnRegister(shiftAmountReg, runtimeValueTypeI32)
c.compileReleaseRegisterToStack(loc)
},
verifyFn: func(t *testing.T, env *compilerEnv) {
// at the bottom of stack, the previous value on the CX register must be saved.
actual := env.stack()[callFrameDataSizeInUint64]
require.Equal(t, uint64(100), actual)
},
},
{
name: "CondReg/CX not in use",
shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
// Ignore the pushed const.
loc := c.locationStack.pop()
c.locationStack.markRegisterUnused(loc.register)
// Instead, push the conditional flag value which is supposed be interpreted as 1 (=shiftAmount).
err := c.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(0)))
require.NoError(t, err)
err = c.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(0)))
require.NoError(t, err)
err = c.compileEq(operationPtr(wazeroir.NewOperationEq(wazeroir.UnsignedTypeI32)))
require.NoError(t, err)
},
verifyFn: func(t *testing.T, env *compilerEnv) {},
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler,
&wazeroir.CompilationResult{HasMemory: true})
err := compiler.compilePreamble()
require.NoError(t, err)
err = compiler.compileV128Const(operationPtr(wazeroir.NewOperationV128Const(binary.LittleEndian.Uint64(x[:8]), binary.LittleEndian.Uint64(x[8:]))))
require.NoError(t, err)
err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(shiftAmount)))
require.NoError(t, err)
amdCompiler := compiler.(*amd64Compiler)
tc.shiftAmountSetupFn(t, amdCompiler)
err = amdCompiler.compileV128ShrI64x2SignedImpl()
require.NoError(t, err)
require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters.list()))
err = compiler.compileReturnFunction()
require.NoError(t, err)
code := asm.CodeSegment{}
defer func() { require.NoError(t, code.Unmap()) }()
// Generate and run the code under test.
_, err = compiler.compile(code.NextCodeSection())
require.NoError(t, err)
env.exec(code.Bytes())
lo, hi := env.stackTopAsV128()
var actual [16]byte
binary.LittleEndian.PutUint64(actual[:8], lo)
binary.LittleEndian.PutUint64(actual[8:], hi)
require.Equal(t, exp, actual)
tc.verifyFn(t, env)
})
}
}
// TestAmd64Compiler_compileV128Neg_NaNOnTemporary ensures compileV128Neg for floating point variants works well
// even if the temporary register used by the instruction holds NaN values previously.
func TestAmd64Compiler_compileV128Neg_NaNOnTemporary(t *testing.T) {
tests := []struct {
name string
shape wazeroir.Shape
v, exp [16]byte
}{
{
name: "f32x4",
shape: wazeroir.ShapeF32x4,
v: f32x4(51234.12341, -123, float32(math.Inf(1)), 0.1),
exp: f32x4(-51234.12341, 123, float32(math.Inf(-1)), -0.1),
},
{
name: "f32x4",
shape: wazeroir.ShapeF32x4,
v: f32x4(51234.12341, 0, float32(math.Inf(1)), 0.1),
exp: f32x4(-51234.12341, float32(math.Copysign(0, -1)), float32(math.Inf(-1)), -0.1),
},
{
name: "f64x2",
shape: wazeroir.ShapeF64x2,
v: f64x2(1.123, math.Inf(-1)),
exp: f64x2(-1.123, math.Inf(1)),
},
{
name: "f64x2",
shape: wazeroir.ShapeF64x2,
v: f64x2(0, math.Inf(-1)),
exp: f64x2(math.Copysign(0, -1), math.Inf(1)),
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler,
&wazeroir.CompilationResult{HasMemory: true})
err := compiler.compilePreamble()
require.NoError(t, err)
err = compiler.compileV128Const(operationPtr(wazeroir.NewOperationV128Const(binary.LittleEndian.Uint64(tc.v[:8]), binary.LittleEndian.Uint64(tc.v[8:]))))
require.NoError(t, err)
// Ensures that the previous state of temporary register used by Neg holds
// NaN values.
err = compiler.compileV128Const(operationPtr(wazeroir.NewOperationV128Const(math.Float64bits(math.NaN()), math.Float64bits(math.NaN()))))
require.NoError(t, err)
// Mark that the temp register is available for Neg instruction below.
loc := compiler.runtimeValueLocationStack().popV128()
compiler.runtimeValueLocationStack().markRegisterUnused(loc.register)
// Now compiling Neg where it uses temporary register holding NaN values at this point.
err = compiler.compileV128Neg(operationPtr(wazeroir.NewOperationV128Neg(tc.shape)))
require.NoError(t, err)
err = compiler.compileReturnFunction()
require.NoError(t, err)
code := asm.CodeSegment{}
defer func() { require.NoError(t, code.Unmap()) }()
// Generate and run the code under test.
_, err = compiler.compile(code.NextCodeSection())
require.NoError(t, err)
env.exec(code.Bytes())
require.Equal(t, nativeCallStatusCodeReturned, env.callEngine().statusCode)
lo, hi := env.stackTopAsV128()
var actual [16]byte
binary.LittleEndian.PutUint64(actual[:8], lo)
binary.LittleEndian.PutUint64(actual[8:], hi)
require.Equal(t, tc.exp, actual)
})
}
}