From f0694a8d35a3c55dfb44717584630aba0b473f87 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 3 Dec 2025 15:48:47 -0800 Subject: [PATCH 01/11] builds --- src/ir/table-utils.cpp | 76 ++++++++++++++++++++++++++ src/ir/table-utils.h | 33 ++++++++++++ src/passes/Directize.cpp | 113 +++++---------------------------------- 3 files changed, 121 insertions(+), 101 deletions(-) diff --git a/src/ir/table-utils.cpp b/src/ir/table-utils.cpp index 124eb2aa3e3..f6c0d114e6b 100644 --- a/src/ir/table-utils.cpp +++ b/src/ir/table-utils.cpp @@ -79,4 +79,80 @@ bool usesExpressions(ElementSegment* curr, Module* module) { return !allElementsRefFunc || hasSpecializedType; } +TableInfoMap computeTableInfo(Module& wasm, bool initialContentsImmutable) { + // Set up the initial info. + TableInfoMap tables; + if (wasm.tables.empty()) { + return tables; + } + for (auto& table : wasm.tables) { + tables[table->name].initialContentsImmutable = initialContentsImmutable; + tables[table->name].flatTable = + std::make_unique(wasm, *table); + } + + // Next, look at the imports and exports. + + for (auto& table : wasm.tables) { + if (table->imported()) { + tables[table->name].mayBeModified = true; + } + } + + for (auto& ex : wasm.exports) { + if (ex->kind == ExternalKind::Table) { + tables[*ex->getInternalName()].mayBeModified = true; + } + } + + // Find which tables have sets, by scanning for instructions. Only do so if we + // might learn anything new. + auto hasUnmodifiableTable = false; + for (auto& [_, info] : tables) { + if (!info.mayBeModified) { + hasUnmodifiableTable = true; + break; + } + } + if (hasUnmodifiableTable) { + using TablesWithSet = std::unordered_set; + + ModuleUtils::ParallelFunctionAnalysis analysis( + wasm, [&](Function* func, TablesWithSet& tablesWithSet) { + if (func->imported()) { + return; + } + + struct Finder : public PostWalker { + TablesWithSet& tablesWithSet; + + Finder(TablesWithSet& tablesWithSet) : tablesWithSet(tablesWithSet) {} + + void visitTableSet(TableSet* curr) { + tablesWithSet.insert(curr->table); + } + void visitTableFill(TableFill* curr) { + tablesWithSet.insert(curr->table); + } + void visitTableCopy(TableCopy* curr) { + tablesWithSet.insert(curr->destTable); + } + void visitTableInit(TableInit* curr) { + tablesWithSet.insert(curr->table); + } + }; + + Finder(tablesWithSet).walkFunction(func); + }); + + for (auto& [_, names] : analysis.map) { + for (auto name : names) { + tables[name].mayBeModified = true; + } + } + } + + return tables; +} + } // namespace wasm::TableUtils diff --git a/src/ir/table-utils.h b/src/ir/table-utils.h index 130a9f84947..254447441fd 100644 --- a/src/ir/table-utils.h +++ b/src/ir/table-utils.h @@ -120,6 +120,39 @@ std::set getFunctionsNeedingElemDeclare(Module& wasm); // do so, and some do not, depending on their type and use.) bool usesExpressions(ElementSegment* curr, Module* module); +// Information about a table's optimizability. +struct TableInfo { + // Whether the table may be modifed at runtime, either because it is imported + // or exported, or table.set operations exist for it in the code. + bool mayBeModified = false; + + // Whether we can assume that the initial contents are immutable. See the + // toplevel comment. + bool initialContentsImmutable = false; + + std::unique_ptr flatTable; + + // Whether we can optimize using this table's data, that is, we know something + // useful about the data there at compile time. The specifics about what we + // know are in the above fields. + bool canOptimize() const { + // We can optimize if: + // * Either the table can't be modified at all, or it can be modified but + // the initial contents are immutable (so we can optimize those + // contents, even if other things might be appended later). + // * The table is flat (so we can see what is in it). + return (!mayBeModified || initialContentsImmutable) && flatTable->valid; + } +}; + +// A map of tables to their info. +using TableInfoMap = std::unordered_map; + +// Compute a map with table optimizability info. We can be told that the initial +// contents of the tables are immutable (that is, existing data is not +// overwritten, but new things may be appended). +TableInfoMap computeTableInfo(Module& wasm, bool initialContentsImmutable=false); + } // namespace wasm::TableUtils #endif // wasm_ir_table_h diff --git a/src/passes/Directize.cpp b/src/passes/Directize.cpp index 3a458772467..a52b2b89430 100644 --- a/src/passes/Directize.cpp +++ b/src/passes/Directize.cpp @@ -46,28 +46,6 @@ namespace wasm { namespace { -struct TableInfo { - // Whether the table may be modifed at runtime, either because it is imported - // or exported, or table.set operations exist for it in the code. - bool mayBeModified = false; - - // Whether we can assume that the initial contents are immutable. See the - // toplevel comment. - bool initialContentsImmutable = false; - - std::unique_ptr flatTable; - - bool canOptimize() const { - // We can optimize if: - // * Either the table can't be modified at all, or it can be modified but - // the initial contents are immutable (so we can optimize them). - // * The table is flat. - return (!mayBeModified || initialContentsImmutable) && flatTable->valid; - } -}; - -using TableInfoMap = std::unordered_map; - struct FunctionDirectizer : public WalkerPass> { bool isFunctionParallel() override { return true; } @@ -75,7 +53,7 @@ struct FunctionDirectizer : public WalkerPass> { return std::make_unique(tables); } - FunctionDirectizer(const TableInfoMap& tables) : tables(tables) {} + FunctionDirectizer(const TableUtils::TableInfoMap& tables) : tables(tables) {} void visitCallIndirect(CallIndirect* curr) { auto& table = tables.at(curr->table); @@ -114,7 +92,7 @@ struct FunctionDirectizer : public WalkerPass> { } private: - const TableInfoMap& tables; + const TableUtils::TableInfoMap& tables; bool changedTypes = false; @@ -123,7 +101,7 @@ struct FunctionDirectizer : public WalkerPass> { // that is, whether we know a direct call target, or we know it will trap, or // if we know nothing. CallUtils::IndirectCallInfo getTargetInfo(Expression* target, - const TableInfo& table, + const TableUtils::TableInfo& table, CallIndirect* original) { auto* c = target->dynCast(); if (!c) { @@ -165,7 +143,7 @@ struct FunctionDirectizer : public WalkerPass> { // with an unreachable. void makeDirectCall(const std::vector& operands, Expression* c, - const TableInfo& table, + const TableUtils::TableInfo& table, CallIndirect* original) { auto info = getTargetInfo(c, table, original); if (std::get_if(&info)) { @@ -211,84 +189,17 @@ struct Directize : public Pass { auto initialContentsImmutable = hasArgument("directize-initial-contents-immutable"); - // Set up the initial info. - TableInfoMap tables; - for (auto& table : module->tables) { - tables[table->name].initialContentsImmutable = initialContentsImmutable; - tables[table->name].flatTable = - std::make_unique(*module, *table); - } - - // Next, look at the imports and exports. - - for (auto& table : module->tables) { - if (table->imported()) { - tables[table->name].mayBeModified = true; - } - } - - for (auto& ex : module->exports) { - if (ex->kind == ExternalKind::Table) { - tables[*ex->getInternalName()].mayBeModified = true; - } - } - - // This may already be enough information to know that we can't optimize - // anything. If so, skip scanning all the module contents. - auto canOptimize = [&]() { - for (auto& [_, info] : tables) { - if (info.canOptimize()) { - return true; - } - } - return false; - }; - - if (!canOptimize()) { - return; - } - - // Find which tables have sets. + auto tables = TableUtils::computeTableInfo(*module, initialContentsImmutable); - using TablesWithSet = std::unordered_set; - - ModuleUtils::ParallelFunctionAnalysis analysis( - *module, [&](Function* func, TablesWithSet& tablesWithSet) { - if (func->imported()) { - return; - } - - struct Finder : public PostWalker { - TablesWithSet& tablesWithSet; - - Finder(TablesWithSet& tablesWithSet) : tablesWithSet(tablesWithSet) {} - - void visitTableSet(TableSet* curr) { - tablesWithSet.insert(curr->table); - } - void visitTableFill(TableFill* curr) { - tablesWithSet.insert(curr->table); - } - void visitTableCopy(TableCopy* curr) { - tablesWithSet.insert(curr->destTable); - } - void visitTableInit(TableInit* curr) { - tablesWithSet.insert(curr->table); - } - }; - - Finder(tablesWithSet).walkFunction(func); - }); - - for (auto& [_, names] : analysis.map) { - for (auto name : names) { - tables[name].mayBeModified = true; + // Stop if we cannot optimize anything. + auto hasOptimizableTable = false; + for (auto& [_, info] : tables) { + if (info.canOptimize()) { + hasOptimizableTable = true; + break; } } - - // Perhaps the new information about tables with sets shows we cannot - // optimize. - if (!canOptimize()) { + if (!hasOptimizableTable) { return; } From 34d0f8a9dae309a7f9d5924b6134359fbe6c8e73 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 3 Dec 2025 15:48:54 -0800 Subject: [PATCH 02/11] frmt --- src/ir/table-utils.h | 3 ++- src/passes/Directize.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ir/table-utils.h b/src/ir/table-utils.h index 254447441fd..917d317e8b3 100644 --- a/src/ir/table-utils.h +++ b/src/ir/table-utils.h @@ -151,7 +151,8 @@ using TableInfoMap = std::unordered_map; // Compute a map with table optimizability info. We can be told that the initial // contents of the tables are immutable (that is, existing data is not // overwritten, but new things may be appended). -TableInfoMap computeTableInfo(Module& wasm, bool initialContentsImmutable=false); +TableInfoMap computeTableInfo(Module& wasm, + bool initialContentsImmutable = false); } // namespace wasm::TableUtils diff --git a/src/passes/Directize.cpp b/src/passes/Directize.cpp index a52b2b89430..b7a078adf0c 100644 --- a/src/passes/Directize.cpp +++ b/src/passes/Directize.cpp @@ -189,7 +189,8 @@ struct Directize : public Pass { auto initialContentsImmutable = hasArgument("directize-initial-contents-immutable"); - auto tables = TableUtils::computeTableInfo(*module, initialContentsImmutable); + auto tables = + TableUtils::computeTableInfo(*module, initialContentsImmutable); // Stop if we cannot optimize anything. auto hasOptimizableTable = false; From c9189a11483ccc31c7c2e161d7d1468fe03398e3 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 3 Dec 2025 15:53:56 -0800 Subject: [PATCH 03/11] move rume.ci.ref --- src/passes/RemoveUnusedModuleElements.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 9432af2ddc0..c38b923a72f 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -172,11 +172,6 @@ struct Noter : public PostWalker> { // the heap type we call with. reference({ModuleElementKind::Table, curr->table}); noteIndirectCall(curr->table, curr->heapType); - // Note a possible call of a function reference as well, as something might - // be written into the table during runtime. With precise tracking of what - // is written into the table we could do better here; we could also see - // which tables are immutable. TODO - noteCallRef(curr->heapType); } void visitCallRef(CallRef* curr) { @@ -434,6 +429,12 @@ struct Analyzer { reference({ModuleElementKind::ElementSegment, elemInfo.name}); } } + + // Note a possible call of a function reference as well, as something might + // be written into the table during runtime. With precise tracking of what + // is written into the table we could do better here; we could also see + // which tables are immutable. TODO + useCallRefType(type); } void useRefFunc(Name func) { From 3f15228649fb7b8d849654f7b65ab4eca234e991 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 3 Dec 2025 16:17:31 -0800 Subject: [PATCH 04/11] start opt in rume --- src/passes/RemoveUnusedModuleElements.cpp | 17 ++- .../remove-unused-module-elements-tables.wast | 107 ++++++++++++++++++ 2 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 test/lit/passes/remove-unused-module-elements-tables.wast diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index c38b923a72f..eaa2a572796 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -45,6 +45,7 @@ #include "ir/module-utils.h" #include "ir/struct-utils.h" #include "ir/subtypes.h" +#include "ir/table-utils.h" #include "ir/utils.h" #include "pass.h" #include "support/insert_ordered.h" @@ -408,6 +409,8 @@ struct Analyzer { std::unordered_set usedIndirectCalls; + std::optional tableInfoMap; + void useIndirectCall(IndirectCall call) { auto [_, inserted] = usedIndirectCalls.insert(call); if (!inserted) { @@ -430,11 +433,15 @@ struct Analyzer { } } - // Note a possible call of a function reference as well, as something might - // be written into the table during runtime. With precise tracking of what - // is written into the table we could do better here; we could also see - // which tables are immutable. TODO - useCallRefType(type); + // Note a possible call of a function reference as well, if something else + // might be written into the table during runtime. + // TODO: Add an option for immutable initial content like Directize? + if (!tableInfoMap) { + tableInfoMap = TableUtils::computeTableInfo(*module); + } + if ((*tableInfoMap)[table].mayBeModified) { + useCallRefType(type); + } } void useRefFunc(Name func) { diff --git a/test/lit/passes/remove-unused-module-elements-tables.wast b/test/lit/passes/remove-unused-module-elements-tables.wast new file mode 100644 index 00000000000..4b72299ea81 --- /dev/null +++ b/test/lit/passes/remove-unused-module-elements-tables.wast @@ -0,0 +1,107 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: foreach %s %t wasm-opt --remove-unused-module-elements --closed-world -all -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt --remove-unused-module-elements -all -S -o - | filecheck %s --check-prefix OPEN_WORLD + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $foo (func)) + ;; OPEN_WORLD: (rec + ;; OPEN_WORLD-NEXT: (type $foo (func)) + (type $foo (func)) + ;; CHECK: (type $bar (func)) + ;; OPEN_WORLD: (type $bar (func)) + (type $bar (func)) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (table $table 10 funcref) + ;; OPEN_WORLD: (type $2 (func)) + + ;; OPEN_WORLD: (table $table 10 funcref) + (table $table 10 funcref) + ;; CHECK: (elem $table (i32.const 0) $foo-in-table $bar) + ;; OPEN_WORLD: (elem $table (i32.const 0) $foo-in-table $bar) + (elem $table (i32.const 0) $foo-in-table $bar) + + ;; CHECK: (elem declare func $foo-not-in-table) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (func $export (type $2) + ;; CHECK-NEXT: (call_indirect $table (type $foo) + ;; CHECK-NEXT: (i32.const 5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $foo-not-in-table) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (elem declare func $foo-not-in-table) + + ;; OPEN_WORLD: (export "export" (func $export)) + + ;; OPEN_WORLD: (func $export (type $2) + ;; OPEN_WORLD-NEXT: (call_indirect $table (type $foo) + ;; OPEN_WORLD-NEXT: (i32.const 5) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (ref.func $foo-not-in-table) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $export (export "export") + ;; Call type $foo and nothing else. + (call_indirect $table (type $foo) + (i32.const 5) + ) + ;; Refer to $foo-not-in-table. + (drop + (ref.func $foo-not-in-table) + ) + ) + + ;; CHECK: (func $foo-in-table (type $foo) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $foo-in-table (type $foo) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (i32.const 10) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $foo-in-table (type $foo) + ;; This should not change: type $foo is called, and this is in the table. + (drop (i32.const 10)) + ) + + ;; CHECK: (func $foo-not-in-table (type $foo) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $foo-not-in-table (type $foo) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (i32.const 20) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $foo-not-in-table (type $foo) + ;; This can be made unreachable: its type is called, but it is not in the + ;; table, and we can see the table's contents: no table.set etc. can put us + ;; there, and it is not imported/exported. That we are referred to is not + ;; enough to keep our contents alive, at least not in closed world (in open + ;; world, our reference might escape and be called outside). + (drop (i32.const 20)) + ) + + ;; CHECK: (func $bar (type $bar) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $bar (type $bar) + ;; OPEN_WORLD-NEXT: (unreachable) + ;; OPEN_WORLD-NEXT: ) + (func $bar (type $bar) + ;; This can be made unreachable: its type is not even called, even though it + ;; is in the table. + (drop (i32.const 30)) + ) +) From 95eb6516de0b89825ca68d96371f830a512b1a5e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 3 Dec 2025 16:22:40 -0800 Subject: [PATCH 05/11] test --- .../remove-unused-module-elements-tables.wast | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/test/lit/passes/remove-unused-module-elements-tables.wast b/test/lit/passes/remove-unused-module-elements-tables.wast index 4b72299ea81..a95cf5da903 100644 --- a/test/lit/passes/remove-unused-module-elements-tables.wast +++ b/test/lit/passes/remove-unused-module-elements-tables.wast @@ -105,3 +105,111 @@ (drop (i32.const 30)) ) ) + +;; As above, but now the table is exported. It might be written to from the +;; outside, preventing some opts. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $foo (func)) + ;; OPEN_WORLD: (rec + ;; OPEN_WORLD-NEXT: (type $foo (func)) + (type $foo (func)) + ;; CHECK: (type $bar (func)) + ;; OPEN_WORLD: (type $bar (func)) + (type $bar (func)) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (table $table 10 funcref) + ;; OPEN_WORLD: (type $2 (func)) + + ;; OPEN_WORLD: (table $table 10 funcref) + (table $table 10 funcref) + ;; CHECK: (elem $table (i32.const 0) $foo-in-table $bar) + ;; OPEN_WORLD: (elem $table (i32.const 0) $foo-in-table $bar) + (elem $table (i32.const 0) $foo-in-table $bar) + + ;; CHECK: (elem declare func $foo-not-in-table) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (export "table" (table $table)) + ;; OPEN_WORLD: (elem declare func $foo-not-in-table) + + ;; OPEN_WORLD: (export "export" (func $export)) + + ;; OPEN_WORLD: (export "table" (table $table)) + (export "table" (table $table)) ;; this was added + + ;; CHECK: (func $export (type $2) + ;; CHECK-NEXT: (call_indirect $table (type $foo) + ;; CHECK-NEXT: (i32.const 5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $foo-not-in-table) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $export (type $2) + ;; OPEN_WORLD-NEXT: (call_indirect $table (type $foo) + ;; OPEN_WORLD-NEXT: (i32.const 5) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (ref.func $foo-not-in-table) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $export (export "export") + (call_indirect $table (type $foo) + (i32.const 5) + ) + (drop + (ref.func $foo-not-in-table) + ) + ) + + ;; CHECK: (func $foo-in-table (type $foo) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $foo-in-table (type $foo) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (i32.const 10) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $foo-in-table (type $foo) + (drop (i32.const 10)) + ) + + ;; CHECK: (func $foo-not-in-table (type $foo) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $foo-not-in-table (type $foo) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (i32.const 20) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $foo-not-in-table (type $foo) + ;; Optimization changes here: the table is public, so we must assume this + ;; could be in the table, written there from outside. Even in closed world, + ;; we change nothing here. + (drop (i32.const 20)) + ) + + ;; CHECK: (func $bar (type $bar) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $bar (type $bar) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (i32.const 30) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $bar (type $bar) + ;; This changes too: In open world, we cannot assume this is not called, and + ;; leave it alone. + (drop (i32.const 30)) + ) +) From 35b6d433f8efb60a10704da481779401db4659fc Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 3 Dec 2025 16:25:40 -0800 Subject: [PATCH 06/11] test --- .../remove-unused-module-elements-tables.wast | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/test/lit/passes/remove-unused-module-elements-tables.wast b/test/lit/passes/remove-unused-module-elements-tables.wast index a95cf5da903..4f3f151c626 100644 --- a/test/lit/passes/remove-unused-module-elements-tables.wast +++ b/test/lit/passes/remove-unused-module-elements-tables.wast @@ -53,6 +53,7 @@ (func $export (export "export") ;; Call type $foo and nothing else. (call_indirect $table (type $foo) + ;; TODO: we could track indexes in the table. (i32.const 5) ) ;; Refer to $foo-not-in-table. @@ -179,6 +180,7 @@ ;; OPEN_WORLD-NEXT: ) ;; OPEN_WORLD-NEXT: ) (func $foo-in-table (type $foo) + ;; As above. (drop (i32.const 10)) ) @@ -213,3 +215,108 @@ (drop (i32.const 30)) ) ) + +;; As above, but now the table has a table.set. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $foo (func)) + ;; OPEN_WORLD: (rec + ;; OPEN_WORLD-NEXT: (type $foo (func)) + (type $foo (func)) + ;; CHECK: (type $bar (func)) + ;; OPEN_WORLD: (type $bar (func)) + (type $bar (func)) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (table $table 10 funcref) + ;; OPEN_WORLD: (type $2 (func)) + + ;; OPEN_WORLD: (table $table 10 funcref) + (table $table 10 funcref) + ;; CHECK: (elem $table (i32.const 0) $foo-in-table $bar) + ;; OPEN_WORLD: (elem $table (i32.const 0) $foo-in-table $bar) + (elem $table (i32.const 0) $foo-in-table $bar) + + ;; CHECK: (elem declare func $foo-not-in-table) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (func $export (type $2) + ;; CHECK-NEXT: (call_indirect $table (type $foo) + ;; CHECK-NEXT: (i32.const 5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $table + ;; CHECK-NEXT: (i32.const 7) + ;; CHECK-NEXT: (ref.func $foo-not-in-table) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (elem declare func $foo-not-in-table) + + ;; OPEN_WORLD: (export "export" (func $export)) + + ;; OPEN_WORLD: (func $export (type $2) + ;; OPEN_WORLD-NEXT: (call_indirect $table (type $foo) + ;; OPEN_WORLD-NEXT: (i32.const 5) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (table.set $table + ;; OPEN_WORLD-NEXT: (i32.const 7) + ;; OPEN_WORLD-NEXT: (ref.func $foo-not-in-table) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $export (export "export") + (call_indirect $table (type $foo) + (i32.const 5) + ) + (table.set $table + (i32.const 7) + (ref.func $foo-not-in-table) + ) + ) + + ;; CHECK: (func $foo-in-table (type $foo) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $foo-in-table (type $foo) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (i32.const 10) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $foo-in-table (type $foo) + ;; As above. + (drop (i32.const 10)) + ) + + ;; CHECK: (func $foo-not-in-table (type $foo) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $foo-not-in-table (type $foo) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (i32.const 20) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $foo-not-in-table (type $foo) + ;; The reference taken of this function might be table.set'ed, so we can do + ;; nothing here. + (drop (i32.const 20)) + ) + + ;; CHECK: (func $bar (type $bar) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $bar (type $bar) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (i32.const 30) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $bar (type $bar) + ;; As above. + (drop (i32.const 30)) + ) +) From d2f9a8c4a646098b0493f6fe57c2c56d8558c58d Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 16 Dec 2025 10:25:42 -0800 Subject: [PATCH 07/11] move comment --- src/ir/table-utils.h | 7 +++++-- src/passes/Directize.cpp | 7 ++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ir/table-utils.h b/src/ir/table-utils.h index 917d317e8b3..e6cc7754ee5 100644 --- a/src/ir/table-utils.h +++ b/src/ir/table-utils.h @@ -126,8 +126,11 @@ struct TableInfo { // or exported, or table.set operations exist for it in the code. bool mayBeModified = false; - // Whether we can assume that the initial contents are immutable. See the - // toplevel comment. + // Whether we can assume that the initial contents are immutable. That is, if + // a table looks like [a, b, c] in the wasm, and we see a call to index 1, we + // will assume it must call b. It is possible that the table is appended to, + // but in this mode we assume the initial contents are not overwritten. This + // is the case for output from LLVM, for example. bool initialContentsImmutable = false; std::unique_ptr flatTable; diff --git a/src/passes/Directize.cpp b/src/passes/Directize.cpp index b7a078adf0c..5d548d30730 100644 --- a/src/passes/Directize.cpp +++ b/src/passes/Directize.cpp @@ -23,11 +23,8 @@ // // --pass-arg=directize-initial-contents-immutable // -// then the initial tables' contents are assumed to be immutable. That is, if -// a table looks like [a, b, c] in the wasm, and we see a call to index 1, we -// will assume it must call b. It is possible that the table is appended to, but -// in this mode we assume the initial contents are not overwritten. This is the -// case for output from LLVM, for example. +// then the initial tables' contents are assumed to be immutable (see +// TableUtils::TableInfo). // #include From 774ea7d7941b37e52eabadd3466570b9eddac8e8 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 16 Dec 2025 10:27:34 -0800 Subject: [PATCH 08/11] clarify --- src/ir/table-utils.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ir/table-utils.h b/src/ir/table-utils.h index e6cc7754ee5..a939880e760 100644 --- a/src/ir/table-utils.h +++ b/src/ir/table-utils.h @@ -131,6 +131,11 @@ struct TableInfo { // will assume it must call b. It is possible that the table is appended to, // but in this mode we assume the initial contents are not overwritten. This // is the case for output from LLVM, for example. + // + // This is a weaker property than mayBeModified (if the table cannot be + // modified at all, we can definitely assume the initial contents we see are + // not mutated), but is useful in the case that things are appended to the + // table (as e.g. dynamic linking does in Emscripten). bool initialContentsImmutable = false; std::unique_ptr flatTable; From 5e884b29332b71ee0f50bed0b452baa8ad322f74 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 16 Dec 2025 10:32:16 -0800 Subject: [PATCH 09/11] clarify relations between terms --- src/ir/table-utils.h | 21 +++++++++++++-------- src/passes/Directize.cpp | 4 ++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/ir/table-utils.h b/src/ir/table-utils.h index a939880e760..cee88fcdbc7 100644 --- a/src/ir/table-utils.h +++ b/src/ir/table-utils.h @@ -135,20 +135,25 @@ struct TableInfo { // This is a weaker property than mayBeModified (if the table cannot be // modified at all, we can definitely assume the initial contents we see are // not mutated), but is useful in the case that things are appended to the - // table (as e.g. dynamic linking does in Emscripten). + // table (as e.g. dynamic linking does in Emscripten, which passes in a flag + // to set this mode; in general, this is an invariant about the program that + // we must be informed about, not one that we can infer - there can be + // table.sets, for example, and this property implies that those sets never + // overwrite initial data). bool initialContentsImmutable = false; std::unique_ptr flatTable; - // Whether we can optimize using this table's data, that is, we know something - // useful about the data there at compile time. The specifics about what we - // know are in the above fields. - bool canOptimize() const { - // We can optimize if: + // Whether we can optimize using this table's data on the entry level, that + // is, individual entries in the table are known to us, so calls through the + // table with known indexes can be inferred, etc. + bool canOptimizeByEntry() const { + // To infer entries, we require: // * Either the table can't be modified at all, or it can be modified but // the initial contents are immutable (so we can optimize those - // contents, even if other things might be appended later). - // * The table is flat (so we can see what is in it). + // contents, even if other things might be appended later, which we + // cannot infer). + // * The table is flat (so we can see what is in it, by index). return (!mayBeModified || initialContentsImmutable) && flatTable->valid; } }; diff --git a/src/passes/Directize.cpp b/src/passes/Directize.cpp index 5d548d30730..38f5df3e34f 100644 --- a/src/passes/Directize.cpp +++ b/src/passes/Directize.cpp @@ -54,7 +54,7 @@ struct FunctionDirectizer : public WalkerPass> { void visitCallIndirect(CallIndirect* curr) { auto& table = tables.at(curr->table); - if (!table.canOptimize()) { + if (!table.canOptimizeByEntry()) { return; } // If the target is constant, we can emit a direct call. @@ -192,7 +192,7 @@ struct Directize : public Pass { // Stop if we cannot optimize anything. auto hasOptimizableTable = false; for (auto& [_, info] : tables) { - if (info.canOptimize()) { + if (info.canOptimizeByEntry()) { hasOptimizableTable = true; break; } From 34bffd308b5f39fc83c1df9c8ef4f7eb01ae89e9 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 16 Dec 2025 13:35:26 -0800 Subject: [PATCH 10/11] unindent --- src/ir/table-utils.cpp | 66 ++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/ir/table-utils.cpp b/src/ir/table-utils.cpp index f6c0d114e6b..364f7ba324a 100644 --- a/src/ir/table-utils.cpp +++ b/src/ir/table-utils.cpp @@ -114,41 +114,43 @@ TableInfoMap computeTableInfo(Module& wasm, bool initialContentsImmutable) { break; } } - if (hasUnmodifiableTable) { - using TablesWithSet = std::unordered_set; + if (!hasUnmodifiableTable) { + return tables; + } - ModuleUtils::ParallelFunctionAnalysis analysis( - wasm, [&](Function* func, TablesWithSet& tablesWithSet) { - if (func->imported()) { - return; - } + using TablesWithSet = std::unordered_set; - struct Finder : public PostWalker { - TablesWithSet& tablesWithSet; - - Finder(TablesWithSet& tablesWithSet) : tablesWithSet(tablesWithSet) {} - - void visitTableSet(TableSet* curr) { - tablesWithSet.insert(curr->table); - } - void visitTableFill(TableFill* curr) { - tablesWithSet.insert(curr->table); - } - void visitTableCopy(TableCopy* curr) { - tablesWithSet.insert(curr->destTable); - } - void visitTableInit(TableInit* curr) { - tablesWithSet.insert(curr->table); - } - }; - - Finder(tablesWithSet).walkFunction(func); - }); - - for (auto& [_, names] : analysis.map) { - for (auto name : names) { - tables[name].mayBeModified = true; + ModuleUtils::ParallelFunctionAnalysis analysis( + wasm, [&](Function* func, TablesWithSet& tablesWithSet) { + if (func->imported()) { + return; } + + struct Finder : public PostWalker { + TablesWithSet& tablesWithSet; + + Finder(TablesWithSet& tablesWithSet) : tablesWithSet(tablesWithSet) {} + + void visitTableSet(TableSet* curr) { + tablesWithSet.insert(curr->table); + } + void visitTableFill(TableFill* curr) { + tablesWithSet.insert(curr->table); + } + void visitTableCopy(TableCopy* curr) { + tablesWithSet.insert(curr->destTable); + } + void visitTableInit(TableInit* curr) { + tablesWithSet.insert(curr->table); + } + }; + + Finder(tablesWithSet).walkFunction(func); + }); + + for (auto& [_, names] : analysis.map) { + for (auto name : names) { + tables[name].mayBeModified = true; } } From f06c16fb3a38532c24313e38ace52f78ef48746b Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 16 Dec 2025 13:54:17 -0800 Subject: [PATCH 11/11] separate ref from set --- .../passes/remove-unused-module-elements-tables.wast | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/lit/passes/remove-unused-module-elements-tables.wast b/test/lit/passes/remove-unused-module-elements-tables.wast index 4f3f151c626..710be00b7b3 100644 --- a/test/lit/passes/remove-unused-module-elements-tables.wast +++ b/test/lit/passes/remove-unused-module-elements-tables.wast @@ -250,6 +250,9 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (table.set $table ;; CHECK-NEXT: (i32.const 7) + ;; CHECK-NEXT: (ref.null nofunc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (ref.func $foo-not-in-table) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -263,6 +266,9 @@ ;; OPEN_WORLD-NEXT: ) ;; OPEN_WORLD-NEXT: (table.set $table ;; OPEN_WORLD-NEXT: (i32.const 7) + ;; OPEN_WORLD-NEXT: (ref.null nofunc) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (drop ;; OPEN_WORLD-NEXT: (ref.func $foo-not-in-table) ;; OPEN_WORLD-NEXT: ) ;; OPEN_WORLD-NEXT: ) @@ -272,6 +278,12 @@ ) (table.set $table (i32.const 7) + (ref.null func) + ) + ;; Take the reference of $foo-not-in-table, so that it is referred to but + ;; not in the table. The table.set will make our analysis believe it might + ;; be there (we do not track the flow of values precisely). + (drop (ref.func $foo-not-in-table) ) )