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 }