summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt10
-rwxr-xr-xauto_update_tests.py17
-rwxr-xr-xcheck.py20
-rw-r--r--scripts/test/shared.py1
-rw-r--r--src/support/command-line.cpp6
-rw-r--r--src/support/json.h398
-rw-r--r--src/tools/wasm-metadce.cpp471
-rw-r--r--test/metadce/all-outside.wast2
-rw-r--r--test/metadce/all-outside.wast.dced3
-rw-r--r--test/metadce/all-outside.wast.dced.stdout5
-rw-r--r--test/metadce/all-outside.wast.graph.txt39
-rw-r--r--test/metadce/no-outside.wast12
-rw-r--r--test/metadce/no-outside.wast.dced3
-rw-r--r--test/metadce/no-outside.wast.dced.stdout6
-rw-r--r--test/metadce/no-outside.wast.graph.txt4
-rw-r--r--test/metadce/outside.wast34
-rw-r--r--test/metadce/outside.wast.dced26
-rw-r--r--test/metadce/outside.wast.dced.stdout11
-rw-r--r--test/metadce/outside.wast.graph.txt12
-rw-r--r--test/metadce/rooted-export.wast12
-rw-r--r--test/metadce/rooted-export.wast.dced8
-rw-r--r--test/metadce/rooted-export.wast.dced.stdout2
-rw-r--r--test/metadce/rooted-export.wast.graph.txt8
-rw-r--r--test/metadce/spanning_cycle.wast10
-rw-r--r--test/metadce/spanning_cycle.wast.dced9
-rw-r--r--test/metadce/spanning_cycle.wast.dced.stdout0
-rw-r--r--test/metadce/spanning_cycle.wast.graph.txt17
-rw-r--r--test/metadce/spanning_cycle_unrooted.wast10
-rw-r--r--test/metadce/spanning_cycle_unrooted.wast.dced3
-rw-r--r--test/metadce/spanning_cycle_unrooted.wast.dced.stdout3
-rw-r--r--test/metadce/spanning_cycle_unrooted.wast.graph.txt12
-rw-r--r--test/metadce/threaded.wast25
-rw-r--r--test/metadce/threaded.wast.dced23
-rw-r--r--test/metadce/threaded.wast.dced.stdout1
-rw-r--r--test/metadce/threaded.wast.graph.txt40
-rw-r--r--test/metadce/threaded_cycle.wast25
-rw-r--r--test/metadce/threaded_cycle.wast.dced24
-rw-r--r--test/metadce/threaded_cycle.wast.dced.stdout0
-rw-r--r--test/metadce/threaded_cycle.wast.graph.txt40
-rw-r--r--test/metadce/threaded_unrooted.wast25
-rw-r--r--test/metadce/threaded_unrooted.wast.dced3
-rw-r--r--test/metadce/threaded_unrooted.wast.dced.stdout12
-rw-r--r--test/metadce/threaded_unrooted.wast.graph.txt39
-rw-r--r--test/metadce/threaded_unrooted_cycle.wast25
-rw-r--r--test/metadce/threaded_unrooted_cycle.wast.dced3
-rw-r--r--test/metadce/threaded_unrooted_cycle.wast.dced.stdout12
-rw-r--r--test/metadce/threaded_unrooted_cycle.wast.graph.txt39
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'
diff --git a/check.py b/check.py
index b7c7cf3ed..64648b567 100755
--- a/check.py
+++ b/check.py
@@ -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"]
+ }
+]
+