#include <cstdio>
#include <cstdlib>
#include <memory>
#include "ir/branch-utils.h"
#include "ir/iteration.h"
#include "ir/literal-utils.h"
#include "ir/properties.h"
#include "pass.h"
#include "support/colors.h"
#include "support/command-line.h"
#include "support/file.h"
#include "support/path.h"
#include "support/timing.h"
#include "wasm-builder.h"
#include "wasm-io.h"
#include "wasm-validator.h"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
std::string GetLastErrorStdStr() {
DWORD error = GetLastError();
if (error) {
LPVOID lpMsgBuf;
DWORD bufLen = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
error,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf,
0,
NULL);
if (bufLen) {
LPCSTR lpMsgStr = (LPCSTR)lpMsgBuf;
std::string result(lpMsgStr, lpMsgStr + bufLen);
LocalFree(lpMsgBuf);
return result;
}
}
return std::string();
}
#endif
using namespace wasm;
size_t timeout = 2;
struct ProgramResult {
int code;
std::string output;
double time;
ProgramResult() = default;
ProgramResult(std::string command) { getFromExecution(command); }
#ifdef _WIN32
void getFromExecution(std::string command) {
Timer timer;
timer.start();
SECURITY_ATTRIBUTES saAttr;
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = TRUE;
saAttr.lpSecurityDescriptor = NULL;
HANDLE hChildStd_OUT_Rd;
HANDLE hChildStd_OUT_Wr;
if (
!CreatePipe(&hChildStd_OUT_Rd, &hChildStd_OUT_Wr, &saAttr, 0) ||
!SetHandleInformation(hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0)) {
Fatal() << "CreatePipe \"" << command
<< "\" failed: " << GetLastErrorStdStr() << ".\n";
}
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.hStdError = hChildStd_OUT_Wr;
si.hStdOutput = hChildStd_OUT_Wr;
si.dwFlags |= STARTF_USESTDHANDLES;
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcess(NULL, (LPSTR)command.c_str(), NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi) ) {
Fatal() << "CreateProcess \"" << command
<< "\" failed: " << GetLastErrorStdStr() << ".\n";
}
DWORD retVal = WaitForSingleObject(pi.hProcess, timeout * 1000);
if (retVal == WAIT_TIMEOUT) {
printf("Command timeout: %s", command.c_str());
TerminateProcess(pi.hProcess, -1);
}
DWORD dwordExitCode;
if (!GetExitCodeProcess(pi.hProcess, &dwordExitCode)) {
Fatal() << "GetExitCodeProcess failed: " << GetLastErrorStdStr() << ".\n";
}
code = (int)dwordExitCode;
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
{
const int BUFSIZE = 4096;
DWORD dwRead, dwTotal, dwTotalRead = 0;
CHAR chBuf[BUFSIZE];
BOOL bSuccess = FALSE;
PeekNamedPipe(hChildStd_OUT_Rd, NULL, 0, NULL, &dwTotal, NULL);
while (dwTotalRead < dwTotal) {
bSuccess =
ReadFile(hChildStd_OUT_Rd, chBuf, BUFSIZE - 1, &dwRead, NULL);
if (!bSuccess || dwRead == 0)
break;
chBuf[dwRead] = 0;
dwTotalRead += dwRead;
output.append(chBuf);
}
}
timer.stop();
time = timer.getTotal();
}
#else
void getFromExecution(std::string command) {
Timer timer;
timer.start();
code = system(("timeout " + std::to_string(timeout) + "s " + command +
" > /dev/null 2> /dev/null")
.c_str());
const int MAX_BUFFER = 1024;
char buffer[MAX_BUFFER];
FILE* stream = popen(
("timeout " + std::to_string(timeout) + "s " + command + " 2> /dev/null")
.c_str(),
"r");
while (fgets(buffer, MAX_BUFFER, stream) != NULL) {
output.append(buffer);
}
pclose(stream);
timer.stop();
time = timer.getTotal() / 2;
}
#endif
bool operator==(ProgramResult& other) {
return code == other.code && output == other.output;
}
bool operator!=(ProgramResult& other) { return !(*this == other); }
bool failed() { return code != 0; }
void dump(std::ostream& o) {
o << "[ProgramResult] code: " << code << " stdout: \n"
<< output << "[====]\nin " << time << " seconds\n[/ProgramResult]\n";
}
};
namespace std {
inline std::ostream& operator<<(std::ostream& o, ProgramResult& result) {
result.dump(o);
return o;
}
}
ProgramResult expected;
static std::unordered_set<Name> functionsWeTriedToRemove;
struct Reducer
: public WalkerPass<PostWalker<Reducer, UnifiedExpressionVisitor<Reducer>>> {
std::string command, test, working;
bool binary, deNan, verbose, debugInfo;
Reducer(std::string command,
std::string test,
std::string working,
bool binary,
bool deNan,
bool verbose,
bool debugInfo)
: command(command), test(test), working(working), binary(binary),
deNan(deNan), verbose(verbose), debugInfo(debugInfo) {}
void reduceUsingPasses() {
std::vector<std::string> passes = {
"-Oz",
"-Os",
"-O1",
"-O2",
"-O3",
"-O4",
"--flatten -Os",
"--flatten -O3",
"--flatten --local-cse -Os",
"--coalesce-locals --vacuum",
"--dce",
"--duplicate-function-elimination",
"--inlining",
"--inlining-optimizing",
"--optimize-level=3 --inlining-optimizing",
"--memory-packing",
"--remove-unused-names --merge-blocks --vacuum",
"--optimize-instructions",
"--precompute",
"--remove-imports",
"--remove-memory",
"--remove-unused-names --remove-unused-brs",
"--remove-unused-module-elements",
"--remove-unused-nonfunction-module-elements",
"--reorder-functions",
"--reorder-locals",
"--simplify-locals --vacuum",
"--strip",
"--vacuum"};
auto oldSize = file_size(working);
bool more = true;
while (more) {
more = false;
for (auto pass : passes) {
std::string currCommand = Path::getBinaryenBinaryTool("wasm-opt") + " ";
currCommand += working + " --detect-features -o " + test + " " + pass;
if (debugInfo) {
currCommand += " -g ";
}
if (!binary) {
currCommand += " -S ";
}
if (verbose) {
std::cerr << "| trying pass command: " << currCommand << "\n";
}
if (!ProgramResult(currCommand).failed()) {
auto newSize = file_size(test);
if (newSize < oldSize) {
if (ProgramResult(command) == expected) {
std::cerr << "| command \"" << currCommand
<< "\" succeeded, reduced size to " << newSize
<< ", and preserved the property\n";
copy_file(test, working);
more = true;
oldSize = newSize;
}
}
}
}
}
if (verbose) {
std::cerr << "| done with passes for now\n";
}
}
size_t reduceDestructively(int factor_) {
factor = factor_;
loadWorking();
reduced = 0;
funcsSeen = 0;
ProgramResult result;
if (!writeAndTestReduction(result)) {
std::cerr << "\n|! WARNING: writing before destructive reduction fails, "
"very unlikely reduction can work\n"
<< result << '\n';
}
walkModule(getModule());
return reduced;
}
void loadWorking() {
module = make_unique<Module>();
Module wasm;
ModuleReader reader;
reader.read(working, *module);
wasm.features = FeatureSet::All;
builder = make_unique<Builder>(*module);
setModule(module.get());
}
size_t reduced;
Expression* beforeReduction;
std::unique_ptr<Module> module;
std::unique_ptr<Builder> builder;
Index funcsSeen;
int factor;
bool writeAndTestReduction() {
ProgramResult result;
return writeAndTestReduction(result);
}
bool writeAndTestReduction(ProgramResult& out) {
ModuleWriter writer;
writer.setBinary(binary);
writer.setDebugInfo(debugInfo);
writer.write(*getModule(), test);
out.getFromExecution(command);
return out == expected;
}
bool shouldTryToReduce(size_t bonus = 1) {
static size_t counter = 0;
counter += bonus;
return (counter % factor) <= bonus;
}
bool isOkReplacement(Expression* with) {
if (deNan) {
if (auto* c = with->dynCast<Const>()) {
if (c->value.isNaN()) {
return false;
}
}
}
return true;
}
bool tryToReplaceCurrent(Expression* with) {
if (!isOkReplacement(with)) {
return false;
}
auto* curr = getCurrent();
if (curr->type != with->type) {
return false;
}
if (!shouldTryToReduce()) {
return false;
}
replaceCurrent(with);
if (!writeAndTestReduction()) {
replaceCurrent(curr);
return false;
}
std::cerr << "| tryToReplaceCurrent succeeded (in " << getLocation()
<< ")\n";
noteReduction();
return true;
}
void noteReduction(size_t amount = 1) {
reduced += amount;
copy_file(test, working);
}
bool tryToReplaceChild(Expression*& child, Expression* with) {
if (!isOkReplacement(with)) {
return false;
}
if (child->type != with->type) {
return false;
}
if (!shouldTryToReduce()) {
return false;
}
auto* before = child;
child = with;
if (!writeAndTestReduction()) {
child = before;
return false;
}
std::cerr << "| tryToReplaceChild succeeded (in " << getLocation()
<< ")\n";
noteReduction();
return true;
}
std::string getLocation() {
if (getFunction()) {
return getFunction()->name.str;
}
return "(non-function context)";
}
void visitExpression(Expression* curr) {
if (curr->type == Type::none) {
if (tryToReduceCurrentToNop()) {
return;
}
} else if (curr->type.isConcrete()) {
if (tryToReduceCurrentToConst()) {
return;
}
} else {
assert(curr->type == Type::unreachable);
if (tryToReduceCurrentToUnreachable()) {
return;
}
}
if (auto* iff = curr->dynCast<If>()) {
if (iff->type == Type::none) {
if (tryToReplaceCurrent(builder->makeDrop(iff->condition))) {
return;
}
}
handleCondition(iff->condition);
} else if (auto* br = curr->dynCast<Break>()) {
handleCondition(br->condition);
} else if (auto* select = curr->dynCast<Select>()) {
handleCondition(select->condition);
} else if (auto* sw = curr->dynCast<Switch>()) {
handleCondition(sw->condition);
for (auto& target : sw->targets) {
if (target != sw->default_) {
auto old = target;
target = sw->default_;
if (!tryToReplaceCurrent(curr)) {
target = old;
}
}
}
while (sw->targets.size() > 1) {
auto last = sw->targets.back();
sw->targets.pop_back();
if (!tryToReplaceCurrent(curr)) {
sw->targets.push_back(last);
break;
}
}
} else if (auto* block = curr->dynCast<Block>()) {
if (!shouldTryToReduce()) {
return;
}
auto& list = block->list;
if (list.size() == 1 &&
!BranchUtils::BranchSeeker::has(block, block->name)) {
if (tryToReplaceCurrent(block->list[0])) {
return;
}
}
Index i = 0;
while (list.size() > 1 && i < list.size()) {
auto* curr = list[i];
if (curr->is<Nop>() && shouldTryToReduce()) {
for (Index j = i; j < list.size() - 1; j++) {
list[j] = list[j + 1];
}
list.pop_back();
if (writeAndTestReduction()) {
std::cerr << "| block-nop removed\n";
noteReduction();
return;
}
list.push_back(nullptr);
for (Index j = list.size() - 1; j > i; j--) {
list[j] = list[j - 1];
}
list[i] = curr;
}
i++;
}
return; } else if (auto* loop = curr->dynCast<Loop>()) {
if (shouldTryToReduce() &&
!BranchUtils::BranchSeeker::has(loop, loop->name)) {
tryToReplaceCurrent(loop->body);
}
return; }
for (auto* child : ChildIterator(curr)) {
if (child->type.isConcrete() && curr->type == Type::none) {
if (tryToReplaceCurrent(builder->makeDrop(child))) {
return;
}
} else {
if (tryToReplaceCurrent(child)) {
return;
}
}
}
if (curr->type.isSingle() && !curr->is<Unary>()) {
for (auto* child : ChildIterator(curr)) {
if (child->type == curr->type) {
continue; }
if (!child->type.isSingle()) {
continue; }
Expression* fixed = nullptr;
TODO_SINGLE_COMPOUND(curr->type);
switch (curr->type.getBasic()) {
case Type::i32: {
TODO_SINGLE_COMPOUND(child->type);
switch (child->type.getBasic()) {
case Type::i32:
WASM_UNREACHABLE("invalid type");
case Type::i64:
fixed = builder->makeUnary(WrapInt64, child);
break;
case Type::f32:
fixed = builder->makeUnary(TruncSFloat32ToInt32, child);
break;
case Type::f64:
fixed = builder->makeUnary(TruncSFloat64ToInt32, child);
break;
case Type::v128:
case Type::funcref:
case Type::externref:
case Type::exnref:
case Type::anyref:
case Type::eqref:
case Type::i31ref:
continue; case Type::none:
case Type::unreachable:
WASM_UNREACHABLE("unexpected type");
}
break;
}
case Type::i64: {
TODO_SINGLE_COMPOUND(child->type);
switch (child->type.getBasic()) {
case Type::i32:
fixed = builder->makeUnary(ExtendSInt32, child);
break;
case Type::i64:
WASM_UNREACHABLE("invalid type");
case Type::f32:
fixed = builder->makeUnary(TruncSFloat32ToInt64, child);
break;
case Type::f64:
fixed = builder->makeUnary(TruncSFloat64ToInt64, child);
break;
case Type::v128:
case Type::funcref:
case Type::externref:
case Type::exnref:
case Type::anyref:
case Type::eqref:
case Type::i31ref:
continue; case Type::none:
case Type::unreachable:
WASM_UNREACHABLE("unexpected type");
}
break;
}
case Type::f32: {
TODO_SINGLE_COMPOUND(child->type);
switch (child->type.getBasic()) {
case Type::i32:
fixed = builder->makeUnary(ConvertSInt32ToFloat32, child);
break;
case Type::i64:
fixed = builder->makeUnary(ConvertSInt64ToFloat32, child);
break;
case Type::f32:
WASM_UNREACHABLE("unexpected type");
case Type::f64:
fixed = builder->makeUnary(DemoteFloat64, child);
break;
case Type::v128:
case Type::funcref:
case Type::externref:
case Type::exnref:
case Type::anyref:
case Type::eqref:
case Type::i31ref:
continue; case Type::none:
case Type::unreachable:
WASM_UNREACHABLE("unexpected type");
}
break;
}
case Type::f64: {
TODO_SINGLE_COMPOUND(child->type);
switch (child->type.getBasic()) {
case Type::i32:
fixed = builder->makeUnary(ConvertSInt32ToFloat64, child);
break;
case Type::i64:
fixed = builder->makeUnary(ConvertSInt64ToFloat64, child);
break;
case Type::f32:
fixed = builder->makeUnary(PromoteFloat32, child);
break;
case Type::f64:
WASM_UNREACHABLE("unexpected type");
case Type::v128:
case Type::funcref:
case Type::externref:
case Type::exnref:
case Type::anyref:
case Type::eqref:
case Type::i31ref:
continue; case Type::none:
case Type::unreachable:
WASM_UNREACHABLE("unexpected type");
}
break;
}
case Type::v128:
case Type::funcref:
case Type::externref:
case Type::exnref:
case Type::anyref:
case Type::eqref:
case Type::i31ref:
continue; case Type::none:
case Type::unreachable:
WASM_UNREACHABLE("unexpected type");
}
assert(fixed->type == curr->type);
if (tryToReplaceCurrent(fixed)) {
return;
}
}
}
}
void visitFunction(Function* curr) {
if (!curr->imported()) {
visitExpression(curr->body);
}
funcsSeen++;
static int last = 0;
int percentage = (100 * funcsSeen) / getModule()->functions.size();
if (std::abs(percentage - last) >= 5) {
std::cerr << "| " << percentage << "% of funcs complete\n";
last = percentage;
}
}
void visitTable(Table* curr) {
std::cerr << "| try to simplify table\n";
Name first;
for (auto& segment : curr->segments) {
for (auto item : segment.data) {
first = item;
break;
}
if (!first.isNull()) {
break;
}
}
visitSegmented(curr, first, 100);
}
void visitMemory(Memory* curr) {
std::cerr << "| try to simplify memory\n";
visitSegmented(curr, 0, 2);
}
template<typename T, typename U>
void visitSegmented(T* curr, U zero, size_t bonus) {
bool justShrank = false;
bool shrank = false;
for (auto& segment : curr->segments) {
auto& data = segment.data;
size_t skip = 1;
for (size_t i = 0; i < data.size() && !data.empty(); i++) {
if (!justShrank && !shouldTryToReduce(bonus)) {
continue;
}
auto save = data;
for (size_t j = 0; j < skip; j++) {
if (!data.empty()) {
data.pop_back();
}
}
auto justShrank = writeAndTestReduction();
if (justShrank) {
std::cerr << "| shrank segment (skip: " << skip << ")\n";
shrank = true;
noteReduction();
skip = std::min(size_t(factor), 2 * skip);
} else {
data = save;
break;
}
}
}
for (auto& segment : curr->segments) {
if (segment.data.empty()) {
continue;
}
for (auto& item : segment.data) {
if (!shouldTryToReduce(bonus)) {
continue;
}
if (item == zero) {
continue;
}
auto save = item;
item = zero;
if (writeAndTestReduction()) {
std::cerr << "| zeroed segment\n";
noteReduction();
} else {
item = save;
}
if (shrank) {
break;
}
}
}
}
void visitModule(Module* curr) {
assert(curr == module.get());
std::cerr << "| try to remove functions\n";
std::vector<Name> functionNames;
for (auto& func : module->functions) {
functionNames.push_back(func->name);
}
size_t skip = 1;
bool justRemoved = false;
for (size_t i = 0; i < functionNames.size(); i++) {
if (!justRemoved &&
functionsWeTriedToRemove.count(functionNames[i]) == 1 &&
!shouldTryToReduce(std::max((factor / 100) + 1, 1000))) {
continue;
}
std::vector<Name> names;
for (size_t j = 0; names.size() < skip && i + j < functionNames.size();
j++) {
auto name = functionNames[i + j];
if (module->getFunctionOrNull(name)) {
names.push_back(name);
functionsWeTriedToRemove.insert(name);
}
}
if (names.size() == 0) {
continue;
}
std::cout << "| try to remove " << names.size()
<< " functions (skip: " << skip << ")\n";
justRemoved = tryToRemoveFunctions(names);
if (justRemoved) {
noteReduction(names.size());
i += skip;
skip = std::min(size_t(factor), 2 * skip);
} else {
skip = std::max(skip / 2, size_t(1)); }
}
std::cerr << "| try to remove exports (with factor " << factor << ")\n";
std::vector<Export> exports;
for (auto& exp : module->exports) {
exports.push_back(*exp);
}
skip = 1;
for (size_t i = 0; i < exports.size(); i++) {
if (!shouldTryToReduce(std::max((factor / 100) + 1, 1000))) {
continue;
}
std::vector<Export> currExports;
for (size_t j = 0; currExports.size() < skip && i + j < exports.size();
j++) {
auto exp = exports[i + j];
if (module->getExportOrNull(exp.name)) {
currExports.push_back(exp);
module->removeExport(exp.name);
}
}
ProgramResult result;
if (!writeAndTestReduction(result)) {
for (auto exp : currExports) {
module->addExport(new Export(exp));
}
skip = std::max(skip / 2, size_t(1)); } else {
std::cerr << "| removed " << currExports.size() << " exports\n";
noteReduction(currExports.size());
i += skip;
skip = std::min(size_t(factor), 2 * skip);
}
}
if (module->functions.size() == 1 && module->exports.empty() &&
module->table.segments.empty()) {
auto* func = module->functions[0].get();
if (!func->imported() && !Properties::isNamedControlFlow(func->body)) {
auto funcSig = func->sig;
auto* funcBody = func->body;
for (auto* child : ChildIterator(func->body)) {
if (!(child->type.isConcrete() || child->type == Type::none)) {
continue; }
func->sig.results = child->type;
func->body = child;
if (writeAndTestReduction()) {
std::cerr << "| altered function result type\n";
noteReduction(1);
break;
}
func->sig = funcSig;
func->body = funcBody;
}
}
}
}
bool tryToRemoveFunctions(std::vector<Name> names) {
for (auto name : names) {
module->removeFunction(name);
}
struct FunctionReferenceRemover
: public PostWalker<FunctionReferenceRemover> {
std::unordered_set<Name> names;
std::vector<Name> exportsToRemove;
FunctionReferenceRemover(std::vector<Name>& vec) {
for (auto name : vec) {
names.insert(name);
}
}
void visitCall(Call* curr) {
if (names.count(curr->target)) {
replaceCurrent(Builder(*getModule()).replaceWithIdenticalType(curr));
}
}
void visitExport(Export* curr) {
if (names.count(curr->value)) {
exportsToRemove.push_back(curr->name);
}
}
void visitTable(Table* curr) {
Name other;
for (auto& segment : curr->segments) {
for (auto name : segment.data) {
if (!names.count(name)) {
other = name;
break;
}
}
if (!other.isNull()) {
break;
}
}
if (other.isNull()) {
return; }
for (auto& segment : curr->segments) {
for (auto& name : segment.data) {
if (names.count(name)) {
name = other;
}
}
}
}
void doWalkModule(Module* module) {
PostWalker<FunctionReferenceRemover>::doWalkModule(module);
for (auto name : exportsToRemove) {
module->removeExport(name);
}
}
};
FunctionReferenceRemover referenceRemover(names);
referenceRemover.walkModule(module.get());
if (WasmValidator().validate(
*module, WasmValidator::Globally | WasmValidator::Quiet) &&
writeAndTestReduction()) {
std::cerr << "| removed " << names.size() << " functions\n";
return true;
} else {
loadWorking(); return false;
}
}
void handleCondition(Expression*& condition) {
if (!condition) {
return;
}
if (condition->is<Const>()) {
return;
}
auto* c = builder->makeConst(int32_t(0));
if (!tryToReplaceChild(condition, c)) {
c->value = Literal(int32_t(1));
tryToReplaceChild(condition, c);
}
}
bool tryToReduceCurrentToNop() {
auto* curr = getCurrent();
if (curr->is<Nop>()) {
return false;
}
Nop nop;
if (tryToReplaceCurrent(&nop)) {
replaceCurrent(builder->makeNop());
return true;
}
return false;
}
bool tryToReduceCurrentToConst() {
auto* curr = getCurrent();
if (curr->is<Const>()) {
return false;
}
if (curr->type.isNullable()) {
RefNull* n = builder->makeRefNull(curr->type);
return tryToReplaceCurrent(n);
}
if (curr->type.isTuple()) {
Expression* n =
builder->makeConstantExpression(Literal::makeZeros(curr->type));
return tryToReplaceCurrent(n);
}
Const* c = builder->makeConst(int32_t(0));
if (tryToReplaceCurrent(c)) {
return true;
}
c->value = Literal::makeOne(curr->type);
c->type = curr->type;
return tryToReplaceCurrent(c);
}
bool tryToReduceCurrentToUnreachable() {
auto* curr = getCurrent();
if (curr->is<Unreachable>()) {
return false;
}
Unreachable un;
if (tryToReplaceCurrent(&un)) {
replaceCurrent(builder->makeUnreachable());
return true;
}
return false;
}
};
int main(int argc, const char* argv[]) {
std::string input, test, working, command;
std::string binDir = Path::getDirName(argv[0]);
bool binary = true, deNan = false, verbose = false, debugInfo = false,
force = false;
Options options("wasm-reduce",
"Reduce a wasm file to a smaller one that has the same "
"behavior on a given command");
options
.add("--command",
"-cmd",
"The command to run on the test, that we want to reduce while keeping "
"the command's output identical. "
"We look at the command's return code and stdout here (TODO: stderr), "
"and we reduce while keeping those unchanged.",
Options::Arguments::One,
[&](Options* o, const std::string& argument) { command = argument; })
.add("--test",
"-t",
"Test file (this will be written to to test, the given command should "
"read it when we call it)",
Options::Arguments::One,
[&](Options* o, const std::string& argument) { test = argument; })
.add("--working",
"-w",
"Working file (this will contain the current good state while doing "
"temporary computations, "
"and will contain the final best result at the end)",
Options::Arguments::One,
[&](Options* o, const std::string& argument) { working = argument; })
.add("--binaries",
"-b",
"binaryen binaries location (bin/ directory)",
Options::Arguments::One,
[&](Options* o, const std::string& argument) {
binDir = argument + Path::getPathSeparator();
})
.add("--text",
"-S",
"Emit intermediate files as text, instead of binary (also make sure "
"the test and working files have a .wat or .wast suffix)",
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) { binary = false; })
.add("--denan",
"",
"Avoid nans when reducing",
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) { deNan = true; })
.add("--verbose",
"-v",
"Verbose output mode",
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) { verbose = true; })
.add("--debugInfo",
"-g",
"Keep debug info in binaries",
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) { debugInfo = true; })
.add("--force",
"-f",
"Force the reduction attempt, ignoring problems that imply it is "
"unlikely to succeed",
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) { force = true; })
.add("--timeout",
"-to",
"A timeout to apply to each execution of the command, in seconds "
"(default: 2)",
Options::Arguments::One,
[&](Options* o, const std::string& argument) {
timeout = atoi(argument.c_str());
std::cout << "|applying timeout: " << timeout << "\n";
})
.add_positional(
"INFILE",
Options::Arguments::One,
[&](Options* o, const std::string& argument) { input = argument; });
options.parse(argc, argv);
if (test.size() == 0) {
Fatal() << "test file not provided\n";
}
if (working.size() == 0) {
Fatal() << "working file not provided\n";
}
if (!binary) {
Colors::setEnabled(false);
}
Path::setBinaryenBinDir(binDir);
std::cerr << "|wasm-reduce\n";
std::cerr << "|input: " << input << '\n';
std::cerr << "|test: " << test << '\n';
std::cerr << "|working: " << working << '\n';
std::cerr << "|bin dir: " << binDir << '\n';
copy_file(input, test);
expected.getFromExecution(command);
std::cerr << "|expected result:\n" << expected << '\n';
std::cerr << "|!! Make sure the above is what you expect! !!\n\n";
auto stopIfNotForced = [&](std::string message, ProgramResult& result) {
std::cerr << "|! " << message << '\n' << result << '\n';
if (!force) {
Fatal() << "|! stopping, as it is very unlikely reduction can succeed "
"(use -f to ignore this check)";
}
};
if (expected.time + 1 >= timeout) {
stopIfNotForced("execution time is dangerously close to the timeout - you "
"should probably increase the timeout",
expected);
}
std::cerr
<< "|checking that command has different behavior on invalid binary (this "
"verifies that the test file is used by the command)\n";
{
{
std::ofstream dst(test, std::ios::binary);
dst << "waka waka\n";
}
ProgramResult result(command);
if (result == expected) {
stopIfNotForced(
"running command on an invalid module should give different results",
result);
}
}
std::cerr << "|checking that command has expected behavior on canonicalized "
"(read-written) binary\n";
{
auto cmd = Path::getBinaryenBinaryTool("wasm-opt") + " " + input +
" --detect-features -o " + test;
if (!binary) {
cmd += " -S";
}
ProgramResult readWrite(cmd);
if (readWrite.failed()) {
stopIfNotForced("failed to read and write the binary", readWrite);
} else {
ProgramResult result(command);
if (result != expected) {
stopIfNotForced("running command on the canonicalized module should "
"give the same results",
result);
}
}
}
copy_file(input, working);
auto workingSize = file_size(working);
std::cerr << "|input size: " << workingSize << "\n";
std::cerr << "|starting reduction!\n";
int factor = workingSize * 2;
size_t lastDestructiveReductions = 0;
size_t lastPostPassesSize = 0;
bool stopping = false;
while (1) {
Reducer reducer(command, test, working, binary, deNan, verbose, debugInfo);
std::cerr << "| reduce using passes...\n";
auto oldSize = file_size(working);
reducer.reduceUsingPasses();
auto newSize = file_size(working);
auto passProgress = oldSize - newSize;
std::cerr << "| after pass reduction: " << newSize << "\n";
if (stopping) {
break;
}
if (lastPostPassesSize && newSize >= lastPostPassesSize) {
std::cerr << "| progress has stopped, skipping to the end\n";
if (factor == 1) {
stopping = true;
} else {
factor = 1;
}
}
lastPostPassesSize = newSize;
std::cerr << "| pass progress: " << passProgress
<< ", last destructive: " << lastDestructiveReductions << '\n';
if (passProgress >= 4 * lastDestructiveReductions) {
std::cerr << "| progress is good, do not quickly decrease factor\n";
} else {
if (factor > 10) {
factor = (factor / 3) + 1;
} else {
factor = (factor + 1) / 2; }
}
assert(newSize > 4); factor = std::min(factor, int(newSize) / 4);
while (1) {
std::cerr << "| reduce destructively... (factor: " << factor << ")\n";
lastDestructiveReductions = reducer.reduceDestructively(factor);
if (lastDestructiveReductions > 0) {
break;
}
if (factor == 1) {
stopping = true;
break;
}
factor = std::max(
1, factor / 4); }
std::cerr << "| destructive reduction led to size: " << file_size(working)
<< '\n';
}
std::cerr << "|finished, final size: " << file_size(working) << "\n";
copy_file(working, test); }