diff --git a/src/support/file.cpp b/src/support/file.cpp index 830b34f98c4..d092ae302d8 100644 --- a/src/support/file.cpp +++ b/src/support/file.cpp @@ -126,8 +126,7 @@ wasm::Output::Output(const std::string& filename, Flags::BinaryOption binary) return buffer; }()) {} -void wasm::write_file(const std::string& filename, - const std::vector& bytes) { +void wasm::write_file(const std::string& filename, std::string_view bytes) { wasm::Output out(filename, Flags::Binary); out.write(bytes.data(), bytes.size()); } diff --git a/src/support/file.h b/src/support/file.h index 584f63cf125..8f302e599d3 100644 --- a/src/support/file.h +++ b/src/support/file.h @@ -70,7 +70,11 @@ class Output { }; // Write the bytes to the given file. -void write_file(const std::string& filename, const std::vector& bytes); +void write_file(const std::string& filename, std::string_view bytes); +inline void write_file(const std::string& filename, + const std::vector& bytes) { + write_file(filename, std::string_view(bytes.data(), bytes.size())); +} // Copies a file to another file void copy_file(std::string input, std::string output); diff --git a/src/tools/wasm-reduce/wasm-reduce.cpp b/src/tools/wasm-reduce/wasm-reduce.cpp index 4caa99c6c84..1ffd2699d43 100644 --- a/src/tools/wasm-reduce/wasm-reduce.cpp +++ b/src/tools/wasm-reduce/wasm-reduce.cpp @@ -39,6 +39,7 @@ #include "support/delta_debugging.h" #include "support/file.h" #include "support/hash.h" +#include "support/js-embedded-module.h" #include "support/path.h" #include "support/timing.h" #include "tools/tool-options.h" @@ -222,11 +223,6 @@ inline std::ostream& operator<<(std::ostream& o, ProgramResult& result) { ProgramResult expected; -// Removing functions is extremely beneficial and efficient. We aggressively -// try to remove functions, unless we've seen they can't be removed, in which -// case we may try again but much later. -static std::unordered_set functionsWeTriedToRemove; - // The index of the working file we save, when saveAllWorkingFiles. We must // store this globally so that the difference instances of Reducer do not // overlap. @@ -316,6 +312,47 @@ struct WasmTestCaseHandler : public TestCaseHandler { } }; +// Handles JavaScript test cases containing embedded WebAssembly byte arrays. +struct JsTestCaseHandler : public TestCaseHandler { + std::string workingJs; + size_t moduleStart = 0; + size_t moduleEnd = 0; + std::vector moduleBytes; + + explicit JsTestCaseHandler(std::string workingJs) + : workingJs(std::move(workingJs)) {} + + void setActiveModule(const EmbeddedModule& mod) { + moduleStart = mod.start; + moduleEnd = mod.end; + moduleBytes = mod.bytes; + } + + std::vector readWorking(const std::string& working) override { + // Return the cached working bytes instead of reading them back from the + // file because we wouldn't know which of potentially many modules embedded + // in the file is the current one. + return moduleBytes; + } + + void writeWorking(const std::vector& bytes, + const std::string& working) override { + std::string formatted = formatByteArray(bytes); + workingJs.replace(moduleStart, moduleEnd - moduleStart, formatted); + moduleEnd = moduleStart + formatted.size(); + moduleBytes = bytes; + write_file(working, workingJs); + } + + void writeTest(const std::vector& bytes, + const std::string& test) override { + std::string formatted = formatByteArray(bytes); + std::string candidateJs = workingJs.substr(0, moduleStart) + formatted + + workingJs.substr(moduleEnd); + write_file(test, candidateJs); + } +}; + struct Reducer : public WalkerPass>> { TestCaseHandler& handler; @@ -1635,7 +1672,7 @@ int main(int argc, const char* argv[]) { // By default, look for binaries alongside our own binary. std::string binDir = Path::getDirName(argv[0]); bool binary = true, deNan = false, verbose = false, debugInfo = false, - force = false; + force = false, js = false; const std::string WasmReduceOption = "wasm-reduce options"; @@ -1735,6 +1772,12 @@ More documentation can be found at WasmReduceOption, Options::Arguments::Zero, [&](Options* o, const std::string& argument) { binary = false; }) + .add("--js", + "", + "Reduce embedded WebAssembly modules inside a JavaScript file", + WasmReduceOption, + Options::Arguments::Zero, + [&](Options* o, const std::string& argument) { js = true; }) .add("--denan", "", "Avoid nans when reducing", @@ -1795,6 +1838,10 @@ More documentation can be found at [&](Options* o, const std::string& argument) { input = argument; }); options.parse(argc, argv); + if (js && !binary) { + Fatal() << "--js and --text cannot be used together\n"; + } + if (debugInfo) { extraFlags += " -g "; } @@ -1833,15 +1880,40 @@ More documentation can be found at copy_file(input, working); - WasmTestCaseHandler handler; - reduceModule(handler, - command, - test, - working, - binary, - deNan, - verbose, - debugInfo, - force, - options); + if (js) { + auto workingJs = read_file(working, Flags::Binary); + auto modules = findEmbeddedModules(workingJs); + if (modules.empty()) { + Fatal() << "no embedded wasm modules found in " << input << "\n"; + } + JsTestCaseHandler handler(std::move(workingJs)); + for (size_t i = modules.size(); i > 0; --i) { + size_t idx = i - 1; + std::cerr << "|reducing embedded module " << idx << " of " + << modules.size() << "\n"; + handler.setActiveModule(modules[idx]); + reduceModule(handler, + command, + test, + working, + binary, + deNan, + verbose, + debugInfo, + force, + options); + } + } else { + WasmTestCaseHandler handler; + reduceModule(handler, + command, + test, + working, + binary, + deNan, + verbose, + debugInfo, + force, + options); + } } diff --git a/test/lit/help/wasm-reduce.test b/test/lit/help/wasm-reduce.test index 20a2575a81e..92c12b7810f 100644 --- a/test/lit/help/wasm-reduce.test +++ b/test/lit/help/wasm-reduce.test @@ -85,6 +85,9 @@ ;; CHECK-NEXT: working files have a .wat or .wast ;; CHECK-NEXT: suffix) ;; CHECK-EMPTY: +;; CHECK-NEXT: --js Reduce embedded WebAssembly modules +;; CHECK-NEXT: inside a JavaScript file +;; CHECK-EMPTY: ;; CHECK-NEXT: --denan Avoid nans when reducing ;; CHECK-EMPTY: ;; CHECK-NEXT: --verbose,-v Verbose output mode diff --git a/test/lit/lit.cfg.py b/test/lit/lit.cfg.py index 148e89e9031..ad1c35f2644 100644 --- a/test/lit/lit.cfg.py +++ b/test/lit/lit.cfg.py @@ -67,3 +67,8 @@ def is_exe(fpath): V8 = os.environ.get('V8') or which('v8') or which('d8') if V8: config.substitutions.append(('v8', V8)) + +NODE = os.environ.get('NODE') or which('node') +if NODE: + config.available_features.add('node') + config.substitutions.append(('node', NODE)) diff --git a/test/lit/wasm-reduce/js-multi.test b/test/lit/wasm-reduce/js-multi.test new file mode 100644 index 00000000000..4fb186d88da --- /dev/null +++ b/test/lit/wasm-reduce/js-multi.test @@ -0,0 +1,87 @@ +// REQUIRES: node + +// Reduce multiple embedded Wasm modules in a JS file in reverse order (last to +// first) and verify that both modules are minimized without offset drift. +// RUN: wasm-reduce %s --js -t %t.test.js -w %t.working.js '--command=node %t.test.js' 2>&1 | filecheck %s --check-prefix=LOG +// RUN: filecheck %s < %t.working.js +// RUN: node %t.working.js | filecheck %s --check-prefix=OUT + +// LOG: |reducing embedded module 1 of 2 +// LOG: |input size: 52 +// LOG: |finished, final size: 34 +// LOG: |reducing embedded module 0 of 2 +// LOG: |input size: 52 +// LOG: |finished, final size: 34 + +// Reduced module 0: (module (func (export "a") (result i32) (i32.const 10))) +// Reduced module 1: (module (func (export "b") (result i32) (i32.const 32))) +// CHECK: const bytes0 = new Uint8Array([ +// CHECK-NEXT: 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, +// CHECK-NEXT: 0x02, 0x01, 0x00, 0x07, 0x05, 0x01, 0x01, 0x61, 0x00, 0x00, 0x0a, 0x06, 0x01, 0x04, 0x00, 0x41, +// CHECK-NEXT: 0x0a, 0x0b +// CHECK-NEXT: ]); +// CHECK: const bytes1 = new Uint8Array([ +// CHECK-NEXT: 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, +// CHECK-NEXT: 0x02, 0x01, 0x00, 0x07, 0x05, 0x01, 0x01, 0x62, 0x00, 0x00, 0x0a, 0x06, 0x01, 0x04, 0x00, 0x41, +// CHECK-NEXT: 0x20, 0x0b +// CHECK-NEXT: ]); +// CHECK-NEXT: const inst0 = new WebAssembly.Instance(new WebAssembly.Module(bytes0)); +// CHECK-NEXT: const inst1 = new WebAssembly.Instance(new WebAssembly.Module(bytes1)); +// CHECK-NEXT: console.log(inst0.exports.a() + inst1.exports.b()); + +// OUT: 42 + +// Original module 0 (52 bytes): +// (module +// (func $unused0 (param i32) (result i32) +// (i32.sub (local.get 0) (i32.const 77)) +// ) +// (func (export "a") (result i32) +// (i32.add (i32.const 7) (i32.const 3)) +// ) +// ) +const bytes0 = new Uint8Array([ + // Magic ("\0asm") and version (1) + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + // Type section: 2 types + 0x01, 0x0a, 0x02, + 0x60, 0x01, 0x7f, 0x01, 0x7f, // type 0: (func (param i32) (result i32)) [removed with $unused0] + 0x60, 0x00, 0x01, 0x7f, // type 1: (func (result i32)) [kept] + // Function section: 2 functions (type 0 [removed], type 1 [kept]) + 0x03, 0x03, 0x02, 0x00, 0x01, + // Export section: export "a" -> func 1 + 0x07, 0x05, 0x01, 0x01, 0x61, 0x00, 0x01, + // Code section: 2 function bodies + 0x0a, 0x12, 0x02, + 0x08, 0x00, 0x20, 0x00, 0x41, 0xcd, 0x00, 0x6b, 0x0b, // func 0 ($unused0): (i32.sub (local.get 0) (i32.const 77)) [removed] + 0x07, 0x00, 0x41, 0x07, 0x41, 0x03, 0x6a, 0x0b // func 1 ("a"): (i32.add (i32.const 7) (i32.const 3)) [simplified to (i32.const 10)] +]); + +// Original module 1 (52 bytes): +// (module +// (func $unused1 (param i32) (result i32) +// (i32.xor (local.get 0) (i32.const 88)) +// ) +// (func (export "b") (result i32) +// (i32.add (i32.const 30) (i32.const 2)) +// ) +// ) +const bytes1 = new Uint8Array([ + // Magic ("\0asm") and version (1) + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + // Type section: 2 types + 0x01, 0x0a, 0x02, + 0x60, 0x01, 0x7f, 0x01, 0x7f, // type 0: (func (param i32) (result i32)) [removed with $unused1] + 0x60, 0x00, 0x01, 0x7f, // type 1: (func (result i32)) [kept] + // Function section: 2 functions (type 0 [removed], type 1 [kept]) + 0x03, 0x03, 0x02, 0x00, 0x01, + // Export section: export "b" -> func 1 + 0x07, 0x05, 0x01, 0x01, 0x62, 0x00, 0x01, + // Code section: 2 function bodies + 0x0a, 0x12, 0x02, + 0x08, 0x00, 0x20, 0x00, 0x41, 0xd8, 0x00, 0x73, 0x0b, // func 0 ($unused1): (i32.xor (local.get 0) (i32.const 88)) [removed] + 0x07, 0x00, 0x41, 0x1e, 0x41, 0x02, 0x6a, 0x0b // func 1 ("b"): (i32.add (i32.const 30) (i32.const 2)) [simplified to (i32.const 32)] +]); +const inst0 = new WebAssembly.Instance(new WebAssembly.Module(bytes0)); +const inst1 = new WebAssembly.Instance(new WebAssembly.Module(bytes1)); +console.log(inst0.exports.a() + inst1.exports.b()); diff --git a/test/lit/wasm-reduce/js.test b/test/lit/wasm-reduce/js.test new file mode 100644 index 00000000000..c311772e3f4 --- /dev/null +++ b/test/lit/wasm-reduce/js.test @@ -0,0 +1,64 @@ +// REQUIRES: node + +// Verify that --js and --text cannot be used together. +// RUN: not wasm-reduce %s --js --text -t %t.test.js -w %t.working.js '--command=node %t.test.js' 2>&1 | filecheck %s --check-prefix=TEXT-ERR +// TEXT-ERR: Fatal: --js and --text cannot be used together + +// Verify that an error is reported when no embedded Wasm modules are found. +// RUN: echo "console.log('no wasm');" > %t.empty.js +// RUN: not wasm-reduce %t.empty.js --js -t %t.test.js -w %t.working.js '--command=node %t.test.js' 2>&1 | filecheck %s --check-prefix=EMPTY-ERR +// EMPTY-ERR: Fatal: no embedded wasm modules found + +// Reduce a single embedded Wasm module in a JS file and verify the reduced JS +// output and execution behavior. +// RUN: wasm-reduce %s --js -t %t.test.js -w %t.working.js '--command=node %t.test.js' 2>&1 | filecheck %s --check-prefix=LOG +// RUN: filecheck %s < %t.working.js +// RUN: node %t.working.js | filecheck %s --check-prefix=OUT + +// LOG: |reducing embedded module 0 of 1 +// LOG: |input size: 57 +// LOG: |finished, final size: 39 + +// Reduced module: +// (module +// (func (export "answer") (result i32) +// (i32.const 42) +// ) +// ) +// CHECK: const bytes = new Uint8Array([ +// CHECK-NEXT: 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, +// CHECK-NEXT: 0x02, 0x01, 0x00, 0x07, 0x0a, 0x01, 0x06, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x00, 0x00, 0x0a, +// CHECK-NEXT: 0x06, 0x01, 0x04, 0x00, 0x41, 0x2a, 0x0b +// CHECK-NEXT: ]); +// CHECK-NEXT: const inst = new WebAssembly.Instance(new WebAssembly.Module(bytes)); +// CHECK-NEXT: console.log(inst.exports.answer()); + +// OUT: 42 + +// Original module (57 bytes): +// (module +// (func $unused (param i32) (result i32) +// (i32.mul (local.get 0) (i32.const 99)) +// ) +// (func (export "answer") (result i32) +// (i32.add (i32.const 40) (i32.const 2)) +// ) +// ) +const bytes = new Uint8Array([ + // Magic ("\0asm") and version (1) + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + // Type section: 2 types + 0x01, 0x0a, 0x02, + 0x60, 0x01, 0x7f, 0x01, 0x7f, // type 0: (func (param i32) (result i32)) [removed with $unused] + 0x60, 0x00, 0x01, 0x7f, // type 1: (func (result i32)) [kept] + // Function section: 2 functions (type 0 [removed], type 1 [kept]) + 0x03, 0x03, 0x02, 0x00, 0x01, + // Export section: export "answer" -> func 1 + 0x07, 0x0a, 0x01, 0x06, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x00, 0x01, + // Code section: 2 function bodies + 0x0a, 0x12, 0x02, + 0x08, 0x00, 0x20, 0x00, 0x41, 0xe3, 0x00, 0x6c, 0x0b, // func 0 ($unused): (i32.mul (local.get 0) (i32.const 99)) [removed] + 0x07, 0x00, 0x41, 0x28, 0x41, 0x02, 0x6a, 0x0b // func 1 ("answer"): (i32.add (i32.const 40) (i32.const 2)) [simplified to (i32.const 42)] +]); +const inst = new WebAssembly.Instance(new WebAssembly.Module(bytes)); +console.log(inst.exports.answer());