diff options
47 files changed, 1506 insertions, 4 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 005067e64..5e27b602f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -232,6 +232,16 @@ SET_PROPERTY(TARGET wasm-merge PROPERTY CXX_STANDARD 11) SET_PROPERTY(TARGET wasm-merge PROPERTY CXX_STANDARD_REQUIRED ON) INSTALL(TARGETS wasm-merge DESTINATION bin) +SET(wasm-metadce_SOURCES + src/tools/wasm-metadce.cpp +) +ADD_EXECUTABLE(wasm-metadce + ${wasm-metadce_SOURCES}) +TARGET_LINK_LIBRARIES(wasm-metadce wasm asmjs emscripten-optimizer passes ir cfg support wasm) +SET_PROPERTY(TARGET wasm-metadce PROPERTY CXX_STANDARD 11) +SET_PROPERTY(TARGET wasm-metadce PROPERTY CXX_STANDARD_REQUIRED ON) +INSTALL(TARGETS wasm-metadce DESTINATION bin) + SET(asm2wasm_SOURCES src/tools/asm2wasm.cpp src/wasm-emscripten.cpp diff --git a/auto_update_tests.py b/auto_update_tests.py index 77dabbb42..9b2134da6 100755 --- a/auto_update_tests.py +++ b/auto_update_tests.py @@ -5,11 +5,10 @@ import os, sys, subprocess, difflib from scripts.test.support import run_command, split_wast from scripts.test.shared import ( ASM2WASM, MOZJS, S2WASM, WASM_SHELL, WASM_OPT, WASM_AS, WASM_DIS, - WASM_CTOR_EVAL, WASM_MERGE, WASM_REDUCE, WASM2ASM, + WASM_CTOR_EVAL, WASM_MERGE, WASM_REDUCE, WASM2ASM, WASM_METADCE, BINARYEN_INSTALL_DIR, has_shell_timeout) from scripts.test.wasm2asm import tests, spec_tests, extra_tests, assert_tests - print '[ processing and updating testcases... ]\n' for asm in sorted(os.listdir('test')): @@ -293,6 +292,20 @@ for wasm in assert_tests: out = run_command(cmd) with open(traps_expected_file, 'w') as o: o.write(out) +print '\n[ checking wasm-metadce... ]\n' + +for t in os.listdir(os.path.join('test', 'metadce')): + if t.endswith(('.wast', '.wasm')): + print '..', t + t = os.path.join('test', 'metadce', t) + graph = t + '.graph.txt' + cmd = WASM_METADCE + [t, '--graph-file=' + graph, '-o', 'a.wast', '-S'] + stdout = run_command(cmd) + actual = open('a.wast').read() + out = t + '.dced' + with open(out, 'w') as o: o.write(actual) + with open(out + '.stdout', 'w') as o: o.write(stdout) + if has_shell_timeout(): print '\n[ checking wasm-reduce ]\n' @@ -23,7 +23,7 @@ import sys from scripts.test.support import run_command, split_wast from scripts.test.shared import ( BIN_DIR, EMCC, MOZJS, NATIVECC, NATIVEXX, NODEJS, S2WASM_EXE, - WASM_AS, WASM_CTOR_EVAL, WASM_OPT, WASM_SHELL, WASM_MERGE, WASM_SHELL_EXE, + WASM_AS, WASM_CTOR_EVAL, WASM_OPT, WASM_SHELL, WASM_MERGE, WASM_SHELL_EXE, WASM_METADCE, WASM_DIS, WASM_REDUCE, binary_format_check, delete_from_orbit, fail, fail_with_error, fail_if_not_identical, fail_if_not_contained, has_vanilla_emcc, has_vanilla_llvm, minify_check, num_failures, options, tests, @@ -209,6 +209,23 @@ def run_ctor_eval_tests(): with open(out) as f: fail_if_not_identical(f.read(), actual) +def run_wasm_metadce_tests(): + print '\n[ checking wasm-metadce ]\n' + + for t in os.listdir(os.path.join('test', 'metadce')): + if t.endswith(('.wast', '.wasm')): + print '..', t + t = os.path.join('test', 'metadce', t) + graph = t + '.graph.txt' + cmd = WASM_METADCE + [t, '--graph-file=' + graph, '-o', 'a.wast', '-S'] + stdout = run_command(cmd) + expected = t + '.dced' + with open('a.wast') as seen: + with open(expected) as correct: + fail_if_not_identical(seen.read(), correct.read()) + with open(expected + '.stdout') as correct: + fail_if_not_identical(stdout, correct.read()) + def run_wasm_reduce_tests(): print '\n[ checking wasm-reduce ]\n' @@ -560,6 +577,7 @@ def main(): run_wasm_dis_tests() run_wasm_merge_tests() run_ctor_eval_tests() + run_wasm_metadce_tests() if has_shell_timeout(): run_wasm_reduce_tests() diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 88d6688fa..9933f85e1 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -168,6 +168,7 @@ WASM_SHELL = [os.path.join(options.binaryen_bin, 'wasm-shell')] WASM_MERGE = [os.path.join(options.binaryen_bin, 'wasm-merge')] S2WASM = [os.path.join(options.binaryen_bin, 's2wasm')] WASM_REDUCE = [os.path.join(options.binaryen_bin, 'wasm-reduce')] +WASM_METADCE = [os.path.join(options.binaryen_bin, 'wasm-metadce')] S2WASM_EXE = S2WASM[0] WASM_SHELL_EXE = WASM_SHELL[0] diff --git a/src/support/command-line.cpp b/src/support/command-line.cpp index e415715c3..384dc5dcd 100644 --- a/src/support/command-line.cpp +++ b/src/support/command-line.cpp @@ -28,7 +28,7 @@ void printWrap(std::ostream& os, int leftPad, const std::string& content) { std::string nextWord; std::string pad(leftPad, ' '); for (int i = 0; i <= len; ++i) { - if (i != len && content[i] != ' ') { + if (i != len && content[i] != ' ' && content[i] != '\n') { nextWord += content[i]; } else { if (static_cast<int>(nextWord.size()) > space) { @@ -39,6 +39,10 @@ void printWrap(std::ostream& os, int leftPad, const std::string& content) { space -= nextWord.size() + 1; if (space > 0) os << ' '; nextWord.clear(); + if (content[i] == '\n') { + os << '\n'; + space = SCREEN_WIDTH - leftPad; + } } } } diff --git a/src/support/json.h b/src/support/json.h new file mode 100644 index 000000000..034cdc9b9 --- /dev/null +++ b/src/support/json.h @@ -0,0 +1,398 @@ +/* + * 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. + */ + +// An arena-free version of emscripten-optimizer/simple_ast.h's JSON +// class TODO: use this instead of that + +#ifndef wasm_support_json_h +#define wasm_support_json_h + +#include <algorithm> +#include <cassert> +#include <cmath> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <functional> +#include <iomanip> +#include <iostream> +#include <limits> +#include <memory> +#include <ostream> +#include <set> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +#include "emscripten-optimizer/istring.h" +#include "support/safe_integer.h" + +namespace json { + +typedef cashew::IString IString; + +// Main value type +struct Value { + struct Ref : public std::shared_ptr<Value> { + Ref() : std::shared_ptr<Value>() {} + Ref(Value* value) : std::shared_ptr<Value>(value) {} + + Ref& operator[](size_t x) { + return (*this->get())[x]; + } + Ref& operator[](IString x) { + return (*this->get())[x]; + } + }; + + enum Type { + String = 0, + Number = 1, + Array = 2, + Null = 3, + Bool = 4, + Object = 5, + }; + + Type type; + + typedef std::vector<Ref> ArrayStorage; + typedef std::unordered_map<IString, Ref> ObjectStorage; + +#ifdef _MSC_VER // MSVC does not allow unrestricted unions: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2544.pdf + IString str; +#endif + union { // TODO: optimize +#ifndef _MSC_VER + IString str; +#endif + double num; + ArrayStorage *arr; // manually allocated/freed + bool boo; + ObjectStorage *obj; // manually allocated/freed + Ref ref; + }; + + // constructors all copy their input + Value() : type(Null), num(0) {} + explicit Value(const char *s) : type(Null) { + setString(s); + } + explicit Value(double n) : type(Null) { + setNumber(n); + } + explicit Value(ArrayStorage &a) : type(Null) { + setArray(); + *arr = a; + } + // no bool constructor - would endanger the double one (int might convert the wrong way) + + ~Value() { + free(); + } + + void free() { + if (type == Array) { + delete arr; + arr = nullptr; + } else if (type == Object) { + delete obj; + obj = nullptr; + } + type = Null; + num = 0; + } + + Value& setString(const char *s) { + free(); + type = String; + str.set(s); + return *this; + } + Value& setString(const IString &s) { + free(); + type = String; + str.set(s); + return *this; + } + Value& setNumber(double n) { + free(); + type = Number; + num = n; + return *this; + } + Value& setArray(ArrayStorage &a) { + free(); + type = Array; + arr = new ArrayStorage; + *arr = a; + return *this; + } + Value& setArray(size_t size_hint=0) { + free(); + type = Array; + arr = new ArrayStorage; + arr->reserve(size_hint); + return *this; + } + Value& setNull() { + free(); + type = Null; + return *this; + } + Value& setBool(bool b) { // Bool in the name, as otherwise might overload over int + free(); + type = Bool; + boo = b; + return *this; + } + Value& setObject() { + free(); + type = Object; + obj = new ObjectStorage(); + return *this; + } + + bool isString() { return type == String; } + bool isNumber() { return type == Number; } + bool isArray() { return type == Array; } + bool isNull() { return type == Null; } + bool isBool() { return type == Bool; } + bool isObject() { return type == Object; } + + bool isBool(bool b) { return type == Bool && b == boo; } // avoid overloading == as it might overload over int + + const char* getCString() { + assert(isString()); + return str.str; + } + IString& getIString() { + assert(isString()); + return str; + } + double& getNumber() { + assert(isNumber()); + return num; + } + ArrayStorage& getArray() { + assert(isArray()); + return *arr; + } + bool& getBool() { + assert(isBool()); + return boo; + } + + int32_t getInteger() { // convenience function to get a known integer + assert(fmod(getNumber(), 1) == 0); + int32_t ret = getNumber(); + assert(double(ret) == getNumber()); // no loss in conversion + return ret; + } + + Value& operator=(const Value& other) { + free(); + switch (other.type) { + case String: + setString(other.str); + break; + case Number: + setNumber(other.num); + break; + case Array: + setArray(*other.arr); + break; + case Null: + setNull(); + break; + case Bool: + setBool(other.boo); + break; + default: + abort(); // TODO + } + return *this; + } + + bool operator==(const Value& other) { + if (type != other.type) return false; + switch (other.type) { + case String: + return str == other.str; + case Number: + return num == other.num; + case Array: + return this == &other; // if you want a deep compare, use deepCompare + case Null: + break; + case Bool: + return boo == other.boo; + case Object: + return this == &other; // if you want a deep compare, use deepCompare + default: + abort(); + } + return true; + } + + char* parse(char* curr) { + #define is_json_space(x) (x == 32 || x == 9 || x == 10 || x == 13) /* space, tab, linefeed/newline, or return */ + #define skip() { while (*curr && is_json_space(*curr)) curr++; } + skip(); + if (*curr == '"') { + // String + curr++; + char *close = strchr(curr, '"'); + assert(close); + *close = 0; // end this string, and reuse it straight from the input + setString(curr); + curr = close+1; + } else if (*curr == '[') { + // Array + curr++; + skip(); + setArray(); + while (*curr != ']') { + Ref temp = Ref(new Value()); + arr->push_back(temp); + curr = temp->parse(curr); + skip(); + if (*curr == ']') break; + assert(*curr == ','); + curr++; + skip(); + } + curr++; + } else if (*curr == 'n') { + // Null + assert(strncmp(curr, "null", 4) == 0); + setNull(); + curr += 4; + } else if (*curr == 't') { + // Bool true + assert(strncmp(curr, "true", 4) == 0); + setBool(true); + curr += 4; + } else if (*curr == 'f') { + // Bool false + assert(strncmp(curr, "false", 5) == 0); + setBool(false); + curr += 5; + } else if (*curr == '{') { + // Object + curr++; + skip(); + setObject(); + while (*curr != '}') { + assert(*curr == '"'); + curr++; + char *close = strchr(curr, '"'); + assert(close); + *close = 0; // end this string, and reuse it straight from the input + IString key(curr); + curr = close+1; + skip(); + assert(*curr == ':'); + curr++; + skip(); + Ref value = Ref(new Value()); + curr = value->parse(curr); + (*obj)[key] = value; + skip(); + if (*curr == '}') break; + assert(*curr == ','); + curr++; + skip(); + } + curr++; + } else { + // Number + char *after; + setNumber(strtod(curr, &after)); + curr = after; + } + return curr; + } + + void stringify(std::ostream &os, bool pretty=false); + + // String operations + + // Number operations + + // Array operations + + size_t size() { + assert(isArray()); + return arr->size(); + } + + void setSize(size_t size) { + assert(isArray()); + auto old = arr->size(); + if (old != size) arr->resize(size); + if (old < size) { + for (auto i = old; i < size; i++) { + (*arr)[i] = Ref(new Value()); + } + } + } + + Ref& operator[](unsigned x) { + assert(isArray()); + return (*arr)[x]; + } + + Value& push_back(Ref r) { + assert(isArray()); + arr->push_back(r); + return *this; + } + Ref pop_back() { + assert(isArray()); + Ref ret = arr->back(); + arr->pop_back(); + return ret; + } + + Ref back() { + assert(isArray()); + if (arr->size() == 0) return nullptr; + return arr->back(); + } + + // Null operations + + // Bool operations + + // Object operations + + Ref& operator[](IString x) { + assert(isObject()); + return (*obj)[x]; + } + + bool has(IString x) { + assert(isObject()); + return obj->count(x) > 0; + } +}; + +typedef Value::Ref Ref; + +} // namespace json + +#endif // wasm_support_json_h diff --git a/src/tools/wasm-metadce.cpp b/src/tools/wasm-metadce.cpp new file mode 100644 index 000000000..b6447f58a --- /dev/null +++ b/src/tools/wasm-metadce.cpp @@ -0,0 +1,471 @@ +/* + * 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. + */ + +// +// Performs DCE on a graph containing a wasm module. The caller provides +// the graph, this tool fills in the wasm module's parts. It can then +// remove exports that are unused, for example, which is impossible +// otherwise (since we wouldn't know if the outside needs them). +// +// TODO: Currently all functions in the table are considered roots, +// as the outside may call them. In the future we probably want +// to refine that. + +#include <memory> + +#include "pass.h" +#include "support/command-line.h" +#include "support/file.h" +#include "support/json.h" +#include "support/colors.h" +#include "wasm-io.h" +#include "wasm-builder.h" +#include "ir/import-utils.h" + +using namespace wasm; + +// Generic reachability graph of abstract nodes + +struct DCENode { + Name name; + std::vector<Name> reaches; // the other nodes this one can reach + DCENode() {} + DCENode(Name name) : name(name) {} +}; + +// A meta DCE graph with wasm integration +struct MetaDCEGraph { + std::unordered_map<Name, DCENode> nodes; + std::unordered_set<Name> roots; + + std::unordered_map<Name, Name> importToDCENode; // import internal name => DCE name + std::unordered_map<Name, Name> exportToDCENode; // export exported name => DCE name + std::unordered_map<Name, Name> functionToDCENode; // function name => DCE name + std::unordered_map<Name, Name> globalToDCENode; // global name => DCE name + + std::unordered_map<Name, Name> DCENodeToImport; // reverse maps + std::unordered_map<Name, Name> DCENodeToExport; + std::unordered_map<Name, Name> DCENodeToFunction; + std::unordered_map<Name, Name> DCENodeToGlobal; + + Module& wasm; + + MetaDCEGraph(Module& wasm) : wasm(wasm) {} + + // populate the graph with info from the wasm, integrating with potentially-existing + // nodes for imports and exports that the graph may already contain. + void scanWebAssembly() { + // Add an entry for everything we might need ahead of time, so parallel work + // does not alter parent state, just adds to things pointed by it, independently + // (each thread will add for one function, etc.) + for (auto& func : wasm.functions) { + auto dceName = getName("func", func->name.str); + DCENodeToFunction[dceName] = func->name; + functionToDCENode[func->name] = dceName; + nodes[dceName] = DCENode(dceName); + } + for (auto& global : wasm.globals) { + auto dceName = getName("global", global->name.str); + DCENodeToGlobal[dceName] = global->name; + globalToDCENode[global->name] = dceName; + nodes[dceName] = DCENode(dceName); + } + for (auto& imp : wasm.imports) { + if (importToDCENode.find(imp->name) == importToDCENode.end()) { + auto dceName = getName("import", imp->name.str); + DCENodeToImport[dceName] = imp->name; + importToDCENode[imp->name] = dceName; + nodes[dceName] = DCENode(dceName); + } + } + for (auto& exp : wasm.exports) { + if (exportToDCENode.find(exp->name) == exportToDCENode.end()) { + auto dceName = getName("export", exp->name.str); + DCENodeToExport[dceName] = exp->name; + exportToDCENode[exp->name] = dceName; + nodes[dceName] = DCENode(dceName); + } + // we can also link the export to the thing being exported + auto& node = nodes[exportToDCENode[exp->name]]; + if (exp->kind == ExternalKind::Function) { + if (wasm.getFunctionOrNull(exp->value)) { + node.reaches.push_back(functionToDCENode[exp->value]); + } else { + node.reaches.push_back(importToDCENode[exp->value]); + } + } else if (exp->kind == ExternalKind::Global) { + if (wasm.getGlobalOrNull(exp->value)) { + node.reaches.push_back(globalToDCENode[exp->value]); + } else { + node.reaches.push_back(importToDCENode[exp->value]); + } + } + } + + // A parallel scanner for function bodies + struct Scanner : public WalkerPass<PostWalker<Scanner>> { + bool isFunctionParallel() override { return true; } + + Scanner(MetaDCEGraph* parent) : parent(parent) {} + + Scanner* create() override { + return new Scanner(parent); + } + + void visitCall(Call* curr) { + parent->nodes[parent->functionToDCENode[getFunction()->name]].reaches.push_back( + parent->functionToDCENode[curr->target] + ); + } + void visitCallImport(CallImport* curr) { + assert(parent->functionToDCENode.count(getFunction()->name) > 0); + parent->nodes[parent->functionToDCENode[getFunction()->name]].reaches.push_back( + parent->importToDCENode[curr->target] + ); + } + void visitGetGlobal(GetGlobal* curr) { + handleGlobal(curr->name); + } + void visitSetGlobal(SetGlobal* curr) { + handleGlobal(curr->name); + } + + private: + MetaDCEGraph* parent; + + void handleGlobal(Name name) { + if (getModule()->getGlobalOrNull(name)) return; + // it's an import + parent->nodes[parent->functionToDCENode[getFunction()->name]].reaches.push_back( + parent->importToDCENode[name] + ); + } + }; + + PassRunner runner(&wasm); + runner.setIsNested(true); + runner.add<Scanner>(this); + runner.run(); + + // also scan segment offsets + Scanner scanner(this); + scanner.setModule(&wasm); + for (auto& segment : wasm.table.segments) { + scanner.walk(segment.offset); + // TODO: currently, all functions in the table are roots, but we + // should add an option to refine that + for (auto& name : segment.data) { + roots.insert(functionToDCENode[name]); + } + } + for (auto& segment : wasm.memory.segments) { + scanner.walk(segment.offset); + } + } + +private: + // gets a unique name for the graph + Name getName(std::string prefix1, std::string prefix2) { + while (1) { + auto curr = Name(prefix1 + '$' + prefix2 + '$' + std::to_string(nameIndex++)); + if (nodes.find(curr) == nodes.end()) { + return curr; + } + } + } + + Index nameIndex = 0; + + std::unordered_set<Name> reached; + +public: + // Perform the DCE: simple marking from the roots + void deadCodeElimination() { + std::vector<Name> queue; + for (auto root : roots) { + reached.insert(root); + queue.push_back(root); + } + while (queue.size() > 0) { + auto name = queue.back(); + queue.pop_back(); + auto& node = nodes[name]; + for (auto target : node.reaches) { + if (reached.find(target) == reached.end()) { + reached.insert(target); + queue.push_back(target); + } + } + } + } + + // Apply to the wasm + void apply() { + // Remove the unused exports + std::vector<Name> toRemove; + for (auto& exp : wasm.exports) { + auto name = exp->name; + auto dceName = exportToDCENode[name]; + if (reached.find(dceName) == reached.end()) { + toRemove.push_back(name); + } + } + for (auto name : toRemove) { + wasm.removeExport(name); + } + // Now they are gone, standard optimization passes can do the rest! + PassRunner passRunner(&wasm); + passRunner.add("remove-unused-module-elements"); + passRunner.run(); + } + + // Print out everything we found is not used, and so can be + // removed, including on the outside + void printAllUnused() { + std::set<std::string> unused; + for (auto& pair : nodes) { + auto name = pair.first; + if (reached.find(name) == reached.end()) { + unused.insert(name.str); + } + } + for (auto& name : unused) { + std::cout << "unused: " << name << '\n'; + } + } + + // A debug utility, prints out the graph + void dump() { + std::cout << "=== graph ===\n"; + for (auto root : roots) { + std::cout << "root: " << root.str << '\n'; + } + for (auto& pair : nodes) { + auto name = pair.first; + auto& node = pair.second; + std::cout << "node: " << name.str << '\n'; + if (DCENodeToImport.find(name) != DCENodeToImport.end()) { + auto* imp = wasm.getImport(DCENodeToImport[name]); + std::cout << " is import " << DCENodeToImport[name] << ", " << imp->module.str << '.' << imp->base.str << '\n'; + } + if (DCENodeToExport.find(name) != DCENodeToExport.end()) { + std::cout << " is export " << DCENodeToExport[name].str << ", " << wasm.getExport(DCENodeToExport[name])->value << '\n'; + } + if (DCENodeToFunction.find(name) != DCENodeToFunction.end()) { + std::cout << " is function " << DCENodeToFunction[name] << '\n'; + } + if (DCENodeToGlobal.find(name) != DCENodeToGlobal.end()) { + std::cout << " is function " << DCENodeToGlobal[name] << '\n'; + } + for (auto target : node.reaches) { + std::cout << " reaches: " << target.str << '\n'; + } + } + std::cout << "=============\n"; + } +}; + +// +// main +// + +int main(int argc, const char* argv[]) { + Name entry; + std::vector<std::string> passes; + bool emitBinary = true; + bool debugInfo = false; + std::string graphFile; + + Options options("wasm-metadce", "This tool performs dead code elimination (DCE) on a larger space " + "that the wasm module is just a part of. For example, if you have " + "JS and wasm that are connected, this can DCE the combined graph. " + "By doing so, it is able to eliminate wasm module exports, which " + "otherwise regular optimizations cannot.\n\n" + "This tool receives a representation of the reachability graph " + "that the wasm module resides in, which contains abstract nodes " + "and connections showing what they reach. Some of those nodes " + "can represent the wasm module's imports and exports. The tool " + "then completes the graph by adding the internal parts of the " + "module, and does DCE on the entire thing.\n\n" + "This tool will output a wasm module with dead code eliminated, " + "and metadata describing the things in the rest of the graph " + "that can be eliminated as well.\n\n" + "The graph description file should represent the graph in the following " + "JSON-like notation (note, this is not true JSON, things like " + "comments, escaping, single-quotes, etc. are not supported):\n\n" + " [\n" + " {\n" + " \"name\": \"entity1\",\n" + " \"reaches\": [\"entity2, \"entity3\"],\n" + " \"root\": true\n" + " },\n" + " {\n" + " \"name\": \"entity2\",\n" + " \"reaches\": [\"entity1, \"entity4\"]\n" + " },\n" + " {\n" + " \"name\": \"entity3\",\n" + " \"reaches\": [\"entity1\"],\n" + " \"export\": \"export1\"\n" + " },\n" + " {\n" + " \"name\": \"entity4\",\n" + " \"import\": [\"module\", \"import1\"]\n" + " },\n" + " ]\n\n" + "Each entity has a name and an optional list of the other " + "entities it reaches. It can also be marked as a root, " + "export (with the export string), or import (with the " + "module and import strings). DCE then computes what is " + "reachable from the roots."); + + options + .add("--output", "-o", "Output file (stdout if not specified)", + Options::Arguments::One, + [](Options* o, const std::string& argument) { + o->extra["output"] = argument; + Colors::disable(); + }) + .add("--emit-text", "-S", "Emit text instead of binary for the output file", + Options::Arguments::Zero, + [&](Options *o, const std::string &argument) { emitBinary = false; }) + .add("--debuginfo", "-g", "Emit names section and debug info", + Options::Arguments::Zero, + [&](Options *o, const std::string &arguments) { debugInfo = true; }) + .add("--graph-file", "-f", "Filename of the graph description file", + Options::Arguments::One, + [&](Options* o, const std::string& argument) { + graphFile = argument; + }) + .add_positional("INFILE", Options::Arguments::One, + [](Options* o, const std::string& argument) { + o->extra["infile"] = argument; + }); + options.parse(argc, argv); + + if (graphFile.size() == 0) { + Fatal() << "no graph file provided."; + } + + auto input(read_file<std::string>(options.extra["infile"], Flags::Text, Flags::Release)); + + Module wasm; + + { + if (options.debug) std::cerr << "reading...\n"; + ModuleReader reader; + reader.setDebug(options.debug); + + try { + reader.read(options.extra["infile"], wasm); + } catch (ParseException& p) { + p.dump(std::cerr); + Fatal() << "error in parsing wasm input"; + } + } + + auto graphInput(read_file<std::string>(graphFile, Flags::Text, Flags::Release)); + auto* copy = strdup(graphInput.c_str()); + json::Value outside; + outside.parse(copy); + + // parse the JSON into our graph, doing all the JSON parsing here, leaving + // the abstract computation for the class itself + const json::IString NAME("name"), + REACHES("reaches"), + ROOT("root"), + EXPORT("export"), + IMPORT("import"); + + MetaDCEGraph graph(wasm); + + if (!outside.isArray()) { + Fatal() << "input graph must be a JSON array of nodes. see --help for the form"; + } + auto size = outside.size(); + for (size_t i = 0; i < size; i++) { + json::Ref ref = outside[i]; + if (!ref->isObject()) { + Fatal() << "nodes in input graph must be JSON objects. see --help for the form"; + } + if (!ref->has(NAME)) { + Fatal() << "nodes in input graph must have a name. see --help for the form"; + } + DCENode node(ref[NAME]->getIString()); + if (ref->has(REACHES)) { + json::Ref reaches = ref[REACHES]; + if (!reaches->isArray()) { + Fatal() << "node.reaches must be an array. see --help for the form"; + } + auto size = reaches->size(); + for (size_t j = 0; j < size; j++) { + json::Ref name = reaches[j]; + if (!name->isString()) { + Fatal() << "node.reaches items must be strings. see --help for the form"; + } + node.reaches.push_back(name->getIString()); + } + } + if (ref->has(ROOT)) { + json::Ref root = ref[ROOT]; + if (!root->isBool() || !root->getBool()) { + Fatal() << "node.root, if it exists, must be true. see --help for the form"; + } + graph.roots.insert(node.name); + } + if (ref->has(EXPORT)) { + json::Ref exp = ref[EXPORT]; + if (!exp->isString()) { + Fatal() << "node.export, if it exists, must be a string. see --help for the form"; + } + graph.exportToDCENode[exp->getIString()] = node.name; + graph.DCENodeToExport[node.name] = exp->getIString(); + } + if (ref->has(IMPORT)) { + json::Ref imp = ref[IMPORT]; + if (!imp->isArray() || imp->size() != 2 || !imp[0]->isString() || !imp[1]->isString()) { + Fatal() << "node.import, if it exists, must be an array of two strings. see --help for the form"; + } + auto importName = ImportUtils::getImport(wasm, imp[0]->getIString(), imp[1]->getIString())->name; + graph.importToDCENode[importName] = node.name; + graph.DCENodeToImport[node.name] = importName; + } + // TODO: optimize this copy with a clever move + graph.nodes[node.name] = node; + } + + // The external graph is now populated. Scan the module + graph.scanWebAssembly(); + + // Perform the DCE + graph.deadCodeElimination(); + + // Apply to the wasm + graph.apply(); + + if (options.extra.count("output") > 0) { + ModuleWriter writer; + writer.setBinary(emitBinary); + writer.setDebugInfo(debugInfo); + writer.write(wasm, options.extra["output"]); + } + + // Print out everything that we found is removable, the outside might use that + graph.printAllUnused(); + + // Clean up + free(copy); +} diff --git a/test/metadce/all-outside.wast b/test/metadce/all-outside.wast new file mode 100644 index 000000000..f84d2580e --- /dev/null +++ b/test/metadce/all-outside.wast @@ -0,0 +1,2 @@ +(module) + diff --git a/test/metadce/all-outside.wast.dced b/test/metadce/all-outside.wast.dced new file mode 100644 index 000000000..421c01b53 --- /dev/null +++ b/test/metadce/all-outside.wast.dced @@ -0,0 +1,3 @@ +(module + (memory $0 0) +) diff --git a/test/metadce/all-outside.wast.dced.stdout b/test/metadce/all-outside.wast.dced.stdout new file mode 100644 index 000000000..e61742921 --- /dev/null +++ b/test/metadce/all-outside.wast.dced.stdout @@ -0,0 +1,5 @@ +unused: island +unused: lonely +unused: loop1 +unused: loop2 +unused: reverse diff --git a/test/metadce/all-outside.wast.graph.txt b/test/metadce/all-outside.wast.graph.txt new file mode 100644 index 000000000..b874ff778 --- /dev/null +++ b/test/metadce/all-outside.wast.graph.txt @@ -0,0 +1,39 @@ +[ + { + "name": "root1", + "reaches": ["two", "three"], + "root": true + }, + { + "name": "root2", + "reaches": ["three"], + "root": true + }, + { + "name": "two", + "reaches": ["two"], + }, + { + "name": "three", + }, + { + "name": "island", + "reaches": ["island"], + }, + { + "name": "loop1", + "reaches": ["loop2"], + }, + { + "name": "loop2", + "reaches": ["loop1"], + }, + { + "name": "lonely", + }, + { + "name": "reverse", + "reaches": ["root1"], + }, +] + diff --git a/test/metadce/no-outside.wast b/test/metadce/no-outside.wast new file mode 100644 index 000000000..038a693de --- /dev/null +++ b/test/metadce/no-outside.wast @@ -0,0 +1,12 @@ +(module + (import "env" "js_func" (func $a_js_func)) + (import "env" "js_func_unused" (func $an_unused_js_func)) + (export "wasm_func" (func $a_wasm_func)) + (export "wasm_func_unused" (func $an_unused_wasm_func)) + (func $a_wasm_func + (call_import $a_js_func) + ) + (func $an_unused_wasm_func + ) +) + diff --git a/test/metadce/no-outside.wast.dced b/test/metadce/no-outside.wast.dced new file mode 100644 index 000000000..421c01b53 --- /dev/null +++ b/test/metadce/no-outside.wast.dced @@ -0,0 +1,3 @@ +(module + (memory $0 0) +) diff --git a/test/metadce/no-outside.wast.dced.stdout b/test/metadce/no-outside.wast.dced.stdout new file mode 100644 index 000000000..32ed99f0e --- /dev/null +++ b/test/metadce/no-outside.wast.dced.stdout @@ -0,0 +1,6 @@ +unused: export$wasm_func$4 +unused: export$wasm_func_unused$5 +unused: func$a_wasm_func$0 +unused: func$an_unused_wasm_func$1 +unused: import$a_js_func$2 +unused: import$an_unused_js_func$3 diff --git a/test/metadce/no-outside.wast.graph.txt b/test/metadce/no-outside.wast.graph.txt new file mode 100644 index 000000000..dcc120e00 --- /dev/null +++ b/test/metadce/no-outside.wast.graph.txt @@ -0,0 +1,4 @@ +[ +] + + diff --git a/test/metadce/outside.wast b/test/metadce/outside.wast new file mode 100644 index 000000000..dc5aeaabc --- /dev/null +++ b/test/metadce/outside.wast @@ -0,0 +1,34 @@ +(module + (import "env" "js_func" (func $a_js_func)) + (import "env" "js_func_unused" (func $an_unused_js_func)) + (import "env" "DYNAMICTOP_PTR" (global $DYNAMICTOP_PTR$asm2wasm$import i32)) + (import "env" "DYNAMICTOP_PTR_unused" (global $DYNAMICTOP_PTR$asm2wasm$import_unused i32)) + (import "env" "memory" (memory $0 256 256)) + (import "env" "table" (table 10 10 anyfunc)) + + (export "wasm_func" (func $a_wasm_func)) + (export "wasm_func_unused" (func $an_unused_wasm_func)) + + (global $__THREW__ (mut i32) (i32.const 0)) + (global $__THREW__unused (mut i32) (i32.const 0)) + (global $from_segment (mut i32) (i32.const 0)) + (global $from_segment_2 (mut i32) (i32.const 0)) + (global $from_segment_never_used (mut i32) (i32.const 0)) + + (data (i32.const 1024) "abcd") + (data (get_global $from_segment) "abcd") + (elem (get_global $from_segment_2) $table_func) + + (func $a_wasm_func + (call_import $a_js_func) + (drop (get_global $DYNAMICTOP_PTR$asm2wasm$import)) + (drop (get_global $__THREW__)) + ) + (func $an_unused_wasm_func + (drop (get_global $DYNAMICTOP_PTR$asm2wasm$import_unused)) + (drop (get_global $__THREW__unused)) + ) + (func $table_func + ) +) + diff --git a/test/metadce/outside.wast.dced b/test/metadce/outside.wast.dced new file mode 100644 index 000000000..083be0dda --- /dev/null +++ b/test/metadce/outside.wast.dced @@ -0,0 +1,26 @@ +(module + (type $FUNCSIG$v (func)) + (import "env" "js_func" (func $a_js_func)) + (import "env" "DYNAMICTOP_PTR" (global $DYNAMICTOP_PTR$asm2wasm$import i32)) + (import "env" "memory" (memory $0 256 256)) + (import "env" "table" (table 10 10 anyfunc)) + (global $__THREW__ (mut i32) (i32.const 0)) + (global $from_segment (mut i32) (i32.const 0)) + (global $from_segment_2 (mut i32) (i32.const 0)) + (elem (get_global $from_segment_2) $table_func) + (data (i32.const 1024) "abcd") + (data (get_global $from_segment) "abcd") + (export "wasm_func" (func $a_wasm_func)) + (func $a_wasm_func (; 1 ;) (type $FUNCSIG$v) + (call $a_js_func) + (drop + (get_global $DYNAMICTOP_PTR$asm2wasm$import) + ) + (drop + (get_global $__THREW__) + ) + ) + (func $table_func (; 2 ;) (type $FUNCSIG$v) + (nop) + ) +) diff --git a/test/metadce/outside.wast.dced.stdout b/test/metadce/outside.wast.dced.stdout new file mode 100644 index 000000000..ed95ab195 --- /dev/null +++ b/test/metadce/outside.wast.dced.stdout @@ -0,0 +1,11 @@ +unused: export$wasm_func_unused$14 +unused: func$an_unused_wasm_func$1 +unused: global$__THREW__$3 +unused: global$__THREW__unused$4 +unused: global$from_segment$5 +unused: global$from_segment_2$6 +unused: global$from_segment_never_used$7 +unused: import$0$12 +unused: import$DYNAMICTOP_PTR$asm2wasm$import_unused$11 +unused: import$an_unused_js_func$9 +unused: import$import$table$0$13 diff --git a/test/metadce/outside.wast.graph.txt b/test/metadce/outside.wast.graph.txt new file mode 100644 index 000000000..fbb5131a0 --- /dev/null +++ b/test/metadce/outside.wast.graph.txt @@ -0,0 +1,12 @@ +[ + { + "name": "outside-entity", + "reaches": ["inside-wasm-func"], + "root": true + }, + { + "name": "inside-wasm-func", + "export": "wasm_func" + } +] + diff --git a/test/metadce/rooted-export.wast b/test/metadce/rooted-export.wast new file mode 100644 index 000000000..c2c88ec40 --- /dev/null +++ b/test/metadce/rooted-export.wast @@ -0,0 +1,12 @@ +(module + (export "wasm_func_a" (func $a_wasm_func)) + (export "wasm_func_b" (func $b_wasm_func)) + + (func $a_wasm_func + (unreachable) + ) + (func $b_wasm_func + (unreachable) + ) +) + diff --git a/test/metadce/rooted-export.wast.dced b/test/metadce/rooted-export.wast.dced new file mode 100644 index 000000000..6648c2791 --- /dev/null +++ b/test/metadce/rooted-export.wast.dced @@ -0,0 +1,8 @@ +(module + (type $0 (func)) + (memory $0 0) + (export "wasm_func_b" (func $b_wasm_func)) + (func $b_wasm_func (; 0 ;) (type $0) + (unreachable) + ) +) diff --git a/test/metadce/rooted-export.wast.dced.stdout b/test/metadce/rooted-export.wast.dced.stdout new file mode 100644 index 000000000..0a0cce75a --- /dev/null +++ b/test/metadce/rooted-export.wast.dced.stdout @@ -0,0 +1,2 @@ +unused: export$wasm_func_a$2 +unused: func$a_wasm_func$0 diff --git a/test/metadce/rooted-export.wast.graph.txt b/test/metadce/rooted-export.wast.graph.txt new file mode 100644 index 000000000..274fb0d18 --- /dev/null +++ b/test/metadce/rooted-export.wast.graph.txt @@ -0,0 +1,8 @@ +[ + { + "name": "rooted-export", + "root": true, + "export": "wasm_func_b" + } +] + diff --git a/test/metadce/spanning_cycle.wast b/test/metadce/spanning_cycle.wast new file mode 100644 index 000000000..83a1c786f --- /dev/null +++ b/test/metadce/spanning_cycle.wast @@ -0,0 +1,10 @@ +(module + (import "env" "js_func" (func $a_js_func)) + + (export "wasm_func_a" (func $a_wasm_func)) + + (func $a_wasm_func + (call $a_js_func) + ) +) + diff --git a/test/metadce/spanning_cycle.wast.dced b/test/metadce/spanning_cycle.wast.dced new file mode 100644 index 000000000..9d2b4ced3 --- /dev/null +++ b/test/metadce/spanning_cycle.wast.dced @@ -0,0 +1,9 @@ +(module + (type $FUNCSIG$v (func)) + (import "env" "js_func" (func $a_js_func)) + (memory $0 0) + (export "wasm_func_a" (func $a_wasm_func)) + (func $a_wasm_func (; 1 ;) (type $FUNCSIG$v) + (call $a_js_func) + ) +) diff --git a/test/metadce/spanning_cycle.wast.dced.stdout b/test/metadce/spanning_cycle.wast.dced.stdout new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/test/metadce/spanning_cycle.wast.dced.stdout diff --git a/test/metadce/spanning_cycle.wast.graph.txt b/test/metadce/spanning_cycle.wast.graph.txt new file mode 100644 index 000000000..4674999f9 --- /dev/null +++ b/test/metadce/spanning_cycle.wast.graph.txt @@ -0,0 +1,17 @@ +[ + { + "name": "root", + "root": true, + "reaches": ["wasm_export"], + }, + { + "name": "wasm_export", + "export": "wasm_func_a" + }, + { + "name": "outside_js_function", + "reaches": ["wasm_export"], + "import": ["env", "js_func"] + } +] + diff --git a/test/metadce/spanning_cycle_unrooted.wast b/test/metadce/spanning_cycle_unrooted.wast new file mode 100644 index 000000000..83a1c786f --- /dev/null +++ b/test/metadce/spanning_cycle_unrooted.wast @@ -0,0 +1,10 @@ +(module + (import "env" "js_func" (func $a_js_func)) + + (export "wasm_func_a" (func $a_wasm_func)) + + (func $a_wasm_func + (call $a_js_func) + ) +) + diff --git a/test/metadce/spanning_cycle_unrooted.wast.dced b/test/metadce/spanning_cycle_unrooted.wast.dced new file mode 100644 index 000000000..421c01b53 --- /dev/null +++ b/test/metadce/spanning_cycle_unrooted.wast.dced @@ -0,0 +1,3 @@ +(module + (memory $0 0) +) diff --git a/test/metadce/spanning_cycle_unrooted.wast.dced.stdout b/test/metadce/spanning_cycle_unrooted.wast.dced.stdout new file mode 100644 index 000000000..240c77b95 --- /dev/null +++ b/test/metadce/spanning_cycle_unrooted.wast.dced.stdout @@ -0,0 +1,3 @@ +unused: func$a_wasm_func$0 +unused: outside_js_function +unused: wasm_export diff --git a/test/metadce/spanning_cycle_unrooted.wast.graph.txt b/test/metadce/spanning_cycle_unrooted.wast.graph.txt new file mode 100644 index 000000000..229174f16 --- /dev/null +++ b/test/metadce/spanning_cycle_unrooted.wast.graph.txt @@ -0,0 +1,12 @@ +[ + { + "name": "wasm_export", + "export": "wasm_func_a" + }, + { + "name": "outside_js_function", + "reaches": ["wasm_export"], + "import": ["env", "js_func"] + } +] + diff --git a/test/metadce/threaded.wast b/test/metadce/threaded.wast new file mode 100644 index 000000000..28db7562f --- /dev/null +++ b/test/metadce/threaded.wast @@ -0,0 +1,25 @@ +(module + (import "env" "js_func1" (func $js_func_1)) + (import "env" "js_func2" (func $js_func_2)) + (import "env" "js_func3" (func $js_func_3)) + (import "env" "js_func4" (func $js_func_4)) + + (export "wasm_func1" (func $wasm_func_1)) + (export "wasm_func2" (func $wasm_func_2)) + (export "wasm_func3" (func $wasm_func_3)) + (export "wasm_func4" (func $wasm_func_4)) + + (func $wasm_func_1 + (call $js_func_2) + ) + (func $wasm_func_2 + (call $js_func_3) + ) + (func $wasm_func_3 + (call $js_func_4) + ) + (func $wasm_func_4 + (nop) + ) +) + diff --git a/test/metadce/threaded.wast.dced b/test/metadce/threaded.wast.dced new file mode 100644 index 000000000..5e26e4666 --- /dev/null +++ b/test/metadce/threaded.wast.dced @@ -0,0 +1,23 @@ +(module + (type $FUNCSIG$v (func)) + (import "env" "js_func2" (func $js_func_2)) + (import "env" "js_func3" (func $js_func_3)) + (import "env" "js_func4" (func $js_func_4)) + (memory $0 0) + (export "wasm_func1" (func $wasm_func_1)) + (export "wasm_func2" (func $wasm_func_2)) + (export "wasm_func3" (func $wasm_func_3)) + (export "wasm_func4" (func $wasm_func_4)) + (func $wasm_func_1 (; 3 ;) (type $FUNCSIG$v) + (call $js_func_2) + ) + (func $wasm_func_2 (; 4 ;) (type $FUNCSIG$v) + (call $js_func_3) + ) + (func $wasm_func_3 (; 5 ;) (type $FUNCSIG$v) + (call $js_func_4) + ) + (func $wasm_func_4 (; 6 ;) (type $FUNCSIG$v) + (nop) + ) +) diff --git a/test/metadce/threaded.wast.dced.stdout b/test/metadce/threaded.wast.dced.stdout new file mode 100644 index 000000000..aac52dca5 --- /dev/null +++ b/test/metadce/threaded.wast.dced.stdout @@ -0,0 +1 @@ +unused: outside_js_function1 diff --git a/test/metadce/threaded.wast.graph.txt b/test/metadce/threaded.wast.graph.txt new file mode 100644 index 000000000..37aae119b --- /dev/null +++ b/test/metadce/threaded.wast.graph.txt @@ -0,0 +1,40 @@ +[ + { + "name": "wasm_export1", + "export": "wasm_func1", + "root": true + }, + { + "name": "wasm_export2", + "export": "wasm_func2" + }, + { + "name": "wasm_export3", + "export": "wasm_func3" + }, + { + "name": "wasm_export4", + "export": "wasm_func4" + }, + { + "name": "outside_js_function1", + "reaches": ["wasm_export1"], + "import": ["env", "js_func1"] + }, + { + "name": "outside_js_function2", + "reaches": ["wasm_export2"], + "import": ["env", "js_func2"] + }, + { + "name": "outside_js_function3", + "reaches": ["wasm_export3"], + "import": ["env", "js_func3"] + }, + { + "name": "outside_js_function4", + "reaches": ["wasm_export4"], + "import": ["env", "js_func4"] + } +] + diff --git a/test/metadce/threaded_cycle.wast b/test/metadce/threaded_cycle.wast new file mode 100644 index 000000000..7521b4ae9 --- /dev/null +++ b/test/metadce/threaded_cycle.wast @@ -0,0 +1,25 @@ +(module + (import "env" "js_func1" (func $js_func_1)) + (import "env" "js_func2" (func $js_func_2)) + (import "env" "js_func3" (func $js_func_3)) + (import "env" "js_func4" (func $js_func_4)) + + (export "wasm_func1" (func $wasm_func_1)) + (export "wasm_func2" (func $wasm_func_2)) + (export "wasm_func3" (func $wasm_func_3)) + (export "wasm_func4" (func $wasm_func_4)) + + (func $wasm_func_1 + (call $js_func_2) + ) + (func $wasm_func_2 + (call $js_func_3) + ) + (func $wasm_func_3 + (call $js_func_4) + ) + (func $wasm_func_4 + (call $js_func_1) + ) +) + diff --git a/test/metadce/threaded_cycle.wast.dced b/test/metadce/threaded_cycle.wast.dced new file mode 100644 index 000000000..91b5b45e0 --- /dev/null +++ b/test/metadce/threaded_cycle.wast.dced @@ -0,0 +1,24 @@ +(module + (type $FUNCSIG$v (func)) + (import "env" "js_func1" (func $js_func_1)) + (import "env" "js_func2" (func $js_func_2)) + (import "env" "js_func3" (func $js_func_3)) + (import "env" "js_func4" (func $js_func_4)) + (memory $0 0) + (export "wasm_func1" (func $wasm_func_1)) + (export "wasm_func2" (func $wasm_func_2)) + (export "wasm_func3" (func $wasm_func_3)) + (export "wasm_func4" (func $wasm_func_4)) + (func $wasm_func_1 (; 4 ;) (type $FUNCSIG$v) + (call $js_func_2) + ) + (func $wasm_func_2 (; 5 ;) (type $FUNCSIG$v) + (call $js_func_3) + ) + (func $wasm_func_3 (; 6 ;) (type $FUNCSIG$v) + (call $js_func_4) + ) + (func $wasm_func_4 (; 7 ;) (type $FUNCSIG$v) + (call $js_func_1) + ) +) diff --git a/test/metadce/threaded_cycle.wast.dced.stdout b/test/metadce/threaded_cycle.wast.dced.stdout new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/test/metadce/threaded_cycle.wast.dced.stdout diff --git a/test/metadce/threaded_cycle.wast.graph.txt b/test/metadce/threaded_cycle.wast.graph.txt new file mode 100644 index 000000000..bd5e7a32e --- /dev/null +++ b/test/metadce/threaded_cycle.wast.graph.txt @@ -0,0 +1,40 @@ +[ + { + "name": "wasm_export1", + "export": "wasm_func1", + }, + { + "name": "wasm_export2", + "export": "wasm_func2" + }, + { + "name": "wasm_export3", + "export": "wasm_func3", + "root": true + }, + { + "name": "wasm_export4", + "export": "wasm_func4" + }, + { + "name": "outside_js_function1", + "reaches": ["wasm_export1"], + "import": ["env", "js_func1"] + }, + { + "name": "outside_js_function2", + "reaches": ["wasm_export2"], + "import": ["env", "js_func2"] + }, + { + "name": "outside_js_function3", + "reaches": ["wasm_export3"], + "import": ["env", "js_func3"] + }, + { + "name": "outside_js_function4", + "reaches": ["wasm_export4"], + "import": ["env", "js_func4"] + } +] + diff --git a/test/metadce/threaded_unrooted.wast b/test/metadce/threaded_unrooted.wast new file mode 100644 index 000000000..28db7562f --- /dev/null +++ b/test/metadce/threaded_unrooted.wast @@ -0,0 +1,25 @@ +(module + (import "env" "js_func1" (func $js_func_1)) + (import "env" "js_func2" (func $js_func_2)) + (import "env" "js_func3" (func $js_func_3)) + (import "env" "js_func4" (func $js_func_4)) + + (export "wasm_func1" (func $wasm_func_1)) + (export "wasm_func2" (func $wasm_func_2)) + (export "wasm_func3" (func $wasm_func_3)) + (export "wasm_func4" (func $wasm_func_4)) + + (func $wasm_func_1 + (call $js_func_2) + ) + (func $wasm_func_2 + (call $js_func_3) + ) + (func $wasm_func_3 + (call $js_func_4) + ) + (func $wasm_func_4 + (nop) + ) +) + diff --git a/test/metadce/threaded_unrooted.wast.dced b/test/metadce/threaded_unrooted.wast.dced new file mode 100644 index 000000000..421c01b53 --- /dev/null +++ b/test/metadce/threaded_unrooted.wast.dced @@ -0,0 +1,3 @@ +(module + (memory $0 0) +) diff --git a/test/metadce/threaded_unrooted.wast.dced.stdout b/test/metadce/threaded_unrooted.wast.dced.stdout new file mode 100644 index 000000000..0080ac5ae --- /dev/null +++ b/test/metadce/threaded_unrooted.wast.dced.stdout @@ -0,0 +1,12 @@ +unused: func$wasm_func_1$0 +unused: func$wasm_func_2$1 +unused: func$wasm_func_3$2 +unused: func$wasm_func_4$3 +unused: outside_js_function1 +unused: outside_js_function2 +unused: outside_js_function3 +unused: outside_js_function4 +unused: wasm_export1 +unused: wasm_export2 +unused: wasm_export3 +unused: wasm_export4 diff --git a/test/metadce/threaded_unrooted.wast.graph.txt b/test/metadce/threaded_unrooted.wast.graph.txt new file mode 100644 index 000000000..9b4a76304 --- /dev/null +++ b/test/metadce/threaded_unrooted.wast.graph.txt @@ -0,0 +1,39 @@ +[ + { + "name": "wasm_export1", + "export": "wasm_func1", + }, + { + "name": "wasm_export2", + "export": "wasm_func2" + }, + { + "name": "wasm_export3", + "export": "wasm_func3" + }, + { + "name": "wasm_export4", + "export": "wasm_func4" + }, + { + "name": "outside_js_function1", + "reaches": ["wasm_export1"], + "import": ["env", "js_func1"] + }, + { + "name": "outside_js_function2", + "reaches": ["wasm_export2"], + "import": ["env", "js_func2"] + }, + { + "name": "outside_js_function3", + "reaches": ["wasm_export3"], + "import": ["env", "js_func3"] + }, + { + "name": "outside_js_function4", + "reaches": ["wasm_export4"], + "import": ["env", "js_func4"] + } +] + diff --git a/test/metadce/threaded_unrooted_cycle.wast b/test/metadce/threaded_unrooted_cycle.wast new file mode 100644 index 000000000..7521b4ae9 --- /dev/null +++ b/test/metadce/threaded_unrooted_cycle.wast @@ -0,0 +1,25 @@ +(module + (import "env" "js_func1" (func $js_func_1)) + (import "env" "js_func2" (func $js_func_2)) + (import "env" "js_func3" (func $js_func_3)) + (import "env" "js_func4" (func $js_func_4)) + + (export "wasm_func1" (func $wasm_func_1)) + (export "wasm_func2" (func $wasm_func_2)) + (export "wasm_func3" (func $wasm_func_3)) + (export "wasm_func4" (func $wasm_func_4)) + + (func $wasm_func_1 + (call $js_func_2) + ) + (func $wasm_func_2 + (call $js_func_3) + ) + (func $wasm_func_3 + (call $js_func_4) + ) + (func $wasm_func_4 + (call $js_func_1) + ) +) + diff --git a/test/metadce/threaded_unrooted_cycle.wast.dced b/test/metadce/threaded_unrooted_cycle.wast.dced new file mode 100644 index 000000000..421c01b53 --- /dev/null +++ b/test/metadce/threaded_unrooted_cycle.wast.dced @@ -0,0 +1,3 @@ +(module + (memory $0 0) +) diff --git a/test/metadce/threaded_unrooted_cycle.wast.dced.stdout b/test/metadce/threaded_unrooted_cycle.wast.dced.stdout new file mode 100644 index 000000000..0080ac5ae --- /dev/null +++ b/test/metadce/threaded_unrooted_cycle.wast.dced.stdout @@ -0,0 +1,12 @@ +unused: func$wasm_func_1$0 +unused: func$wasm_func_2$1 +unused: func$wasm_func_3$2 +unused: func$wasm_func_4$3 +unused: outside_js_function1 +unused: outside_js_function2 +unused: outside_js_function3 +unused: outside_js_function4 +unused: wasm_export1 +unused: wasm_export2 +unused: wasm_export3 +unused: wasm_export4 diff --git a/test/metadce/threaded_unrooted_cycle.wast.graph.txt b/test/metadce/threaded_unrooted_cycle.wast.graph.txt new file mode 100644 index 000000000..9b4a76304 --- /dev/null +++ b/test/metadce/threaded_unrooted_cycle.wast.graph.txt @@ -0,0 +1,39 @@ +[ + { + "name": "wasm_export1", + "export": "wasm_func1", + }, + { + "name": "wasm_export2", + "export": "wasm_func2" + }, + { + "name": "wasm_export3", + "export": "wasm_func3" + }, + { + "name": "wasm_export4", + "export": "wasm_func4" + }, + { + "name": "outside_js_function1", + "reaches": ["wasm_export1"], + "import": ["env", "js_func1"] + }, + { + "name": "outside_js_function2", + "reaches": ["wasm_export2"], + "import": ["env", "js_func2"] + }, + { + "name": "outside_js_function3", + "reaches": ["wasm_export3"], + "import": ["env", "js_func3"] + }, + { + "name": "outside_js_function4", + "reaches": ["wasm_export4"], + "import": ["env", "js_func4"] + } +] + |