From f2fe7e26267bfd00233e195f992b22fb980e4e20 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 18 Sep 2026 16:31:39 -0700 Subject: [PATCH 1/2] [wasm-reduce] Add --js mode for embedded modules In the new mode, parse the input JS file to find all its embedded modules, then run a full reduction on each of them in sequence. Add a new JSTestCaseHandler that handles splicing the module bytes into the JS at the correct location. Reducing each module in sequence is simple, but in principle can miss reduction opportunities that would require changing multiple modules at once, for example reducing types that are used in inter-module communication. Its expected that the current implementation will be good enough in the common case. --- src/tools/wasm-reduce/wasm-reduce.cpp | 106 +++++++++++++++++++++----- test/lit/help/wasm-reduce.test | 3 + test/lit/lit.cfg.py | 5 ++ test/lit/wasm-reduce/js-multi.test | 87 +++++++++++++++++++++ test/lit/wasm-reduce/js.test | 64 ++++++++++++++++ 5 files changed, 248 insertions(+), 17 deletions(-) create mode 100644 test/lit/wasm-reduce/js-multi.test create mode 100644 test/lit/wasm-reduce/js.test diff --git a/src/tools/wasm-reduce/wasm-reduce.cpp b/src/tools/wasm-reduce/wasm-reduce.cpp index c75483b2e73..1ed9b454917 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. @@ -322,6 +318,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; @@ -1641,7 +1678,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"; @@ -1741,6 +1778,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", @@ -1801,6 +1844,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 "; } @@ -1839,15 +1886,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()); From 4e924285d160242f4f6722af09f5fad611bc886a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 18 Sep 2026 16:52:54 -0700 Subject: [PATCH 2/2] string_view write_file --- src/support/file.cpp | 3 +-- src/support/file.h | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) 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);