wa-lang.org/wazero@v1.0.2/internal/engine/compiler/impl_vec_amd64_test.go (about)

     1  package compiler
     2  
     3  import (
     4  	"encoding/binary"
     5  	"math"
     6  	"testing"
     7  
     8  	"wa-lang.org/wazero/internal/asm"
     9  	"wa-lang.org/wazero/internal/asm/amd64"
    10  	"wa-lang.org/wazero/internal/testing/require"
    11  	"wa-lang.org/wazero/internal/wasm"
    12  	"wa-lang.org/wazero/internal/wazeroir"
    13  )
    14  
    15  // TestAmd64Compiler_V128Shuffle_ConstTable_MiddleOfFunction ensures that flushing constant table in the middle of
    16  // function works well by intentionally setting amd64.AssemblerImpl MaxDisplacementForConstantPool = 0.
    17  func TestAmd64Compiler_V128Shuffle_ConstTable_MiddleOfFunction(t *testing.T) {
    18  	env := newCompilerEnvironment()
    19  	compiler := env.requireNewCompiler(t, newCompiler,
    20  		&wazeroir.CompilationResult{HasMemory: true, Signature: &wasm.FunctionType{}})
    21  
    22  	err := compiler.compilePreamble()
    23  	require.NoError(t, err)
    24  
    25  	lanes := [16]byte{1, 1, 1, 1, 0, 0, 0, 0, 10, 10, 10, 10, 0, 0, 0, 0}
    26  	v := [16]byte{0: 0xa, 1: 0xb, 10: 0xc}
    27  	w := [16]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
    28  	exp := [16]byte{
    29  		0xb, 0xb, 0xb, 0xb,
    30  		0xa, 0xa, 0xa, 0xa,
    31  		0xc, 0xc, 0xc, 0xc,
    32  		0xa, 0xa, 0xa, 0xa,
    33  	}
    34  
    35  	err = compiler.compileV128Const(&wazeroir.OperationV128Const{
    36  		Lo: binary.LittleEndian.Uint64(v[:8]),
    37  		Hi: binary.LittleEndian.Uint64(v[8:]),
    38  	})
    39  	require.NoError(t, err)
    40  
    41  	err = compiler.compileV128Const(&wazeroir.OperationV128Const{
    42  		Lo: binary.LittleEndian.Uint64(w[:8]),
    43  		Hi: binary.LittleEndian.Uint64(w[8:]),
    44  	})
    45  	require.NoError(t, err)
    46  
    47  	err = compiler.compileV128Shuffle(&wazeroir.OperationV128Shuffle{Lanes: lanes})
    48  	require.NoError(t, err)
    49  
    50  	assembler := compiler.(*amd64Compiler).assembler.(*amd64.AssemblerImpl)
    51  	assembler.MaxDisplacementForConstantPool = 0 // Ensures that constant table for shuffle will be flushed immediately.
    52  
    53  	err = compiler.compileReturnFunction()
    54  	require.NoError(t, err)
    55  
    56  	// Generate and run the code under test.
    57  	code, _, err := compiler.compile()
    58  	require.NoError(t, err)
    59  	env.exec(code)
    60  
    61  	lo, hi := env.stackTopAsV128()
    62  	var actual [16]byte
    63  	binary.LittleEndian.PutUint64(actual[:8], lo)
    64  	binary.LittleEndian.PutUint64(actual[8:], hi)
    65  	require.Equal(t, exp, actual)
    66  }
    67  
    68  func TestAmd64Compiler_compileV128ShrI64x2SignedImpl(t *testing.T) {
    69  	x := [16]byte{
    70  		0, 0, 0, 0x80, 0, 0, 0, 0x80,
    71  		0, 0, 0, 0x80, 0, 0, 0, 0x80,
    72  	}
    73  	exp := [16]byte{
    74  		0, 0, 0, 0x40, 0, 0, 0, 0x80 | 0x80>>1,
    75  		0, 0, 0, 0x40, 0, 0, 0, 0x80 | 0x80>>1,
    76  	}
    77  	shiftAmount := uint32(1)
    78  
    79  	tests := []struct {
    80  		name               string
    81  		shiftAmountSetupFn func(t *testing.T, c *amd64Compiler)
    82  		verifyFn           func(t *testing.T, env *compilerEnv)
    83  	}{
    84  		{
    85  			name: "RegR10/CX not in use",
    86  			shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
    87  				// Move the shift amount to R10.
    88  				loc := c.locationStack.peek()
    89  				oldReg, newReg := loc.register, amd64.RegR10
    90  				c.assembler.CompileRegisterToRegister(amd64.MOVQ, oldReg, newReg)
    91  				loc.setRegister(newReg)
    92  				c.locationStack.markRegisterUnused(oldReg)
    93  				c.locationStack.markRegisterUsed(newReg)
    94  			},
    95  			verifyFn: func(t *testing.T, env *compilerEnv) {},
    96  		},
    97  		{
    98  			name: "RegR10/CX not in use and CX is next free register",
    99  			shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
   100  				// Move the shift amount to R10.
   101  				loc := c.locationStack.peek()
   102  				oldReg, newReg := loc.register, amd64.RegR10
   103  				c.assembler.CompileRegisterToRegister(amd64.MOVQ, oldReg, newReg)
   104  				loc.setRegister(newReg)
   105  				c.locationStack.markRegisterUnused(oldReg)
   106  				c.locationStack.markRegisterUsed(newReg)
   107  
   108  				// Ensures that the next free becomes CX.
   109  				newUnreservedRegs := make([]asm.Register, len(c.locationStack.unreservedVectorRegisters))
   110  				copy(newUnreservedRegs, c.locationStack.unreservedGeneralPurposeRegisters)
   111  				for i, r := range newUnreservedRegs {
   112  					// If CX register is found, we swap it with the first register in the list.
   113  					// This forces runtimeLocationStack to take CX as a first free register.
   114  					if r == amd64.RegCX {
   115  						newUnreservedRegs[0], newUnreservedRegs[i] = newUnreservedRegs[i], newUnreservedRegs[0]
   116  					}
   117  				}
   118  				c.locationStack.unreservedGeneralPurposeRegisters = newUnreservedRegs
   119  			},
   120  			verifyFn: func(t *testing.T, env *compilerEnv) {},
   121  		},
   122  		{
   123  			name: "RegR10/CX in use",
   124  			shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
   125  				// Pop the shift amount and vector values temporarily.
   126  				shiftAmountLocation := c.locationStack.pop()
   127  				vec := c.locationStack.popV128()
   128  
   129  				// Move the shift amount to R10.
   130  				oldReg, newReg := shiftAmountLocation.register, amd64.RegR10
   131  				c.assembler.CompileRegisterToRegister(amd64.MOVQ, oldReg, newReg)
   132  				c.locationStack.markRegisterUnused(oldReg)
   133  				c.locationStack.markRegisterUsed(newReg)
   134  
   135  				// Create the previous usage of CX register.
   136  				c.pushRuntimeValueLocationOnRegister(amd64.RegCX, runtimeValueTypeI32)
   137  				c.assembler.CompileConstToRegister(amd64.MOVQ, 100, amd64.RegCX)
   138  
   139  				// push the operands back to the location registers.
   140  				c.pushVectorRuntimeValueLocationOnRegister(vec.register)
   141  				c.pushRuntimeValueLocationOnRegister(newReg, runtimeValueTypeI32)
   142  			},
   143  			verifyFn: func(t *testing.T, env *compilerEnv) {
   144  				// at the bottom of stack, the previous value on the CX register must be saved.
   145  				actual := env.stack()[callFrameDataSizeInUint64]
   146  				require.Equal(t, uint64(100), actual)
   147  			},
   148  		},
   149  		{
   150  			name: "Stack/CX not in use",
   151  			shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
   152  				// Release the shift amount value to the stack.
   153  				loc := c.locationStack.peek()
   154  				c.compileReleaseRegisterToStack(loc)
   155  			},
   156  			verifyFn: func(t *testing.T, env *compilerEnv) {},
   157  		},
   158  		{
   159  			name: "Stack/CX in use",
   160  			shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
   161  				// Pop the shift amount and vector values temporarily.
   162  				shiftAmountReg := c.locationStack.pop().register
   163  				require.NotEqual(t, amd64.RegCX, shiftAmountReg)
   164  				vec := c.locationStack.popV128()
   165  
   166  				// Create the previous usage of CX register.
   167  				c.pushRuntimeValueLocationOnRegister(amd64.RegCX, runtimeValueTypeI32)
   168  				c.assembler.CompileConstToRegister(amd64.MOVQ, 100, amd64.RegCX)
   169  
   170  				// push the operands back to the location registers.
   171  				c.pushVectorRuntimeValueLocationOnRegister(vec.register)
   172  				// Release the shift amount value to the stack.
   173  				loc := c.pushRuntimeValueLocationOnRegister(shiftAmountReg, runtimeValueTypeI32)
   174  				c.compileReleaseRegisterToStack(loc)
   175  			},
   176  			verifyFn: func(t *testing.T, env *compilerEnv) {
   177  				// at the bottom of stack, the previous value on the CX register must be saved.
   178  				actual := env.stack()[callFrameDataSizeInUint64]
   179  				require.Equal(t, uint64(100), actual)
   180  			},
   181  		},
   182  		{
   183  			name: "CondReg/CX not in use",
   184  			shiftAmountSetupFn: func(t *testing.T, c *amd64Compiler) {
   185  				// Ignore the pushed const.
   186  				loc := c.locationStack.pop()
   187  				c.locationStack.markRegisterUnused(loc.register)
   188  
   189  				// Instead, push the conditional flag value which is supposed be interpreted as 1 (=shiftAmount).
   190  				err := c.compileConstI32(&wazeroir.OperationConstI32{Value: 0})
   191  				require.NoError(t, err)
   192  				err = c.compileConstI32(&wazeroir.OperationConstI32{Value: 0})
   193  				require.NoError(t, err)
   194  				err = c.compileEq(&wazeroir.OperationEq{Type: wazeroir.UnsignedTypeI32})
   195  				require.NoError(t, err)
   196  			},
   197  			verifyFn: func(t *testing.T, env *compilerEnv) {},
   198  		},
   199  	}
   200  
   201  	for _, tc := range tests {
   202  		tc := tc
   203  		t.Run(tc.name, func(t *testing.T) {
   204  			env := newCompilerEnvironment()
   205  			compiler := env.requireNewCompiler(t, newCompiler,
   206  				&wazeroir.CompilationResult{HasMemory: true, Signature: &wasm.FunctionType{}})
   207  
   208  			err := compiler.compilePreamble()
   209  			require.NoError(t, err)
   210  
   211  			err = compiler.compileV128Const(&wazeroir.OperationV128Const{
   212  				Lo: binary.LittleEndian.Uint64(x[:8]),
   213  				Hi: binary.LittleEndian.Uint64(x[8:]),
   214  			})
   215  			require.NoError(t, err)
   216  
   217  			err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: shiftAmount})
   218  			require.NoError(t, err)
   219  
   220  			amdCompiler := compiler.(*amd64Compiler)
   221  			tc.shiftAmountSetupFn(t, amdCompiler)
   222  
   223  			err = amdCompiler.compileV128ShrI64x2SignedImpl()
   224  			require.NoError(t, err)
   225  
   226  			require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters))
   227  
   228  			err = compiler.compileReturnFunction()
   229  			require.NoError(t, err)
   230  
   231  			// Generate and run the code under test.
   232  			code, _, err := compiler.compile()
   233  			require.NoError(t, err)
   234  			env.exec(code)
   235  
   236  			lo, hi := env.stackTopAsV128()
   237  			var actual [16]byte
   238  			binary.LittleEndian.PutUint64(actual[:8], lo)
   239  			binary.LittleEndian.PutUint64(actual[8:], hi)
   240  			require.Equal(t, exp, actual)
   241  
   242  			tc.verifyFn(t, env)
   243  		})
   244  	}
   245  }
   246  
   247  // TestAmd64Compiler_compileV128Neg_NaNOnTemporary ensures compileV128Neg for floating point variants works well
   248  // even if the temporary register used by the instruction holds NaN values previously.
   249  func TestAmd64Compiler_compileV128Neg_NaNOnTemporary(t *testing.T) {
   250  	tests := []struct {
   251  		name   string
   252  		shape  wazeroir.Shape
   253  		v, exp [16]byte
   254  	}{
   255  		{
   256  			name:  "f32x4",
   257  			shape: wazeroir.ShapeF32x4,
   258  			v:     f32x4(51234.12341, -123, float32(math.Inf(1)), 0.1),
   259  			exp:   f32x4(-51234.12341, 123, float32(math.Inf(-1)), -0.1),
   260  		},
   261  		{
   262  			name:  "f32x4",
   263  			shape: wazeroir.ShapeF32x4,
   264  			v:     f32x4(51234.12341, 0, float32(math.Inf(1)), 0.1),
   265  			exp:   f32x4(-51234.12341, float32(math.Copysign(0, -1)), float32(math.Inf(-1)), -0.1),
   266  		},
   267  		{
   268  			name:  "f64x2",
   269  			shape: wazeroir.ShapeF64x2,
   270  			v:     f64x2(1.123, math.Inf(-1)),
   271  			exp:   f64x2(-1.123, math.Inf(1)),
   272  		},
   273  		{
   274  			name:  "f64x2",
   275  			shape: wazeroir.ShapeF64x2,
   276  			v:     f64x2(0, math.Inf(-1)),
   277  			exp:   f64x2(math.Copysign(0, -1), math.Inf(1)),
   278  		},
   279  	}
   280  
   281  	for _, tc := range tests {
   282  		tc := tc
   283  		t.Run(tc.name, func(t *testing.T) {
   284  			env := newCompilerEnvironment()
   285  			compiler := env.requireNewCompiler(t, newCompiler,
   286  				&wazeroir.CompilationResult{HasMemory: true, Signature: &wasm.FunctionType{}})
   287  
   288  			err := compiler.compilePreamble()
   289  			require.NoError(t, err)
   290  
   291  			err = compiler.compileV128Const(&wazeroir.OperationV128Const{
   292  				Lo: binary.LittleEndian.Uint64(tc.v[:8]),
   293  				Hi: binary.LittleEndian.Uint64(tc.v[8:]),
   294  			})
   295  			require.NoError(t, err)
   296  
   297  			// Ensures that the previous state of temporary register used by Neg holds
   298  			// NaN values.
   299  			err = compiler.compileV128Const(&wazeroir.OperationV128Const{
   300  				Lo: math.Float64bits(math.NaN()),
   301  				Hi: math.Float64bits(math.NaN()),
   302  			})
   303  			require.NoError(t, err)
   304  
   305  			// Mark that the temp register is available for Neg instruction below.
   306  			loc := compiler.runtimeValueLocationStack().popV128()
   307  			compiler.runtimeValueLocationStack().markRegisterUnused(loc.register)
   308  
   309  			// Now compiling Neg where it uses temporary register holding NaN values at this point.
   310  			err = compiler.compileV128Neg(&wazeroir.OperationV128Neg{Shape: tc.shape})
   311  			require.NoError(t, err)
   312  
   313  			err = compiler.compileReturnFunction()
   314  			require.NoError(t, err)
   315  
   316  			// Generate and run the code under test.
   317  			code, _, err := compiler.compile()
   318  			require.NoError(t, err)
   319  			env.exec(code)
   320  
   321  			require.Equal(t, nativeCallStatusCodeReturned, env.callEngine().statusCode)
   322  
   323  			lo, hi := env.stackTopAsV128()
   324  			var actual [16]byte
   325  			binary.LittleEndian.PutUint64(actual[:8], lo)
   326  			binary.LittleEndian.PutUint64(actual[8:], hi)
   327  			require.Equal(t, tc.exp, actual)
   328  		})
   329  	}
   330  }