summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlon Zakai <azakai@google.com>2023-05-16 11:03:45 -0700
committerGitHub <noreply@github.com>2023-05-16 11:03:45 -0700
commit972e659bf59740c3ee44129812f95bec143d01a6 (patch)
treef86d70fa692a45e3dfbf951b0d1af06204d4ecf7
parent44cd751d9feda7c4b4b6c9d6af1e71541b90abac (diff)
downloadbinaryen-972e659bf59740c3ee44129812f95bec143d01a6.tar.gz
binaryen-972e659bf59740c3ee44129812f95bec143d01a6.tar.bz2
binaryen-972e659bf59740c3ee44129812f95bec143d01a6.zip
Reintroduce wasm-merge (#5709)
We used to have a wasm-merge tool but removed it for a lack of use cases. Recently use cases have been showing up in the wasm GC space and elsewhere, as people are using more diverse toolchains together, for example a project might build some C++ code alongside some wasm GC code. Merging those wasm files together can allow for nice optimizations like inlining and better DCE etc., so it makes sense to have a tool for merging. Background: * Removal: #1969 * Requests: * wasm-merge - why it has been deleted #2174 * Compiling and linking wat files #2276 * wasm-link? #2767 This PR is a compete rewrite of wasm-merge, not a restoration of the original codebase. The original code was quite messy (my fault), and also, since then we've added multi-memory and multi-table which makes things a lot simpler. The linking semantics are as described in the "wasm-link" issue #2767 : all we do is merge normal wasm files together and connect imports and export. That is, we have a graph of modules and their names, and each import to a module name can be resolved to that module. Basically, like a JS bundler would do for JS, or, in other words, we do the same operations as JS code would do to glue wasm modules together at runtime, but at compile time. See the README update in this PR for a concrete example. There are no plans to do more than that simple bundling, so this should not really overlap with wasm-ld's use cases. This should be fairly fast as it works in linear time on the total input code. However, it won't be as fast as wasm-ld, of course, as it does build Binaryen IR for each module. An advantage to working on Binaryen IR is that we can easily do some global DCE after merging, and further optimizations are possible later.
-rw-r--r--CHANGELOG.md5
-rw-r--r--README.md147
-rwxr-xr-xscripts/fuzz_opt.py69
-rwxr-xr-xscripts/update_help_checks.py2
-rw-r--r--src/ir/module-utils.h48
-rw-r--r--src/tools/CMakeLists.txt1
-rw-r--r--src/tools/wasm-merge.cpp588
-rw-r--r--test/lit/help/wasm-merge.test149
-rw-r--r--test/lit/merge/cycle.wat84
-rw-r--r--test/lit/merge/cycle.wat.second24
-rw-r--r--test/lit/merge/cycle.wat.third24
-rw-r--r--test/lit/merge/export_options.wat59
-rw-r--r--test/lit/merge/export_options.wat.second15
-rw-r--r--test/lit/merge/export_options_default.wat15
-rw-r--r--test/lit/merge/export_options_default.wat.second7
-rw-r--r--test/lit/merge/fusing.wat94
-rw-r--r--test/lit/merge/fusing.wat.second28
-rw-r--r--test/lit/merge/memory_data.wat39
-rw-r--r--test/lit/merge/memory_data.wat.second13
-rw-r--r--test/lit/merge/renamings.wat350
-rw-r--r--test/lit/merge/renamings.wat.second123
-rw-r--r--test/lit/merge/start.flip.wat30
-rw-r--r--test/lit/merge/start.flip.wat.second8
-rw-r--r--test/lit/merge/start.wat33
-rw-r--r--test/lit/merge/start.wat.second15
-rw-r--r--test/lit/merge/start3.wat36
-rw-r--r--test/lit/merge/start3.wat.second16
-rw-r--r--test/lit/merge/start3.wat.third9
-rw-r--r--test/lit/merge/table_elem.wat114
-rw-r--r--test/lit/merge/table_elem.wat.second37
30 files changed, 2171 insertions, 11 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bad6aa3f..13e84e8ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,11 @@ full changeset diff at the end of each section.
Current Trunk
-------------
+- Add a `wasm-merge` tool. This is a full rewrite of the previous `wasm-merge`
+ tool that was removed from the tree in the past. The new version is much
+ simpler after recent improvements to multi-memory and multi-table. The
+ rewrite was motivated by new use cases for merging modules in the context of
+ WasmGC.
- Some C and JS API functions now refer to data and element segments by name
instead of index.
- The --nominal and --hybrid command line options and related API functions have
diff --git a/README.md b/README.md
index a6f2590b1..c2869afaa 100644
--- a/README.md
+++ b/README.md
@@ -223,6 +223,9 @@ This repository contains code that builds the following tools in `bin/`:
performs emscripten-specific passes over it.
* **wasm-ctor-eval**: A tool that can execute functions (or parts of functions)
at compile time.
+ * **wasm-merge**: Merges multiple wasm files into a single file, connecting
+ corresponding imports to exports as it does so. Like a bundler for JS, but
+ for wasm.
* **binaryen.js**: A standalone JavaScript library that exposes Binaryen methods for [creating and optimizing Wasm modules](https://github.com/WebAssembly/binaryen/blob/main/test/binaryen.js/hello-world.js). For builds, see [binaryen.js on npm](https://www.npmjs.com/package/binaryen) (or download it directly from [github](https://raw.githubusercontent.com/AssemblyScript/binaryen.js/master/index.js), [rawgit](https://cdn.rawgit.com/AssemblyScript/binaryen.js/master/index.js), or [unpkg](https://unpkg.com/binaryen@latest/index.js)). Minimal requirements: Node.js v15.8 or Chrome v75 or Firefox v78.
Usage instructions for each are below.
@@ -562,6 +565,150 @@ as mentioned earlier, but there is no limitation on what you can execute here.
Any export from the wasm can be executed, if its contents are suitable. For
example, in Emscripten `wasm-ctor-eval` is even run on `main()` when possible.
+### wasm-merge
+
+`wasm-merge` combines wasm files together. For example, imagine you have a
+project that uses wasm files from multiple toolchains. Then it can be helpful to
+merge them all into a single wasm file before shipping, since in a single wasm
+file the calls between the modules become just normal calls inside a module,
+which allows them to be inlined, dead code eliminated, and so forth, potentially
+improving speed and size.
+
+For example, imagine we have these two wasm files:
+
+```wat
+;; a.wasm
+(module
+ (import "second" "bar" (func $second.bar))
+
+ (export "main" (func $func))
+
+ (func $func
+ (call $second.bar)
+ )
+)
+```
+
+```wat
+;; b.wasm
+(module
+ (import "outside" "log" (func $log (param i32)))
+
+ (export "bar" (func $func))
+
+ (func $func
+ (call $log
+ (i32.const 42)
+ )
+ )
+)
+```
+
+The filenames on your local drive are `a.wasm` and `b.wasm`, but for merging /
+bundling purposes let's say that the first is known as `"first"` and the second
+as `"second"`. That is, we want the first module's import of `"second.bar"` to
+call the function `$func` in the second module. Here is a wasm-merge command for
+that:
+
+```
+wasm-merge a.wasm first b.wasm second -o output.wasm
+```
+
+We give it the first wasm file, then its name, and then the second wasm file
+and then its name. The merged output is this:
+
+```wat
+(module
+ (import "second" "bar" (func $second.bar))
+ (import "outside" "log" (func $log (param i32)))
+
+ (export "main" (func $func))
+ (export "bar" (func $func_2))
+
+ (func $func
+ (call $func_2)
+ )
+
+ (func $func_2
+ (call $log
+ (i32.const 42)
+ )
+ )
+)
+```
+
+`wasm-merge` combined the two files into one, merging their functions, imports,
+etc., all while fixing up name conflicts and connecting corresponding imports to
+exports. In particular, note how `$func` calls `$func_2`, which is exactly what
+we wanted: `$func_2` is the function from the second module (renamed to avoid a
+name collision).
+
+Note that the wasm output in this example could benefit from additional
+optimization. First, the call to `$func_2` can now be easily inlined, so we can
+run `wasm-opt -O3` to do that for us. Also, we may not need all the imports and
+exports, for which we can run
+[wasm-metadce](https://github.com/WebAssembly/binaryen/wiki/Pruning-unneeded-code-in-wasm-files-with-wasm-metadce#example-pruning-exports).
+A good workflow could be to run `wasm-merge`, then `wasm-metadce`, then finish
+with `wasm-opt`.
+
+`wasm-merge` is kind of like a bundler for wasm files, in the sense of a "JS
+bundler" but for wasm. That is, with the wasm files above, imagine that we had
+this JS code to instantiate and connect them at runtime:
+
+```js
+// Compile the first module.
+var first = await fetch("a.wasm");
+first = new WebAssembly.Module(first);
+
+// Compile the first module.
+var second = await fetch("b.wasm");
+second = new WebAssembly.Module(second);
+
+// Instantiate the second, with a JS import.
+second = new WebAssembly.Instance(second, {
+ outside: {
+ log: (value) => {
+ console.log('value:', value);
+ }
+ }
+});
+
+// Instantiate the first, importing from the second.
+first = new WebAssembly.Instance(first, {
+ second: second.exports
+});
+
+// Call the main function.
+first.exports.main();
+```
+
+What `wasm-merge` does is basically what that JS does: it hooks up imports to
+exports, resolving names using the module names you provided. That is, by
+running `wasm-merge` we are moving the work of connecting the modules from
+runtime to compile time. As a result, after running `wasm-merge` we need a lot
+less JS to get the same result:
+
+```js
+// Compile the single module.
+var merged = await fetch("merged.wasm");
+merged = new WebAssembly.Module(merged);
+
+// Instantiate it with a JS import.
+merged = new WebAssembly.Instance(merged, {
+ outside: {
+ log: (value) => {
+ console.log('value:', value);
+ }
+ }
+});
+
+// Call the main function.
+merged.exports.main();
+```
+
+We still need to fetch and compile the merged wasm, and to provide it the JS
+import, but the work to connect two wasm modules is not needed any more.
+
## Testing
```
diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py
index 8dc3041bd..452ab84f8 100755
--- a/scripts/fuzz_opt.py
+++ b/scripts/fuzz_opt.py
@@ -82,6 +82,11 @@ def random_size():
return random.randint(INPUT_SIZE_MIN, 2 * INPUT_SIZE_MEAN - INPUT_SIZE_MIN)
+def make_random_input(input_size, raw_input_data):
+ with open(raw_input_data, 'wb') as f:
+ f.write(bytes([random.randint(0, 255) for x in range(input_size)]))
+
+
def run(cmd, stderr=None, silent=False):
if not silent:
print(' '.join(cmd))
@@ -1284,6 +1289,62 @@ class CtorEval(TestCaseHandler):
compare_between_vms(fix_output(wasm_exec), fix_output(evalled_wasm_exec), 'CtorEval')
+# Tests wasm-merge
+class Merge(TestCaseHandler):
+ frequency = 0.15
+
+ def handle(self, wasm):
+ # generate a second wasm file to merge. note that we intentionally pick
+ # a smaller size than the main wasm file, so that reduction is
+ # effective (i.e., as we reduce the main wasm to small sizes, we also
+ # end up with small secondary wasms)
+ # TODO: add imports and exports that connect between the two
+ wasm_size = os.stat(wasm).st_size
+ second_size = min(wasm_size, random_size())
+ second_input = abspath('second_input.dat')
+ make_random_input(second_size, second_input)
+ second_wasm = abspath('second.wasm')
+ run([in_bin('wasm-opt'), second_input, '-ttf', '-o', second_wasm] + FUZZ_OPTS + FEATURE_OPTS)
+
+ # sometimes also optimize the second module
+ if random.random() < 0.5:
+ opts = get_random_opts()
+ run([in_bin('wasm-opt'), second_wasm, '-o', second_wasm, '-all'] + FEATURE_OPTS + opts)
+
+ # merge the wasm files. note that we must pass -all, as even if the two
+ # inputs are MVP, the output may have multiple tables and multiple
+ # memories (and we must also do that in the commands later down).
+ #
+ # Use --skip-export-conflicts as we only look at the first module's
+ # exports for now - we don't care about the second module's.
+ # TODO: compare the second module's exports as well, but we'd need
+ # to handle renaming of conflicting exports.
+ merged = abspath('merged.wasm')
+ run([in_bin('wasm-merge'), wasm, 'first',
+ abspath('second.wasm'), 'second', '-o', merged,
+ '--skip-export-conflicts'] + FEATURE_OPTS + ['-all'])
+
+ # sometimes also optimize the merged module
+ if random.random() < 0.5:
+ opts = get_random_opts()
+ run([in_bin('wasm-opt'), merged, '-o', merged, '-all'] + FEATURE_OPTS + opts)
+
+ # verify that merging in the second module did not alter the output.
+ output = run_bynterp(wasm, ['--fuzz-exec-before', '-all'])
+ output = fix_output(output)
+ merged_output = run_bynterp(merged, ['--fuzz-exec-before', '-all'])
+ merged_output = fix_output(merged_output)
+
+ # a complication is that the second module's exports are appended, so we
+ # have extra output. to handle that, just prune the tail, so that we
+ # only compare the original exports from the first module.
+ # TODO: compare the second module's exports to themselves as well, but
+ # they may have been renamed due to overlaps...
+ merged_output = merged_output[:len(output)]
+
+ compare_between_vms(output, merged_output, 'Merge')
+
+
# Check that the text format round-trips without error.
class RoundtripText(TestCaseHandler):
frequency = 0.05
@@ -1306,6 +1367,7 @@ testcase_handlers = [
Asyncify(),
TrapsNeverHappen(),
CtorEval(),
+ Merge(),
# FIXME: Re-enable after https://github.com/WebAssembly/binaryen/issues/3989
# RoundtripText()
]
@@ -1329,7 +1391,7 @@ def test_one(random_input, given_wasm):
randomize_fuzz_settings()
pick_initial_contents()
- opts = randomize_opt_flags()
+ opts = get_random_opts()
print('randomized opts:', '\n ' + '\n '.join(opts))
print()
@@ -1503,7 +1565,7 @@ requires_closed_world = {("--type-refining",),
("--type-merging",)}
-def randomize_opt_flags():
+def get_random_opts():
flag_groups = []
has_flatten = False
@@ -1643,8 +1705,7 @@ if __name__ == '__main__':
'iters/sec, ', total_wasm_size / elapsed,
'wasm_bytes/sec, ', ignored_vm_runs,
'ignored\n')
- with open(raw_input_data, 'wb') as f:
- f.write(bytes([random.randint(0, 255) for x in range(input_size)]))
+ make_random_input(input_size, raw_input_data)
assert os.path.getsize(raw_input_data) == input_size
# remove the generated wasm file, so that we can tell if the fuzzer
# fails to create one
diff --git a/scripts/update_help_checks.py b/scripts/update_help_checks.py
index e439b2ecc..1756a0686 100755
--- a/scripts/update_help_checks.py
+++ b/scripts/update_help_checks.py
@@ -27,7 +27,7 @@ test_dir = os.path.join(root_dir, 'test', 'lit', 'help')
TOOLS = ['wasm-opt', 'wasm-as', 'wasm-dis', 'wasm2js', 'wasm-ctor-eval',
'wasm-shell', 'wasm-reduce', 'wasm-metadce', 'wasm-split',
- 'wasm-fuzz-types', 'wasm-emscripten-finalize']
+ 'wasm-fuzz-types', 'wasm-emscripten-finalize', 'wasm-merge']
def main():
diff --git a/src/ir/module-utils.h b/src/ir/module-utils.h
index 7dfd2d42d..912d78efd 100644
--- a/src/ir/module-utils.h
+++ b/src/ir/module-utils.h
@@ -132,12 +132,9 @@ inline DataSegment* copyDataSegment(const DataSegment* segment, Module& out) {
return out.addDataSegment(std::move(ret));
}
-inline void copyModule(const Module& in, Module& out) {
- // we use names throughout, not raw pointers, so simple copying is fine
- // for everything *but* expressions
- for (auto& curr : in.exports) {
- out.addExport(new Export(*curr));
- }
+// Copies named toplevel module items (things of kind ModuleItemKind). See
+// copyModule() for something that also copies exports, the start function, etc.
+inline void copyModuleItems(const Module& in, Module& out) {
for (auto& curr : in.functions) {
copyFunction(curr.get(), out);
}
@@ -159,6 +156,15 @@ inline void copyModule(const Module& in, Module& out) {
for (auto& curr : in.dataSegments) {
copyDataSegment(curr.get(), out);
}
+}
+
+inline void copyModule(const Module& in, Module& out) {
+ // we use names throughout, not raw pointers, so simple copying is fine
+ // for everything *but* expressions
+ for (auto& curr : in.exports) {
+ out.addExport(std::make_unique<Export>(*curr));
+ }
+ copyModuleItems(in, out);
out.start = in.start;
out.customSections = in.customSections;
out.debugInfoFileNames = in.debugInfoFileNames;
@@ -354,6 +360,36 @@ template<typename T> inline void iterImports(Module& wasm, T visitor) {
iterImportedTags(wasm, visitor);
}
+// Iterates over all importable module items. The visitor provided should have
+// signature void(ExternalKind, Importable*).
+template<typename T> inline void iterImportable(Module& wasm, T visitor) {
+ for (auto& curr : wasm.functions) {
+ if (curr->imported()) {
+ visitor(ExternalKind::Function, curr.get());
+ }
+ }
+ for (auto& curr : wasm.tables) {
+ if (curr->imported()) {
+ visitor(ExternalKind::Table, curr.get());
+ }
+ }
+ for (auto& curr : wasm.memories) {
+ if (curr->imported()) {
+ visitor(ExternalKind::Memory, curr.get());
+ }
+ }
+ for (auto& curr : wasm.globals) {
+ if (curr->imported()) {
+ visitor(ExternalKind::Global, curr.get());
+ }
+ }
+ for (auto& curr : wasm.tags) {
+ if (curr->imported()) {
+ visitor(ExternalKind::Tag, curr.get());
+ }
+ }
+}
+
// Helper class for performing an operation on all the functions in the module,
// in parallel, with an Info object for each one that can contain results of
// some computation that the operation performs.
diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt
index 943479923..3d12b4e34 100644
--- a/src/tools/CMakeLists.txt
+++ b/src/tools/CMakeLists.txt
@@ -17,6 +17,7 @@ binaryen_add_executable(wasm-ctor-eval wasm-ctor-eval.cpp)
if(NOT BUILD_EMSCRIPTEN_TOOLS_ONLY)
binaryen_add_executable(wasm-shell wasm-shell.cpp)
binaryen_add_executable(wasm-reduce wasm-reduce.cpp)
+ binaryen_add_executable(wasm-merge wasm-merge.cpp)
binaryen_add_executable(wasm-fuzz-types "${fuzzing_SOURCES};wasm-fuzz-types.cpp")
endif()
diff --git a/src/tools/wasm-merge.cpp b/src/tools/wasm-merge.cpp
new file mode 100644
index 000000000..ff2ab6d46
--- /dev/null
+++ b/src/tools/wasm-merge.cpp
@@ -0,0 +1,588 @@
+/*
+ * Copyright 2023 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.
+ */
+
+//
+// A WebAssembly merger: loads multiple files, mashes them together, and emits
+// the result. Unlike wasm-ld, this does not have the full semantics of native
+// linkers. Instead, wasm-merge does at compile time what you can do with JS at
+// runtime: connect some wasm modules together by hooking up imports to exports.
+// The result of wasm-merge is a single module that behaves the same as the
+// multiple original modules, but you don't need that JS to set up the
+// connections between the modules any more, and DCE and inlining can help
+// inside the module, etc. In other words, wasm-merge is sort of like a wasm
+// bundler, where "bundler" means something similar to JS bundlers. (While JS is
+// mentioned here a lot, wasm-merge could in principle also be helpful with
+// optimizing other ways of connecting modules at compile time instead of
+// runtime, like perhaps the component model for wasm that is in development.)
+//
+// The specific merging model here is to take N wasm modules, each with a given
+// name:
+//
+// wasm_1, wasm_2, ... , wasm_N
+// name_1, name_2, ... , name_N
+//
+// We resolve imports and exports using those names as we merge all the code
+// into the final module. That is, if wasm_i imports "foo.bar", and wasm_j has
+// name name_j == "foo" and it exports a function "bar" then wasm_i's import of
+// "foo.bar" will turn into a reference to the proper item from wasm_j that
+// corresponds to that export:
+//
+// (module "first"
+// (import "foo" "bar" (func $foo.bar))
+// (func $other
+// (call $foo.bar)
+// )
+// )
+//
+// (module "foo"
+// (func $f (export "bar")
+// ..
+// )
+// )
+//
+// => wasm-merge =>
+//
+// (module
+// ..
+// (func $other
+// (call $f) ;; call $f directly since "foo.bar" resolved as $f
+// )
+// (func $f
+// ..
+// )
+// )
+//
+// We call that process "fusing" of imports to exports. Note that we don't
+// bother to optimize here - we don't remove either the export or the import,
+// even if we fuse - as it is simple to leave that for later optimizations
+// (removing unwanted exports can be done using wasm-metadce, see
+// https://github.com/WebAssembly/binaryen/wiki/Pruning-unneeded-code-in-wasm-files-with-wasm-metadce#example-pruning-exports
+// ).
+//
+// Note that we allow "forward references" - a reference from an earlier module
+// to a later one. If one instantiates the wasm modules in sequence then that is
+// impossible to do, and to work around it e.g. emscripten dynamic linking
+// support will add a thunk. Note that ES6 modules support such circular imports
+// for JS, but they are considered annoying even there; one of the solutions to
+// such import loops in JS is to merge the modules together, and similarly
+// wasm-merge can help wasm build systems avoid such cycles. (Note that ES6
+// module support for *wasm* is not intended to support cycles, unlike JS, and
+// so avoiding cycles is important.)
+//
+// Despite resolving imports and exports without regard for the order of
+// modules, the order does matter in one way: if the modules have start
+// functions then those are called in the given order of the modules.
+//
+// wasm-merge works in linear time (linear in the total code in all the linked
+// modules). Each input module is traversed once to fix up names before being
+// merged, and at the end we traverse the entire merged module once to fuse
+// imports and exports.
+//
+
+#include "ir/module-utils.h"
+#include "ir/names.h"
+#include "support/colors.h"
+#include "support/file.h"
+#include "wasm-io.h"
+#include "wasm-validator.h"
+#include "wasm.h"
+
+#include "tool-options.h"
+
+using namespace wasm;
+
+namespace {
+
+// The module we'll merge into. This is a singleton and it is simple to just
+// have it as a global rather than pass it around all the time.
+Module merged;
+
+// Name conflicts on functions etc. are resolved by renaming things in a way
+// that only matters internally. Conflicting export names, however, are
+// observable, and so the user must decide how they want wasm-merge to handle
+// that.
+enum ExportMergeMode {
+ // Error on name conflicts. This is the least surprising mode, and the one
+ // used by default.
+ ErrorOnExportConflicts,
+ // Rename conflicting exports. Later exports will get a suffix added to them
+ // to make them unique. For example, this is useful if you merge several
+ // modules that each have a "main" export, and it's fine if those are renamed
+ // to "main", "main_1", "main_2" etc. - you'll decide when to call each of
+ // those and do so at the right time.
+ RenameExportConflicts,
+ // Silently ignore export conflicts, that is, later exports that overlap with
+ // previous ones are simply skipped. This can be useful when the first module
+ // is the main program, which exports things to the outside, while other
+ // modules are libraries of code that only provide things to the main module
+ // but not to the outside.
+ SkipExportConflicts,
+} exportMergeMode = ErrorOnExportConflicts;
+
+// Merging two modules is mostly straightforward: copy the functions etc. of the
+// first module into the second, with some renaming to avoid name collisions.
+// The only other thing we need to handle is the mapping of imports to exports,
+// as explained earlier. The way we handle this is to first combine the modules
+// into a single module, then connect imports and imports. To do that we track
+// the origin of each export.
+//
+// For example, in the example from earlier we have this as the second module:
+//
+// (module "foo"
+// (func $f (export "bar")
+// ..
+// )
+// )
+//
+// We will annotate that exported function as being from module "foo", so that
+// we can resolve imports to "foo.bar" to it. The ExportInfo data structure
+// tracks the extra info we need for exports as we go.
+struct ExportInfo {
+ // The name of the module this export originally appeared in, as just
+ // explained.
+ Name moduleName;
+ // The name of the export itself, which is the basename (the export will be
+ // used as module.base). This is normally just the same as export->name, but
+ // we need to stash it here because exports may be renamed when merged in, if
+ // there is overlap with the name of another export, and imports refer to the
+ // original name.
+ Name baseName;
+};
+std::unordered_map<Export*, ExportInfo> exportModuleMap;
+
+// A map of [kind of thing in the module] to [old name => new name] for things
+// of that kind. For example, the NameUpdates for functions is a map of old
+// function names to new function names.
+using NameUpdates = std::unordered_map<Name, Name>;
+using KindNameUpdates = std::unordered_map<ModuleItemKind, NameUpdates>;
+
+// Apply a set of name changes to a module.
+void updateNames(Module& wasm, KindNameUpdates& kindNameUpdates) {
+ if (kindNameUpdates.empty()) {
+ return;
+ }
+
+ struct NameMapper
+ : public WalkerPass<
+ PostWalker<NameMapper, UnifiedExpressionVisitor<NameMapper>>> {
+ bool isFunctionParallel() override { return true; }
+
+ std::unique_ptr<Pass> create() override {
+ return std::make_unique<NameMapper>(kindNameUpdates);
+ }
+
+ KindNameUpdates& kindNameUpdates;
+
+ NameMapper(KindNameUpdates& kindNameUpdates)
+ : kindNameUpdates(kindNameUpdates) {}
+
+ void visitExpression(Expression* curr) {
+#define DELEGATE_ID curr->_id
+
+#define DELEGATE_START(id) [[maybe_unused]] auto* cast = curr->cast<id>();
+
+#define DELEGATE_GET_FIELD(id, field) cast->field
+
+#define DELEGATE_FIELD_TYPE(id, field)
+#define DELEGATE_FIELD_HEAPTYPE(id, field)
+#define DELEGATE_FIELD_CHILD(id, field)
+#define DELEGATE_FIELD_OPTIONAL_CHILD(id, field)
+#define DELEGATE_FIELD_INT(id, field)
+#define DELEGATE_FIELD_INT_ARRAY(id, field)
+#define DELEGATE_FIELD_LITERAL(id, field)
+#define DELEGATE_FIELD_NAME(id, field)
+#define DELEGATE_FIELD_NAME_VECTOR(id, field)
+#define DELEGATE_FIELD_SCOPE_NAME_DEF(id, field)
+#define DELEGATE_FIELD_SCOPE_NAME_USE(id, field)
+#define DELEGATE_FIELD_SCOPE_NAME_USE_VECTOR(id, field)
+#define DELEGATE_FIELD_ADDRESS(id, field)
+
+#define DELEGATE_FIELD_NAME_KIND(id, field, kind) \
+ if (cast->field.is()) { \
+ mapName(kind, cast->field); \
+ }
+
+#include "wasm-delegations-fields.def"
+ }
+
+ // Aside from expressions, we have a few other things we need to update at
+ // the module scope.
+ void mapModuleFields(Module& wasm) {
+ for (auto& curr : wasm.exports) {
+ mapName(ModuleItemKind(curr->kind), curr->value);
+ }
+ for (auto& curr : wasm.elementSegments) {
+ mapName(ModuleItemKind::Table, curr->table);
+ }
+ for (auto& curr : wasm.dataSegments) {
+ mapName(ModuleItemKind::Memory, curr->memory);
+ }
+
+ mapName(ModuleItemKind::Function, wasm.start);
+ }
+
+ private:
+ void mapName(ModuleItemKind kind, Name& name) {
+ auto iter = kindNameUpdates.find(kind);
+ if (iter == kindNameUpdates.end()) {
+ return;
+ }
+ auto& nameUpdates = iter->second;
+ auto iter2 = nameUpdates.find(name);
+ if (iter2 != nameUpdates.end()) {
+ name = iter2->second;
+ }
+ }
+ } nameMapper(kindNameUpdates);
+
+ PassRunner runner(&wasm);
+ nameMapper.run(&runner, &wasm);
+ nameMapper.runOnModuleCode(&runner, &wasm);
+ nameMapper.mapModuleFields(wasm);
+}
+
+// Scan an input module to find the names of the items it contains, and pick new
+// names for them that do not cause conflicts with things already in the merged
+// module.
+void renameInputItems(Module& input) {
+ // Pick the names, and apply them to the items themselves.
+ // TODO Add ModuleUtils::iterAll + getValidName(kind, ..)? Then we could
+ // avoid hardcoded loops here, but it's unclear those would help
+ // anywhere else.
+ KindNameUpdates kindNameUpdates;
+
+ // Add a mapping of a name to a new name, in a particular kind. If the new
+ // name is the same as the old, do nothing.
+ auto maybeAdd = [&](ModuleItemKind kind, Name& name, const Name newName) {
+ if (newName != name) {
+ kindNameUpdates[kind][name] = newName;
+ name = newName;
+ }
+ };
+
+ for (auto& curr : input.functions) {
+ auto name = Names::getValidFunctionName(merged, curr->name);
+ maybeAdd(ModuleItemKind::Function, curr->name, name);
+ }
+ for (auto& curr : input.globals) {
+ auto name = Names::getValidGlobalName(merged, curr->name);
+ maybeAdd(ModuleItemKind::Global, curr->name, name);
+ }
+ for (auto& curr : input.tags) {
+ auto name = Names::getValidTagName(merged, curr->name);
+ maybeAdd(ModuleItemKind::Tag, curr->name, name);
+ }
+ for (auto& curr : input.elementSegments) {
+ auto name = Names::getValidElementSegmentName(merged, curr->name);
+ maybeAdd(ModuleItemKind::ElementSegment, curr->name, name);
+ }
+ for (auto& curr : input.memories) {
+ auto name = Names::getValidMemoryName(merged, curr->name);
+ maybeAdd(ModuleItemKind::Memory, curr->name, name);
+ }
+ for (auto& curr : input.dataSegments) {
+ auto name = Names::getValidDataSegmentName(merged, curr->name);
+ maybeAdd(ModuleItemKind::DataSegment, curr->name, name);
+ }
+ for (auto& curr : input.tables) {
+ auto name = Names::getValidTableName(merged, curr->name);
+ maybeAdd(ModuleItemKind::Table, curr->name, name);
+ }
+
+ // Apply the names to their uses.
+ updateNames(input, kindNameUpdates);
+}
+
+void copyModuleContents(Module& input, Name inputName) {
+ // First, copy the regular module items (functions, globals) etc. which we
+ // have proper names for, and can just copy.
+ ModuleUtils::copyModuleItems(input, merged);
+
+ // We must handle exports in a special way, as we need to note their origin
+ // module as we copy them in (also, they are not importable or exportable, so
+ // the ModuleUtils function above does not handle them).
+ for (auto& curr : input.exports) {
+ auto copy = std::make_unique<Export>(*curr);
+
+ // Note the module origin and original name of this export, for later fusing
+ // of imports to exports.
+ exportModuleMap[copy.get()] = ExportInfo{inputName, curr->name};
+
+ // An export may already exist with that name, so fix it up.
+ copy->name = Names::getValidExportName(merged, copy->name);
+ if (copy->name != curr->name) {
+ if (exportMergeMode == ErrorOnExportConflicts) {
+ Fatal() << "Export name conflict: " << curr->name << " (consider"
+ << " --rename-export-conflicts or"
+ << " --skip-export-conflicts)\n";
+ } else if (exportMergeMode == SkipExportConflicts) {
+ // Skip the addExport below us.
+ continue;
+ }
+ }
+ // Add the export.
+ merged.addExport(std::move(copy));
+ }
+
+ // Start functions must be merged.
+ if (input.start.is()) {
+ if (!merged.start.is()) {
+ // No previous start; just refer to the new one.
+ merged.start = input.start;
+ } else {
+ // Merge them, keeping the order. Note that we need to create a new
+ // function as there may be other references.
+ Builder builder(merged);
+ auto mergedName = Names::getValidFunctionName(merged, "merged.start");
+ auto* oldStart = merged.getFunction(merged.start);
+ auto* oldStartBody = ExpressionManipulator::copy(oldStart->body, merged);
+ auto* newStart = merged.getFunction(input.start);
+ auto* newStartBody = ExpressionManipulator::copy(newStart->body, merged);
+ auto* mergedBody = builder.makeSequence(oldStartBody, newStartBody);
+ auto mergedFunc = builder.makeFunction(
+ mergedName, Signature{Type::none, Type::none}, {}, mergedBody);
+ merged.addFunction(std::move(mergedFunc));
+ merged.start = mergedName;
+ }
+ }
+
+ // TODO: type names, features, debug info, custom sections, dylink info, etc.
+}
+
+// Find pairs of matching imports and exports, and make uses of the import refer
+// to the exported item (which has been merged into the module).
+void fuseImportsAndExports() {
+ // First, scan the exports and build a map. We build a map of [module name] to
+ // [export name => internal name]. For example, consider this module:
+ //
+ // (module "module_A"
+ // (func $foo (export "bar"))
+ // )
+ //
+ // Then the ModuleExportMap will be:
+ //
+ // {
+ // "module_A": {
+ // "bar": "foo";
+ // }
+ // }
+ //
+ using ModuleExportMap = std::unordered_map<Name, NameUpdates>;
+
+ // A map of ModuleExportMaps, one per item kind (one for functions, one for
+ // globals, etc.).
+ using KindModuleExportMaps =
+ std::unordered_map<ExternalKind, ModuleExportMap>;
+ KindModuleExportMaps kindModuleExportMaps;
+
+ for (auto& ex : merged.exports) {
+ assert(exportModuleMap.count(ex.get()));
+ ExportInfo& exportInfo = exportModuleMap[ex.get()];
+ kindModuleExportMaps[ex->kind][exportInfo.moduleName][exportInfo.baseName] =
+ ex->value;
+ }
+
+ // Find all the imports and see which have corresponding exports, which means
+ // there is an internal item we can refer to. We build up a map of the names
+ // that we should update.
+ KindNameUpdates kindNameUpdates;
+ ModuleUtils::iterImportable(merged, [&](ExternalKind kind, Importable* curr) {
+ if (curr->imported()) {
+ auto internalName = kindModuleExportMaps[kind][curr->module][curr->base];
+ if (internalName.is()) {
+ // We found something to fuse! Add it to the maps for renaming.
+ kindNameUpdates[ModuleItemKind(kind)][curr->name] = internalName;
+ }
+ }
+ });
+
+ // Update the things we found.
+ updateNames(merged, kindNameUpdates);
+}
+
+// Merges an input module into an existing target module. The input module can
+// be modified, as it will no longer be needed (so it is intentionally not
+// marked as const here).
+void mergeInto(Module& input, Name inputName) {
+ // Rename things in the input module so that there are no conflicts with names
+ // in the merged module. We do so in place for efficiency.
+ renameInputItems(input);
+
+ // The input module's items can now be copied into the target module safely,
+ // as names will not conflict.
+ copyModuleContents(input, inputName);
+}
+
+} // anonymous namespace
+
+int main(int argc, const char* argv[]) {
+ std::vector<std::string> inputFiles;
+ std::vector<std::string> inputFileNames;
+ bool emitBinary = true;
+ bool debugInfo = false;
+
+ const std::string WasmMergeOption = "wasm-merge options";
+
+ ToolOptions options("wasm-merge",
+ R"(Merge wasm files into one.
+
+For example,
+
+ wasm-merge foo.wasm foo bar.wasm bar -o merged.wasm
+
+will read foo.wasm and bar.wasm, with names 'foo' and 'bar' respectively, so if the second imports from 'foo', we will see that as an import from the first module after the merge. The merged output will be written to merged.wasm.
+
+Note that filenames and modules names are interleaved (which is hopefully less confusing).)");
+
+ options
+ .add("--output",
+ "-o",
+ "Output file (stdout if not specified)",
+ WasmMergeOption,
+ Options::Arguments::One,
+ [](Options* o, const std::string& argument) {
+ o->extra["output"] = argument;
+ Colors::setEnabled(false);
+ })
+ .add_positional("INFILE1 NAME1 INFILE2 NAME2 [..]",
+ Options::Arguments::N,
+ [&](Options* o, const std::string& argument) {
+ if (inputFiles.size() == inputFileNames.size()) {
+ inputFiles.push_back(argument);
+ } else {
+ inputFileNames.push_back(argument);
+ }
+ })
+ .add("--rename-export-conflicts",
+ "-rec",
+ "Rename exports to avoid conflicts (rather than error)",
+ WasmMergeOption,
+ Options::Arguments::Zero,
+ [&](Options* o, const std::string& argument) {
+ exportMergeMode = RenameExportConflicts;
+ })
+ .add("--skip-export-conflicts",
+ "-sec",
+ "Skip exports that conflict with previous ones",
+ WasmMergeOption,
+ Options::Arguments::Zero,
+ [&](Options* o, const std::string& argument) {
+ exportMergeMode = SkipExportConflicts;
+ })
+ .add("--emit-text",
+ "-S",
+ "Emit text instead of binary for the output file",
+ WasmMergeOption,
+ Options::Arguments::Zero,
+ [&](Options* o, const std::string& argument) { emitBinary = false; })
+ .add("--debuginfo",
+ "-g",
+ "Emit names section and debug info",
+ WasmMergeOption,
+ Options::Arguments::Zero,
+ [&](Options* o, const std::string& arguments) { debugInfo = true; });
+ options.parse(argc, argv);
+
+ if (inputFiles.size() != inputFileNames.size()) {
+ Fatal() << "Please provide an import name for each input file. "
+ "In particular, the number of positional inputs must be even as "
+ "each wasm binary must be followed by its name.";
+ }
+
+ // Process the inputs.
+ // TODO: If the inputs are a very large number of small modules then it might
+ // make sense to parallelize this. (If so, then changing the existing
+ // parallelism above in NameMapper might make sense.)
+
+ for (Index i = 0; i < inputFiles.size(); i++) {
+ auto inputFile = inputFiles[i];
+ auto inputFileName = inputFileNames[i];
+
+ if (options.debug) {
+ std::cerr << "reading input '" << inputFile << "' as '" << inputFileName
+ << "'...\n";
+ }
+
+ // For the first input, we'll just read it in directly. For later inputs,
+ // we read them and then merge.
+ std::unique_ptr<Module> laterInput;
+ Module* currModule;
+ if (i == 0) {
+ currModule = &merged;
+ } else {
+ laterInput = std::make_unique<Module>();
+ currModule = laterInput.get();
+ }
+
+ options.applyFeatures(*currModule);
+
+ ModuleReader reader;
+ try {
+ reader.read(inputFile, *currModule);
+ } catch (ParseException& p) {
+ p.dump(std::cerr);
+ Fatal() << "error in parsing wasm input: " << inputFile;
+ }
+
+ if (options.passOptions.validate) {
+ if (!WasmValidator().validate(*currModule)) {
+ std::cout << *currModule << '\n';
+ Fatal() << "error in validating input: " << inputFile;
+ }
+ }
+
+ if (!laterInput) {
+ // This is the very first module, which we read directly into |merged|.
+ // The only other operation we need to do is note the exports for later.
+ for (auto& curr : merged.exports) {
+ exportModuleMap[curr.get()] = ExportInfo{inputFileName, curr->name};
+ }
+ } else {
+ // This is a later module: do a full merge.
+ mergeInto(*currModule, inputFileName);
+
+ if (options.passOptions.validate) {
+ if (!WasmValidator().validate(merged)) {
+ std::cout << merged << '\n';
+ Fatal() << "error in validating merged after: " << inputFile;
+ }
+ }
+ }
+ }
+
+ // Fuse imports and exports now that everything is all together in the merged
+ // module.
+ fuseImportsAndExports();
+
+ // Remove unused things. This is obviously a useful optimization, but it also
+ // makes using the output easier: if an import was resolved by an export
+ // during the merge, then that import will have no more uses and it will be
+ // optimized out (while if we didn't optimize it out then instantiating the
+ // module would still be forced to provide something for that import).
+ {
+ PassRunner passRunner(&merged);
+ passRunner.add("remove-unused-module-elements");
+ passRunner.run();
+ }
+
+ // Output.
+ if (options.extra.count("output") > 0) {
+ ModuleWriter writer;
+ writer.setBinary(emitBinary);
+ writer.setDebugInfo(debugInfo);
+ writer.write(merged, options.extra["output"]);
+ }
+}
diff --git a/test/lit/help/wasm-merge.test b/test/lit/help/wasm-merge.test
new file mode 100644
index 000000000..541f37d5a
--- /dev/null
+++ b/test/lit/help/wasm-merge.test
@@ -0,0 +1,149 @@
+;; RUN: wasm-merge --help | filecheck %s
+;; CHECK: ================================================================================
+;; CHECK-NEXT: wasm-merge INFILE1 NAME1 INFILE2 NAME2 [..]
+;; CHECK-NEXT:
+;; CHECK-NEXT: Merge wasm files into one.
+;; CHECK-NEXT:
+;; CHECK-NEXT: For example,
+;; CHECK-NEXT:
+;; CHECK-NEXT: wasm-merge foo.wasm foo bar.wasm bar -o merged.wasm
+;; CHECK-NEXT:
+;; CHECK-NEXT: will read foo.wasm and bar.wasm, with names 'foo' and 'bar' respectively, so if
+;; CHECK-NEXT: the second imports from 'foo', we will see that as an import from the first
+;; CHECK-NEXT: module after the merge. The merged output will be written to merged.wasm.
+;; CHECK-NEXT:
+;; CHECK-NEXT: Note that filenames and modules names are interleaved (which is hopefully less
+;; CHECK-NEXT: confusing).
+;; CHECK-NEXT: ================================================================================
+;; CHECK-NEXT:
+;; CHECK-NEXT:
+;; CHECK-NEXT: wasm-merge options:
+;; CHECK-NEXT: -------------------
+;; CHECK-NEXT:
+;; CHECK-NEXT: --output,-o Output file (stdout if not specified)
+;; CHECK-NEXT:
+;; CHECK-NEXT: --rename-export-conflicts,-rec Rename exports to avoid conflicts (rather
+;; CHECK-NEXT: than error)
+;; CHECK-NEXT:
+;; CHECK-NEXT: --skip-export-conflicts,-sec Skip exports that conflict with previous
+;; CHECK-NEXT: ones
+;; CHECK-NEXT:
+;; CHECK-NEXT: --emit-text,-S Emit text instead of binary for the
+;; CHECK-NEXT: output file
+;; CHECK-NEXT:
+;; CHECK-NEXT: --debuginfo,-g Emit names section and debug info
+;; CHECK-NEXT:
+;; CHECK-NEXT:
+;; CHECK-NEXT: Tool options:
+;; CHECK-NEXT: -------------
+;; CHECK-NEXT:
+;; CHECK-NEXT: --mvp-features,-mvp Disable all non-MVP features
+;; CHECK-NEXT:
+;; CHECK-NEXT: --all-features,-all Enable all features
+;; CHECK-NEXT:
+;; CHECK-NEXT: --detect-features (deprecated - this flag does nothing)
+;; CHECK-NEXT:
+;; CHECK-NEXT: --quiet,-q Emit less verbose output and hide trivial
+;; CHECK-NEXT: warnings.
+;; CHECK-NEXT:
+;; CHECK-NEXT: --experimental-poppy Parse wast files as Poppy IR for testing
+;; CHECK-NEXT: purposes.
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-sign-ext Enable sign extension operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-sign-ext Disable sign extension operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-threads Enable atomic operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-threads Disable atomic operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-mutable-globals Enable mutable globals
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-mutable-globals Disable mutable globals
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-nontrapping-float-to-int Enable nontrapping float-to-int
+;; CHECK-NEXT: operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-nontrapping-float-to-int Disable nontrapping float-to-int
+;; CHECK-NEXT: operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-simd Enable SIMD operations and types
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-simd Disable SIMD operations and types
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-bulk-memory Enable bulk memory operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-bulk-memory Disable bulk memory operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-exception-handling Enable exception handling operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-exception-handling Disable exception handling operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-tail-call Enable tail call operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-tail-call Disable tail call operations
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-reference-types Enable reference types
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-reference-types Disable reference types
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-multivalue Enable multivalue functions
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-multivalue Disable multivalue functions
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-gc Enable garbage collection
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-gc Disable garbage collection
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-memory64 Enable memory64
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-memory64 Disable memory64
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-gc-nn-locals Enable GC non-null locals
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-gc-nn-locals Disable GC non-null locals
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-relaxed-simd Enable relaxed SIMD
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-relaxed-simd Disable relaxed SIMD
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-extended-const Enable extended const expressions
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-extended-const Disable extended const expressions
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-strings Enable strings
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-strings Disable strings
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-multi-memories Enable multi-memories
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-multi-memories Disable multi-memories
+;; CHECK-NEXT:
+;; CHECK-NEXT: --enable-typed-function-references Deprecated compatibility flag
+;; CHECK-NEXT:
+;; CHECK-NEXT: --disable-typed-function-references Deprecated compatibility flag
+;; CHECK-NEXT:
+;; CHECK-NEXT: --no-validation,-n Disables validation, assumes inputs are
+;; CHECK-NEXT: correct
+;; CHECK-NEXT:
+;; CHECK-NEXT: --pass-arg,-pa An argument passed along to optimization
+;; CHECK-NEXT: passes being run. Must be in the form
+;; CHECK-NEXT: KEY@VALUE
+;; CHECK-NEXT:
+;; CHECK-NEXT: --closed-world,-cw Assume code outside of the module does
+;; CHECK-NEXT: not inspect or interact with GC and
+;; CHECK-NEXT: function references, even if they are
+;; CHECK-NEXT: passed out. The outside may hold on to
+;; CHECK-NEXT: them and pass them back in, but not
+;; CHECK-NEXT: inspect their contents or call them.
+;; CHECK-NEXT:
+;; CHECK-NEXT:
+;; CHECK-NEXT: General options:
+;; CHECK-NEXT: ----------------
+;; CHECK-NEXT:
+;; CHECK-NEXT: --version Output version information and exit
+;; CHECK-NEXT:
+;; CHECK-NEXT: --help,-h Show this help message and exit
+;; CHECK-NEXT:
+;; CHECK-NEXT: --debug,-d Print debug information to stderr
+;; CHECK-NEXT:
diff --git a/test/lit/merge/cycle.wat b/test/lit/merge/cycle.wat
new file mode 100644
index 000000000..9862b3355
--- /dev/null
+++ b/test/lit/merge/cycle.wat
@@ -0,0 +1,84 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+;; RUN: wasm-merge %s first %s.second second %s.third third --rename-export-conflicts -all -S -o - | filecheck %s
+
+;; Test a cycle of imports: the first module imports from the second, which
+;; imports from the third, and we have a reverse cycle as well.
+
+(module
+ (import "second" "forward" (func $second.forward))
+
+ (import "second" "reverse" (func $second.reverse))
+
+ (import "third" "forward" (func $third.forward))
+
+ (import "third" "reverse" (func $third.reverse))
+
+
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (export "forward" (func $forward))
+
+ ;; CHECK: (export "reverse" (func $reverse))
+
+ ;; CHECK: (export "forward_2" (func $forward_6))
+
+ ;; CHECK: (export "reverse_3" (func $reverse_6))
+
+ ;; CHECK: (export "forward_4" (func $forward_12))
+
+ ;; CHECK: (export "reverse_5" (func $reverse_12))
+
+ ;; CHECK: (func $forward (type $none_=>_none)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $forward_6)
+ ;; CHECK-NEXT: )
+ (func $forward (export "forward")
+ (drop
+ (i32.const 1)
+ )
+ (call $second.forward)
+ )
+
+ ;; CHECK: (func $reverse (type $none_=>_none)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.const -1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $reverse_12)
+ ;; CHECK-NEXT: )
+ (func $reverse (export "reverse")
+ (drop
+ (i32.const -1)
+ )
+ (call $third.reverse)
+ )
+)
+;; CHECK: (func $forward_6 (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const 2)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (call $forward_12)
+;; CHECK-NEXT: )
+
+;; CHECK: (func $reverse_6 (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const -2)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (call $reverse)
+;; CHECK-NEXT: )
+
+;; CHECK: (func $forward_12 (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const 3)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (call $forward)
+;; CHECK-NEXT: )
+
+;; CHECK: (func $reverse_12 (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const -3)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (call $reverse_6)
+;; CHECK-NEXT: )
diff --git a/test/lit/merge/cycle.wat.second b/test/lit/merge/cycle.wat.second
new file mode 100644
index 000000000..b110043b4
--- /dev/null
+++ b/test/lit/merge/cycle.wat.second
@@ -0,0 +1,24 @@
+(module
+ (import "first" "forward" (func $first.forward))
+
+ (import "first" "reverse" (func $first.reverse))
+
+ (import "third" "forward" (func $third.forward))
+
+ (import "third" "reverse" (func $third.reverse))
+
+ (func $forward (export "forward")
+ (drop
+ (i32.const 2)
+ )
+ (call $third.forward)
+ )
+
+ (func $reverse (export "reverse")
+ (drop
+ (i32.const -2)
+ )
+ (call $first.reverse)
+ )
+)
+
diff --git a/test/lit/merge/cycle.wat.third b/test/lit/merge/cycle.wat.third
new file mode 100644
index 000000000..03cc08f04
--- /dev/null
+++ b/test/lit/merge/cycle.wat.third
@@ -0,0 +1,24 @@
+(module
+ (import "first" "forward" (func $first.forward))
+
+ (import "first" "reverse" (func $first.reverse))
+
+ (import "second" "forward" (func $second.forward))
+
+ (import "second" "reverse" (func $second.reverse))
+
+ (func $forward (export "forward")
+ (drop
+ (i32.const 3)
+ )
+ (call $first.forward)
+ )
+
+ (func $reverse (export "reverse")
+ (drop
+ (i32.const -3)
+ )
+ (call $second.reverse)
+ )
+)
+
diff --git a/test/lit/merge/export_options.wat b/test/lit/merge/export_options.wat
new file mode 100644
index 000000000..26bc9d382
--- /dev/null
+++ b/test/lit/merge/export_options.wat
@@ -0,0 +1,59 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+
+;; If asked to, we rename the conflicts. The second "func" export will become
+;; "func_1".
+;; RUN: wasm-merge %s first %s.second second --rename-export-conflicts -S -o - | filecheck %s --check-prefix RENAME
+
+;; If asked to, we can skip conflicting exports from later modules. The second
+;; "func" export will not exist.
+;; RUN: wasm-merge %s first %s.second second --skip-export-conflicts -S -o - | filecheck %s --check-prefix SKIP_C
+
+(module
+ ;; RENAME: (type $none_=>_none (func))
+
+ ;; RENAME: (export "func" (func $func0))
+
+ ;; RENAME: (export "func_1" (func $func1))
+
+ ;; RENAME: (export "other" (func $func2))
+
+ ;; RENAME: (func $func0
+ ;; RENAME-NEXT: (drop
+ ;; RENAME-NEXT: (i32.const 0)
+ ;; RENAME-NEXT: )
+ ;; RENAME-NEXT: )
+ ;; SKIP_C: (type $none_=>_none (func))
+
+ ;; SKIP_C: (export "func" (func $func0))
+
+ ;; SKIP_C: (export "other" (func $func2))
+
+ ;; SKIP_C: (func $func0
+ ;; SKIP_C-NEXT: (drop
+ ;; SKIP_C-NEXT: (i32.const 0)
+ ;; SKIP_C-NEXT: )
+ ;; SKIP_C-NEXT: )
+ (func $func0 (export "func")
+ ;; This export also appears in the second module.
+ (drop
+ (i32.const 0)
+ )
+ )
+)
+;; RENAME: (func $func1
+;; RENAME-NEXT: (drop
+;; RENAME-NEXT: (i32.const 1)
+;; RENAME-NEXT: )
+;; RENAME-NEXT: )
+
+;; RENAME: (func $func2
+;; RENAME-NEXT: (drop
+;; RENAME-NEXT: (i32.const 2)
+;; RENAME-NEXT: )
+;; RENAME-NEXT: )
+
+;; SKIP_C: (func $func2
+;; SKIP_C-NEXT: (drop
+;; SKIP_C-NEXT: (i32.const 2)
+;; SKIP_C-NEXT: )
+;; SKIP_C-NEXT: )
diff --git a/test/lit/merge/export_options.wat.second b/test/lit/merge/export_options.wat.second
new file mode 100644
index 000000000..adcc24bd2
--- /dev/null
+++ b/test/lit/merge/export_options.wat.second
@@ -0,0 +1,15 @@
+(module
+ (func $func1 (export "func")
+ ;; This export will conflict.
+ (drop
+ (i32.const 1)
+ )
+ )
+
+ (func $func2 (export "other")
+ ;; This export will not conflict.
+ (drop
+ (i32.const 2)
+ )
+ )
+)
diff --git a/test/lit/merge/export_options_default.wat b/test/lit/merge/export_options_default.wat
new file mode 100644
index 000000000..bb1dea30e
--- /dev/null
+++ b/test/lit/merge/export_options_default.wat
@@ -0,0 +1,15 @@
+;; By default we error on an export name conflict.
+;; (This is the same as export_options.wat, but a "not" test and a test with
+;; automatic updates cannot be in the same file.)
+
+;; RUN: not wasm-merge %s first %s.second second 2>&1 | filecheck %s
+;; CHECK: Fatal: Export name conflict: func (consider --rename-export-conflicts or --skip-export-conflicts)
+
+(module
+ (func $func0 (export "func")
+ ;; This export also appears in the second module.
+ (drop
+ (i32.const 0)
+ )
+ )
+)
diff --git a/test/lit/merge/export_options_default.wat.second b/test/lit/merge/export_options_default.wat.second
new file mode 100644
index 000000000..6e045c7dd
--- /dev/null
+++ b/test/lit/merge/export_options_default.wat.second
@@ -0,0 +1,7 @@
+(module
+ (func $func1 (export "func")
+ (drop
+ (i32.const 1)
+ )
+ )
+)
diff --git a/test/lit/merge/fusing.wat b/test/lit/merge/fusing.wat
new file mode 100644
index 000000000..645634b4d
--- /dev/null
+++ b/test/lit/merge/fusing.wat
@@ -0,0 +1,94 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+
+;; RUN: wasm-merge %s first %s.second second --rename-export-conflicts -all -S -o - | filecheck %s
+
+;; Test that we fuse imports to exports across modules.
+;;
+;; We test functions and memories here, and not every possible entity in a
+;; comprehensive way, since they all go through the same code path. (But we test
+;; two to at least verify we differentiate them.)
+
+(module
+ ;; The first two imports here will be resolved to direct calls into the
+ ;; second module's merged contents.
+ (import "second" "foo" (func $other.foo))
+
+ (import "second" "bar" (func $other.bar))
+
+ (import "second" "mem" (memory $other.mem 1))
+
+ ;; This import will remain unresolved.
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (import "third" "missing" (func $other.missing))
+ (import "third" "missing" (func $other.missing))
+
+ ;; CHECK: (memory $second.mem 2)
+
+ ;; CHECK: (export "foo" (func $first.foo))
+
+ ;; CHECK: (export "bar" (func $bar))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (export "mem" (memory $second.mem))
+
+ ;; CHECK: (export "foo_4" (func $second.foo))
+
+ ;; CHECK: (export "bar_5" (func $bar_6))
+
+ ;; CHECK: (func $first.foo (type $none_=>_none)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $second.foo)
+ ;; CHECK-NEXT: )
+ (func $first.foo (export "foo")
+ (drop
+ (i32.const 1)
+ )
+ (call $other.foo)
+ )
+
+ ;; CHECK: (func $bar (type $none_=>_none)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.const 2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $bar_6)
+ ;; CHECK-NEXT: (call $other.missing)
+ ;; CHECK-NEXT: )
+ (func $bar (export "bar")
+ (drop
+ (i32.const 2)
+ )
+ (call $other.bar)
+ (call $other.missing)
+ )
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (i32.load
+ ;; CHECK-NEXT: (i32.const 10)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ ;; Load from the memory imported from the second module.
+ (i32.load $other.mem
+ (i32.const 10)
+ )
+ )
+)
+;; CHECK: (func $second.foo (type $none_=>_none)
+;; CHECK-NEXT: (call $first.foo)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const 3)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $bar_6 (type $none_=>_none)
+;; CHECK-NEXT: (call $bar)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const 4)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
diff --git a/test/lit/merge/fusing.wat.second b/test/lit/merge/fusing.wat.second
new file mode 100644
index 000000000..387e57bb4
--- /dev/null
+++ b/test/lit/merge/fusing.wat.second
@@ -0,0 +1,28 @@
+(module
+ ;; Use the same internal name as in first, so $other.foo will need to be
+ ;; deduplicated.
+ (import "first" "foo" (func $other.foo))
+
+ ;; Use a different prefix than in first ($main instead of $other).
+ (import "first" "bar" (func $main.bar))
+
+ (memory $second.mem 2)
+
+ (export "mem" (memory $second.mem))
+
+ (func $second.foo (export "foo")
+ (call $other.foo)
+ (drop
+ (i32.const 3)
+ )
+ )
+
+ ;; Use the same internal name as in first, so this will need to be
+ ;; deduplicated.
+ (func $bar (export "bar")
+ (call $main.bar)
+ (drop
+ (i32.const 4)
+ )
+ )
+)
diff --git a/test/lit/merge/memory_data.wat b/test/lit/merge/memory_data.wat
new file mode 100644
index 000000000..d796dc322
--- /dev/null
+++ b/test/lit/merge/memory_data.wat
@@ -0,0 +1,39 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+
+;; RUN: wasm-merge %s first %s.second second --rename-export-conflicts -all -S -o - | filecheck %s
+
+;; Test we rename memories and data segments properly at the module scope.
+;; Memory $bar has a name collision, and both of the element segments' names.
+;; This test verifies that data segments refer to the right tables even after
+;; such name changes.
+
+(module
+ ;; CHECK: (memory $foo 1)
+ (memory $foo 1)
+
+ ;; CHECK: (memory $bar 10)
+ (memory $bar 10)
+
+ ;; CHECK: (memory $other 100)
+
+ ;; CHECK: (memory $bar_2 1000)
+
+ ;; CHECK: (data $a (i32.const 0) "a")
+ (data $a (memory $foo) (i32.const 0) "a")
+
+ ;; CHECK: (data $b (memory $bar) (i32.const 0) "b")
+ (data $b (memory $bar) (i32.const 0) "b")
+
+ ;; CHECK: (data $a_2 (memory $other) (i32.const 0) "a2")
+
+ ;; CHECK: (data $b_2 (memory $bar_2) (i32.const 0) "b2")
+
+ ;; CHECK: (export "keepalive" (memory $foo))
+ (export "keepalive" (memory $foo))
+
+ ;; CHECK: (export "keepalive1" (memory $bar))
+ (export "keepalive1" (memory $bar))
+)
+;; CHECK: (export "keepalive_2" (memory $other))
+
+;; CHECK: (export "keepalive1_3" (memory $bar_2))
diff --git a/test/lit/merge/memory_data.wat.second b/test/lit/merge/memory_data.wat.second
new file mode 100644
index 000000000..0ea34dbcd
--- /dev/null
+++ b/test/lit/merge/memory_data.wat.second
@@ -0,0 +1,13 @@
+(module
+ (memory $other 100)
+
+ (memory $bar 1000)
+
+ (data $a (memory $other) (i32.const 0) "a2")
+
+ (data $b (memory $bar) (i32.const 0) "b2")
+
+ (export "keepalive" (memory $other))
+
+ (export "keepalive1" (memory $bar))
+)
diff --git a/test/lit/merge/renamings.wat b/test/lit/merge/renamings.wat
new file mode 100644
index 000000000..fd01803f2
--- /dev/null
+++ b/test/lit/merge/renamings.wat
@@ -0,0 +1,350 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+;; RUN: wasm-merge %s first %s.second second --rename-export-conflicts -all -S -o - | filecheck %s
+
+;; Test that we rename items in the second module to avoid name collisions.
+
+(module
+ ;; CHECK: (type $array (array (mut funcref)))
+ (type $array (array (mut (ref null func))))
+
+ ;; This tag has a conflict in second.wat, and so second.wat's $foo
+ ;; will be renamed.
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $ref|$array|_=>_none (func (param (ref $array))))
+
+ ;; CHECK: (type $i32_=>_none (func (param i32)))
+
+ ;; CHECK: (type $i64_=>_none (func (param i64)))
+
+ ;; CHECK: (type $f32_=>_none (func (param f32)))
+
+ ;; CHECK: (type $f64_=>_none (func (param f64)))
+
+ ;; CHECK: (global $foo i32 (i32.const 1))
+ (global $foo i32 (i32.const 1))
+
+ ;; This global has a conflict in second.wat, and so second.wat's $bar
+ ;; will be renamed.
+ ;; CHECK: (global $bar i32 (i32.const 2))
+ (global $bar i32 (i32.const 2))
+
+ ;; This memory has a conflict in second.wat, and so second.wat's $foo
+ ;; will be renamed.
+ ;; CHECK: (global $other i32 (i32.const 3))
+
+ ;; CHECK: (global $bar_2 i32 (i32.const 4))
+
+ ;; CHECK: (memory $foo 10 20)
+ (memory $foo 10 20)
+
+ ;; CHECK: (memory $bar 30 40)
+ (memory $bar 30 40)
+
+ ;; CHECK: (memory $foo_2 50 60)
+
+ ;; CHECK: (memory $other 70 80)
+
+ ;; CHECK: (data $foo (i32.const 1) "abc")
+ (data $foo (i32.const 1) "abc")
+
+ ;; This data segment has a conflict in second.wat, and so second.wat's $bar
+ ;; will be renamed.
+ ;; CHECK: (data $bar (i32.const 2) "def")
+ (data $bar (i32.const 2) "def")
+
+ ;; This table has a conflict in second.wat, and so second.wat's $foo
+ ;; will be renamed.
+ ;; CHECK: (data $other (memory $foo_2) (i32.const 3) "ghi")
+
+ ;; CHECK: (data $bar_2 (memory $foo_2) (i32.const 4) "jkl")
+
+ ;; CHECK: (table $foo 10 20 funcref)
+ (table $foo 10 20 funcref)
+
+ ;; CHECK: (table $bar 30 40 funcref)
+ (table $bar 30 40 funcref)
+
+ ;; CHECK: (table $foo_2 50 60 funcref)
+
+ ;; CHECK: (table $other 70 80 funcref)
+
+ ;; CHECK: (elem $foo func $foo $bar)
+ (elem $foo (ref null func) $foo $bar)
+
+ ;; This elem has a conflict in second.wat, and so second.wat's $bar
+ ;; will be renamed.
+ ;; CHECK: (elem $bar func $bar $foo)
+ (elem $bar (ref null func) $bar $foo)
+
+ ;; CHECK: (elem $other func $foo_3 $other)
+
+ ;; CHECK: (elem $bar_2 func $other $foo_3)
+
+ ;; CHECK: (tag $foo (param i32))
+ (tag $foo (param i32))
+
+ ;; CHECK: (tag $bar (param i64))
+ (tag $bar (param i64))
+
+ ;; This export has a conflict in second.wat, and so second.wat's $foo
+ ;; will be renamed.
+ ;; CHECK: (tag $foo_2 (param f32))
+
+ ;; CHECK: (tag $other (param f64))
+
+ ;; CHECK: (export "foo" (func $foo))
+ (export "foo" (func $foo))
+
+ ;; CHECK: (export "bar" (func $bar))
+ (export "bar" (func $bar))
+
+ ;; CHECK: (export "keepalive" (func $uses))
+ (export "keepalive" (func $uses))
+
+ ;; CHECK: (export "foo_3" (func $foo_3))
+
+ ;; CHECK: (export "other" (func $other))
+
+ ;; CHECK: (export "keepalive_5" (func $uses.second))
+
+ ;; CHECK: (export "other-b" (func $other))
+
+ ;; CHECK: (func $foo (type $none_=>_none)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $foo
+ ;; This function has a conflict in second.wat, and so second.wat's $foo
+ ;; will be renamed.
+ (drop
+ (i32.const 1)
+ )
+ )
+
+ ;; CHECK: (func $bar (type $none_=>_none)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.const 2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $bar
+ (drop
+ (i32.const 2)
+ )
+ )
+
+ ;; CHECK: (func $uses (type $ref|$array|_=>_none) (param $array (ref $array))
+ ;; CHECK-NEXT: (try $try
+ ;; CHECK-NEXT: (do
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (catch $foo
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (pop i32)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (try $try0
+ ;; CHECK-NEXT: (do
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (catch $bar
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (pop i64)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.load $foo
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.load $bar
+ ;; CHECK-NEXT: (i32.const 2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (data.drop $foo)
+ ;; CHECK-NEXT: (data.drop $bar)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (table.get $foo
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (table.get $bar
+ ;; CHECK-NEXT: (i32.const 2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (array.init_elem $array $foo
+ ;; CHECK-NEXT: (local.get $array)
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (i32.const 2)
+ ;; CHECK-NEXT: (i32.const 3)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (array.init_elem $array $bar
+ ;; CHECK-NEXT: (local.get $array)
+ ;; CHECK-NEXT: (i32.const 4)
+ ;; CHECK-NEXT: (i32.const 5)
+ ;; CHECK-NEXT: (i32.const 6)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (global.get $foo)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (global.get $bar)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $foo)
+ ;; CHECK-NEXT: (call $bar)
+ ;; CHECK-NEXT: )
+ (func $uses (param $array (ref $array))
+ ;; Tags.
+ (try
+ (do)
+ (catch $foo
+ (drop
+ (pop i32)
+ )
+ )
+ )
+ (try
+ (do)
+ (catch $bar
+ (drop
+ (pop i64)
+ )
+ )
+ )
+
+ ;; Memories
+ (drop
+ (i32.load $foo
+ (i32.const 1)
+ )
+ )
+ (drop
+ (i32.load $bar
+ (i32.const 2)
+ )
+ )
+
+ ;; Data segments
+ (data.drop $foo)
+ (data.drop $bar)
+
+ ;; Tables
+ (drop
+ (table.get $foo
+ (i32.const 1)
+ )
+ )
+ (drop
+ (table.get $bar
+ (i32.const 2)
+ )
+ )
+
+ ;; Element segments
+ (array.init_elem $array $foo
+ (local.get $array)
+ (i32.const 1)
+ (i32.const 2)
+ (i32.const 3)
+ )
+ (array.init_elem $array $bar
+ (local.get $array)
+ (i32.const 4)
+ (i32.const 5)
+ (i32.const 6)
+ )
+
+ ;; Globals
+ (drop
+ (global.get $foo)
+ )
+ (drop
+ (global.get $bar)
+ )
+
+ ;; Functions.
+ (call $foo)
+ (call $bar)
+ )
+)
+;; CHECK: (func $foo_3 (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const 3)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $other (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const 4)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $uses.second (type $ref|$array|_=>_none) (param $array (ref $array))
+;; CHECK-NEXT: (try $try
+;; CHECK-NEXT: (do
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (catch $foo_2
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (pop f32)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (try $try0
+;; CHECK-NEXT: (do
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (catch $other
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (pop f64)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.load $foo_2
+;; CHECK-NEXT: (i32.const 3)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.load $other
+;; CHECK-NEXT: (i32.const 4)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (data.drop $other)
+;; CHECK-NEXT: (data.drop $bar_2)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (table.get $foo_2
+;; CHECK-NEXT: (i32.const 3)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (table.get $other
+;; CHECK-NEXT: (i32.const 4)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (array.init_elem $array $other
+;; CHECK-NEXT: (local.get $array)
+;; CHECK-NEXT: (i32.const 7)
+;; CHECK-NEXT: (i32.const 8)
+;; CHECK-NEXT: (i32.const 9)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (array.init_elem $array $bar_2
+;; CHECK-NEXT: (local.get $array)
+;; CHECK-NEXT: (i32.const 10)
+;; CHECK-NEXT: (i32.const 11)
+;; CHECK-NEXT: (i32.const 12)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (global.get $other)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (global.get $bar_2)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (call $foo_3)
+;; CHECK-NEXT: (call $other)
+;; CHECK-NEXT: )
diff --git a/test/lit/merge/renamings.wat.second b/test/lit/merge/renamings.wat.second
new file mode 100644
index 000000000..da02e0438
--- /dev/null
+++ b/test/lit/merge/renamings.wat.second
@@ -0,0 +1,123 @@
+(module
+ (type $array (array (mut (ref null func))))
+
+ (tag $foo (param f32))
+
+ (tag $other (param f64))
+
+ (memory $foo 50 60)
+
+ (memory $other 70 80)
+
+ (data $other (i32.const 3) "ghi")
+
+ (data $bar (i32.const 4) "jkl")
+
+ (table $foo 50 60 funcref)
+
+ (table $other 70 80 funcref)
+
+ (elem $other (ref null func) $foo $other)
+
+ (elem $bar (ref null func) $other $foo)
+
+ (global $other i32 (i32.const 3))
+
+ (global $bar i32 (i32.const 4))
+
+ (export "foo" (func $foo))
+
+ (export "other" (func $other))
+
+ (export "keepalive" (func $uses.second))
+
+ ;; Also test having a different name for the export as for the internal
+ ;; thing it refers to, to test we don't assume they are identical.
+ (export "other-b" (func $other))
+
+ (func $foo
+ (drop
+ (i32.const 3)
+ )
+ )
+
+ (func $other
+ (drop
+ (i32.const 4)
+ )
+ )
+
+ (func $uses.second (param $array (ref $array))
+ ;; Tags.
+ (try
+ (do)
+ (catch $foo
+ (drop
+ (pop f32)
+ )
+ )
+ )
+ (try
+ (do)
+ (catch $other
+ (drop
+ (pop f64)
+ )
+ )
+ )
+
+ ;; Memories
+ (drop
+ (i32.load $foo
+ (i32.const 3)
+ )
+ )
+ (drop
+ (i32.load $other
+ (i32.const 4)
+ )
+ )
+
+ ;; Data segments
+ (data.drop $other)
+ (data.drop $bar)
+
+ ;; Tables
+ (drop
+ (table.get $foo
+ (i32.const 3)
+ )
+ )
+ (drop
+ (table.get $other
+ (i32.const 4)
+ )
+ )
+
+ ;; Element segments
+ (array.init_elem $array $other
+ (local.get $array)
+ (i32.const 7)
+ (i32.const 8)
+ (i32.const 9)
+ )
+ (array.init_elem $array $bar
+ (local.get $array)
+ (i32.const 10)
+ (i32.const 11)
+ (i32.const 12)
+ )
+
+ ;; Globals
+ (drop
+ (global.get $other)
+ )
+ (drop
+ (global.get $bar)
+ )
+
+ ;; Functions.
+ (call $foo)
+ (call $other)
+ )
+)
diff --git a/test/lit/merge/start.flip.wat b/test/lit/merge/start.flip.wat
new file mode 100644
index 000000000..cf7d93ca5
--- /dev/null
+++ b/test/lit/merge/start.flip.wat
@@ -0,0 +1,30 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+;; RUN: wasm-merge %s first %s.second second --rename-export-conflicts -all -S -o - | filecheck %s
+
+;; Like start, but flipped - now only the first module has a start.
+
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (export "start" (func $start_1))
+
+ ;; CHECK: (start $start)
+ (start $start)
+
+ ;; CHECK: (func $start (type $none_=>_none)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $start
+ (drop
+ (i32.const 0)
+ )
+ )
+)
+
+;; CHECK: (func $start_1 (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const 1)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
diff --git a/test/lit/merge/start.flip.wat.second b/test/lit/merge/start.flip.wat.second
new file mode 100644
index 000000000..89dcf4f97
--- /dev/null
+++ b/test/lit/merge/start.flip.wat.second
@@ -0,0 +1,8 @@
+(module
+ (func $start (export "start")
+ ;; Not a start function, but the name overlaps so it will get deduplicated.
+ (drop
+ (i32.const 1)
+ )
+ )
+)
diff --git a/test/lit/merge/start.wat b/test/lit/merge/start.wat
new file mode 100644
index 000000000..19a9bcb91
--- /dev/null
+++ b/test/lit/merge/start.wat
@@ -0,0 +1,33 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+;; RUN: wasm-merge %s first %s.second second --rename-export-conflicts -all -S -o - | filecheck %s
+
+;; Test that we merge start functions. The first module here has none, but the
+;; second does, so we'll refer to its start function in the merged module.
+
+(module
+ (func $start
+ ;; This function has the name start, but is *not* the start function. The
+ ;; other module's start will need to get a new deduplicated name.
+ (drop
+ (i32.const 0)
+ )
+ )
+)
+;; CHECK: (type $none_=>_none (func))
+
+;; CHECK: (export "start" (func $start_1))
+
+;; CHECK: (export "user" (func $user))
+
+;; CHECK: (start $start_1)
+
+;; CHECK: (func $start_1 (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const 1)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $user (type $none_=>_none)
+;; CHECK-NEXT: (call $start_1)
+;; CHECK-NEXT: (call $start_1)
+;; CHECK-NEXT: )
diff --git a/test/lit/merge/start.wat.second b/test/lit/merge/start.wat.second
new file mode 100644
index 000000000..0f00feb7a
--- /dev/null
+++ b/test/lit/merge/start.wat.second
@@ -0,0 +1,15 @@
+(module
+ (start $start)
+
+ (func $start (export "start")
+ (drop
+ (i32.const 1)
+ )
+ )
+
+ (func $user (export "user")
+ ;; These calls must go to the function $start here (with body "1").
+ (call $start)
+ (call $start)
+ )
+)
diff --git a/test/lit/merge/start3.wat b/test/lit/merge/start3.wat
new file mode 100644
index 000000000..ab4c555ab
--- /dev/null
+++ b/test/lit/merge/start3.wat
@@ -0,0 +1,36 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+;; RUN: wasm-merge %s first %s.second second %s.third third -all -S -o - | filecheck %s
+
+;; Test that we merge start functions. The first module here has none, but the
+;; second and third do, so we'll first copy in the second's and then merge in
+;; the third's.
+
+(module
+)
+;; CHECK: (type $none_=>_none (func))
+
+;; CHECK: (export "start" (func $start))
+
+;; CHECK: (export "user" (func $user))
+
+;; CHECK: (start $merged.start)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const 1)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $user (type $none_=>_none)
+;; CHECK-NEXT: (call $start)
+;; CHECK-NEXT: (call $start)
+;; CHECK-NEXT: )
+
+;; CHECK: (func $merged.start (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const 1)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (i32.const 2)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
diff --git a/test/lit/merge/start3.wat.second b/test/lit/merge/start3.wat.second
new file mode 100644
index 000000000..ec66c2f5b
--- /dev/null
+++ b/test/lit/merge/start3.wat.second
@@ -0,0 +1,16 @@
+(module
+ (start $start)
+
+ (func $start (export "start")
+ (drop
+ (i32.const 1)
+ )
+ )
+
+ (func $user (export "user")
+ ;; These calls must go to the function $start here (with body "1") and not
+ ;; to the modified start that has the third module's content merged in.
+ (call $start)
+ (call $start)
+ )
+)
diff --git a/test/lit/merge/start3.wat.third b/test/lit/merge/start3.wat.third
new file mode 100644
index 000000000..508ffdb2a
--- /dev/null
+++ b/test/lit/merge/start3.wat.third
@@ -0,0 +1,9 @@
+(module
+ (start $start)
+
+ (func $start
+ (drop
+ (i32.const 2)
+ )
+ )
+)
diff --git a/test/lit/merge/table_elem.wat b/test/lit/merge/table_elem.wat
new file mode 100644
index 000000000..3fec4d865
--- /dev/null
+++ b/test/lit/merge/table_elem.wat
@@ -0,0 +1,114 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+
+;; RUN: wasm-merge %s first %s.second second --rename-export-conflicts -all -S -o - | filecheck %s
+
+;; Test we rename tables and element segments properly at the module scope.
+;; Table $foo has a name collision, and both of the element segments' names do
+;; as well. This test verifies that element segments refer to the right tables
+;; even after such name changes.
+
+(module
+ ;; CHECK: (type $vec (array funcref))
+ (type $vec (array funcref))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (table $foo 1 funcref)
+ (table $foo 1 funcref)
+
+ ;; CHECK: (table $bar 10 funcref)
+ (table $bar 10 funcref)
+
+ ;; CHECK: (table $foo_2 100 funcref)
+
+ ;; CHECK: (table $other 1000 funcref)
+
+ ;; CHECK: (elem $a (table $foo) (i32.const 0) func)
+ (elem $a (table $foo) (i32.const 0) func)
+
+ ;; CHECK: (elem $b (table $bar) (i32.const 0) func)
+ (elem $b (table $bar) (i32.const 0) func)
+
+ (func "keepalive2"
+ (drop
+ (table.get $foo
+ (i32.const 1)
+ )
+ )
+ (drop
+ (table.get $bar
+ (i32.const 1)
+ )
+ )
+ ;; GC operations are the only things that can keep alive an elem segment.
+ (drop
+ (array.new_elem $vec $a
+ (i32.const 1)
+ (i32.const 2)
+ )
+ )
+ (drop
+ (array.new_elem $vec $b
+ (i32.const 3)
+ (i32.const 4)
+ )
+ )
+ )
+)
+;; CHECK: (elem $a_2 (table $foo_2) (i32.const 0) func)
+
+;; CHECK: (elem $b_2 (table $other) (i32.const 0) func)
+
+;; CHECK: (export "keepalive2" (func $0))
+
+;; CHECK: (export "keepalive2_1" (func $0_1))
+
+;; CHECK: (func $0 (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (table.get $foo
+;; CHECK-NEXT: (i32.const 1)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (table.get $bar
+;; CHECK-NEXT: (i32.const 1)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (array.new_elem $vec $a
+;; CHECK-NEXT: (i32.const 1)
+;; CHECK-NEXT: (i32.const 2)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (array.new_elem $vec $b
+;; CHECK-NEXT: (i32.const 3)
+;; CHECK-NEXT: (i32.const 4)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $0_1 (type $none_=>_none)
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (table.get $foo_2
+;; CHECK-NEXT: (i32.const 1)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (table.get $other
+;; CHECK-NEXT: (i32.const 1)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (array.new_elem $vec $a_2
+;; CHECK-NEXT: (i32.const 5)
+;; CHECK-NEXT: (i32.const 6)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (array.new_elem $vec $b_2
+;; CHECK-NEXT: (i32.const 7)
+;; CHECK-NEXT: (i32.const 8)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
diff --git a/test/lit/merge/table_elem.wat.second b/test/lit/merge/table_elem.wat.second
new file mode 100644
index 000000000..7aa960c7f
--- /dev/null
+++ b/test/lit/merge/table_elem.wat.second
@@ -0,0 +1,37 @@
+(module
+ (type $vec (array funcref))
+
+ (table $foo 100 funcref)
+
+ (table $other 1000 funcref)
+
+ (elem $a (table $foo) (i32.const 0) func)
+
+ (elem $b (table $other) (i32.const 0) func)
+
+ (func "keepalive2"
+ (drop
+ (table.get $foo
+ (i32.const 1)
+ )
+ )
+ (drop
+ (table.get $other
+ (i32.const 1)
+ )
+ )
+ ;; GC operations are the only things that can keep alive an elem segment.
+ (drop
+ (array.new_elem $vec $a
+ (i32.const 5)
+ (i32.const 6)
+ )
+ )
+ (drop
+ (array.new_elem $vec $b
+ (i32.const 7)
+ (i32.const 8)
+ )
+ )
+ )
+)