diff --git a/scripts/fuzz_shell.js b/scripts/fuzz_shell.js index 2c7681cb501..1856c815b3e 100644 --- a/scripts/fuzz_shell.js +++ b/scripts/fuzz_shell.js @@ -387,27 +387,12 @@ function hashCombine(seed, value) { /* async */ function callExports(ordering) { // Call the exports we were told, or if we were not given an explicit list, // call them all. - var relevantExports = exportsToCall || exportList; - - if (ordering !== undefined) { - // Copy the list, and sort it in the simple Fisher-Yates manner. - // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm - relevantExports = relevantExports.slice(0); - for (var i = 0; i < relevantExports.length - 1; i++) { - // Pick the index of the item to place at index |i|. - ordering = hashCombine(ordering, i); - // The number of items to pick from begins at the full length, then - // decreases with i. - var j = i + (ordering % (relevantExports.length - i)); - // Swap the item over here. - var t = relevantExports[j]; - relevantExports[j] = relevantExports[i]; - relevantExports[i] = t; - } - } + let relevantExports = exportsToCall || exportList; - for (var e of relevantExports) { - var name, value; + // Build the list of call tasks to run, one for each relevant export. + let tasks = []; + for (let e of relevantExports) { + let name, value; if (typeof e === 'string') { // We are given a string name to call. Look it up in the global namespace. name = e; @@ -423,16 +408,78 @@ function hashCombine(seed, value) { continue; } + // A task is a name + a function to call. For an export, the function is + // simply a call of the export. + tasks.push({ name: name, func: /* async */ () => callFunc(value) }); + } + + // Reverse the array, so the first task is at the end, for efficient + // popping in the common case. + tasks.reverse(); + + // Execute tasks while they remain. + while (tasks.length) { + let task; + if (ordering === undefined) { + // Use the natural order. + task = tasks.pop(); + } else { + // Pick a random task. + ordering = hashCombine(ordering, tasks.length); + let i = ordering % tasks.length; + task = tasks.splice(i, 1)[0]; + } + + // Execute the task. + console.log('[fuzz-exec] calling ' + task.name); + let result; try { - console.log('[fuzz-exec] calling ' + name); - // TODO: Based on |ordering|, do not always await, leaving a promise - // for later, so we interleave stacks. - var result = /* await */ callFunc(value); - if (typeof result !== 'undefined') { - console.log('[fuzz-exec] note result: ' + name + ' => ' + printed(result)); - } + result = task.func(); } catch (e) { console.log('exception thrown: ' + e); + continue; + } + + if (JSPI) { + // When we are changing up the order, in JSPI we can also leave some + // promises unresolved until later, which lets us interleave them. Note we + // never defer a task more than once, and we only defer a promise (which + // we check for using .then). + // TODO: Deferring more than once may make sense, by chaining promises in + // JS (that would not add wasm execution in the middle, but might + // find JS issues in principle). We could also link promises by + // depending on each other, ensuring certain orders of execution. + if (ordering !== undefined && !task.deferred && result && + typeof result == 'object' && typeof result.then === 'function') { + // Hash with -1 here, just to get something different than the hashing a + // few lines above. + ordering = hashCombine(ordering, -1); + if (ordering & 1) { + // Defer it for later. Reuse the existing task for simplicity. + console.log(`(jspi: defer ${task.name})`); + task.func = /* async */ () => { + console.log(`(jspi: finish ${task.name})`); + return /* await */ result; + }; + task.deferred = true; + tasks.push(task); + continue; + } + // Otherwise, continue down. + } + + // Await it right now. + try { + result = /* await */ result; + } catch (e) { + console.log('exception thrown: ' + e); + continue; + } + } + + // Log the result. + if (typeof result !== 'undefined') { + console.log('[fuzz-exec] note result: ' + task.name + ' => ' + printed(result)); } } } diff --git a/test/lit/d8/fuzz_shell_jspi.wast b/test/lit/d8/fuzz_shell_jspi.wast new file mode 100644 index 00000000000..cc1e35f0274 --- /dev/null +++ b/test/lit/d8/fuzz_shell_jspi.wast @@ -0,0 +1,81 @@ +(module + (import "fuzzing-support" "log-i32" (func $log (param i32))) + + (func $a (export "a") (result i32) + (i32.const 10) + ) + + (func $b (export "b") (result i32) + (i32.const 20) + ) + + (func $c (export "c") (result i32) + (i32.const 30) + ) + + (func $d (export "d") (result i32) + (i32.const 40) + ) + + (func $e (export "e") (result i32) + (i32.const 50) + ) +) + +;; Apply JSPI: first, prepend JSPI = 1. + +;; RUN: echo "JSPI = 1;" > %t.js + +;; Second, remove comments around async and await: feed fuzz_shell.js into node +;; as stdin, so all node needs to do is read stdin, do the replacements, and +;; write to stdout. + +;; RUN: cat %S/../../../scripts/fuzz_shell.js | node -e "process.stdout.write(require('fs').readFileSync(0, 'utf-8').replace(/[/][*] async [*][/]/g, 'async').replace(/[/][*] await [*][/]/g, 'await'))" >> %t.js + +;; Append another run with a random seed, so we reorder and delay execution. +;; RUN: echo "callExports(42);" >> %t.js + +;; Run that JS shell with our wasm. +;; RUN: wasm-opt %s -o %t.wasm -q +;; RUN: v8 --wasm-staging %t.js -- %t.wasm | filecheck %s +;; +;; The output here looks a little out of order, in particular because we do not +;; |await| the toplevel callExports() calls. That |await| is only valid if we +;; pass --module, which we do not fuzz with. As a result, the first await +;; operation in the first callExports() leaves that function and continues to +;; the next, but we do get around to executing all the things we need. In +;; particular, the output here should contain two "node result" lines for each +;; of the 5 functions (one from each callExports()). The important thing is that +;; we get a random-like ordering, which includes some defers (each of which has +;; a later finish), showing that we interleave stacks. +;; +;; CHECK: [fuzz-exec] calling a +;; CHECK: [fuzz-exec] calling b +;; CHECK: [fuzz-exec] note result: a => 10 +;; CHECK: [fuzz-exec] calling b +;; CHECK: [fuzz-exec] note result: b => 20 +;; CHECK: [fuzz-exec] calling a +;; CHECK: (jspi: defer a) +;; CHECK: [fuzz-exec] calling d +;; CHECK: (jspi: defer d) +;; CHECK: [fuzz-exec] calling e +;; CHECK: [fuzz-exec] note result: b => 20 +;; CHECK: [fuzz-exec] calling c +;; CHECK: [fuzz-exec] note result: e => 50 +;; CHECK: [fuzz-exec] calling c +;; CHECK: (jspi: defer c) +;; CHECK: [fuzz-exec] calling c +;; CHECK: (jspi: finish c) +;; CHECK: [fuzz-exec] note result: c => 30 +;; CHECK: [fuzz-exec] calling d +;; CHECK: [fuzz-exec] note result: c => 30 +;; CHECK: [fuzz-exec] calling d +;; CHECK: (jspi: finish d) +;; CHECK: [fuzz-exec] note result: d => 40 +;; CHECK: [fuzz-exec] calling e +;; CHECK: [fuzz-exec] note result: d => 40 +;; CHECK: [fuzz-exec] calling a +;; CHECK: (jspi: finish a) +;; CHECK: [fuzz-exec] note result: a => 10 +;; CHECK: [fuzz-exec] note result: e => 50 + diff --git a/test/lit/node/fuzz_shell_orders.wast b/test/lit/node/fuzz_shell_orders.wast index 6d51c4e1b2c..76fd40f4548 100644 --- a/test/lit/node/fuzz_shell_orders.wast +++ b/test/lit/node/fuzz_shell_orders.wast @@ -32,7 +32,7 @@ ;; Append another run with a seed that leads to a different order ;; ;; RUN: cp %S/../../../scripts/fuzz_shell.js %t.js -;; RUN: echo "callExports(1337);" >> %t.js +;; RUN: echo "callExports(34);" >> %t.js ;; RUN: node %t.js %t.wasm | filecheck %s --check-prefix=APPENDED ;; ;; The original order: a,b,c