#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 "ir/utils.h"
#include "pass.h"
#include "support/colors.h"
#include "support/command-line.h"
#include "support/file.h"
#include "support/hash.h"
#include "support/path.h"
#include "support/timing.h"
#include "tool-options.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;
static size_t timeout = 2;
static std::string extraFlags = "-all";
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;
ToolOptions& toolOptions;
Reducer(std::string command,
std::string test,
std::string working,
bool binary,
bool deNan,
bool verbose,
bool debugInfo,
ToolOptions& toolOptions)
: command(command), test(test), working(working), binary(binary),
deNan(deNan), verbose(verbose), debugInfo(debugInfo),
toolOptions(toolOptions) {}
void reduceUsingPasses() {
std::vector<std::string> passes = {
"-Oz",
"-Os",
"-O1",
"-O2",
"-O3",
"-O4",
"--flatten -Os",
"--flatten -O3",
"--flatten --simplify-locals-notee-nostructure --local-cse -Os",
"--type-ssa -Os --type-merging",
"--gufa -O1",
"--coalesce-locals --vacuum",
"--dae",
"--dae-optimizing",
"--dce",
"--duplicate-function-elimination",
"--gto",
"--inlining",
"--inlining-optimizing",
"--optimize-level=3 --inlining-optimizing",
"--local-cse",
"--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-globals",
"--simplify-locals --vacuum",
"--strip",
"--remove-unused-types",
"--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 + " -o " + test + " " + pass + " " + extraFlags;
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 << '\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 = std::make_unique<Module>();
ModuleReader reader;
try {
reader.read(working, *module);
} catch (ParseException& p) {
p.dump(std::cerr);
std::cerr << '\n';
Fatal() << "error in parsing working wasm binary";
}
if (!module->hasFeaturesSection) {
module->features = FeatureSet::All;
}
toolOptions.applyFeatures(*module);
builder = std::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;
}
size_t decisionCounter = 0;
bool shouldTryToReduce(size_t bonus = 1) {
assert(bonus > 0);
decisionCounter += bonus;
return (decisionCounter % factor) <= bonus;
}
size_t deterministicRandom(size_t max) {
assert(max > 0);
hash_combine(decisionCounter, max);
return decisionCounter % max;
}
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.toString();
}
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; } else if (curr->is<Drop>()) {
if (curr->type == Type::none) {
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;
if (!curr->type.isBasic() || !child->type.isBasic()) {
continue;
}
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:
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:
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:
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:
continue;
case Type::none:
case Type::unreachable:
WASM_UNREACHABLE("unexpected type");
}
break;
}
case Type::v128:
continue;
case Type::none:
case Type::unreachable:
WASM_UNREACHABLE("unexpected type");
}
assert(fixed->type == curr->type);
if (tryToReplaceCurrent(fixed)) {
return;
}
}
}
}
void visitFunction(Function* curr) {
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 visitDataSegment(DataSegment* curr) {
bool shrank = false;
shrank = shrinkByReduction(curr, 2);
reduceByZeroing(
curr, 0, [](char item) { return item == 0; }, 2, shrank);
}
template<typename T, typename U, typename C>
void
reduceByZeroing(T* segment, U zero, C isZero, size_t bonus, bool shrank) {
for (auto& item : segment->data) {
if (!shouldTryToReduce(bonus) || isZero(item)) {
continue;
}
auto save = item;
item = zero;
if (writeAndTestReduction()) {
std::cerr << "| zeroed elem segment\n";
noteReduction();
} else {
item = save;
}
if (shrank) {
break;
}
}
}
template<typename T> bool shrinkByReduction(T* segment, size_t bonus) {
bool justShrank = false;
auto& data = segment->data;
size_t skip = 1;
for (size_t i = 0; i < data.size() && !data.empty(); i++) {
if (justShrank || shouldTryToReduce(bonus)) {
auto save = data;
for (size_t j = 0; j < skip; j++) {
if (data.empty()) {
break;
} else {
data.pop_back();
}
}
justShrank = writeAndTestReduction();
if (justShrank) {
std::cerr << "| shrank segment from " << save.size() << " => "
<< data.size() << " (skip: " << skip << ")\n";
noteReduction();
skip = std::min(size_t(factor), 2 * skip);
} else {
data = std::move(save);
return false;
}
}
}
return true;
}
void shrinkElementSegments() {
std::cerr << "| try to simplify elem segments\n";
bool shrank = false;
for (auto& segment : module->elementSegments) {
shrank = shrinkByReduction(segment.get(), 1) || shrank;
}
auto it =
std::find_if_not(module->elementSegments.begin(),
module->elementSegments.end(),
[&](auto& segment) { return segment->data.empty(); });
Expression* first = nullptr;
if (it != module->elementSegments.end()) {
first = it->get()->data[0];
}
if (first == nullptr) {
return;
}
for (auto& segment : module->elementSegments) {
reduceByZeroing(
segment.get(),
first,
[&](Expression* entry) {
if (entry->is<RefNull>()) {
return true;
} else if (first->is<RefNull>()) {
return false;
} else {
auto* f = first->cast<RefFunc>();
auto* e = entry->cast<RefFunc>();
return f->func == e->func;
}
},
1,
shrank);
}
}
bool reduceFunctions() {
std::vector<Name> functionNames;
for (auto& func : module->functions) {
functionNames.push_back(func->name);
}
auto numFuncs = functionNames.size();
if (numFuncs == 0) {
return false;
}
size_t skip = 1;
size_t maxSkip = 1;
bool justReduced = true;
size_t base = deterministicRandom(numFuncs);
std::cerr << "| try to remove functions (base: " << base
<< ", decisionCounter: " << decisionCounter << ", numFuncs "
<< numFuncs << ")\n";
for (size_t x = 0; x < functionNames.size(); x++) {
size_t i = (base + x) % numFuncs;
if (!justReduced &&
functionsWeTriedToRemove.count(functionNames[i]) == 1 &&
!shouldTryToReduce(std::max((factor / 5) + 1, 20000))) {
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::cerr << "| trying at i=" << i << " of size " << names.size()
<< "\n";
justReduced = tryToEmptyFunctions(names) || tryToRemoveFunctions(names);
if (justReduced) {
noteReduction(names.size());
x += skip - 1;
skip = std::min(size_t(factor), 2 * skip);
maxSkip = std::max(skip, maxSkip);
} else {
skip = std::max(skip / 2, size_t(1)); x += factor / 100;
}
}
return maxSkip > 2;
}
void visitModule(Module* curr) {
assert(curr == module.get());
curr = nullptr;
while (reduceFunctions()) {
}
shrinkElementSegments();
std::cerr << "| try to remove exports (with factor " << factor << ")\n";
std::vector<Export> exports;
for (auto& exp : module->exports) {
exports.push_back(*exp);
}
size_t 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);
}
}
bool allTablesEmpty =
std::all_of(module->elementSegments.begin(),
module->elementSegments.end(),
[&](auto& segment) { return segment->data.empty(); });
if (module->functions.size() == 1 && module->exports.empty() &&
allTablesEmpty) {
auto* func = module->functions[0].get();
if (!func->imported() && !Properties::isNamedControlFlow(func->body)) {
auto funcType = func->type;
auto* funcBody = func->body;
for (auto* child : ChildIterator(func->body)) {
if (!(child->type.isConcrete() || child->type == Type::none)) {
continue; }
func->type = Signature(funcType.getSignature().params, child->type);
func->body = child;
if (writeAndTestReduction()) {
std::cerr << "| altered function result type\n";
noteReduction(1);
break;
}
func->type = funcType;
func->body = funcBody;
}
}
}
}
bool tryToEmptyFunctions(std::vector<Name> names) {
std::vector<Expression*> oldBodies;
size_t actuallyEmptied = 0;
for (auto name : names) {
auto* func = module->getFunction(name);
auto* oldBody = func->body;
oldBodies.push_back(oldBody);
if (func->imported() || oldBody->is<Unreachable>() ||
oldBody->is<Nop>()) {
continue;
}
actuallyEmptied++;
bool useUnreachable = func->getResults() != Type::none;
if (useUnreachable) {
func->body = builder->makeUnreachable();
} else {
func->body = builder->makeNop();
}
}
if (actuallyEmptied > 0 && writeAndTestReduction()) {
std::cerr << "| emptied " << actuallyEmptied << " / "
<< names.size() << " functions\n";
return true;
} else {
for (size_t i = 0; i < names.size(); i++) {
module->getFunction(names[i])->body = oldBodies[i];
}
return false;
}
}
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 visitRefFunc(RefFunc* curr) {
if (names.count(curr->func)) {
replaceCurrent(Builder(*getModule()).replaceWithIdenticalType(curr));
}
}
void visitExport(Export* curr) {
if (names.count(curr->value)) {
exportsToRemove.push_back(curr->name);
}
}
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->type.isNullable() && !curr->is<RefNull>()) {
RefNull* n = builder->makeRefNull(curr->type.getHeapType());
return tryToReplaceCurrent(n);
}
if (curr->type.isTuple() && curr->type.isDefaultable()) {
Expression* n =
builder->makeConstantExpression(Literal::makeZeros(curr->type));
if (ExpressionAnalyzer::equal(n, curr)) {
return false;
}
return tryToReplaceCurrent(n);
}
if (!curr->type.isNumber()) {
return false;
}
auto* existing = curr->dynCast<Const>();
if (existing && existing->value.isZero()) {
return false;
}
auto* c = builder->makeConst(Literal::makeZero(curr->type));
if (tryToReplaceCurrent(c)) {
return true;
}
if (existing &&
existing->value == Literal::makeFromInt32(1, existing->type)) {
return false;
}
c->value = Literal::makeOne(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;
const std::string WasmReduceOption = "wasm-reduce options";
ToolOptions 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.",
WasmReduceOption,
Options::Arguments::One,
[&](Options* o, const std::string& argument) { command = argument; })
.add("--test",
"-t",
"Test file (this will be written to test, the given command should "
"read it when we call it)",
WasmReduceOption,
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)",
WasmReduceOption,
Options::Arguments::One,
[&](Options* o, const std::string& argument) { working = argument; })
.add("--binaries",
"-b",
"binaryen binaries location (bin/ directory)",
WasmReduceOption,
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)",
WasmReduceOption,
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) { binary = false; })
.add("--denan",
"",
"Avoid nans when reducing",
WasmReduceOption,
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) { deNan = true; })
.add("--verbose",
"-v",
"Verbose output mode",
WasmReduceOption,
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) { verbose = true; })
.add("--debugInfo",
"-g",
"Keep debug info in binaries",
WasmReduceOption,
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",
WasmReduceOption,
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)",
WasmReduceOption,
Options::Arguments::One,
[&](Options* o, const std::string& argument) {
timeout = atoi(argument.c_str());
std::cout << "|applying timeout: " << timeout << "\n";
})
.add("--extra-flags",
"-ef",
"Extra commandline flags to pass to wasm-opt while reducing. "
"(default: --enable-all)",
WasmReduceOption,
Options::Arguments::One,
[&](Options* o, const std::string& argument) {
extraFlags = argument;
std::cout << "|applying extraFlags: " << extraFlags << "\n";
})
.add_positional(
"INFILE",
Options::Arguments::One,
[&](Options* o, const std::string& argument) { input = argument; });
options.parse(argc, argv);
if (debugInfo) {
extraFlags += " -g ";
}
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';
std::cerr << "|extra flags: " << extraFlags << '\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);
}
if (!force) {
std::cerr << "|checking that command has different behavior on different "
"inputs (this "
"verifies that the test file is used by the command)\n";
{
std::ofstream dst(test, std::ios::binary);
dst << "waka waka\n";
}
ProgramResult resultOnInvalid(command);
if (resultOnInvalid == expected) {
Module emptyModule;
ModuleWriter writer;
writer.setBinary(true);
writer.write(emptyModule, test);
ProgramResult resultOnValid(command);
if (resultOnValid == expected) {
Fatal()
<< "running the command on the given input gives the same result as "
"when running it on either a trivial valid wasm or a file with "
"nonsense in it. does the script not look at the test file (" +
test + ")? (use -f to ignore this check)";
}
}
}
std::cerr << "|checking that command has expected behavior on canonicalized "
"(read-written) binary\n";
{
auto cmd = Path::getBinaryenBinaryTool("wasm-opt") + " " + input + " -o " +
test + " " + extraFlags;
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 = binary ? workingSize * 2 : workingSize / 10;
size_t lastDestructiveReductions = 0;
size_t lastPostPassesSize = 0;
bool stopping = false;
while (1) {
Reducer reducer(
command, test, working, binary, deNan, verbose, debugInfo, options);
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 = (factor + 1) / 2; }
}
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";
factor = std::max(1, (factor * 9) / 10);
} 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); }