Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/support/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<char>& 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());
}
Expand Down
6 changes: 5 additions & 1 deletion src/support/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ class Output {
};

// Write the bytes to the given file.
void write_file(const std::string& filename, const std::vector<char>& bytes);
void write_file(const std::string& filename, std::string_view bytes);
inline void write_file(const std::string& filename,
const std::vector<char>& 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);
Expand Down
106 changes: 89 additions & 17 deletions src/tools/wasm-reduce/wasm-reduce.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<Name> 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.
Expand Down Expand Up @@ -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<char> 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<char> 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<char>& 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<char>& 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<PostWalker<Reducer, UnifiedExpressionVisitor<Reducer>>> {
TestCaseHandler& handler;
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 ";
}
Expand Down Expand Up @@ -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<std::string>(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);
}
}
3 changes: 3 additions & 0 deletions test/lit/help/wasm-reduce.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions test/lit/lit.cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
87 changes: 87 additions & 0 deletions test/lit/wasm-reduce/js-multi.test
Original file line number Diff line number Diff line change
@@ -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());
64 changes: 64 additions & 0 deletions test/lit/wasm-reduce/js.test
Original file line number Diff line number Diff line change
@@ -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());
Loading