random-tests.js (19719B)
1 // This file tests multi-value returns. It creates a chain of wasm functions 2 // 3 // fnStart -> fnMid0 -> fnMid1 -> fnMid2 -> fnMid3 -> fnEnd 4 // 5 // When run, fnStart creates 12 (or in the non-simd case, 8) random values, of 6 // various types. It then passes them to fnMid0. That reorders them and 7 // passes them on to fnMid1, etc, until they arrive at fnEnd. 8 // 9 // fnEnd makes a small and reversible change to each value. It then reorders 10 // them and returns all of them. The returned values get passed back along 11 // the chain, being randomly reordered at each step, until they arrive back at 12 // fnStart. 13 // 14 // fnStart backs out the changes made in fnEnd and checks that the resulting 15 // values are the same as the originals it created. If they are not, the test 16 // has failed. 17 // 18 // If the test passes, we can be sure each value got passed along the chain 19 // and back again correctly, despite being in a probably different argument or 20 // return position each time (due to the reordering). As a side effect, this 21 // test also is a pretty good test of argument passing. The number of values 22 // (10) is chosen so as to be larger than the number of args that can be 23 // passed in regs on any target; hence it also tests the logic for passing 24 // args in regs vs memory too. 25 // 26 // The whole process (generate and run a test program) is repeated 120 times. 27 // 28 // Doing this requires some supporting functions to be defined in wasm, by 29 // `funcs_util` and `funcs_rng` (a random number generator). 30 // 31 // It is almost impossible to understand what the tests do by reading the JS 32 // below. Reading the generated wasm is required. Search for "if (0)" below. 33 34 // Some utility functions for use in the generated code. 35 function funcs_util(simdEnabled) { 36 let t = 37 (simdEnabled ? 38 `;; Create a v128 value from 2 i64 values 39 (func $v128_from_i64HL (export "v128_from_i64HL") 40 (param $i64hi i64) (param $i64lo i64) (result v128) 41 (local $vec v128) 42 (local.set $vec (i64x2.replace_lane 0 (local.get $vec) (local.get $i64lo))) 43 (local.set $vec (i64x2.replace_lane 1 (local.get $vec) (local.get $i64hi))) 44 (local.get $vec) 45 ) 46 ;; Split a v128 up into pieces. 47 (func $v128hi (export "v128hi") (param $vec v128) (result i64) 48 (return (i64x2.extract_lane 1 (local.get $vec))) 49 ) 50 (func $v128lo (export "v128lo") (param $vec v128) (result i64) 51 (return (i64x2.extract_lane 0 (local.get $vec))) 52 ) 53 ;; Return an i32 value, which is 0 if the args are identical and 1 otherwise. 54 (func $v128ne (export "v128ne") 55 (param $vec1 v128) (param $vec2 v128) (result i32) 56 (return (v128.any_true (v128.xor (local.get $vec1) (local.get $vec2)))) 57 )` 58 : ``/* simd not enabled*/ 59 ) + 60 `;; Move an i32 value forwards and backwards. 61 (func $step_i32 (export "step_i32") (param $n i32) (result i32) 62 (return (i32.add (local.get $n) (i32.const 1337))) 63 ) 64 (func $unstep_i32 (export "unstep_i32") (param $n i32) (result i32) 65 (return (i32.sub (local.get $n) (i32.const 1337))) 66 ) 67 ;; Move an i64 value forwards and backwards. 68 (func $step_i64 (export "step_i64") (param $n i64) (result i64) 69 (return (i64.add (local.get $n) (i64.const 4771))) 70 ) 71 (func $unstep_i64 (export "unstep_i64") (param $n i64) (result i64) 72 (return (i64.sub (local.get $n) (i64.const 4771))) 73 ) 74 ;; Move a f32 value forwards and backwards. This is a bit tricky because 75 ;; we need to guarantee that the backwards move exactly cancels out the 76 ;; forward move. So we multiply/divide exactly by 2 on the basis that that 77 ;; will change the exponent but not the mantissa, at least for normalised 78 ;; numbers. 79 (func $step_f32 (export "step_f32") (param $n f32) (result f32) 80 (return (f32.mul (local.get $n) (f32.const 2.0))) 81 ) 82 (func $unstep_f32 (export "unstep_f32") (param $n f32) (result f32) 83 (return (f32.div (local.get $n) (f32.const 2.0))) 84 ) 85 ;; Move a f64 value forwards and backwards. 86 (func $step_f64 (export "step_f64") (param $n f64) (result f64) 87 (return (f64.mul (local.get $n) (f64.const 4.0))) 88 ) 89 (func $unstep_f64 (export "unstep_f64") (param $n f64) (result f64) 90 (return (f64.div (local.get $n) (f64.const 4.0))) 91 )` 92 + (simdEnabled ? 93 `;; Move a v128 value forwards and backwards. 94 (func $step_v128 (export "step_v128") (param $vec v128) (result v128) 95 (return (call $v128_from_i64HL 96 (i64.add (call $v128hi (local.get $vec)) (i64.const 1234)) 97 (i64.add (call $v128lo (local.get $vec)) (i64.const 4321)) 98 )) 99 ) 100 (func $unstep_v128 (export "unstep_v128") (param $vec v128) (result v128) 101 (return (call $v128_from_i64HL 102 (i64.sub (call $v128hi (local.get $vec)) (i64.const 1234)) 103 (i64.sub (call $v128lo (local.get $vec)) (i64.const 4321)) 104 )) 105 )` 106 : ``/* simd not enabled*/ 107 ); 108 return t; 109 } 110 111 // A Pseudo-RNG based on the C standard. The core function generates only 16 112 // random bits. We have to use it twice to generate a 32-bit random number 113 // and four times for a 64-bit random number. 114 let decls_rng = 115 `;; The RNG's state 116 (global $rand_state 117 (mut i32) (i32.const 1) 118 )`; 119 function funcs_rng(simdEnabled) { 120 let t = 121 `;; Set the seed 122 (func $rand_setSeed (param $seed i32) 123 (global.set $rand_state (local.get $seed)) 124 ) 125 ;; Generate a 16-bit random number 126 (func $rand_i16 (export "rand_i16") (result i32) 127 (local $t i32) 128 ;; update $rand_state 129 (local.set $t (global.get $rand_state)) 130 (local.set $t (i32.mul (local.get $t) (i32.const 1103515245))) 131 (local.set $t (i32.add (local.get $t) (i32.const 12345))) 132 (global.set $rand_state (local.get $t)) 133 ;; pull 16 random bits out of it 134 (local.set $t (i32.shr_u (local.get $t) (i32.const 15))) 135 (local.set $t (i32.and (local.get $t) (i32.const 0xFFFF))) 136 (local.get $t) 137 ) 138 ;; Generate a 32-bit random number 139 (func $rand_i32 (export "rand_i32") (result i32) 140 (local $t i32) 141 (local.set $t (call $rand_i16)) 142 (local.set $t (i32.shl (local.get $t) (i32.const 16))) 143 (local.set $t (i32.or (local.get $t) (call $rand_i16))) 144 (local.get $t) 145 ) 146 ;; Generate a 64-bit random number 147 (func $rand_i64 (export "rand_i64") (result i64) 148 (local $t i64) 149 (local.set $t (i64.extend_i32_u (call $rand_i16))) 150 (local.set $t (i64.shl (local.get $t) (i64.const 16))) 151 (local.set $t (i64.or (local.get $t) (i64.extend_i32_u (call $rand_i16)))) 152 (local.set $t (i64.shl (local.get $t) (i64.const 16))) 153 (local.set $t (i64.or (local.get $t) (i64.extend_i32_u (call $rand_i16)))) 154 (local.set $t (i64.shl (local.get $t) (i64.const 16))) 155 (local.set $t (i64.or (local.get $t) (i64.extend_i32_u (call $rand_i16)))) 156 (local.get $t) 157 ) 158 ;; Generate a 32-bit random float. This is something of a kludge in as much 159 ;; as it does it by converting a random signed-int32 to a float32, which 160 ;; means that we don't get any NaNs, infinities, denorms, etc, but OTOH 161 ;; there's somewhat less randomness then there would be if we had allowed 162 ;; such denorms in. 163 (func $rand_f32 (export "rand_f32") (result f32) 164 (f32.convert_i32_s (call $rand_i32)) 165 ) 166 ;; And similarly for 64-bit random floats 167 (func $rand_f64 (export "rand_f64") (result f64) 168 (f64.convert_i64_s (call $rand_i64)) 169 )` 170 + (simdEnabled ? 171 `;; Generate a random 128-bit vector. 172 (func $rand_v128 (export "rand_v128") (result v128) 173 (call $v128_from_i64HL (call $rand_i64) (call $rand_i64)) 174 )` 175 : ``/* simd not enabled*/ 176 ); 177 return t; 178 } 179 180 // Helpers for function generation 181 function strcmp(s1,s2) { 182 if (s1 < s2) return -1; 183 if (s1 > s2) return 1; 184 return 0; 185 } 186 187 // This generates the last function in the chain. It merely returns its 188 // arguments in a different order, but first applies the relevant `_step` 189 // function to each value. This is the only place in the process where 190 // the passed/return values are modified. Hence it gives us a way to be 191 // sure that the values made it all the way from the start function to the 192 // end of the chain (here) and back to the start function. Back in the 193 // start function, we will apply the relevant `_unstep` function to each 194 // returned value, which should give the value that was sent up the chain 195 // originally. 196 // 197 // Here and below, the following naming scheme is used: 198 // 199 // * taIn -- types of arguments that come in to this function 200 // * taOut -- types of arguments that this function passes 201 // to the next in the chain 202 // * trOut -- types of results that this function returns 203 // * trIn -- types of results that the next function in the chain 204 // returns to this function 205 // 206 // Hence 'a' vs 'r' distinguishes argument from return types, and 'In' vs 207 // 'Out' distinguishes values coming "in" to the function from those going 208 // "out". The 'a'/'r' naming scheme is also used in the generated wasm (text). 209 function genEnd(myFuncName, taIn, trOut) { 210 assertEq(taIn.length, trOut.length); 211 let params = taIn.map(pair => `(param $a${pair.name} ${pair.type})`) 212 .join(` `); 213 let retTys = trOut.map(pair => pair.type).join(` `); 214 let t = 215 `(func $${myFuncName} (export "${myFuncName}") ` + 216 ` ${params} (result ${retTys})\n` + 217 trOut.map(pair => 218 ` (call $step_${pair.type} (local.get $a${pair.name}))`) 219 .join(`\n`) + `\n` + 220 `)`; 221 return t; 222 } 223 224 // This generates an intermediate function in the chain. It takes args as 225 // specified by `taIn`, rearranges them to match `taOut`, passes those to the 226 // next function in the chain. From which it receives return values as 227 // described by `trIn`, which it rearranges to match `trOut`, and returns 228 // those. Overall, then, it permutes values both in the calling direction and 229 // in the returning direction. 230 function genMiddle(myFuncName, nextFuncName, taIn, trOut, taOut, trIn) { 231 assertEq(taIn.length, taOut.length); 232 assertEq(taIn.length, trIn.length); 233 assertEq(taIn.length, trOut.length); 234 let params = taIn.map(pair => `(param $a${pair.name} ${pair.type})`) 235 .join(` `); 236 let retTys = trOut.map(pair => pair.type).join(` `); 237 let trInSorted = trIn.toSorted((p1,p2) => strcmp(p1.name,p2.name)); 238 let t = 239 `(func $${myFuncName} (export "${myFuncName}") ` + 240 ` ${params} (result ${retTys})\n` + 241 // Declare locals 242 trInSorted 243 .map(pair => ` (local $r${pair.name} ${pair.type})`) 244 .join(`\n`) + `\n` + 245 // Make the call 246 ` (call $${nextFuncName} ` + 247 taOut.map(pair => `(local.get $a${pair.name})`).join(` `) + `)\n` + 248 // Capture the returned values 249 trIn.toReversed() 250 .map(pair => ` (local.set $r${pair.name})`).join(`\n`) + `\n` + 251 // Return 252 ` (return ` + trOut.map(pair => `(local.get $r${pair.name})`) 253 .join (` `) + `)\n` + 254 `)`; 255 return t; 256 } 257 258 // This generates the first function in the chain. It creates random values 259 // for the initial arguments, passes them to the next arg in the chain, 260 // receives results, and checks that the results are as expected. 261 // NOTE! The generated function returns zero on success, non-zero on failure. 262 function genStart(myFuncName, nextFuncName, taOut, trIn) { 263 assertEq(taOut.length, trIn.length); 264 let taOutSorted = taOut.toSorted((p1,p2) => strcmp(p1.name,p2.name)); 265 let trInSorted = trIn.toSorted((p1,p2) => strcmp(p1.name,p2.name)); 266 // `taOutSorted` and `trInSorted` should be identical. 267 assertEq(taOutSorted.length, trInSorted.length); 268 for (let i = 0; i < taOutSorted.length; i++) { 269 assertEq(taOutSorted[i].name, trInSorted[i].name); 270 assertEq(taOutSorted[i].type, trInSorted[i].type); 271 } 272 let t = 273 `(func $${myFuncName} (export "${myFuncName}") (result i32)\n` + 274 // Declare locals 275 taOutSorted 276 .map(pair => ` (local $a${pair.name} ${pair.type})`) 277 .join(`\n`) + `\n` + 278 trInSorted 279 .map(pair => ` (local $r${pair.name} ${pair.type})`) 280 .join(`\n`) + `\n` + 281 ` (local $anyNotEqual i32)\n` + 282 // Set up the initial values to be fed up the chain of calls and back 283 // down again. We expect them to be the same when they finally arrive 284 // back. Note we re-initialise the (wasm-side) RNG even though this 285 // isn't actually necessary. 286 ` (call $rand_setSeed (i32.const 1))\n` + 287 taOutSorted 288 .map(pair => ` (local.set $a${pair.name} (call $rand_${pair.type}))`) 289 .join(`\n`) + `\n` + 290 // Actually make the call 291 ` (call $${nextFuncName} ` + 292 taOut.map(pair => `(local.get $a${pair.name})`).join(` `) + `)\n` + 293 // Capture the returned values 294 trIn.toReversed() 295 .map(pair => ` (local.set $r${pair.name})`).join(`\n`) + `\n` + 296 297 // For each returned value, apply the relevant `_unstep` function, 298 // then compare it against the original. It should be the same, so 299 // accumulate any signs of difference in $anyNotEqual. Since 300 // `taOutSorted` and `trInSorted` are identical we can iterate over 301 // either. 302 taOutSorted 303 .map(pair => 304 ` (local.set $anyNotEqual \n` + 305 ` (i32.or (local.get $anyNotEqual)\n` + 306 ` (` + 307 // v128 doesn't have a suitable .ne operator, so call a helper fn 308 (pair.type === `v128` ? `call $v128ne` : `${pair.type}.ne`) + 309 ` (local.get $a${pair.name})` + 310 ` (call $unstep_${pair.type} (local.get $r${pair.name})))))` 311 ) 312 .join(`\n`) + `\n` + 313 ` (return (local.get $anyNotEqual))\n` + 314 `)`; 315 return t; 316 } 317 318 // A pseudo-random number generator that is independent of the one baked into 319 // each wasm program generated. This is for use in JS only. It isn't great, 320 // but at least it starts from a fixed place, which Math.random doesn't. This 321 // produces a function `rand4js`, which takes an argument `n` and produces an 322 // integer value in the range `0 .. n-1` inclusive. `n` needs to be less than 323 // or equal to 2^21 for this to work at all, and it needs to be much less than 324 // 2^21 (say, no more than 2^14) in order to get a reasonably even 325 // distribution of the values generated. 326 let rand4js_txt = 327 `(module 328 (global $rand4js_state (mut i32) (i32.const 1)) 329 (func $rand4js (export "rand4js") (param $maxPlus1 i32) (result i32) 330 (local $t i32) 331 ;; update $rand4js_state 332 (local.set $t (global.get $rand4js_state)) 333 (local.set $t (i32.mul (local.get $t) (i32.const 1103515245))) 334 (local.set $t (i32.add (local.get $t) (i32.const 12345))) 335 (global.set $rand4js_state (local.get $t)) 336 ;; Note, the low order bits are not very random. Hence we dump the 337 ;; low-order 11 bits. This leaves us with at best 21 usable bits. 338 (local.set $t (i32.shr_u (local.get $t) (i32.const 11))) 339 (i32.rem_u (local.get $t) (local.get $maxPlus1)) 340 ) 341 )`; 342 let rand4js = new WebAssembly.Instance( 343 new WebAssembly.Module(wasmTextToBinary(rand4js_txt))) 344 .exports.rand4js; 345 346 // Fisher-Yates scheme for generating random permutations of a sequence. 347 // Result is a new array containing the original items in a different order. 348 // Original is unchanged. 349 function toRandomPermutation(input) { 350 let n = input.length; 351 let result = input.slice(); 352 assertEq(result.length, n); 353 if (n < 2) return result; 354 for (let i = 0; i < n - 1; i++) { 355 let j = i + rand4js(n - i); 356 let t = result[i]; 357 result[i] = result[j]; 358 result[j] = t; 359 } 360 return result; 361 } 362 363 // Top level test runner 364 function testMain(numIters) { 365 // Check whether we can use SIMD. 366 let simdEnabled = wasmSimdEnabled(); 367 368 // Names tagged with types. This is set up to provide 10 values that 369 // potentially can be passed in integer registers (5 x i32, 5 x i64) and 370 // 10 values that potentially can be passed in FP/SIMD registers (3 x f32, 371 // 3 x f64, 4 x v128). This should cover both sides of the 372 // arg-passed-in-reg/arg-passed-in-mem boundary for all of the primary 373 // targets. 374 let val0 = {name: "0", type: "i32"}; 375 let val1 = {name: "1", type: "i32"}; 376 let val2 = {name: "2", type: "i32"}; 377 let val3 = {name: "3", type: "i32"}; 378 let val4 = {name: "4", type: "i32"}; 379 380 let val5 = {name: "5", type: "i64"}; 381 let val6 = {name: "6", type: "i64"}; 382 let val7 = {name: "7", type: "i64"}; 383 let val8 = {name: "8", type: "i64"}; 384 let val9 = {name: "9", type: "i64"}; 385 386 let vala = {name: "a", type: "f32"}; 387 let valb = {name: "b", type: "f32"}; 388 let valc = {name: "c", type: "f32"}; 389 390 let vald = {name: "d", type: "f64"}; 391 let vale = {name: "e", type: "f64"}; 392 let valf = {name: "f", type: "f64"}; 393 394 let valg = {name: "g", type: "v128"}; 395 let valh = {name: "h", type: "v128"}; 396 let vali = {name: "i", type: "v128"}; 397 let valj = {name: "j", type: "v128"}; 398 399 // This is the base name/type vector, 400 // of which we will create random permutations. 401 let baseValVec; 402 if (simdEnabled) { 403 baseValVec 404 = [val0, val1, val2, val3, val4, val5, val6, val7, val8, val9, 405 vala, valb, valc, vald, vale, valf, valg, valh, vali, valj]; 406 } else { 407 baseValVec 408 = [val0, val1, val2, val3, val4, val5, val6, val7, val8, val9, 409 vala, valb, valc, vald, vale, valf]; 410 } 411 412 function summariseVec(valVec) { 413 return valVec.map(pair => pair.name).join(""); 414 } 415 416 print("\nsimdEnabled = " + simdEnabled + "\n"); 417 418 for (let testRun = 0; testRun < numIters; testRun++) { 419 let tx0a = toRandomPermutation(baseValVec); 420 let tx0r = toRandomPermutation(baseValVec); 421 let tx1a = toRandomPermutation(baseValVec); 422 let tx1r = toRandomPermutation(baseValVec); 423 let tx2a = toRandomPermutation(baseValVec); 424 let tx2r = toRandomPermutation(baseValVec); 425 let tx3a = toRandomPermutation(baseValVec); 426 let tx3r = toRandomPermutation(baseValVec); 427 let tx4a = toRandomPermutation(baseValVec); 428 let tx4r = toRandomPermutation(baseValVec); 429 430 // Generate a 5-step chain of functions, each one passing and 431 // returning different permutation of `baseValVec`. The chain is: 432 // fnStart -> fnMid0 -> fnMid1 -> fnMid2 -> fnMid3 -> fnEnd 433 let t_end = genEnd("fnEnd", tx4a, tx4r); 434 let t_mid3 = genMiddle("fnMid3", "fnEnd", tx3a, tx3r, tx4a, tx4r); 435 let t_mid2 = genMiddle("fnMid2", "fnMid3", tx2a, tx2r, tx3a, tx3r); 436 let t_mid1 = genMiddle("fnMid1", "fnMid2", tx1a, tx1r, tx2a, tx2r); 437 let t_mid0 = genMiddle("fnMid0", "fnMid1", tx0a, tx0r, tx1a, tx1r); 438 let t_start = genStart("fnStart", "fnMid0", tx0a, tx0r); 439 440 let txt = "(module (memory 1) " + "\n" + 441 decls_rng + "\n" + 442 funcs_util(simdEnabled) + "\n" + funcs_rng(simdEnabled) + "\n" + 443 t_end + "\n" + 444 t_mid3 + "\n" + t_mid2 + "\n" + t_mid1 + "\n" + t_mid0 + "\n" + 445 t_start + "\n" + 446 ")"; 447 448 if (0) print(txt); 449 450 let mod = new WebAssembly.Module(wasmTextToBinary(txt)); 451 let ins = new WebAssembly.Instance(mod); 452 let fns = ins.exports; 453 454 // result == 0 means success, any other value means failure 455 let result = fns.fnStart(); 456 if (/*failure*/result != 0 || (testRun % 120) == 0) 457 print(" " + testRun + " " + 458 [tx0a,tx0r,tx1a,tx1r,tx2a,tx2r,tx3a,tx3r,tx4a,tx4r] 459 .map(e => summariseVec(e)).join("/") + " " 460 + (result == 0 ? "pass" : "FAIL")); 461 462 assertEq(result, 0); 463 } 464 } 465 466 testMain(/*numIters=*/120);