tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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);