summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAlon Zakai <alonzakai@gmail.com>2017-09-01 12:34:03 -0700
committerGitHub <noreply@github.com>2017-09-01 12:34:03 -0700
commit6de14693110b4f898344ff1cb383caf0d74eb42e (patch)
tree6fbcb75fd3c2285191939a086989e14656908282 /src
parentb013f744e3d70effd9be348cbde7fb93f0a16c6a (diff)
downloadbinaryen-6de14693110b4f898344ff1cb383caf0d74eb42e.tar.gz
binaryen-6de14693110b4f898344ff1cb383caf0d74eb42e.tar.bz2
binaryen-6de14693110b4f898344ff1cb383caf0d74eb42e.zip
wasm-reduce tool (#1139)
Reduce an interesting wasm to a smaller still interesting wasm. This takes an arbitrary command to run, and reduces the wasm as much as it can while keeping the behavior of that command fixed. This can be used to reduce compiler bugs in an arbitrary VM, etc.
Diffstat (limited to 'src')
-rw-r--r--src/passes/RemoveImports.cpp6
-rw-r--r--src/support/file.cpp12
-rw-r--r--src/support/file.h9
-rw-r--r--src/tools/wasm-reduce.cpp661
-rw-r--r--src/wasm-validator.h4
-rw-r--r--src/wasm.h1
-rw-r--r--src/wasm/wasm-validator.cpp19
-rw-r--r--src/wasm/wasm.cpp10
8 files changed, 711 insertions, 11 deletions
diff --git a/src/passes/RemoveImports.cpp b/src/passes/RemoveImports.cpp
index 88fb43520..6de151230 100644
--- a/src/passes/RemoveImports.cpp
+++ b/src/passes/RemoveImports.cpp
@@ -15,7 +15,7 @@
*/
//
-// Removeds imports, and replaces them with nops. This is useful
+// Removes function imports, and replaces them with nops. This is useful
// for running a module through the reference interpreter, which
// does not validate imports for a JS environment (by removing
// imports, we can at least get the reference interpreter to
@@ -42,7 +42,9 @@ struct RemoveImports : public WalkerPass<PostWalker<RemoveImports>> {
void visitModule(Module *curr) {
std::vector<Name> names;
for (auto& import : curr->imports) {
- names.push_back(import->name);
+ if (import->kind == ExternalKind::Function) {
+ names.push_back(import->name);
+ }
}
for (auto& name : names) {
curr->removeImport(name);
diff --git a/src/support/file.cpp b/src/support/file.cpp
index c1d9e6e33..bd6f2d4bd 100644
--- a/src/support/file.cpp
+++ b/src/support/file.cpp
@@ -73,3 +73,15 @@ wasm::Output::Output(const std::string &filename, Flags::BinaryOption binary, Fl
}
return buffer;
}()) {}
+
+void wasm::copy_file(std::string input, std::string output) {
+ std::ifstream src(input, std::ios::binary);
+ std::ofstream dst(output, std::ios::binary);
+ dst << src.rdbuf();
+}
+
+size_t wasm::file_size(std::string filename) {
+ std::ifstream infile(filename, std::ifstream::ate | std::ifstream::binary);
+ return infile.tellg();
+}
+
diff --git a/src/support/file.h b/src/support/file.h
index 0f0ec15f3..b0df8fd06 100644
--- a/src/support/file.h
+++ b/src/support/file.h
@@ -66,6 +66,13 @@ class Output {
std::ofstream outfile;
std::ostream out;
};
-}
+
+// Copies a file to another file
+void copy_file(std::string input, std::string output);
+
+// Retusn the size of a file
+size_t file_size(std::string filename);
+
+} // namespace wasm
#endif // wasm_support_file_h
diff --git a/src/tools/wasm-reduce.cpp b/src/tools/wasm-reduce.cpp
new file mode 100644
index 000000000..355b70278
--- /dev/null
+++ b/src/tools/wasm-reduce.cpp
@@ -0,0 +1,661 @@
+/*
+ * Copyright 2017 WebAssembly Community Group participants
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//
+// Tries to reduce the input wasm into the smallest possible wasm
+// that still generates the same result on a given command. This is
+// useful to reduce bug testcases, for example, if a file crashes
+// the optimizer, you can reduce it to find the smallest file that
+// also crashes it (which generally will show the same bug, in a
+// much more debuggable manner).
+//
+
+#include <memory>
+#include <cstdio>
+#include <cstdlib>
+
+#include "pass.h"
+#include "support/command-line.h"
+#include "support/file.h"
+#include "wasm-io.h"
+#include "wasm-builder.h"
+#include "ast/literal-utils.h"
+#include "wasm-validator.h"
+
+using namespace wasm;
+
+struct ProgramResult {
+ int code;
+ std::string output;
+
+ ProgramResult() {}
+ ProgramResult(std::string command) {
+ getFromExecution(command);
+ }
+
+ // runs the command and notes the output
+ // TODO: also stderr, not just stdout?
+ void getFromExecution(std::string command) {
+ // do this using just core stdio.h and stdlib.h, for portability
+ // sadly this requires two invokes
+ code = system(("timeout 2s " + command + " > /dev/null 2> /dev/null").c_str());
+ const int MAX_BUFFER = 1024;
+ char buffer[MAX_BUFFER];
+ FILE *stream = popen(("timeout 2s " + command + " 2> /dev/null").c_str(), "r");
+ while (fgets(buffer, MAX_BUFFER, stream) != NULL) {
+ output.append(buffer);
+ }
+ pclose(stream);
+ }
+
+ 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 << "[/ProgramResult]\n";
+ }
+};
+
+namespace std {
+
+inline std::ostream& operator<<(std::ostream& o, ProgramResult& result) {
+ result.dump(o);
+ return o;
+}
+
+}
+
+ProgramResult expected;
+
+struct Reducer : public WalkerPass<PostWalker<Reducer, UnifiedExpressionVisitor<Reducer>>> {
+ std::string command, test, working;
+ bool verbose, debugInfo;
+
+ // test is the file we write to that the command will operate on
+ // working is the current temporary state, the reduction so far
+ Reducer(std::string command, std::string test, std::string working, bool verbose, bool debugInfo) : command(command), test(test), working(working), verbose(verbose), debugInfo(debugInfo) {}
+
+ // runs passes in order to reduce, until we can't reduce any more
+ // the criterion here is wasm binary size
+ void reduceUsingPasses() {
+ // run optimization passes until we can't shrink it any more
+ std::vector<std::string> passes = {
+ "-Oz",
+ "-Os",
+ "-O1",
+ "-O2",
+ "-O3",
+ "--coalesce-locals --vacuum",
+ "--dce",
+ "--duplicate-function-elimination",
+ "--inlining",
+ "--inlining-optimizing",
+ "--optimize-level=3 --inlining-optimizing",
+ "--local-cse --vacuum",
+ "--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",
+ "--reorder-functions",
+ "--reorder-locals",
+ "--simplify-locals --vacuum",
+ "--vacuum"
+ };
+ auto oldSize = file_size(working);
+ bool more = true;
+ while (more) {
+ //std::cerr << "| starting passes loop iteration\n";
+ more = false;
+ // try both combining with a generic shrink (so minor pass overhead is compensated for), and without
+ for (auto shrinking : { false, true }) {
+ for (auto pass : passes) {
+ std::string currCommand = "bin/wasm-opt ";
+ if (shrinking) currCommand += " --dce --vacuum ";
+ currCommand += working + " -o " + test + " " + pass;
+ if (debugInfo) currCommand += " -g ";
+ if (verbose) std::cerr << "| trying pass command: " << currCommand << "\n";
+ if (!ProgramResult(currCommand).failed()) {
+ auto newSize = file_size(test);
+ if (newSize < oldSize) {
+ // the pass didn't fail, and the size looks smaller, so promising
+ // see if it is still has the property we are preserving
+ 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";
+ }
+
+ // does one pass of slow and destructive reduction. returns whether it
+ // succeeded or not
+ // the criterion here is a logical change in the program. this may actually
+ // increase wasm size in some cases, but it should allow more reduction later.
+ // @param factor how much to ignore. starting with a high factor skips through
+ // most of the file, which is often faster than going one by one
+ // from the start
+ size_t reduceDestructively(int factor_) {
+ factor = factor_;
+ Module wasm;
+ ModuleReader reader;
+ reader.read(working, wasm);
+ // prepare
+ reduced = 0;
+ builder = make_unique<Builder>(wasm);
+ funcsSeen = 0;
+ // before we do any changes, it should be valid to write out the module:
+ // size should be as expected, and output should be as expected
+ setModule(&wasm);
+ if (!writeAndTestReduction()) {
+ std::cerr << "\n|! WARNING: writing before destructive reduction fails, very unlikely reduction can work\n\n";
+ }
+ // destroy!
+ walkModule(&wasm);
+ return reduced;
+ }
+
+ // destructive reduction state
+
+ size_t reduced;
+ Expression* beforeReduction;
+ std::unique_ptr<Builder> builder;
+ Index funcsSeen;
+ int factor;
+
+ // write the module and see if the command still fails on it as expected
+ bool writeAndTestReduction() {
+ ProgramResult result;
+ return writeAndTestReduction(result);
+ }
+
+ bool writeAndTestReduction(ProgramResult& out) {
+ // write the module out
+ ModuleWriter writer;
+ writer.setBinary(true);
+ writer.setDebugInfo(debugInfo);
+ writer.write(*getModule(), test);
+ // note that it is ok for the destructively-reduced module to be bigger
+ // than the previous - each destructive reduction removes logical code,
+ // and so is strictly better, even if the wasm binary format happens to
+ // encode things slightly less efficiently.
+ // test it
+ out.getFromExecution(command);
+ return out == expected;
+ }
+
+ bool shouldTryToReduce(size_t bonus = 1) {
+ static size_t counter = 0;
+ counter += bonus;
+ return (counter % factor) <= bonus;
+ }
+
+ // tests a reduction on the current traversal node, and undos if it failed
+ bool tryToReplaceCurrent(Expression* with) {
+ auto* curr = getCurrent();
+ //std::cerr << "try " << curr << " => " << with << '\n';
+ 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() {
+ reduced++;
+ copy_file(test, working);
+ }
+
+ // tests a reduction on an arbitrary child
+ bool tryToReplaceChild(Expression*& child, Expression* with) {
+ 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";
+ //std::cerr << "| " << before << " => " << with << '\n';
+ noteReduction();
+ return true;
+ }
+
+ std::string getLocation() {
+ if (getFunction()) return getFunction()->name.str;
+ return "(non-function context)";
+ }
+
+ // visitors. in each we try to remove code in a destructive and nontrivial way.
+ // "nontrivial" means something that optimization passes can't achieve, since we
+ // don't need to duplicate work that they do
+
+ void visitExpression(Expression* curr) {
+ if (curr->type == none) {
+ if (tryToReduceCurrentToNone()) return;
+ } else if (isConcreteWasmType(curr->type)) {
+ if (tryToReduceCurrentToConst()) return;
+ } else {
+ assert(curr->type == unreachable);
+ if (tryToReduceCurrentToUnreachable()) return;
+ }
+ // specific reductions
+ if (auto* iff = curr->dynCast<If>()) {
+ if (iff->type == none) {
+ // perhaps we need just the condition?
+ 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);
+ } else if (auto* set = curr->dynCast<SetLocal>()) {
+ if (set->isTee()) {
+ // maybe we don't need the set
+ tryToReplaceCurrent(set->value);
+ }
+ } else if (auto* unary = curr->dynCast<Unary>()) {
+ // maybe we can pass through
+ tryToReplaceCurrent(unary->value);
+ } else if (auto* binary = curr->dynCast<Binary>()) {
+ // maybe we can pass through
+ if (!tryToReplaceCurrent(binary->left)) {
+ tryToReplaceCurrent(binary->right);
+ }
+ } else if (auto* call = curr->dynCast<Call>()) {
+ handleCall(call);
+ } else if (auto* call = curr->dynCast<CallImport>()) {
+ handleCall(call);
+ } else if (auto* call = curr->dynCast<CallIndirect>()) {
+ if (tryToReplaceCurrent(call->target)) return;
+ handleCall(call);
+ }
+ }
+
+ void visitFunction(Function* curr) {
+ // extra chance to work on the function toplevel element, as if it can
+ // be reduced it's great
+ visitExpression(curr->body);
+ // finish function
+ 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;
+ }
+ }
+
+ // TODO: bisection on segment shrinking?
+
+ 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) {
+ // try to reduce to first function
+ // shrink segment elements
+ for (auto& segment : curr->segments) {
+ auto& data = segment.data;
+ size_t skip = 1; // when we succeed, try to shrink by more and more, similar to bisection
+ for (size_t i = 0; i < data.size() && !data.empty(); i++) {
+ if (!shouldTryToReduce(bonus)) continue;
+ auto save = data;
+ for (size_t j = 0; j < skip; j++) {
+ if (!data.empty()) data.pop_back();
+ }
+ if (writeAndTestReduction()) {
+ std::cerr << "| shrank segment (skip: " << skip << ")\n";
+ noteReduction();
+ skip = std::min(size_t(factor), 2 * skip);
+ } else {
+ data = save;
+ break;
+ }
+ }
+ }
+ // the "opposite" of shrinking: copy a 'zero' element
+ 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;
+ }
+ }
+ }
+ }
+
+ void visitModule(Module* curr) {
+ // try to remove exports
+ std::cerr << "| try to remove exports\n";
+ std::vector<Export> exports;
+ for (auto& exp : curr->exports) {
+ if (!shouldTryToReduce(10000)) continue;
+ exports.push_back(*exp);
+ }
+ for (auto& exp : exports) {
+ curr->removeExport(exp.name);
+ if (!writeAndTestReduction()) {
+ curr->addExport(new Export(exp));
+ } else {
+ std::cerr << "| removed export " << exp.name << '\n';
+ noteReduction();
+ }
+ }
+ // try to remove functions
+ std::cerr << "| try to remove functions\n";
+ std::vector<Function> functions;
+ for (auto& func : curr->functions) {
+ if (!shouldTryToReduce(10000)) continue;
+ functions.push_back(*func);
+ }
+ for (auto& func : functions) {
+ curr->removeFunction(func.name);
+ WasmValidator validator;
+ validator.quiet = true;
+ if (validator.validate(*curr) && writeAndTestReduction()) {
+ std::cerr << "| removed function " << func.name << '\n';
+ noteReduction();
+ } else {
+ curr->addFunction(new Function(func));
+ }
+ }
+ }
+
+ // helpers
+
+ // try to replace condition with always true and always false
+ void handleCondition(Expression*& condition) {
+ if (!condition) return;
+ if (condition->is<Const>()) return;
+ auto* c = builder->makeConst(Literal(int32_t(0)));
+ if (!tryToReplaceChild(condition, c)) {
+ c->value = Literal(int32_t(1));
+ tryToReplaceChild(condition, c);
+ }
+ }
+
+ template<typename T>
+ void handleCall(T* call) {
+ for (auto* op : call->operands) {
+ if (tryToReplaceCurrent(op)) return;
+ }
+ }
+
+ bool tryToReduceCurrentToNone() {
+ auto* curr = getCurrent();
+ if (curr->is<Nop>()) return false;
+ // try to replace with a trivial value
+ Nop nop;
+ if (tryToReplaceCurrent(&nop)) {
+ replaceCurrent(builder->makeNop());
+ return true;
+ }
+ return false;
+ }
+
+ // try to replace a concrete value with a trivial constant
+ bool tryToReduceCurrentToConst() {
+ auto* curr = getCurrent();
+ if (curr->is<Const>()) return false;
+ // try to replace with a trivial value
+ Const* c = builder->makeConst(Literal(int32_t(0)));
+ if (tryToReplaceCurrent(c)) return true;
+ c->value = LiteralUtils::makeLiteralFromInt32(1, curr->type);
+ c->type = curr->type;
+ return tryToReplaceCurrent(c);
+ }
+
+ bool tryToReduceCurrentToUnreachable() {
+ auto* curr = getCurrent();
+ if (curr->is<Unreachable>()) return false;
+ // try to replace with a trivial value
+ Unreachable un;
+ if (tryToReplaceCurrent(&un)) {
+ replaceCurrent(builder->makeUnreachable());
+ return true;
+ }
+ // maybe a return? TODO
+ return false;
+ }
+};
+
+//
+// main
+//
+
+int main(int argc, const char* argv[]) {
+ std::string input, test, working, command;
+ bool 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("--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_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";
+
+ std::cerr << "|input: " << input << '\n';
+ std::cerr << "|test: " << test << '\n';
+ std::cerr << "|working: " << working << '\n';
+
+ // get the expected output
+ copy_file(input, test);
+ expected.getFromExecution(command);
+
+ std::cerr << "|expected result:\n" << expected << '\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)";
+ }
+ };
+
+ std::cerr << "|checking that command has different behavior on invalid binary\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";
+ {
+ // read and write it
+ ProgramResult readWrite(std::string("bin/wasm-opt ") + input + " -o " + test);
+ 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);
+ std::cerr << "|input size: " << file_size(working) << "\n";
+
+ std::cerr << "|starting reduction!\n";
+
+ int factor = 4096;
+ size_t lastDestructiveReductions = 0;
+ size_t lastPostPassesSize = 0;
+
+ bool stopping = false;
+
+ while (1) {
+ Reducer reducer(command, test, working, verbose, debugInfo);
+
+ // run binaryen optimization passes to reduce. passes are fast to run
+ // and can often reduce large amounts of code efficiently, as opposed
+ // to detructive reduction (i.e., that doesn't preserve correctness as
+ // passes do) since destrucive must operate one change at a time
+ 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";
+
+ // always stop after a pass reduction attempt, for final cleanup
+ if (stopping) break;
+
+ // check if the full cycle (destructive/passes) has helped or not
+ if (lastPostPassesSize && newSize >= lastPostPassesSize) {
+ std::cerr << "| progress has stopped, skipping to the end\n";
+ if (factor == 1) {
+ // this is after doing work with factor 1, so after the remaining work, stop
+ stopping = true;
+ } else {
+ // just try to remove all we can and finish up
+ factor = 1;
+ }
+ }
+ lastPostPassesSize = newSize;
+
+ // if destructive reductions lead to useful proportionate pass reductions, keep
+ // going at the same factor, as pass reductions are far faster
+ std::cerr << "| pass progress: " << passProgress << ", last destructive: " << lastDestructiveReductions << '\n';
+ if (passProgress >= 4 * lastDestructiveReductions) {
+ // don't change
+ std::cerr << "| progress is good, do not quickly decrease factor\n";
+ } else {
+ if (factor > 10) {
+ factor = (factor / 3) + 1;
+ } else {
+ factor = (factor + 1) / 2; // stable on 1
+ }
+ }
+
+ // no point in a factor lorger than the size
+ assert(newSize > 4); // wasm modules are >4 bytes anyhow
+ factor = std::min(factor, int(newSize) / 4);
+
+ // try to reduce destructively. if a high factor fails to find anything,
+ // quickly try a lower one (no point in doing passes until we reduce
+ // destructively at least a little)
+ while (1) {
+ std::cerr << "| reduce destructively... (factor: " << factor << ")\n";
+ lastDestructiveReductions = reducer.reduceDestructively(factor);
+ if (lastDestructiveReductions > 0) break;
+ // we failed to reduce destructively
+ if (factor == 1) {
+ stopping = true;
+ break;
+ }
+ factor = std::max(1, factor / 4); // quickly now, try to find *something* we can reduce
+ }
+
+ std::cerr << "| destructive reduction led to size: " << file_size(working) << '\n';
+ }
+ std::cerr << "|finished, final size: " << file_size(working) << "\n";
+ copy_file(working, test); // just to avoid confusion
+}
+
diff --git a/src/wasm-validator.h b/src/wasm-validator.h
index d78b54a47..35304d1c9 100644
--- a/src/wasm-validator.h
+++ b/src/wasm-validator.h
@@ -67,6 +67,8 @@ struct WasmValidator : public PostWalker<WasmValidator> {
bool validateWeb = false;
bool validateGlobally = true;
+ bool quiet = false; // whether to log errors verbosely
+
struct BreakInfo {
WasmType type;
Index arity;
@@ -98,7 +100,7 @@ public:
validateBinaryenIR(module);
}
// print if an error occurred
- if (!valid) {
+ if (!valid && !quiet) {
WasmPrinter::printModule(&module, std::cerr);
}
return valid;
diff --git a/src/wasm.h b/src/wasm.h
index 78aa1577b..0cd5b05e8 100644
--- a/src/wasm.h
+++ b/src/wasm.h
@@ -770,6 +770,7 @@ public:
void removeImport(Name name);
void removeExport(Name name);
+ void removeFunction(Name name);
// TODO: remove* for other elements
void updateMaps();
diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp
index 819602608..e30661e8f 100644
--- a/src/wasm/wasm-validator.cpp
+++ b/src/wasm/wasm-validator.cpp
@@ -62,7 +62,7 @@ void WasmValidator::visitBlock(Block *curr) {
}
if (curr->list.size() > 1) {
for (Index i = 0; i < curr->list.size() - 1; i++) {
- if (!shouldBeTrue(!isConcreteWasmType(curr->list[i]->type), curr, "non-final block elements returning a value must be drop()ed (binaryen's autodrop option might help you)")) {
+ if (!shouldBeTrue(!isConcreteWasmType(curr->list[i]->type), curr, "non-final block elements returning a value must be drop()ed (binaryen's autodrop option might help you)") && !quiet) {
std::cerr << "(on index " << i << ":\n" << curr->list[i] << "\n), type: " << curr->list[i]->type << "\n";
}
}
@@ -170,14 +170,14 @@ void WasmValidator::visitCall(Call *curr) {
if (!validateGlobally) return;
auto* target = getModule()->getFunctionOrNull(curr->target);
if (!shouldBeTrue(!!target, curr, "call target must exist")) {
- if (getModule()->getImportOrNull(curr->target)) {
+ if (getModule()->getImportOrNull(curr->target) && !quiet) {
std::cerr << "(perhaps it should be a CallImport instead of Call?)\n";
}
return;
}
if (!shouldBeTrue(curr->operands.size() == target->params.size(), curr, "call param number must match")) return;
for (size_t i = 0; i < curr->operands.size(); i++) {
- if (!shouldBeEqualOrFirstIsUnreachable(curr->operands[i]->type, target->params[i], curr, "call param types must match")) {
+ if (!shouldBeEqualOrFirstIsUnreachable(curr->operands[i]->type, target->params[i], curr, "call param types must match") && !quiet) {
std::cerr << "(on argument " << i << ")\n";
}
}
@@ -190,7 +190,7 @@ void WasmValidator::visitCallImport(CallImport *curr) {
auto* type = getModule()->getFunctionType(import->functionType);
if (!shouldBeTrue(curr->operands.size() == type->params.size(), curr, "call param number must match")) return;
for (size_t i = 0; i < curr->operands.size(); i++) {
- if (!shouldBeEqualOrFirstIsUnreachable(curr->operands[i]->type, type->params[i], curr, "call param types must match")) {
+ if (!shouldBeEqualOrFirstIsUnreachable(curr->operands[i]->type, type->params[i], curr, "call param types must match") && !quiet) {
std::cerr << "(on argument " << i << ")\n";
}
}
@@ -202,7 +202,7 @@ void WasmValidator::visitCallIndirect(CallIndirect *curr) {
shouldBeEqualOrFirstIsUnreachable(curr->target->type, i32, curr, "indirect call target must be an i32");
if (!shouldBeTrue(curr->operands.size() == type->params.size(), curr, "call param number must match")) return;
for (size_t i = 0; i < curr->operands.size(); i++) {
- if (!shouldBeEqualOrFirstIsUnreachable(curr->operands[i]->type, type->params[i], curr, "call param types must match")) {
+ if (!shouldBeEqualOrFirstIsUnreachable(curr->operands[i]->type, type->params[i], curr, "call param types must match") && !quiet) {
std::cerr << "(on argument " << i << ")\n";
}
}
@@ -534,7 +534,7 @@ void WasmValidator::visitGlobal(Global* curr) {
if (!validateGlobally) return;
shouldBeTrue(curr->init != nullptr, curr->name, "global init must be non-null");
shouldBeTrue(curr->init->is<Const>() || curr->init->is<GetGlobal>(), curr->name, "global init must be valid");
- if (!shouldBeEqual(curr->type, curr->init->type, curr->init, "global init must have correct type")) {
+ if (!shouldBeEqual(curr->type, curr->init->type, curr->init, "global init must have correct type") && !quiet) {
std::cerr << "(on global " << curr->name << ")\n";
}
}
@@ -548,7 +548,7 @@ void WasmValidator::visitFunction(Function *curr) {
if (returnType != unreachable) {
shouldBeEqual(curr->result, returnType, curr->body, "function result must match, if function has returns");
}
- if (!shouldBeTrue(namedBreakTargets.empty(), curr->body, "all named break targets must exist (even if not taken)")) {
+ if (!shouldBeTrue(namedBreakTargets.empty(), curr->body, "all named break targets must exist (even if not taken)") && !quiet) {
std::cerr << "(on label " << *namedBreakTargets.begin() << ")\n";
}
returnType = unreachable;
@@ -592,6 +592,9 @@ void WasmValidator::visitTable(Table* curr) {
for (auto& segment : curr->segments) {
shouldBeEqual(segment.offset->type, i32, segment.offset, "segment offset should be i32");
shouldBeTrue(checkOffset(segment.offset, segment.data.size(), getModule()->table.initial * Table::kPageSize), segment.offset, "segment offset should be reasonable");
+ for (auto name : segment.data) {
+ shouldBeTrue(getModule()->getFunctionOrNull(name) || getModule()->getImportOrNull(name), name, "segment name should be valid");
+ }
}
}
void WasmValidator::visitModule(Module *curr) {
@@ -698,11 +701,13 @@ void WasmValidator::validateBinaryenIR(Module& wasm) {
template <typename T, typename S>
std::ostream& WasmValidator::fail(S text, T curr) {
valid = false;
+ if (quiet) return std::cerr;
auto& ret = printFailureHeader() << text << ", on \n";
return printModuleComponent(curr, ret);
}
std::ostream& WasmValidator::printFailureHeader() {
+ if (quiet) return std::cerr;
Colors::red(std::cerr);
if (getFunction()) {
std::cerr << "[wasm-validator error in function ";
diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp
index ab2db6bd5..4594eddd1 100644
--- a/src/wasm/wasm.cpp
+++ b/src/wasm/wasm.cpp
@@ -716,6 +716,16 @@ void Module::removeExport(Name name) {
exportsMap.erase(name);
}
+void Module::removeFunction(Name name) {
+ for (size_t i = 0; i < functions.size(); i++) {
+ if (functions[i]->name == name) {
+ functions.erase(functions.begin() + i);
+ break;
+ }
+ }
+ functionsMap.erase(name);
+}
+
// TODO: remove* for other elements
void Module::updateMaps() {