Signed-off-by: Achille Roussel <achille.roussel@gmail.com> Co-authored-by: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com>
325 lines
11 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|