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