From b81e665baabe92f9b9214e2be59687edbf32e6f8 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 21 May 2026 00:52:15 +0000 Subject: [PATCH] [wasm-split] Don't split trapping globals Trapping globals, which became possible with the custom-descriptor proposal, should stay in the primary module because we have to preserve their trapping behavior upon instantiation. Also,note that we remove unused globals here: https://github.com/WebAssembly/binaryen/blob/17a90787bae4f4544e25b3e7833da076e0d543de/src/ir/module-splitting.cpp#L896-L903 But trapping globals shouldn't be removed even when they are unused to preserve the trapping behavior. In case someone wants to split modules assuming traps never happen, this also adds `--traps-never-happen` option to wasm-split, in which case we can freely split or remove trapping globals. --- src/ir/module-splitting.cpp | 11 +++++++++++ src/ir/module-splitting.h | 3 +++ src/tools/wasm-split/split-options.cpp | 10 ++++++++++ src/tools/wasm-split/wasm-split.cpp | 1 + test/lit/help/wasm-split.test | 5 +++++ test/lit/wasm-split/trapping-global.wast | 24 ++++++++++++++++++++++++ 6 files changed, 54 insertions(+) create mode 100644 test/lit/wasm-split/trapping-global.wast diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index af31ff20b37..735e747938f 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -73,6 +73,7 @@ // from the IR before splitting. // #include "ir/module-splitting.h" +#include "ir/effects.h" #include "ir/find_all.h" #include "ir/module-utils.h" #include "ir/names.h" @@ -790,6 +791,16 @@ void ModuleSplitter::shareImportableItems() { primaryUsed.globals.insert(tableManager.activeBase.global); } + // Trapping globals should stay in the primary module to preserve the trapping + // behavior upon instantiation. + for (auto& global : primary.globals) { + if (global->init && + EffectAnalyzer(config.passOptions, primary, global->init) + .hasUnremovableSideEffects()) { + primaryUsed.globals.insert(global->name); + } + } + // 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 diff --git a/src/ir/module-splitting.h b/src/ir/module-splitting.h index 8260feb6b8d..fd21d052061 100644 --- a/src/ir/module-splitting.h +++ b/src/ir/module-splitting.h @@ -44,11 +44,14 @@ #ifndef wasm_ir_module_splitting_h #define wasm_ir_module_splitting_h +#include "pass.h" #include "wasm.h" namespace wasm::ModuleSplitting { struct Config { + // Pass options to use for effects analysis + PassOptions passOptions; // A vector of set of functions to split into that secondary. Each function // set belongs to a single secondary module. All others are kept in the // primary module. Must not include the start function if it exists. May or diff --git a/src/tools/wasm-split/split-options.cpp b/src/tools/wasm-split/split-options.cpp index dcea3bcf227..d62d22ad765 100644 --- a/src/tools/wasm-split/split-options.cpp +++ b/src/tools/wasm-split/split-options.cpp @@ -358,6 +358,16 @@ WasmSplitOptions::WasmSplitOptions() {Mode::Split, Mode::MultiSplit, Mode::Instrument}, Options::Arguments::Zero, [&](Options* o, const std::string& arguments) { stripDebug = true; }) + .add("--traps-never-happen", + "-tnh", + "Split under the helpful assumption that no trap is reached at " + "runtime (from load, div/mod, etc.)", + WasmSplitOption, + {Mode::Split, Mode::MultiSplit}, + Options::Arguments::Zero, + [&](Options* o, const std::string& arguments) { + passOptions.trapsNeverHappen = true; + }) .add("--output", "-o", "Output file.", diff --git a/src/tools/wasm-split/wasm-split.cpp b/src/tools/wasm-split/wasm-split.cpp index 82a3e9ab860..8faa1bef488 100644 --- a/src/tools/wasm-split/wasm-split.cpp +++ b/src/tools/wasm-split/wasm-split.cpp @@ -229,6 +229,7 @@ void writePlaceholderMap( void setCommonSplitConfigs(ModuleSplitting::Config& config, const WasmSplitOptions& options) { + config.passOptions = options.passOptions; config.usePlaceholders = options.usePlaceholders; config.minimizeNewExportNames = !options.passOptions.debugInfo; if (options.importNamespace) { diff --git a/test/lit/help/wasm-split.test b/test/lit/help/wasm-split.test index 950d3e5cea9..3503a46675d 100644 --- a/test/lit/help/wasm-split.test +++ b/test/lit/help/wasm-split.test @@ -151,6 +151,11 @@ ;; CHECK-NEXT: --strip-debug [split, multi-split, instrument] Strip ;; CHECK-NEXT: debug info (including the names section) ;; CHECK-NEXT: +;; CHECK-NEXT: --traps-never-happen,-tnh [split, multi-split] Split under the +;; CHECK-NEXT: helpful assumption that no trap is +;; CHECK-NEXT: reached at runtime (from load, div/mod, +;; CHECK-NEXT: etc.) +;; CHECK-NEXT: ;; CHECK-NEXT: --output,-o [instrument, merge-profiles, multi-split] ;; CHECK-NEXT: Output file. ;; CHECK-NEXT: diff --git a/test/lit/wasm-split/trapping-global.wast b/test/lit/wasm-split/trapping-global.wast new file mode 100644 index 00000000000..388973d835d --- /dev/null +++ b/test/lit/wasm-split/trapping-global.wast @@ -0,0 +1,24 @@ +;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --split-funcs=split +;; RUN: wasm-dis -all %t.1.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-split %s -all -g -o1 %t.tnh.1.wasm -o2 %t.tnh.2.wasm --split-funcs=split --traps-never-happen +;; RUN: wasm-dis -all %t.tnh.1.wasm | filecheck %s --check-prefix PRIMARY-TNH + +;; This unused global should NOT be removed by wasm-split because its +;; initializer contains a side effect (a trap due to a null descriptor). +;; However, if we pass --traps-never-happen, we assume traps never occur, so the +;; global will be considered to have no side effects and will be removed. +(module + (rec + (type $struct (descriptor $desc) (struct)) + (type $desc (describes $struct) (struct)) + ) + ;; PRIMARY: (global $trap (ref $struct) + ;; PRIMARY-TNH-NOT: (global $trap (ref $struct) + (global $trap (ref $struct) + (struct.new_desc $struct + (ref.null none) + ) + ) + + (func $split) +)