From 2af24c1835ca12dee8871408fc3cce62dc13d252 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 26 Mar 2026 18:52:42 -0700 Subject: [PATCH 1/4] Add an option for wasm-merge to write a split manifest This manifest can later be given to wasm-split to split the merged module back up such that each function appears in its originating module. This can help simplify a merge-optimize-split workflow. --- src/tools/wasm-merge.cpp | 47 ++++++++++++++++ test/lit/help/wasm-merge.test | 6 ++ test/lit/merge/manifest.wat | 23 ++++++++ test/lit/merge/manifest.wat.second | 6 ++ test/lit/merge/manifest.wat.third | 6 ++ test/lit/wasm-split/merge-split.wast | 62 +++++++++++++++++++++ test/lit/wasm-split/merge-split.wast.second | 6 ++ test/lit/wasm-split/merge-split.wast.third | 6 ++ 8 files changed, 162 insertions(+) create mode 100644 test/lit/merge/manifest.wat create mode 100644 test/lit/merge/manifest.wat.second create mode 100644 test/lit/merge/manifest.wat.third create mode 100644 test/lit/wasm-split/merge-split.wast create mode 100644 test/lit/wasm-split/merge-split.wast.second create mode 100644 test/lit/wasm-split/merge-split.wast.third diff --git a/src/tools/wasm-merge.cpp b/src/tools/wasm-merge.cpp index 3c354f745bf..431f83b0332 100644 --- a/src/tools/wasm-merge.cpp +++ b/src/tools/wasm-merge.cpp @@ -620,6 +620,12 @@ int main(int argc, const char* argv[]) { std::string outputSourceMapFilename; std::string outputSourceMapUrl; + // We can write wasm-split manifests that can later be fed to wasm-split to + // split the merged module back up along the lines of the original modules. + // Map functions to their originating modules so we can write this manifest. + std::string manifestFile; + std::unordered_map functionToModule; + const std::string WasmMergeOption = "wasm-merge options"; ToolOptions options("wasm-merge", @@ -687,6 +693,16 @@ Input source maps can be specified by adding an -ism option right after the modu [&outputSourceMapUrl](Options* o, const std::string& argument) { outputSourceMapUrl = argument; }) + .add("--output-manifest", + "", + "Write a wasm-split manifest to the specified file. This manifest can " + "be given to wasm-split to split the merged module along the lines of " + "the original modules.", + WasmMergeOption, + Options::Arguments::One, + [&manifestFile](Options* o, const std::string& argument) { + manifestFile = argument; + }) .add("--rename-export-conflicts", "-rec", "Rename exports to avoid conflicts (rather than error)", @@ -780,6 +796,15 @@ Input source maps can be specified by adding an -ism option right after the modu // This is a later module: do a full merge. mergeInto(*currModule, inputFileName); + // The functions in the module have been renamed and copied rather than + // moved, so we can get their final names directly. (We don't need this + // for the first module because it does not appear in the manifest.) + for (auto& func : currModule->functions) { + if (!func->imported()) { + functionToModule[func->name] = inputFileName; + } + } + // Validate after each merged module, when we are in pass-debug mode // (this can be quadratic time). if (PassRunner::getPassDebug()) { @@ -822,6 +847,28 @@ Input source maps can be specified by adding an -ism option right after the modu } // Output. + if (!manifestFile.empty()) { + std::ofstream manifest(manifestFile); + // Skip module 0 because it will be the primary module for the split and + // does not need to appear in the manifest. + for (size_t i = 1; i < inputFileNames.size(); i++) { + const auto& moduleName = inputFileNames[i]; + bool first = true; + for (auto& func : merged.functions) { + if (!func->imported() && functionToModule[func->name] == moduleName) { + if (first) { + manifest << moduleName << "\n"; + first = false; + } + manifest << func->name.str << "\n"; + } + } + if (!first) { + manifest << "\n"; + } + } + } + if (options.extra.count("output") > 0) { ModuleWriter writer(options.passOptions); writer.setBinary(emitBinary); diff --git a/test/lit/help/wasm-merge.test b/test/lit/help/wasm-merge.test index 0386c8adf8e..c0c4ee726a1 100644 --- a/test/lit/help/wasm-merge.test +++ b/test/lit/help/wasm-merge.test @@ -34,6 +34,12 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --output-source-map-url,-osu Emit specified string as source map URL ;; CHECK-NEXT: +;; CHECK-NEXT: --output-manifest Write a wasm-split manifest to the +;; CHECK-NEXT: specified file. This manifest can be +;; CHECK-NEXT: given to wasm-split to split the merged +;; CHECK-NEXT: module along the lines of the original +;; CHECK-NEXT: modules. +;; CHECK-NEXT: ;; CHECK-NEXT: --rename-export-conflicts,-rec Rename exports to avoid conflicts (rather ;; CHECK-NEXT: than error) ;; CHECK-NEXT: diff --git a/test/lit/merge/manifest.wat b/test/lit/merge/manifest.wat new file mode 100644 index 00000000000..b0d2ef213ad --- /dev/null +++ b/test/lit/merge/manifest.wat @@ -0,0 +1,23 @@ +;; RUN: wasm-merge %s first %s.second second %s.third third --output-manifest %t.manifest -S -o %t.wasm +;; RUN: cat %t.manifest | filecheck %s + +;; The first module is the primary module and does not appear in the manifest. +;; CHECK-NOT: first +;; CHECK-NOT: foo +;; CHECK-NOT: bar + +;; CHECK: second +;; CHECK-NEXT: baz +;; CHECK-NEXT: +;; CHECK-NEXT: third +;; CHECK-NEXT: qux + +(module + (import "env" "imported_first" (func $imported_first)) + (func $foo (export "foo") + (call $imported_first) + ) + (func $bar (export "bar") + nop + ) +) diff --git a/test/lit/merge/manifest.wat.second b/test/lit/merge/manifest.wat.second new file mode 100644 index 00000000000..afbd6cfefbd --- /dev/null +++ b/test/lit/merge/manifest.wat.second @@ -0,0 +1,6 @@ +(module + (import "env" "imported_second" (func $imported_second)) + (func $baz (export "baz") + (call $imported_second) + ) +) diff --git a/test/lit/merge/manifest.wat.third b/test/lit/merge/manifest.wat.third new file mode 100644 index 00000000000..9071db1c023 --- /dev/null +++ b/test/lit/merge/manifest.wat.third @@ -0,0 +1,6 @@ +(module + (import "env" "imported_third" (func $imported_third)) + (func $qux (export "qux") + (call $imported_third) + ) +) diff --git a/test/lit/wasm-split/merge-split.wast b/test/lit/wasm-split/merge-split.wast new file mode 100644 index 00000000000..7c62d201dcd --- /dev/null +++ b/test/lit/wasm-split/merge-split.wast @@ -0,0 +1,62 @@ +;; RUN: wasm-merge %s first %s.second second %s.third third --output-manifest %t.manifest -S -o %t.wasm +;; RUN: wasm-split %t.wasm --multi-split --manifest %t.manifest -g -o %t.primary.wasm --out-prefix %t. +;; RUN: wasm-dis %t.primary.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-dis %t.second.wasm | filecheck %s --check-prefix SECOND +;; RUN: wasm-dis %t.third.wasm | filecheck %s --check-prefix THIRD + +;; PRIMARY: (module +;; PRIMARY-NEXT: (type $0 (func)) +;; PRIMARY-NEXT: (import "env" "imported_first" (func $imported_first)) +;; PRIMARY-NEXT: (import "env" "imported_second" (func $imported_second)) +;; PRIMARY-NEXT: (import "env" "imported_third" (func $imported_third)) +;; PRIMARY-NEXT: (import "placeholder.second" "0" (func $placeholder_0)) +;; PRIMARY-NEXT: (import "placeholder.third" "1" (func $placeholder_1)) +;; PRIMARY-NEXT: (table $0 2 funcref) +;; PRIMARY-NEXT: (elem $0 (i32.const 0) $placeholder_0 $placeholder_1) +;; PRIMARY-NEXT: (export "first_func" (func $first_func)) +;; PRIMARY-NEXT: (export "second_func" (func $trampoline_second_func)) +;; PRIMARY-NEXT: (export "third_func" (func $trampoline_third_func)) +;; PRIMARY-NEXT: (export "imported_second" (func $imported_second)) +;; PRIMARY-NEXT: (export "imported_third" (func $imported_third)) +;; PRIMARY-NEXT: (export "table" (table $0)) +;; PRIMARY-NEXT: (func $first_func +;; PRIMARY-NEXT: (call $imported_first) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: (func $trampoline_second_func +;; PRIMARY-NEXT: (call_indirect (type $0) +;; PRIMARY-NEXT: (i32.const 0) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: (func $trampoline_third_func +;; PRIMARY-NEXT: (call_indirect (type $0) +;; PRIMARY-NEXT: (i32.const 1) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) + +;; SECOND: (module +;; SECOND-NEXT: (type $0 (func)) +;; SECOND-NEXT: (import "primary" "table" (table $timport$0 2 funcref)) +;; SECOND-NEXT: (import "primary" "imported_second" (func $imported_second)) +;; SECOND-NEXT: (elem $0 (i32.const 0) $second_func) +;; SECOND-NEXT: (func $second_func +;; SECOND-NEXT: (call $imported_second) +;; SECOND-NEXT: ) +;; SECOND-NEXT: ) + +;; THIRD: (module +;; THIRD-NEXT: (type $0 (func)) +;; THIRD-NEXT: (import "primary" "table" (table $timport$0 2 funcref)) +;; THIRD-NEXT: (import "primary" "imported_third" (func $imported_third)) +;; THIRD-NEXT: (elem $0 (i32.const 1) $third_func) +;; THIRD-NEXT: (func $third_func +;; THIRD-NEXT: (call $imported_third) +;; THIRD-NEXT: ) +;; THIRD-NEXT: ) + +(module + (import "env" "imported_first" (func $imported_first)) + (func $first_func (export "first_func") + (call $imported_first) + ) +) diff --git a/test/lit/wasm-split/merge-split.wast.second b/test/lit/wasm-split/merge-split.wast.second new file mode 100644 index 00000000000..e52597945c9 --- /dev/null +++ b/test/lit/wasm-split/merge-split.wast.second @@ -0,0 +1,6 @@ +(module + (import "env" "imported_second" (func $imported_second)) + (func $second_func (export "second_func") + (call $imported_second) + ) +) diff --git a/test/lit/wasm-split/merge-split.wast.third b/test/lit/wasm-split/merge-split.wast.third new file mode 100644 index 00000000000..001ffacd7db --- /dev/null +++ b/test/lit/wasm-split/merge-split.wast.third @@ -0,0 +1,6 @@ +(module + (import "env" "imported_third" (func $imported_third)) + (func $third_func (export "third_func") + (call $imported_third) + ) +) From 91a7b721f4094cc40f44ed2f0464b79cc40707b9 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 27 Mar 2026 08:39:28 -0700 Subject: [PATCH 2/4] simplify --- src/tools/wasm-merge.cpp | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/tools/wasm-merge.cpp b/src/tools/wasm-merge.cpp index 431f83b0332..2cedf6284ec 100644 --- a/src/tools/wasm-merge.cpp +++ b/src/tools/wasm-merge.cpp @@ -622,9 +622,9 @@ int main(int argc, const char* argv[]) { // We can write wasm-split manifests that can later be fed to wasm-split to // split the merged module back up along the lines of the original modules. - // Map functions to their originating modules so we can write this manifest. + // Map modules to their functions so we can write the manifest. std::string manifestFile; - std::unordered_map functionToModule; + std::unordered_map> moduleFuncs; const std::string WasmMergeOption = "wasm-merge options"; @@ -799,9 +799,10 @@ Input source maps can be specified by adding an -ism option right after the modu // The functions in the module have been renamed and copied rather than // moved, so we can get their final names directly. (We don't need this // for the first module because it does not appear in the manifest.) + auto& funcs = moduleFuncs[inputFileName]; for (auto& func : currModule->functions) { if (!func->imported()) { - functionToModule[func->name] = inputFileName; + funcs.push_back(func->name); } } @@ -852,20 +853,21 @@ Input source maps can be specified by adding an -ism option right after the modu // Skip module 0 because it will be the primary module for the split and // does not need to appear in the manifest. for (size_t i = 1; i < inputFileNames.size(); i++) { - const auto& moduleName = inputFileNames[i]; + auto moduleName = inputFileNames[i]; + const auto& funcs = moduleFuncs[moduleName]; + if (funcs.empty()) { + continue; + } + bool first = true; - for (auto& func : merged.functions) { - if (!func->imported() && functionToModule[func->name] == moduleName) { - if (first) { - manifest << moduleName << "\n"; - first = false; - } - manifest << func->name.str << "\n"; + for (auto func : funcs) { + if (first) { + manifest << moduleName << "\n"; + first = false; } + manifest << func << "\n"; } - if (!first) { - manifest << "\n"; - } + manifest << "\n"; } } From f52310859fa43e21d4259c345573e64ab622b63d Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 27 Mar 2026 18:48:28 -0700 Subject: [PATCH 3/4] Apply suggestion from @aheejin Co-authored-by: Heejin Ahn --- src/tools/wasm-merge.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/tools/wasm-merge.cpp b/src/tools/wasm-merge.cpp index 2cedf6284ec..fe4f2438587 100644 --- a/src/tools/wasm-merge.cpp +++ b/src/tools/wasm-merge.cpp @@ -859,14 +859,10 @@ Input source maps can be specified by adding an -ism option right after the modu continue; } - bool first = true; - for (auto func : funcs) { - if (first) { - manifest << moduleName << "\n"; - first = false; - } - manifest << func << "\n"; - } + manifest << moduleName << "\n"; + for (auto func : funcs) { + manifest << func << "\n"; + } manifest << "\n"; } } From d19a79fd7746d4d46acfa3dfa5ecd814e0235c0e Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 27 Mar 2026 18:52:13 -0700 Subject: [PATCH 4/4] whitespace --- src/tools/wasm-merge.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tools/wasm-merge.cpp b/src/tools/wasm-merge.cpp index fe4f2438587..295b0b2d8b0 100644 --- a/src/tools/wasm-merge.cpp +++ b/src/tools/wasm-merge.cpp @@ -859,10 +859,10 @@ Input source maps can be specified by adding an -ism option right after the modu continue; } - manifest << moduleName << "\n"; - for (auto func : funcs) { - manifest << func << "\n"; - } + manifest << moduleName << "\n"; + for (auto func : funcs) { + manifest << func << "\n"; + } manifest << "\n"; } }