From 63e596d6038a9058634c6ad5e45b5eccda6d600e Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 8 May 2026 22:49:17 +0000 Subject: [PATCH 1/3] [wasm-split] Split module elements early Before #8443, we scanned `ref.func`s in global initializers early in `indirectReferencesToSecondaryFunctions` and created trampolines for them and replaced `ref.func $func`s with `ref.func $trampoline_func` if `func` was set to move to a secondary module. But in case the global containing `ref.func $trampoline_func` also ends up moving to the same secondary module, creating trampoline and using it was not necessary, because the global can simply use `ref.func $func` because `func` is in the same secondary module. To fix this, in #8443, we postponed creating trampolines for `ref.func`s in global initializers until `shareImportableItems`. This had a problem, because we end up creating new trampolines late in `shareImportableItems`. But trampolines were designed to go through `indirectCallsToSecondaryFunctions` and `setupTablePatching`, so those late trampolines were invalid, like ```wast (func $trampoline_foo (call $foo) ) ``` when `foo` was in a secondary module. This was supposed to be converted to a `call_indirect` in `indirectCallsToSecondaryFunctions` and the table elements were supposed to set up in `setupTablePatching`. --- This moves `shareImportableItems` before `indirectReferencesToSecondaryFunctions`. Turns out, except for the active table and its base global, we can do all splitting before we make most of the changes related to splitting. This also simplifies `shareImportableItems` because we can now delete code handling the consequences of various transformations. Because the active table and its base global may not be registered as "used" in `shareImportableItems` before `setupTablePatching`, we make sure they are correctly shared with secondary modules in `setupTablePatching`. `active-table-base-global-used-elsewhere.wast` has some edge cases that I feel easy to miss, because now we specially handle the active table and the base global in `setupTablePatching`. `global-reffunc.wast` is the same but just renamed expectation rewritten. `global-reffunc2.wast` is the failing case simplified from #8510. Other test changes are just the changes in the creation order of module elements and not meaningful. Replaces #8542 and fixes #8510. --- scripts/fuzz_opt.py | 2 +- src/ir/module-splitting.cpp | 267 +++++++++--------- ...tive-table-base-global-used-elsewhere.wast | 63 +++++ test/lit/wasm-split/global-funcref.wast | 43 --- test/lit/wasm-split/global-reffunc.wast | 51 ++++ test/lit/wasm-split/global-reffunc2.wast | 53 ++++ test/lit/wasm-split/ref.func.wast | 6 +- test/lit/wasm-split/split-module-items.wast | 8 +- 8 files changed, 304 insertions(+), 189 deletions(-) create mode 100644 test/lit/wasm-split/active-table-base-global-used-elsewhere.wast delete mode 100644 test/lit/wasm-split/global-funcref.wast create mode 100644 test/lit/wasm-split/global-reffunc.wast create mode 100644 test/lit/wasm-split/global-reffunc2.wast diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index c1a4d02c6d5..b089ec7f1f6 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2525,7 +2525,7 @@ def handle(self, wasm): TrapsNeverHappen(), CtorEval(), Merge(), - # Split(), # https://github.com/WebAssembly/binaryen/issues/8510 + Split(), RoundtripText(), ClusterFuzz(), Two(), diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 21d82d013a4..b1719c0536b 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -26,28 +26,28 @@ // placeholder function (and eventually to the original secondary // function), allocating a new table slot for the placeholder if necessary. // -// 4. Replace all references to each secondary module's functions in the +// 4. Export globals, tags, tables, and memories from the primary module and +// import them in the secondary modules. If possible, move those module +// items instead to the secondary modules. +// +// 5. Replace all references to each secondary module's functions in the // primary module's and each other secondary module's table segments with // references to imported placeholder functions. // -// 5. Rewrite direct calls from primary functions to secondary functions to be +// 6. Rewrite direct calls from primary functions to secondary functions to be // indirect calls to their placeholder functions (and eventually to their // original secondary functions), allocating new table slots for the // placeholders if necessary. // -// 6. For each primary function directly called from a secondary function, +// 7. For each primary function directly called from a secondary function, // export the primary function if it is not already exported and import it // into each secondary module using it. // -// 7. For each secondary module, create new active table segments in the +// 8. For each secondary module, create new active table segments in the // module that will replace all the placeholder function references in the // table with references to their corresponding secondary functions upon // instantiation. // -// 8. Export globals, tags, tables, and memories from the primary module and -// import them in the secondary modules. If possible, move those module -// items instead to the secondary modules. -// // Functions can be used or referenced three ways in a WebAssembly module: they // can be exported, called, or referenced with ref.func. The above procedure // introduces a layer of indirection to each of those mechanisms that removes @@ -73,7 +73,6 @@ // from the IR before splitting. // #include "ir/module-splitting.h" -#include "ir/export-utils.h" #include "ir/find_all.h" #include "ir/module-utils.h" #include "ir/names.h" @@ -311,6 +310,10 @@ struct ModuleSplitter { // names. std::unordered_map exportedPrimaryFuncs; + // Map from to their corresponding export names for + // non-function items. + std::unordered_map, Name> exportedPrimaryItems; + // For each table, map placeholder indices to the names of the functions they // replace. std::unordered_map> placeholderMap; @@ -322,32 +325,39 @@ struct ModuleSplitter { static std::unique_ptr initSecondary(const Module& primary); static std::unordered_map initExportedPrimaryFuncs(const Module& primary); + static std::unordered_map, Name> + initExportedPrimaryItems(const Module& primary); // Other helpers void exportImportFunction(Name func, const std::set& modules); + void makeImportExport(Importable& primaryItem, + Importable& secondaryItem, + const std::string& genericExportName, + ExternalKind kind); Name getTrampoline(Name funcName); // Main splitting steps void classifyFunctions(); void moveSecondaryFunctions(); void thunkExportedSecondaryFunctions(); + void shareImportableItems(); void indirectReferencesToSecondaryFunctions(); void indirectCallsToSecondaryFunctions(); void exportImportCalledPrimaryFunctions(); void setupTablePatching(); - void shareImportableItems(); ModuleSplitter(Module& primary, const Config& config) : config(config), primary(primary), tableManager(primary), - exportedPrimaryFuncs(initExportedPrimaryFuncs(primary)) { + exportedPrimaryFuncs(initExportedPrimaryFuncs(primary)), + exportedPrimaryItems(initExportedPrimaryItems(primary)) { classifyFunctions(); moveSecondaryFunctions(); thunkExportedSecondaryFunctions(); + shareImportableItems(); indirectReferencesToSecondaryFunctions(); indirectCallsToSecondaryFunctions(); exportImportCalledPrimaryFunctions(); setupTablePatching(); - shareImportableItems(); } }; @@ -443,6 +453,41 @@ ModuleSplitter::initExportedPrimaryFuncs(const Module& primary) { return functionExportNames; } +std::unordered_map, Name> +ModuleSplitter::initExportedPrimaryItems(const Module& primary) { + std::unordered_map, Name> exports; + for (auto& ex : primary.exports) { + if (ex->kind != ExternalKind::Function) { + if (auto* name = ex->getInternalName()) { + exports[std::make_pair(ex->kind, *name)] = ex->name; + } + } + } + return exports; +} + +void ModuleSplitter::makeImportExport(Importable& primaryItem, + Importable& secondaryItem, + const std::string& genericExportName, + ExternalKind kind) { + secondaryItem.name = primaryItem.name; + secondaryItem.hasExplicitName = primaryItem.hasExplicitName; + secondaryItem.module = config.importNamespace; + auto exportIt = + exportedPrimaryItems.find(std::make_pair(kind, primaryItem.name)); + if (exportIt != exportedPrimaryItems.end()) { + secondaryItem.base = exportIt->second; + } else { + std::string baseName = + config.newExportPrefix + + (config.minimizeNewExportNames ? minified.getName() : genericExportName); + Name exportName = Names::getValidExportName(primary, baseName); + primary.addExport(new Export(exportName, kind, primaryItem.name)); + secondaryItem.base = exportName; + exportedPrimaryItems[std::make_pair(kind, primaryItem.name)] = exportName; + } +} + void ModuleSplitter::exportImportFunction(Name funcName, const std::set& modules) { Name exportName; @@ -508,7 +553,9 @@ Name ModuleSplitter::getTrampoline(Name funcName) { primary, std::string("trampoline_") + funcName.toString()); it->second = trampoline; - // Generate the call and the function. + // Generate the call and the function. We generate a direct call here, but + // this will be converted to a call_indirect in + // indirectCallsToSecondaryFunctions. std::vector args; for (Index i = 0; i < oldFunc->getNumParams(); i++) { args.push_back(builder.makeLocalGet(i, oldFunc->getLocalType(i))); @@ -559,39 +606,6 @@ static void walkSegments(Walker& walker, Module* module) { } void ModuleSplitter::shareImportableItems() { - // Map internal names to (one of) their corresponding export names. Don't - // consider functions because they have already been imported and exported as - // necessary. - std::unordered_map, Name> exports; - for (auto& ex : primary.exports) { - if (ex->kind != ExternalKind::Function) { - if (auto* name = ex->getInternalName()) { - exports[std::make_pair(ex->kind, *name)] = ex->name; - } - } - } - - auto makeImportExport = [&](Importable& primaryItem, - Importable& secondaryItem, - const std::string& genericExportName, - ExternalKind kind) { - secondaryItem.name = primaryItem.name; - secondaryItem.hasExplicitName = primaryItem.hasExplicitName; - secondaryItem.module = config.importNamespace; - auto exportIt = exports.find(std::make_pair(kind, primaryItem.name)); - if (exportIt != exports.end()) { - secondaryItem.base = exportIt->second; - } else { - std::string baseName = - config.newExportPrefix + (config.minimizeNewExportNames - ? minified.getName() - : genericExportName); - Name exportName = Names::getValidExportName(primary, baseName); - primary.addExport(new Export(exportName, kind, primaryItem.name)); - secondaryItem.base = exportName; - exports[std::make_pair(kind, primaryItem.name)] = exportName; - } - }; struct UsedNames { std::unordered_set globals; @@ -718,6 +732,15 @@ void ModuleSplitter::shareImportableItems() { secondaryUsed.push_back(getUsedNames(*secondaryPtr)); } + // We need to assume the active table and its base global are used in the + // primary module, because we will create segments there later + if (tableManager.activeTable) { + primaryUsed.tables.insert(tableManager.activeTable->name); + } + if (tableManager.activeBase.global.size()) { + primaryUsed.globals.insert(tableManager.activeBase.global); + } + // Compute the transitive closure of globals referenced in other globals' // initializers. Since globals can reference other globals, we must ensure // that if a global is used in a module, all its dependencies are also marked @@ -796,24 +819,12 @@ void ModuleSplitter::shareImportableItems() { if (!usedInPrimary && usingSecondaries.size() == 1) { auto* secondary = usingSecondaries[0]; - // In case we copied this table to this secondary module in - // setupTablePatching(), !usedInPrimary can't be satisfied, because the - // primary module should have an element segment that refers to this - // table. assert(!secondary->getTableOrNull(table->name)); ModuleUtils::copyTable(table.get(), *secondary); tablesToRemove.push_back(table->name); } else { for (auto* secondary : usingSecondaries) { - // 1. In case we copied this table to this secondary module in - // setupTablePatching(), secondary.getTableOrNull(table->name) is not - // null, and we need to import it. - // 2. As in the case with other module elements, if the table is used in - // the secondary module's instructions, we need to export it. - auto secondaryTable = secondary->getTableOrNull(table->name); - if (!secondaryTable) { - secondaryTable = ModuleUtils::copyTable(table.get(), *secondary); - } + auto* secondaryTable = ModuleUtils::copyTable(table.get(), *secondary); makeImportExport(*table, *secondaryTable, "table", ExternalKind::Table); } } @@ -841,50 +852,11 @@ void ModuleSplitter::shareImportableItems() { // global that will be moved to a secondary module, like // (global $unused i32 (global.get $a)) // $a is moved to a secondary globalsToRemove.push_back(global->name); - } else if (!inPrimary && usingSecondaries.size() == 1) { - // We are moving this global to this secondary module auto* secondary = usingSecondaries[0]; - auto* secondaryGlobal = ModuleUtils::copyGlobal(global.get(), *secondary); + ModuleUtils::copyGlobal(global.get(), *secondary); globalsToRemove.push_back(global->name); - - if (secondaryGlobal->init) { - // When a global's initializer contains ref.func - for (auto* ref : FindAll(secondaryGlobal->init).list) { - // If ref.func's function is in a different secondary module, we - // create a trampoline here. - if (auto targetIndexIt = funcToSecondaryIndex.find(ref->func); - targetIndexIt != funcToSecondaryIndex.end()) { - if (secondaries[targetIndexIt->second].get() != secondary) { - ref->func = getTrampoline(ref->func); - } - } - // 1. If ref.func's function is in the primary module, we export it - // here. - // 2. If ref.func's function is in a different secondary module and we - // just created a trampoline for it in the primary module above, we - // export the trampoline here. - if (primary.getFunctionOrNull(ref->func)) { - exportImportFunction(ref->func, {secondary}); - } - // If ref.func's function is in the same secondary module, we don't - // need to do anything. The ref.func can directly reference the - // function. - } - } - - } else { // We are NOT moving this global to the secondary module - if (global->init) { - for (auto* ref : FindAll(global->init).list) { - // If we are exporting this global from the primary module, we should - // create a trampoline here, because we skipped doing it for global - // initializers in indirectReferencesToSecondaryFunctions. - if (allSecondaryFuncs.contains(ref->func)) { - ref->func = getTrampoline(ref->func); - } - } - } - + } else { for (auto* secondary : usingSecondaries) { auto* secondaryGlobal = ModuleUtils::copyGlobal(global.get(), *secondary); @@ -946,25 +918,7 @@ void ModuleSplitter::indirectReferencesToSecondaryFunctions() { } } } gatherer(*this); - // We shouldn't use collector.walkModuleCode here, because we don't want to - // walk global initializers. At this point, all globals are still in the - // primary module, so if we walk global initializers here, it will create - // unnecessary trampolines. - // - // For example, we have (global $a funcref (ref.func $foo)), and $foo was - // split into a secondary module. Because $a is at this point still in the - // primary module, $foo will be considered to exist in a different module, so - // this will create a trampoline for $foo. But it is possible that later we - // find out $a is exclusively used by that secondary module and move $a there. - // In that case, $a can just reference $foo locally, but if we scan global - // initializers here, we would have created an unnecessary trampoline for - // $foo. - walkSegments(gatherer, &primary); - for (auto& curr : primary.functions) { - if (!curr->imported()) { - gatherer.walkFunction(curr.get()); - } - } + gatherer.walkModule(&primary); for (auto& secondaryPtr : secondaries) { gatherer.walkModule(secondaryPtr.get()); } @@ -1052,29 +1006,33 @@ void ModuleSplitter::indirectCallsToSecondaryFunctions() { void ModuleSplitter::exportImportCalledPrimaryFunctions() { // Find primary functions called/referred to from the secondary modules. using CalledPrimaryToModules = std::map>; + struct CallCollector : PostWalker { + const std::unordered_set& primaryFuncs; + CalledPrimaryToModules& calledPrimaryToModules; + CallCollector(const std::unordered_set& primaryFuncs, + CalledPrimaryToModules& calledPrimaryToModules) + : primaryFuncs(primaryFuncs), + calledPrimaryToModules(calledPrimaryToModules) {} + void visitCall(Call* curr) { + if (primaryFuncs.contains(curr->target)) { + calledPrimaryToModules[curr->target].insert(getModule()); + } + } + void visitRefFunc(RefFunc* curr) { + if (primaryFuncs.contains(curr->func)) { + calledPrimaryToModules[curr->func].insert(getModule()); + } + } + }; + for (auto& secondaryPtr : secondaries) { Module* secondary = secondaryPtr.get(); ModuleUtils::ParallelFunctionAnalysis callCollector( *secondary, [&](Function* func, CalledPrimaryToModules& calledPrimaryToModules) { - struct CallCollector : PostWalker { - const std::unordered_set& primaryFuncs; - CalledPrimaryToModules& calledPrimaryToModules; - CallCollector(const std::unordered_set& primaryFuncs, - CalledPrimaryToModules& calledPrimaryToModules) - : primaryFuncs(primaryFuncs), - calledPrimaryToModules(calledPrimaryToModules) {} - void visitCall(Call* curr) { - if (primaryFuncs.contains(curr->target)) { - calledPrimaryToModules[curr->target].insert(getModule()); - } - } - void visitRefFunc(RefFunc* curr) { - if (primaryFuncs.contains(curr->func)) { - calledPrimaryToModules[curr->func].insert(getModule()); - } - } - }; + if (func->imported()) { + return; + } CallCollector(primaryFuncs, calledPrimaryToModules) .walkFunctionInModule(func, secondary); }); @@ -1084,6 +1042,9 @@ void ModuleSplitter::exportImportCalledPrimaryFunctions() { calledPrimaryToModules.merge(map); } + CallCollector collector(primaryFuncs, calledPrimaryToModules); + collector.walkModuleCode(secondary); + // Ensure each called primary function is exported and imported for (auto& [func, modules] : calledPrimaryToModules) { exportImportFunction(func, modules); @@ -1142,10 +1103,40 @@ void ModuleSplitter::setupTablePatching() { for (auto& [secondaryPtr, replacedElems] : moduleToReplacedElems) { Module& secondary = *secondaryPtr; + // Import and export the active table. This was not done previously in + // shareImportableItems because the active table was not being used by + // secondaries yet. auto secondaryTable = - ModuleUtils::copyTable(tableManager.activeTable, secondary); + secondary.getTableOrNull(tableManager.activeTable->name); + // If we use an existing table as the active table (e.g. because + // reference-types is disabled) and that table was already being used by an + // existing indirect call (not created by wasm-split), the active table may + // already exist, so we need to check. + if (!secondaryTable) { + secondaryTable = + ModuleUtils::copyTable(tableManager.activeTable, secondary); + makeImportExport(*tableManager.activeTable, + *secondaryTable, + "table", + ExternalKind::Table); + } if (tableManager.activeBase.global.size()) { + // Import and export the active table's base global. This was not done + // previously in shareImportableItems because the active table and its + // base global were not being used by secondaries yet. + auto* primaryGlobal = primary.getGlobal(tableManager.activeBase.global); + auto* secondaryGlobal = + secondary.getGlobalOrNull(tableManager.activeBase.global); + // If the base global is also used elsewhere in the secondary module, like + // in functions, this global may already have moved to the secondary, so + // we need to check. + if (!secondaryGlobal) { + secondaryGlobal = ModuleUtils::copyGlobal(primaryGlobal, secondary); + } + makeImportExport( + *primaryGlobal, *secondaryGlobal, "global", ExternalKind::Global); + assert(tableManager.activeTableSegments.size() == 1 && "Unexpected number of segments with non-const base"); assert(secondary.tables.size() == 1 && secondary.elementSegments.empty()); diff --git a/test/lit/wasm-split/active-table-base-global-used-elsewhere.wast b/test/lit/wasm-split/active-table-base-global-used-elsewhere.wast new file mode 100644 index 00000000000..6c68499e732 --- /dev/null +++ b/test/lit/wasm-split/active-table-base-global-used-elsewhere.wast @@ -0,0 +1,63 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; We need to disable reference-types to reuse the existing table as the active +;; table +;; RUN: wasm-split %s --disable-reference-types --split-funcs=split -g -o1 %t.1.wasm -o2 %t.2.wasm +;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY + +;; This tests the case when an existing table is used as the active table, and +;; the active table and its base global already has existing uses in the +;; secondary module. + +(module + ;; PRIMARY: (type $0 (func)) + ;; SECONDARY: (type $0 (func)) + (type $0 (func)) + (global $base (import "env" "base") i32) + ;; PRIMARY: (import "env" "base" (global $base i32)) + + ;; PRIMARY: (import "placeholder.deferred" "1" (func $placeholder_1)) + + ;; PRIMARY: (table $table 2 funcref) + (table $table 1 funcref) + (elem (global.get $base) $keep) + ;; PRIMARY: (elem $0 (global.get $base) $keep $placeholder_1) + + ;; PRIMARY: (export "table" (table $table)) + + ;; PRIMARY: (export "global" (global $base)) + + ;; PRIMARY: (export "keep" (func $keep)) + + ;; PRIMARY: (func $keep + ;; PRIMARY-NEXT: (call_indirect (type $0) + ;; PRIMARY-NEXT: (i32.add + ;; PRIMARY-NEXT: (global.get $base) + ;; PRIMARY-NEXT: (i32.const 1) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: ) + (func $keep + (call $split) + ) + ;; SECONDARY: (import "primary" "table" (table $table 1 funcref)) + + ;; SECONDARY: (import "primary" "global" (global $base i32)) + + ;; SECONDARY: (import "primary" "keep" (func $keep)) + + ;; SECONDARY: (elem $0 (global.get $base) $keep $split) + + ;; SECONDARY: (func $split + ;; SECONDARY-NEXT: (drop + ;; SECONDARY-NEXT: (global.get $base) + ;; SECONDARY-NEXT: ) + ;; SECONDARY-NEXT: (call_indirect (type $0) + ;; SECONDARY-NEXT: (i32.const 0) + ;; SECONDARY-NEXT: ) + ;; SECONDARY-NEXT: ) + (func $split + (drop (global.get $base)) + (call_indirect (type $0) (i32.const 0)) + ) +) diff --git a/test/lit/wasm-split/global-funcref.wast b/test/lit/wasm-split/global-funcref.wast deleted file mode 100644 index 3faf339546c..00000000000 --- a/test/lit/wasm-split/global-funcref.wast +++ /dev/null @@ -1,43 +0,0 @@ -;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --keep-funcs=keep -;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY -;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY - -;; When a split global ($a here)'s initializer contains a ref.func of a split -;; function, we should NOT create any trampolines, and the split global should -;; direclty refer to the function. - -(module - (global $a funcref (ref.func $split)) - (global $b funcref (ref.func $keep)) - - ;; PRIMARY: (export "keep" (func $keep)) - - ;; PRIMARY-NOT: (export "trampoline_split" - ;; PRIMARY-NOT: (func $trampoline_split - - ;; SECONDARY: (import "primary" "keep" (func $keep (exact))) - - ;; SECONDARY: (global $a funcref (ref.func $split)) - ;; SECONDARY: (global $b funcref (ref.func $keep)) - - ;; PRIMARY: (func $keep - ;; PRIMARY-NEXT: ) - (func $keep) - - ;; SECONDARY: (func $split - ;; SECONDARY-NEXT: (drop - ;; SECONDARY-NEXT: (global.get $a) - ;; SECONDARY-NEXT: ) - ;; SECONDARY-NEXT: (drop - ;; SECONDARY-NEXT: (global.get $b) - ;; SECONDARY-NEXT: ) - ;; SECONDARY-NEXT: ) - (func $split - (drop - (global.get $a) - ) - (drop - (global.get $b) - ) - ) -) diff --git a/test/lit/wasm-split/global-reffunc.wast b/test/lit/wasm-split/global-reffunc.wast new file mode 100644 index 00000000000..84cd04be4d0 --- /dev/null +++ b/test/lit/wasm-split/global-reffunc.wast @@ -0,0 +1,51 @@ +;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --keep-funcs=keep +;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY + +;; When a split global ($a here)'s initializer contains a ref.func of a split +;; function, we should NOT create any trampolines, and the split global should +;; direclty refer to the function. + +(module + (global $a funcref (ref.func $split)) + (global $b funcref (ref.func $keep)) + + (func $keep) + + (func $split + (drop + (global.get $a) + ) + (drop + (global.get $b) + ) + ) +) + +;; PRIMARY: (module +;; PRIMARY-NEXT: (type $0 (func)) +;; PRIMARY-NEXT: (import "placeholder.deferred" "0" (func $placeholder_0)) +;; PRIMARY-NEXT: (table $0 1 funcref) +;; PRIMARY-NEXT: (elem $0 (i32.const 0) $placeholder_0) +;; PRIMARY-NEXT: (export "table" (table $0)) +;; PRIMARY-NEXT: (export "keep" (func $keep)) +;; PRIMARY-NEXT: (func $keep +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) + +;; SECONDARY: (module +;; SECONDARY-NEXT: (type $0 (func)) +;; SECONDARY-NEXT: (import "primary" "table" (table $timport$0 1 funcref)) +;; SECONDARY-NEXT: (import "primary" "keep" (func $keep (exact))) +;; SECONDARY-NEXT: (global $a funcref (ref.func $split)) +;; SECONDARY-NEXT: (global $b funcref (ref.func $keep)) +;; SECONDARY-NEXT: (elem $0 (i32.const 0) $split) +;; SECONDARY-NEXT: (func $split +;; SECONDARY-NEXT: (drop +;; SECONDARY-NEXT: (global.get $a) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: (drop +;; SECONDARY-NEXT: (global.get $b) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) diff --git a/test/lit/wasm-split/global-reffunc2.wast b/test/lit/wasm-split/global-reffunc2.wast new file mode 100644 index 00000000000..9ca92fb3ae2 --- /dev/null +++ b/test/lit/wasm-split/global-reffunc2.wast @@ -0,0 +1,53 @@ +;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --split-funcs=split1,split2 +;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY + +;; Global $g1 is used (exported) in the primary module so it can't move, and +;; global $g2 is only used in the secondary module so it will move there. + +(module + (global $g1 funcref (ref.func $split1)) + (global $g2 funcref (ref.func $split2)) + (export "g1" (global $g1)) + + (func $split1 + (unreachable) + ) + + (func $split2 + (drop + (global.get $g2) + ) + ) +) + +;; PRIMARY-NEXT: (module +;; PRIMARY-NEXT: (type $0 (func)) +;; PRIMARY-NEXT: (import "placeholder.deferred" "0" (func $placeholder_0)) +;; PRIMARY-NEXT: (import "placeholder.deferred" "1" (func $placeholder_1)) +;; PRIMARY-NEXT: (global $g1 funcref (ref.func $trampoline_split1)) +;; PRIMARY-NEXT: (table $0 2 funcref) +;; PRIMARY-NEXT: (elem $0 (i32.const 0) $placeholder_0 $placeholder_1) +;; PRIMARY-NEXT: (export "g1" (global $g1)) +;; PRIMARY-NEXT: (export "table" (table $0)) +;; PRIMARY-NEXT: (func $trampoline_split1 +;; PRIMARY-NEXT: (call_indirect (type $0) +;; PRIMARY-NEXT: (i32.const 0) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) + +;; SECONDARY-NEXT: (module +;; SECONDARY-NEXT: (type $0 (func)) +;; SECONDARY-NEXT: (import "primary" "table" (table $timport$0 2 funcref)) +;; SECONDARY-NEXT: (global $g2 funcref (ref.func $split2)) +;; SECONDARY-NEXT: (elem $0 (i32.const 0) $split1 $split2) +;; SECONDARY-NEXT: (func $split1 +;; SECONDARY-NEXT: (unreachable) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: (func $split2 +;; SECONDARY-NEXT: (drop +;; SECONDARY-NEXT: (global.get $g2) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) diff --git a/test/lit/wasm-split/ref.func.wast b/test/lit/wasm-split/ref.func.wast index 38eea498d72..5cc261dc4cd 100644 --- a/test/lit/wasm-split/ref.func.wast +++ b/test/lit/wasm-split/ref.func.wast @@ -73,7 +73,7 @@ ;; SECONDARY: (import "primary" "prime" (func $prime (exact (type $0)))) - ;; SECONDARY: (elem $0 (i32.const 0) $second-in-table $second) + ;; SECONDARY: (elem $0 (i32.const 0) $second $second-in-table) ;; SECONDARY: (elem declare func $prime) @@ -109,13 +109,13 @@ ;; (but we will get a placeholder, as all split-out functions do). ) ) -;; PRIMARY: (func $trampoline_second-in-table (type $0) +;; PRIMARY: (func $trampoline_second (type $0) ;; PRIMARY-NEXT: (call_indirect $1 (type $0) ;; PRIMARY-NEXT: (i32.const 0) ;; PRIMARY-NEXT: ) ;; PRIMARY-NEXT: ) -;; PRIMARY: (func $trampoline_second (type $0) +;; PRIMARY: (func $trampoline_second-in-table (type $0) ;; PRIMARY-NEXT: (call_indirect $1 (type $0) ;; PRIMARY-NEXT: (i32.const 1) ;; PRIMARY-NEXT: ) diff --git a/test/lit/wasm-split/split-module-items.wast b/test/lit/wasm-split/split-module-items.wast index 340fe27dac8..fdca4737a28 100644 --- a/test/lit/wasm-split/split-module-items.wast +++ b/test/lit/wasm-split/split-module-items.wast @@ -34,16 +34,16 @@ ;; PRIMARY: (tag $keep-tag (type $1) (param i32)) ;; PRIMARY-NEXT: (tag $shared-tag (type $1) (param i32)) - ;; PRIMARY: (export "keep" (func $keep)) - ;; PRIMARY-NEXT: (export "memory" (memory $shared-memory)) + ;; PRIMARY: (export "memory" (memory $shared-memory)) ;; PRIMARY-NEXT: (export "table" (table $shared-table)) - ;; PRIMARY-NEXT: (export "table_3" (table $2)) ;; PRIMARY-NEXT: (export "global" (global $shared-global)) ;; PRIMARY-NEXT: (export "tag" (tag $shared-tag)) + ;; PRIMARY-NEXT: (export "keep" (func $keep)) + ;; PRIMARY-NEXT: (export "table_5" (table $2)) ;; SECONDARY: (import "primary" "memory" (memory $shared-memory 1 1)) - ;; SECONDARY-NEXT: (import "primary" "table_3" (table $timport$0 1 funcref)) ;; SECONDARY-NEXT: (import "primary" "table" (table $shared-table 1 1 funcref)) + ;; SECONDARY-NEXT: (import "primary" "table_5" (table $timport$1 1 funcref)) ;; SECONDARY-NEXT: (import "primary" "global" (global $shared-global i32)) ;; SECONDARY-NEXT: (import "primary" "keep" (func $keep (exact (param i32) (result i32)))) ;; SECONDARY-NEXT: (import "primary" "tag" (tag $shared-tag (type $1) (param i32))) From 45ba532decdb193660f22f14001d411df6c7cac2 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 11 May 2026 19:50:10 +0000 Subject: [PATCH 2/3] Test updates --- test/lit/wasm-split/global-reffunc.wast | 6 ------ test/lit/wasm-split/global-reffunc2.wast | 13 ++++++------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/test/lit/wasm-split/global-reffunc.wast b/test/lit/wasm-split/global-reffunc.wast index 84cd04be4d0..a5afa465408 100644 --- a/test/lit/wasm-split/global-reffunc.wast +++ b/test/lit/wasm-split/global-reffunc.wast @@ -24,10 +24,6 @@ ;; PRIMARY: (module ;; PRIMARY-NEXT: (type $0 (func)) -;; PRIMARY-NEXT: (import "placeholder.deferred" "0" (func $placeholder_0)) -;; PRIMARY-NEXT: (table $0 1 funcref) -;; PRIMARY-NEXT: (elem $0 (i32.const 0) $placeholder_0) -;; PRIMARY-NEXT: (export "table" (table $0)) ;; PRIMARY-NEXT: (export "keep" (func $keep)) ;; PRIMARY-NEXT: (func $keep ;; PRIMARY-NEXT: ) @@ -35,11 +31,9 @@ ;; SECONDARY: (module ;; SECONDARY-NEXT: (type $0 (func)) -;; SECONDARY-NEXT: (import "primary" "table" (table $timport$0 1 funcref)) ;; SECONDARY-NEXT: (import "primary" "keep" (func $keep (exact))) ;; SECONDARY-NEXT: (global $a funcref (ref.func $split)) ;; SECONDARY-NEXT: (global $b funcref (ref.func $keep)) -;; SECONDARY-NEXT: (elem $0 (i32.const 0) $split) ;; SECONDARY-NEXT: (func $split ;; SECONDARY-NEXT: (drop ;; SECONDARY-NEXT: (global.get $a) diff --git a/test/lit/wasm-split/global-reffunc2.wast b/test/lit/wasm-split/global-reffunc2.wast index 9ca92fb3ae2..80ce2275b25 100644 --- a/test/lit/wasm-split/global-reffunc2.wast +++ b/test/lit/wasm-split/global-reffunc2.wast @@ -21,13 +21,12 @@ ) ) -;; PRIMARY-NEXT: (module +;; PRIMARY: (module ;; PRIMARY-NEXT: (type $0 (func)) ;; PRIMARY-NEXT: (import "placeholder.deferred" "0" (func $placeholder_0)) -;; PRIMARY-NEXT: (import "placeholder.deferred" "1" (func $placeholder_1)) ;; PRIMARY-NEXT: (global $g1 funcref (ref.func $trampoline_split1)) -;; PRIMARY-NEXT: (table $0 2 funcref) -;; PRIMARY-NEXT: (elem $0 (i32.const 0) $placeholder_0 $placeholder_1) +;; PRIMARY-NEXT: (table $0 1 funcref) +;; PRIMARY-NEXT: (elem $0 (i32.const 0) $placeholder_0) ;; PRIMARY-NEXT: (export "g1" (global $g1)) ;; PRIMARY-NEXT: (export "table" (table $0)) ;; PRIMARY-NEXT: (func $trampoline_split1 @@ -37,11 +36,11 @@ ;; PRIMARY-NEXT: ) ;; PRIMARY-NEXT: ) -;; SECONDARY-NEXT: (module +;; SECONDARY: (module ;; SECONDARY-NEXT: (type $0 (func)) -;; SECONDARY-NEXT: (import "primary" "table" (table $timport$0 2 funcref)) +;; SECONDARY-NEXT: (import "primary" "table" (table $timport$0 1 funcref)) ;; SECONDARY-NEXT: (global $g2 funcref (ref.func $split2)) -;; SECONDARY-NEXT: (elem $0 (i32.const 0) $split1 $split2) +;; SECONDARY-NEXT: (elem $0 (i32.const 0) $split1) ;; SECONDARY-NEXT: (func $split1 ;; SECONDARY-NEXT: (unreachable) ;; SECONDARY-NEXT: ) From c7c85707db99c75f826e73ef782e87b35d0389b7 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 11 May 2026 20:04:42 +0000 Subject: [PATCH 3/3] Remove unnecessary code --- src/ir/module-splitting.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index b1719c0536b..f4ccaacbc96 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -1030,9 +1030,6 @@ void ModuleSplitter::exportImportCalledPrimaryFunctions() { ModuleUtils::ParallelFunctionAnalysis callCollector( *secondary, [&](Function* func, CalledPrimaryToModules& calledPrimaryToModules) { - if (func->imported()) { - return; - } CallCollector(primaryFuncs, calledPrimaryToModules) .walkFunctionInModule(func, secondary); });