From 38858ed6fcec789b78198af227f4a4d656206c93 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 6 Jul 2026 14:47:38 -0700 Subject: [PATCH 01/17] fix --- src/passes/RemoveUnusedModuleElements.cpp | 30 +++++++++--- ...ve-unused-module-elements-tables-init.wast | 46 +++++++++++++++++++ 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index f35e750d612..c24972c8846 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -289,7 +289,10 @@ struct Analyzer { // Maps each heap type that is in this table to the items it can call: the // functions, and their segments. This takes into account subtyping, that // is, typeItemMap[foo] includes data for subtypes of foo, so that we just - // need to read one place. + // need to read one place. Bottom types are stored without subtyping, + // however: adding a null to all its supertypes would mean adding it to all + // function types, which is wasteful and adds no information; as a result, + // we need to check for nulls specifically and not rely on subtyping. std::unordered_map> typeFuncs; std::unordered_map> typeElems; @@ -331,6 +334,12 @@ struct Analyzer { } type = type->getSuperType(); } + } else if (item->is()) { + // We must note nulls too. An element segment that writes a null may + // trample a previous value, and if so we cannot remove it. + if (elem) { + info.typeElems[item->type.getHeapType()].insert(elem); + } } }; @@ -461,11 +470,20 @@ struct Analyzer { // if it has the right type, we would not trap. (Note that we were using the // fact that we do not distinguish types of traps, in the case without an // initial value - we turned a trap on the wrong type to one on a null.) - if (!options.trapsNeverHappen && module->getTable(table)->init) { - // We only need to reference the elem, so it writes its functions - the - // functions are not actually called, as we just trap. - for (auto& elem : info.allElems) { - reference({ModuleElementKind::ElementSegment, elem}); + if (!options.trapsNeverHappen) { + if (module->getTable(table)->init) { + // We only need to reference the elem, so it writes its functions - the + // functions are not actually called, as we just trap. + for (auto& elem : info.allElems) { + reference({ModuleElementKind::ElementSegment, elem}); + } + } else { + // A related situation is a table with element segments that write + // nulls: they might overwrite a function, causing a trap, if traps can + // happen, so we must preserve such element segments. + for (auto& elem : info.typeElems[type.getBottom()]) { + reference({ModuleElementKind::ElementSegment, elem}); + } } } diff --git a/test/lit/passes/remove-unused-module-elements-tables-init.wast b/test/lit/passes/remove-unused-module-elements-tables-init.wast index c950603861e..34b4cf5250a 100644 --- a/test/lit/passes/remove-unused-module-elements-tables-init.wast +++ b/test/lit/passes/remove-unused-module-elements-tables-init.wast @@ -125,3 +125,49 @@ ) ) +;; Overwrite the first segment's function with a null. The null must be kept +;; around, so we trap - if traps are possible. As a result, elem $second is +;; removed in TNH, but not otherwise. +(module + ;; CHECK: (type $func (func)) + ;; TNH__: (type $func (func)) + (type $func (func)) + + ;; CHECK: (table $table 6 6 funcref) + ;; TNH__: (table $table 6 6 funcref) + (table $table 6 6 funcref) + + ;; CHECK: (elem $first (i32.const 0) $func) + ;; TNH__: (elem $first (i32.const 0) $func) + (elem $first (i32.const 0) $func) + + ;; CHECK: (elem $second (table $table) (i32.const 0) funcref (item (ref.null nofunc))) + (elem $second (table $table) (i32.const 0) funcref (item (ref.null nofunc))) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: ) + ;; TNH__: (export "export" (func $export)) + + ;; TNH__: (func $func (type $func) + ;; TNH__-NEXT: ) + (func $func (type $func) + ) + + ;; CHECK: (func $export (type $func) + ;; CHECK-NEXT: (call_indirect $table (type $func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; TNH__: (func $export (type $func) + ;; TNH__-NEXT: (call_indirect $table (type $func) + ;; TNH__-NEXT: (i32.const 0) + ;; TNH__-NEXT: ) + ;; TNH__-NEXT: ) + (func $export (export "export") + (call_indirect $table (type $func) + (i32.const 0) + ) + ) +) From 82da827bd0caedae466afaa304aeee2de4f5e06e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 6 Jul 2026 14:50:28 -0700 Subject: [PATCH 02/17] fix --- .../remove-unused-module-elements-tables-init.wast | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/lit/passes/remove-unused-module-elements-tables-init.wast b/test/lit/passes/remove-unused-module-elements-tables-init.wast index 34b4cf5250a..d5e1c2dc5f6 100644 --- a/test/lit/passes/remove-unused-module-elements-tables-init.wast +++ b/test/lit/passes/remove-unused-module-elements-tables-init.wast @@ -127,12 +127,16 @@ ;; Overwrite the first segment's function with a null. The null must be kept ;; around, so we trap - if traps are possible. As a result, elem $second is -;; removed in TNH, but not otherwise. +;; removed in TNH, but not otherwise. elem $third, on the other hand, can be +;; removed in both cases, as it contains no nulls, only a function of another +;; type. (module ;; CHECK: (type $func (func)) ;; TNH__: (type $func (func)) (type $func (func)) + (type $other (func (result i32))) + ;; CHECK: (table $table 6 6 funcref) ;; TNH__: (table $table 6 6 funcref) (table $table 6 6 funcref) @@ -144,6 +148,8 @@ ;; CHECK: (elem $second (table $table) (i32.const 0) funcref (item (ref.null nofunc))) (elem $second (table $table) (i32.const 0) funcref (item (ref.null nofunc))) + (elem $third (i32.const 1) $other) + ;; CHECK: (export "export" (func $export)) ;; CHECK: (func $func (type $func) @@ -155,6 +161,10 @@ (func $func (type $func) ) + (func $other (type $other) (result i32) + (i32.const 42) + ) + ;; CHECK: (func $export (type $func) ;; CHECK-NEXT: (call_indirect $table (type $func) ;; CHECK-NEXT: (i32.const 0) From 41346c2559fd799f7d54a0fbef3776fc74f8b269 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 6 Jul 2026 16:06:55 -0700 Subject: [PATCH 03/17] add comment --- src/passes/RemoveUnusedModuleElements.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index c24972c8846..8700cc066cc 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -481,6 +481,10 @@ struct Analyzer { // A related situation is a table with element segments that write // nulls: they might overwrite a function, causing a trap, if traps can // happen, so we must preserve such element segments. + // + // Note that this is in an else, i.e., we never do it when ->init is + // true, above - that is ok, as if ->init were true, we'd loop on + // allElems, which is a larger group than we loop on here. for (auto& elem : info.typeElems[type.getBottom()]) { reference({ModuleElementKind::ElementSegment, elem}); } From 85d88575128c261fe8902f7c316a28fd9a39764d Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 7 Jul 2026 11:39:05 -0700 Subject: [PATCH 04/17] testcase showing a second issue [ci skip] --- ...ve-unused-module-elements-tables-init.wast | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/lit/passes/remove-unused-module-elements-tables-init.wast b/test/lit/passes/remove-unused-module-elements-tables-init.wast index d5e1c2dc5f6..ca9411a2f78 100644 --- a/test/lit/passes/remove-unused-module-elements-tables-init.wast +++ b/test/lit/passes/remove-unused-module-elements-tables-init.wast @@ -181,3 +181,56 @@ ) ) ) + +;; Similar, but overwrite not with a null, but with a function of the wrong type +;; for the call_indirect. Like with a null, we must write that wrongly-typed +;; function, to preserve the trap, if traps are possible, so when not TNH, we +;; must keep elem $second around. XXX +(module + ;; CHECK: (type $func (func)) + ;; TNH__: (type $func (func)) + (type $func (func)) + + (type $other (func (result i32))) + + ;; CHECK: (table $table 6 6 funcref) + ;; TNH__: (table $table 6 6 funcref) + (table $table 6 6 funcref) + + ;; CHECK: (elem $first (i32.const 0) $func) + ;; TNH__: (elem $first (i32.const 0) $func) + (elem $first (i32.const 0) $func) + + (elem $second (i32.const 0) $other) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: ) + ;; TNH__: (export "export" (func $export)) + + ;; TNH__: (func $func (type $func) + ;; TNH__-NEXT: ) + (func $func (type $func) + ) + + (func $other (type $other) (result i32) + (i32.const 42) + ) + + ;; CHECK: (func $export (type $func) + ;; CHECK-NEXT: (call_indirect $table (type $func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; TNH__: (func $export (type $func) + ;; TNH__-NEXT: (call_indirect $table (type $func) + ;; TNH__-NEXT: (i32.const 0) + ;; TNH__-NEXT: ) + ;; TNH__-NEXT: ) + (func $export (export "export") + (call_indirect $table (type $func) + (i32.const 0) + ) + ) +) From b0d15980d4a5ed1c0048557f8b3cfce926073237 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 7 Jul 2026 12:00:42 -0700 Subject: [PATCH 05/17] wip --- src/passes/RemoveUnusedModuleElements.cpp | 62 +++++++++++++++++------ 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 8700cc066cc..be0d895672e 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -49,6 +49,7 @@ #include "ir/utils.h" #include "pass.h" #include "support/insert_ordered.h" +#include "support/space.h" #include "support/stdckdint.h" #include "support/utilities.h" #include "wasm-builder.h" @@ -289,15 +290,20 @@ struct Analyzer { // Maps each heap type that is in this table to the items it can call: the // functions, and their segments. This takes into account subtyping, that // is, typeItemMap[foo] includes data for subtypes of foo, so that we just - // need to read one place. Bottom types are stored without subtyping, - // however: adding a null to all its supertypes would mean adding it to all - // function types, which is wasteful and adds no information; as a result, - // we need to check for nulls specifically and not rely on subtyping. + // need to read one place. std::unordered_map> typeFuncs; std::unordered_map> typeElems; // All elems, regardless of type. std::unordered_set allElems; + + // Whether this table's elems may overlap, i.e., trample each other. In such + // a case, we can optimize less: if segment A writes a function, and B + // tramples it with a null or with a function of another type, then if we + // call that index with the right type for A, the trampling causes a trap - + // so we cannot remove the trampling segment, even though it has nothing + // can can be called, just to preserve the trap. + bool mayHaveOverlappingElems = false; }; std::unordered_map flatTableInfoMap; @@ -319,6 +325,10 @@ struct Analyzer { } void prepare() { + // The spans of element segments for each table. We use this to compute + // |mayHaveOverlappingElems|. + std::unordered_map tableElemSpans; + auto notePossibleFunc = [&](FlatTableInfo& info, Expression* item, Name elem = Name()) { if (auto* refFunc = item->dynCast()) { @@ -354,6 +364,33 @@ struct Analyzer { info.allElems.insert(elem->name); } + // Compute elem overlap. + for (auto& elem : module->elementSegments) { + if (elem->isPassive()) { + continue; + } + + auto& info = flatTableInfoMap[elem->table]; + // There is at least one elem, this one. + assert(!info.allElems.empty()); + if (info.allElems.size() <= 1) { + // This table has just this one elem, so no overlap is possible, even + // if the single elem has a non-constant offset. + continue; + } + + if (auto* c = elem->offset->dynCast()) { + auto start = c->value.getUnsigned(); + auto end = start + elem->data.size(); + if (tableElemSpans[elem->table].addAndCheckOverlap({start, end})) { + info.mayHaveOverlappingElems = true; + } + } else { + // Offset is not constant, so it might overlap with others. + info.mayHaveOverlappingElems = true; + } + } + // If a table has an initial value, it is callable as well. for (auto& table : module->tables) { if (table->init) { @@ -471,23 +508,16 @@ struct Analyzer { // fact that we do not distinguish types of traps, in the case without an // initial value - we turned a trap on the wrong type to one on a null.) if (!options.trapsNeverHappen) { - if (module->getTable(table)->init) { + // A related situation is a table with element segments that trample + // valid values with nulls or functions of the wrong type for a call: + // when they overwrite a valid function, they cause a trap, which we + // must preserve if traps can. + if (module->getTable(table)->init || info.mayHaveOverlappingElems) { // We only need to reference the elem, so it writes its functions - the // functions are not actually called, as we just trap. for (auto& elem : info.allElems) { reference({ModuleElementKind::ElementSegment, elem}); } - } else { - // A related situation is a table with element segments that write - // nulls: they might overwrite a function, causing a trap, if traps can - // happen, so we must preserve such element segments. - // - // Note that this is in an else, i.e., we never do it when ->init is - // true, above - that is ok, as if ->init were true, we'd loop on - // allElems, which is a larger group than we loop on here. - for (auto& elem : info.typeElems[type.getBottom()]) { - reference({ModuleElementKind::ElementSegment, elem}); - } } } From c0657280535e262f9cc5894292cc076cc35b8e94 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 08:49:12 -0700 Subject: [PATCH 06/17] update.tests --- ...emove-unused-module-elements-tables-init.wast | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/lit/passes/remove-unused-module-elements-tables-init.wast b/test/lit/passes/remove-unused-module-elements-tables-init.wast index ca9411a2f78..ac269ab4c03 100644 --- a/test/lit/passes/remove-unused-module-elements-tables-init.wast +++ b/test/lit/passes/remove-unused-module-elements-tables-init.wast @@ -129,12 +129,15 @@ ;; around, so we trap - if traps are possible. As a result, elem $second is ;; removed in TNH, but not otherwise. elem $third, on the other hand, can be ;; removed in both cases, as it contains no nulls, only a function of another -;; type. +;; type - but we do not actually remove it in CHECK, as given the presence of +;; overlapping segments, we give up on analyzing the precise overlap, and do not +;; remove segments from the table. (module ;; CHECK: (type $func (func)) ;; TNH__: (type $func (func)) (type $func (func)) + ;; CHECK: (type $other (func (result i32))) (type $other (func (result i32))) ;; CHECK: (table $table 6 6 funcref) @@ -148,6 +151,7 @@ ;; CHECK: (elem $second (table $table) (i32.const 0) funcref (item (ref.null nofunc))) (elem $second (table $table) (i32.const 0) funcref (item (ref.null nofunc))) + ;; CHECK: (elem $third (i32.const 1) $other) (elem $third (i32.const 1) $other) ;; CHECK: (export "export" (func $export)) @@ -161,6 +165,9 @@ (func $func (type $func) ) + ;; CHECK: (func $other (type $other) (result i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) (func $other (type $other) (result i32) (i32.const 42) ) @@ -185,12 +192,13 @@ ;; Similar, but overwrite not with a null, but with a function of the wrong type ;; for the call_indirect. Like with a null, we must write that wrongly-typed ;; function, to preserve the trap, if traps are possible, so when not TNH, we -;; must keep elem $second around. XXX +;; must keep elem $second around. (module ;; CHECK: (type $func (func)) ;; TNH__: (type $func (func)) (type $func (func)) + ;; CHECK: (type $other (func (result i32))) (type $other (func (result i32))) ;; CHECK: (table $table 6 6 funcref) @@ -201,6 +209,7 @@ ;; TNH__: (elem $first (i32.const 0) $func) (elem $first (i32.const 0) $func) + ;; CHECK: (elem $second (i32.const 0) $other) (elem $second (i32.const 0) $other) ;; CHECK: (export "export" (func $export)) @@ -214,6 +223,9 @@ (func $func (type $func) ) + ;; CHECK: (func $other (type $other) (result i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) (func $other (type $other) (result i32) (i32.const 42) ) From d7667f9653e6144fc0faf34cd74d4a692126f7f9 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 08:50:39 -0700 Subject: [PATCH 07/17] comment --- src/passes/RemoveUnusedModuleElements.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index be0d895672e..5dd3fc56d58 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -303,6 +303,11 @@ struct Analyzer { // call that index with the right type for A, the trampling causes a trap - // so we cannot remove the trampling segment, even though it has nothing // can can be called, just to preserve the trap. + // + // We could be more precise here, as currently we "give up" on analyzing + // exact overlap, and just stop removing element segments. But such an exact + // analysis would be complex, and is rarely needed: when trapsNeverHappen + // this is not an issue, and also segment overlap is rare. bool mayHaveOverlappingElems = false; }; std::unordered_map flatTableInfoMap; From 89992f27877319bd4f35bb9a17fb3d302f3cc0c2 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 08:51:15 -0700 Subject: [PATCH 08/17] comment --- src/passes/RemoveUnusedModuleElements.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 5dd3fc56d58..79ba9a1ecbf 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -302,7 +302,7 @@ struct Analyzer { // tramples it with a null or with a function of another type, then if we // call that index with the right type for A, the trampling causes a trap - // so we cannot remove the trampling segment, even though it has nothing - // can can be called, just to preserve the trap. + // can can be called, forcing us to keep it just to preserve the trap. // // We could be more precise here, as currently we "give up" on analyzing // exact overlap, and just stop removing element segments. But such an exact From f831abf9d5bd47b99cd3a990da6c4e0d62145f01 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 08:52:04 -0700 Subject: [PATCH 09/17] simpl --- src/passes/RemoveUnusedModuleElements.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 79ba9a1ecbf..7dd8e16d3ed 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -349,12 +349,6 @@ struct Analyzer { } type = type->getSuperType(); } - } else if (item->is()) { - // We must note nulls too. An element segment that writes a null may - // trample a previous value, and if so we cannot remove it. - if (elem) { - info.typeElems[item->type.getHeapType()].insert(elem); - } } }; From a10b3d408e691adc868211dec205330a503e3c86 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 08:58:30 -0700 Subject: [PATCH 10/17] clean --- src/passes/RemoveUnusedModuleElements.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 7dd8e16d3ed..06eb569d197 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -370,6 +370,7 @@ struct Analyzer { } auto& info = flatTableInfoMap[elem->table]; + // There is at least one elem, this one. assert(!info.allElems.empty()); if (info.allElems.size() <= 1) { From 18c69b4f0debc26a3e0985b48eb49ce7a8b032c6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 10:47:48 -0700 Subject: [PATCH 11/17] test --- ...ove-unused-module-elements-closed-tnh.wast | 69 +++++++++++++++++++ ...ve-unused-module-elements-tables-init.wast | 2 + 2 files changed, 71 insertions(+) create mode 100644 test/lit/passes/remove-unused-module-elements-closed-tnh.wast diff --git a/test/lit/passes/remove-unused-module-elements-closed-tnh.wast b/test/lit/passes/remove-unused-module-elements-closed-tnh.wast new file mode 100644 index 00000000000..8332e62835e --- /dev/null +++ b/test/lit/passes/remove-unused-module-elements-closed-tnh.wast @@ -0,0 +1,69 @@ +;; 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 -tnh -all -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt --remove-unused-module-elements -all -S -o - | filecheck %s --check-prefix OPEN_WORLD + +;; A table with a single elem with non-constant offset that we can remove. +;; Usually a non-constant segment suggests it might overlap with others, which +;; causes us to keep all elems, but here there is just one, so we can optimize. +;; We require closed world (to know about which calls can reach it - the only +;; call is to type $func, so elem $elem which contains $other is not needed). +;; We also require traps-never-happen, as an elem with nonconstnat offset can +;; trap, normally. When both closed world and tnh, we remove elem $elem. +(module + ;; TNH__: (type $func (func)) + ;; CHECK: (type $func (func)) + ;; OPEN_WORLD: (type $func (func)) + (type $func (func)) + + ;; OPEN_WORLD: (type $other (func (result i32))) + (type $other (func (result i32))) + + ;; OPEN_WORLD: (import "a" "b" (global $offset i32)) + (import "a" "b" (global $offset i32)) + + ;; TNH__: (table $table 6 6 funcref) + ;; CHECK: (table $table 6 6 funcref) + ;; OPEN_WORLD: (table $table 6 6 funcref) + (table $table 6 6 funcref) + + ;; OPEN_WORLD: (elem $elem (global.get $offset) $other) + (elem $elem (global.get $offset) $other) + + ;; OPEN_WORLD: (export "export" (func $export)) + + ;; OPEN_WORLD: (func $other (type $other) (result i32) + ;; OPEN_WORLD-NEXT: (i32.const 42) + ;; OPEN_WORLD-NEXT: ) + (func $other (type $other) (result i32) + (i32.const 42) + ) + + ;; TNH__: (export "export" (func $export)) + + ;; TNH__: (func $export (type $func) + ;; TNH__-NEXT: (call_indirect $table (type $func) + ;; TNH__-NEXT: (i32.const 0) + ;; TNH__-NEXT: ) + ;; TNH__-NEXT: ) + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (func $export (type $func) + ;; CHECK-NEXT: (call_indirect $table (type $func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $export (type $func) + ;; OPEN_WORLD-NEXT: (call_indirect $table (type $func) + ;; OPEN_WORLD-NEXT: (i32.const 0) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $export (export "export") + ;; Call with type $func, but the table contains $other. + (call_indirect $table (type $func) + (i32.const 0) + ) + ) +) + +;; TODO: two segments, one non-constant, one yes, which forces pessimistic overlap assumption diff --git a/test/lit/passes/remove-unused-module-elements-tables-init.wast b/test/lit/passes/remove-unused-module-elements-tables-init.wast index ac269ab4c03..9144dbaa688 100644 --- a/test/lit/passes/remove-unused-module-elements-tables-init.wast +++ b/test/lit/passes/remove-unused-module-elements-tables-init.wast @@ -246,3 +246,5 @@ ) ) ) + +;; TODO test without overlap, JUST Nudge offsests From 6e6b53212d644110835de78883b57ec8f859f613 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 10:49:46 -0700 Subject: [PATCH 12/17] tes --- ...ve-unused-module-elements-tables-init.wast | 90 ++++++++++++++++--- 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/test/lit/passes/remove-unused-module-elements-tables-init.wast b/test/lit/passes/remove-unused-module-elements-tables-init.wast index 9144dbaa688..c42ddeb469b 100644 --- a/test/lit/passes/remove-unused-module-elements-tables-init.wast +++ b/test/lit/passes/remove-unused-module-elements-tables-init.wast @@ -140,7 +140,11 @@ ;; CHECK: (type $other (func (result i32))) (type $other (func (result i32))) + ;; CHECK: (type $2 (func (param i32))) + ;; CHECK: (table $table 6 6 funcref) + ;; TNH__: (type $1 (func (param i32))) + ;; TNH__: (table $table 6 6 funcref) (table $table 6 6 funcref) @@ -172,19 +176,21 @@ (i32.const 42) ) - ;; CHECK: (func $export (type $func) + ;; CHECK: (func $export (type $2) (param $x i32) ;; CHECK-NEXT: (call_indirect $table (type $func) - ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; TNH__: (func $export (type $func) + ;; TNH__: (func $export (type $1) (param $x i32) ;; TNH__-NEXT: (call_indirect $table (type $func) - ;; TNH__-NEXT: (i32.const 0) + ;; TNH__-NEXT: (local.get $x) ;; TNH__-NEXT: ) ;; TNH__-NEXT: ) - (func $export (export "export") + (func $export (export "export") (param $x i32) + ;; Call with an unknown index, so we do not use any tricks about known + ;; indexes in the table. (call_indirect $table (type $func) - (i32.const 0) + (local.get $x) ) ) ) @@ -201,7 +207,11 @@ ;; CHECK: (type $other (func (result i32))) (type $other (func (result i32))) + ;; CHECK: (type $2 (func (param i32))) + ;; CHECK: (table $table 6 6 funcref) + ;; TNH__: (type $1 (func (param i32))) + ;; TNH__: (table $table 6 6 funcref) (table $table 6 6 funcref) @@ -230,21 +240,75 @@ (i32.const 42) ) - ;; CHECK: (func $export (type $func) + ;; CHECK: (func $export (type $2) (param $x i32) + ;; CHECK-NEXT: (call_indirect $table (type $func) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; TNH__: (func $export (type $1) (param $x i32) + ;; TNH__-NEXT: (call_indirect $table (type $func) + ;; TNH__-NEXT: (local.get $x) + ;; TNH__-NEXT: ) + ;; TNH__-NEXT: ) + (func $export (export "export") (param $x i32) + (call_indirect $table (type $func) + (local.get $x) + ) + ) +) + +;; As the last testcase, but now there is no segment overlap, so it is safe to +;; always remove elem $second. +(module + ;; CHECK: (type $func (func)) + ;; TNH__: (type $func (func)) + (type $func (func)) + + (type $other (func (result i32))) + + ;; CHECK: (type $1 (func (param i32))) + + ;; CHECK: (table $table 6 6 funcref) + ;; TNH__: (type $1 (func (param i32))) + + ;; TNH__: (table $table 6 6 funcref) + (table $table 6 6 funcref) + + ;; CHECK: (elem $first (i32.const 0) $func) + ;; TNH__: (elem $first (i32.const 0) $func) + (elem $first (i32.const 0) $func) + + (elem $second (i32.const 1) $other) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: ) + ;; TNH__: (export "export" (func $export)) + + ;; TNH__: (func $func (type $func) + ;; TNH__-NEXT: ) + (func $func (type $func) + ) + + (func $other (type $other) (result i32) + (i32.const 42) + ) + + ;; CHECK: (func $export (type $1) (param $x i32) ;; CHECK-NEXT: (call_indirect $table (type $func) - ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; TNH__: (func $export (type $func) + ;; TNH__: (func $export (type $1) (param $x i32) ;; TNH__-NEXT: (call_indirect $table (type $func) - ;; TNH__-NEXT: (i32.const 0) + ;; TNH__-NEXT: (local.get $x) ;; TNH__-NEXT: ) ;; TNH__-NEXT: ) - (func $export (export "export") + (func $export (export "export") (param $x i32) (call_indirect $table (type $func) - (i32.const 0) + (local.get $x) ) ) ) -;; TODO test without overlap, JUST Nudge offsests From 75a1b26b3da7261576570baaaeedd8dd6b2159a0 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 10:50:09 -0700 Subject: [PATCH 13/17] test --- test/lit/passes/remove-unused-module-elements-tables-init.wast | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lit/passes/remove-unused-module-elements-tables-init.wast b/test/lit/passes/remove-unused-module-elements-tables-init.wast index c42ddeb469b..c6e7f6685d1 100644 --- a/test/lit/passes/remove-unused-module-elements-tables-init.wast +++ b/test/lit/passes/remove-unused-module-elements-tables-init.wast @@ -278,7 +278,7 @@ ;; TNH__: (elem $first (i32.const 0) $func) (elem $first (i32.const 0) $func) - (elem $second (i32.const 1) $other) + (elem $second (i32.const 1) $other) ;; the offset here changed from 0 to 1 ;; CHECK: (export "export" (func $export)) From b8f99e85b557d6e63068d3a67d26ebd9023418e2 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 10:55:20 -0700 Subject: [PATCH 14/17] test --- ...ove-unused-module-elements-closed-tnh.wast | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/test/lit/passes/remove-unused-module-elements-closed-tnh.wast b/test/lit/passes/remove-unused-module-elements-closed-tnh.wast index 8332e62835e..63bae541c7b 100644 --- a/test/lit/passes/remove-unused-module-elements-closed-tnh.wast +++ b/test/lit/passes/remove-unused-module-elements-closed-tnh.wast @@ -66,4 +66,66 @@ ) ) -;; TODO: two segments, one non-constant, one yes, which forces pessimistic overlap assumption +;; As above, but now we have two segments, one constant. We don't know if they +;; overlap, so we normally assume the worst. However, with tnh, we can assume no +;; traps, and remove elem $elem. (We cannot remove elem $second in either case, +;; as it contains a function we have an indirect call for its type.) +(module + ;; TNH__: (type $func (func)) + ;; CHECK: (type $func (func)) + ;; OPEN_WORLD: (type $func (func)) + (type $func (func)) + + ;; OPEN_WORLD: (type $other (func (result i32))) + (type $other (func (result i32))) + + ;; OPEN_WORLD: (import "a" "b" (global $offset i32)) + (import "a" "b" (global $offset i32)) + + ;; TNH__: (table $table 6 6 funcref) + ;; CHECK: (table $table 6 6 funcref) + ;; OPEN_WORLD: (table $table 6 6 funcref) + (table $table 6 6 funcref) + + ;; OPEN_WORLD: (elem $elem (global.get $offset) $other) + (elem $elem (global.get $offset) $other) + + ;; CHECK: (elem $second (i32.const 0) $export) + ;; OPEN_WORLD: (elem $second (i32.const 0) $export) + (elem $second (i32.const 0) $export) + + ;; OPEN_WORLD: (export "export" (func $export)) + + ;; OPEN_WORLD: (func $other (type $other) (result i32) + ;; OPEN_WORLD-NEXT: (i32.const 42) + ;; OPEN_WORLD-NEXT: ) + (func $other (type $other) (result i32) + (i32.const 42) + ) + + ;; TNH__: (export "export" (func $export)) + + ;; TNH__: (func $export (type $func) + ;; TNH__-NEXT: (call_indirect $table (type $func) + ;; TNH__-NEXT: (i32.const 0) + ;; TNH__-NEXT: ) + ;; TNH__-NEXT: ) + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (func $export (type $func) + ;; CHECK-NEXT: (call_indirect $table (type $func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $export (type $func) + ;; OPEN_WORLD-NEXT: (call_indirect $table (type $func) + ;; OPEN_WORLD-NEXT: (i32.const 0) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $export (export "export") + (call_indirect $table (type $func) + (i32.const 0) + ) + ) +) + From 6f2dad172446b64d7b816af82866f4832414cda7 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 10:57:45 -0700 Subject: [PATCH 15/17] typo --- test/lit/passes/remove-unused-module-elements-closed-tnh.wast | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lit/passes/remove-unused-module-elements-closed-tnh.wast b/test/lit/passes/remove-unused-module-elements-closed-tnh.wast index 63bae541c7b..0ccdac9ab71 100644 --- a/test/lit/passes/remove-unused-module-elements-closed-tnh.wast +++ b/test/lit/passes/remove-unused-module-elements-closed-tnh.wast @@ -8,7 +8,7 @@ ;; causes us to keep all elems, but here there is just one, so we can optimize. ;; We require closed world (to know about which calls can reach it - the only ;; call is to type $func, so elem $elem which contains $other is not needed). -;; We also require traps-never-happen, as an elem with nonconstnat offset can +;; We also require traps-never-happen, as an elem with non-constant offset can ;; trap, normally. When both closed world and tnh, we remove elem $elem. (module ;; TNH__: (type $func (func)) From d3104b62c0203e771cb1d56fb1a4b3600449a1c6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 11:00:47 -0700 Subject: [PATCH 16/17] test --- ...ve-unused-module-elements-tables-init.wast | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/test/lit/passes/remove-unused-module-elements-tables-init.wast b/test/lit/passes/remove-unused-module-elements-tables-init.wast index c6e7f6685d1..45f6af14598 100644 --- a/test/lit/passes/remove-unused-module-elements-tables-init.wast +++ b/test/lit/passes/remove-unused-module-elements-tables-init.wast @@ -312,3 +312,59 @@ ) ) +;; As above, but with non-adjacent segment offsets: before we had [0, 1), +;; [1, 2), and now the second befores [2, 3). We can optimize away elem $second +;; as there is no overlap. +(module + ;; CHECK: (type $func (func)) + ;; TNH__: (type $func (func)) + (type $func (func)) + + (type $other (func (result i32))) + + ;; CHECK: (type $1 (func (param i32))) + + ;; CHECK: (table $table 6 6 funcref) + ;; TNH__: (type $1 (func (param i32))) + + ;; TNH__: (table $table 6 6 funcref) + (table $table 6 6 funcref) + + ;; CHECK: (elem $first (i32.const 0) $func) + ;; TNH__: (elem $first (i32.const 0) $func) + (elem $first (i32.const 0) $func) + + (elem $second (i32.const 2) $other) ;; the offset here changed + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: ) + ;; TNH__: (export "export" (func $export)) + + ;; TNH__: (func $func (type $func) + ;; TNH__-NEXT: ) + (func $func (type $func) + ) + + (func $other (type $other) (result i32) + (i32.const 42) + ) + + ;; CHECK: (func $export (type $1) (param $x i32) + ;; CHECK-NEXT: (call_indirect $table (type $func) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; TNH__: (func $export (type $1) (param $x i32) + ;; TNH__-NEXT: (call_indirect $table (type $func) + ;; TNH__-NEXT: (local.get $x) + ;; TNH__-NEXT: ) + ;; TNH__-NEXT: ) + (func $export (export "export") (param $x i32) + (call_indirect $table (type $func) + (local.get $x) + ) + ) +) + From 96a21e018910cedef19f6ed2fa20db39796a9a0e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Jul 2026 11:02:36 -0700 Subject: [PATCH 17/17] test --- ...ve-unused-module-elements-tables-init.wast | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/test/lit/passes/remove-unused-module-elements-tables-init.wast b/test/lit/passes/remove-unused-module-elements-tables-init.wast index 45f6af14598..0caf2200fb8 100644 --- a/test/lit/passes/remove-unused-module-elements-tables-init.wast +++ b/test/lit/passes/remove-unused-module-elements-tables-init.wast @@ -313,7 +313,7 @@ ) ;; As above, but with non-adjacent segment offsets: before we had [0, 1), -;; [1, 2), and now the second befores [2, 3). We can optimize away elem $second +;; [1, 2), and now the second is [2, 3). We can optimize away elem $second ;; as there is no overlap. (module ;; CHECK: (type $func (func)) @@ -368,3 +368,64 @@ ) ) +;; As the last testcase, but the first segment is long enough to cause overlap: +;; we had [0, 1), [2, 3), and now we have [0, 3), [2, 3). We cannot remove +;; elem $second. +(module + ;; CHECK: (type $func (func)) + ;; TNH__: (type $func (func)) + (type $func (func)) + + ;; CHECK: (type $other (func (result i32))) + (type $other (func (result i32))) + + ;; CHECK: (type $2 (func (param i32))) + + ;; CHECK: (table $table 6 6 funcref) + ;; TNH__: (type $1 (func (param i32))) + + ;; TNH__: (table $table 6 6 funcref) + (table $table 6 6 funcref) + + ;; CHECK: (elem $first (i32.const 0) $func $func $func) + ;; TNH__: (elem $first (i32.const 0) $func $func $func) + (elem $first (i32.const 0) $func $func $func) ;; the length here changed + + ;; CHECK: (elem $second (i32.const 2) $other) + (elem $second (i32.const 2) $other) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: ) + ;; TNH__: (export "export" (func $export)) + + ;; TNH__: (func $func (type $func) + ;; TNH__-NEXT: ) + (func $func (type $func) + ) + + ;; CHECK: (func $other (type $other) (result i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $other (type $other) (result i32) + (i32.const 42) + ) + + ;; CHECK: (func $export (type $2) (param $x i32) + ;; CHECK-NEXT: (call_indirect $table (type $func) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; TNH__: (func $export (type $1) (param $x i32) + ;; TNH__-NEXT: (call_indirect $table (type $func) + ;; TNH__-NEXT: (local.get $x) + ;; TNH__-NEXT: ) + ;; TNH__-NEXT: ) + (func $export (export "export") (param $x i32) + (call_indirect $table (type $func) + (local.get $x) + ) + ) +) +