Skip to content

[wasm-split] Split module elements early#8688

Open
aheejin wants to merge 3 commits into
WebAssembly:mainfrom
aheejin:wasm_split_global_first
Open

[wasm-split] Split module elements early#8688
aheejin wants to merge 3 commits into
WebAssembly:mainfrom
aheejin:wasm_split_global_first

Conversation

@aheejin
Copy link
Copy Markdown
Member

@aheejin aheejin commented May 11, 2026

Before #8443, we scanned ref.funcs in global initializers early in indirectReferencesToSecondaryFunctions and created trampolines for them and replaced ref.func $funcs 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.funcs 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

(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.

Before WebAssembly#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 WebAssembly#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 WebAssembly#8510. Other test changes are just
the changes in the creation order of module elements and not meaningful.

Replaces WebAssembly#8542 and fixes WebAssembly#8510.
@aheejin aheejin requested a review from tlively May 11, 2026 19:15
@aheejin aheejin requested a review from a team as a code owner May 11, 2026 19:15
@aheejin aheejin marked this pull request as draft May 11, 2026 19:43
return exports;
}

void ModuleSplitter::makeImportExport(Importable& primaryItem,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is extracted from the same lambda method that used to be in shareImportableItems

}

std::unordered_map<std::pair<ExternalKind, Name>, Name>
ModuleSplitter::initExportedPrimaryItems(const Module& primary) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary to keep track of item exports because we call makeImportExport from multiple functions

Comment on lines +556 to +558
// Generate the call and the function. We generate a direct call here, but
// this will be converted to a call_indirect in
// indirectCallsToSecondaryFunctions.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by comment improvement

void ModuleSplitter::exportImportCalledPrimaryFunctions() {
// Find primary functions called/referred to from the secondary modules.
using CalledPrimaryToModules = std::map<Name, std::set<Module*>>;
struct CallCollector : PostWalker<CallCollector> {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just taken out of ParallelFunctionAnalysis to be reused

@@ -0,0 +1,52 @@
;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --split-funcs=split1,split2
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the test reduced from #8510

@@ -0,0 +1,63 @@
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are some edge cases that I feel easy to miss, because now we specially handle the active table and the base global in setupTablePatching

@@ -0,0 +1,45 @@
;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --keep-funcs=keep
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was renamed from global-funcref.wast (but Github fails to recognize it) and expectation rewritten

@aheejin aheejin marked this pull request as ready for review May 11, 2026 20:08
}

CallCollector collector(primaryFuncs, calledPrimaryToModules);
collector.walkModuleCode(secondary);
Copy link
Copy Markdown
Member Author

@aheejin aheejin May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, we handled ref.funcs in global initializers later in shareImportableItems in a little hacky way:

if (secondaryGlobal->init) {
// When a global's initializer contains ref.func
for (auto* ref : FindAll<RefFunc>(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<RefFunc>(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);
}
}
}

Now we move globals early, we just can walk all module code normally here, and the hack above is not necessary anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wasm-split fuzz bug

1 participant