diff --git a/src/ir/table-utils.cpp b/src/ir/table-utils.cpp index 124eb2aa3e3..364f7ba324a 100644 --- a/src/ir/table-utils.cpp +++ b/src/ir/table-utils.cpp @@ -79,4 +79,82 @@ 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) { + return tables; + } + + 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..cee88fcdbc7 100644 --- a/src/ir/table-utils.h +++ b/src/ir/table-utils.h @@ -120,6 +120,53 @@ 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. 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. + // + // 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, 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 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, which we + // cannot infer). + // * The table is flat (so we can see what is in it, by index). + 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..38f5df3e34f 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 @@ -46,28 +43,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,11 +50,11 @@ 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); - if (!table.canOptimize()) { + if (!table.canOptimizeByEntry()) { return; } // If the target is constant, we can emit a direct call. @@ -114,7 +89,7 @@ struct FunctionDirectizer : public WalkerPass> { } private: - const TableInfoMap& tables; + const TableUtils::TableInfoMap& tables; bool changedTypes = false; @@ -123,7 +98,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 +140,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 +186,18 @@ 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.canOptimizeByEntry()) { + hasOptimizableTable = true; + break; } } - - // Perhaps the new information about tables with sets shows we cannot - // optimize. - if (!canOptimize()) { + if (!hasOptimizableTable) { return; } diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index dad83e62008..07dc1a38caa 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" @@ -172,11 +173,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) { @@ -407,6 +403,8 @@ struct Analyzer { std::unordered_set usedIndirectCalls; + std::optional tableInfoMap; + void useIndirectCall(IndirectCall call) { auto [_, inserted] = usedIndirectCalls.insert(call); if (!inserted) { @@ -422,6 +420,16 @@ struct Analyzer { for (auto& elem : flatTableInfoMap[table].typeElems[type]) { reference({ModuleElementKind::ElementSegment, elem}); } + + // 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..710be00b7b3 --- /dev/null +++ b/test/lit/passes/remove-unused-module-elements-tables.wast @@ -0,0 +1,334 @@ +;; 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) + ;; TODO: we could track indexes in the table. + (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)) + ) +) + +;; 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) + ;; 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) + ;; 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)) + ) +) + +;; 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.null nofunc) + ;; 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: (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: ) + (func $export (export "export") + (call_indirect $table (type $foo) + (i32.const 5) + ) + (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) + ) + ) + + ;; 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)) + ) +)