From bf04f7ebcf617cd0cf4e4a26ebcdcd8beef8db08 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 25 Feb 2026 15:37:26 -0800 Subject: [PATCH 1/7] go --- src/passes/Vacuum.cpp | 16 ++++++++++++---- test/lit/passes/vacuum-tnh.wast | 7 ++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/passes/Vacuum.cpp b/src/passes/Vacuum.cpp index 0bcb3a387e6..8e776a70d1d 100644 --- a/src/passes/Vacuum.cpp +++ b/src/passes/Vacuum.cpp @@ -473,10 +473,18 @@ struct Vacuum : public WalkerPass> { } else { ExpressionManipulator::nop(curr->body); } - if (curr->getResults() == Type::none && - !EffectAnalyzer(getPassOptions(), *getModule(), curr) - .hasUnremovableSideEffects()) { - ExpressionManipulator::nop(curr->body); + if (curr->getResults() == Type::none) { + EffectAnalyzer effects(getPassOptions(), *getModule(), curr); + if (!effects.hasUnremovableSideEffects()) { + // We can remove these contents. Emit a nop, or an unreachable if it + // might trap (even in trapsNeverHappen mode, we don't want to turn an + // unreachable into a nop - the unreachable can be propagated onwards). + if (effects.trap) { + ExpressionManipulator::unreachable(curr->body); + } else { + ExpressionManipulator::nop(curr->body); + } + } } } }; diff --git a/test/lit/passes/vacuum-tnh.wast b/test/lit/passes/vacuum-tnh.wast index 8bfcfd95170..da1e75bdd44 100644 --- a/test/lit/passes/vacuum-tnh.wast +++ b/test/lit/passes/vacuum-tnh.wast @@ -19,7 +19,7 @@ (type $struct (struct (field (mut i32)))) ;; YESTNH: (func $drop (type $4) (param $x i32) (param $y anyref) - ;; YESTNH-NEXT: (nop) + ;; YESTNH-NEXT: (unreachable) ;; YESTNH-NEXT: ) ;; NO_TNH: (func $drop (type $4) (param $x i32) (param $y anyref) ;; NO_TNH-NEXT: (drop @@ -180,14 +180,15 @@ ) ;; YESTNH: (func $toplevel (type $0) - ;; YESTNH-NEXT: (nop) + ;; YESTNH-NEXT: (unreachable) ;; YESTNH-NEXT: ) ;; NO_TNH: (func $toplevel (type $0) ;; NO_TNH-NEXT: (unreachable) ;; NO_TNH-NEXT: ) (func $toplevel ;; A removable side effect at the top level of a function. We can turn this - ;; into a nop. + ;; into a nop, but leave it as unreachable even in TNH, so that it can + ;; propagate to callers. (unreachable) ) From 6604d29855660042fc4f48c9413cecd748b445b5 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 25 Feb 2026 16:30:06 -0800 Subject: [PATCH 2/7] test --- test/lit/passes/inlining_tnh.wat | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/lit/passes/inlining_tnh.wat diff --git a/test/lit/passes/inlining_tnh.wat b/test/lit/passes/inlining_tnh.wat new file mode 100644 index 00000000000..854a525ed0b --- /dev/null +++ b/test/lit/passes/inlining_tnh.wat @@ -0,0 +1,27 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: foreach %s %t wasm-opt --inlining -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt --inlining -tnh -S -o - | filecheck %s + +;; Check that with or without TrapsNeverHappen, we inline calls to trapping +;; functions. That propagates the unreachability for other passes. + +(module + (func $call-trap + (call $trap) + ) + + (func $trap + (unreachable) + ) + + (func $call-trap-value + (drop + (call $trap-value) + ) + ) + + (func $trap-value (result i32) + (unreachable) + ) +) From ab2e9a7f45f33931fa1930e86e20665c028530f0 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 25 Feb 2026 16:32:19 -0800 Subject: [PATCH 3/7] test --- test/lit/passes/inlining_tnh.wat | 55 ++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/test/lit/passes/inlining_tnh.wat b/test/lit/passes/inlining_tnh.wat index 854a525ed0b..f8a52502d99 100644 --- a/test/lit/passes/inlining_tnh.wat +++ b/test/lit/passes/inlining_tnh.wat @@ -7,17 +7,68 @@ ;; functions. That propagates the unreachability for other passes. (module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (func $call-trap + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (block $__inlined_func$trap + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (block $__inlined_func$trap$1 + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) (func $call-trap - (call $trap) + ;; Call twice to avoid the single-caller rules, which always inline. + (if + (i32.const 42) + (then + (call $trap) + ) + (else + (call $trap) + ) + ) ) (func $trap (unreachable) ) + ;; CHECK: (func $call-trap-value + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (block $__inlined_func$trap-value$2 + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (block $__inlined_func$trap-value$3 + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) (func $call-trap-value (drop - (call $trap-value) + (if (result i32) + (i32.const 42) + (then + (call $trap-value) + ) + (else + (call $trap-value) + ) + ) ) ) From b43399bb138330632ec5ebcca7eafe79e0d61694 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 26 Feb 2026 11:27:02 -0800 Subject: [PATCH 4/7] fix --- src/passes/Vacuum.cpp | 10 ++++------ test/lit/passes/vacuum-tnh.wast | 34 ++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/passes/Vacuum.cpp b/src/passes/Vacuum.cpp index 8e776a70d1d..6dabd9c1d7d 100644 --- a/src/passes/Vacuum.cpp +++ b/src/passes/Vacuum.cpp @@ -476,12 +476,10 @@ struct Vacuum : public WalkerPass> { if (curr->getResults() == Type::none) { EffectAnalyzer effects(getPassOptions(), *getModule(), curr); if (!effects.hasUnremovableSideEffects()) { - // We can remove these contents. Emit a nop, or an unreachable if it - // might trap (even in trapsNeverHappen mode, we don't want to turn an - // unreachable into a nop - the unreachable can be propagated onwards). - if (effects.trap) { - ExpressionManipulator::unreachable(curr->body); - } else { + // We can remove these contents. Emit a nop, but not if it might trap - + // even in trapsNeverHappen mode, we don't want to turn an unreachable + // into a nop (as the unreachable can be propagated onwards). + if (!effects.trap) { ExpressionManipulator::nop(curr->body); } } diff --git a/test/lit/passes/vacuum-tnh.wast b/test/lit/passes/vacuum-tnh.wast index da1e75bdd44..6c67c8e1248 100644 --- a/test/lit/passes/vacuum-tnh.wast +++ b/test/lit/passes/vacuum-tnh.wast @@ -19,7 +19,9 @@ (type $struct (struct (field (mut i32)))) ;; YESTNH: (func $drop (type $4) (param $x i32) (param $y anyref) - ;; YESTNH-NEXT: (unreachable) + ;; YESTNH-NEXT: (drop + ;; YESTNH-NEXT: (unreachable) + ;; YESTNH-NEXT: ) ;; YESTNH-NEXT: ) ;; NO_TNH: (func $drop (type $4) (param $x i32) (param $y anyref) ;; NO_TNH-NEXT: (drop @@ -192,6 +194,36 @@ (unreachable) ) + ;; YESTNH: (func $toplevel-might-trap (type $0) + ;; YESTNH-NEXT: (local $0 i32) + ;; YESTNH-NEXT: (local.set $0 + ;; YESTNH-NEXT: (i32.load + ;; YESTNH-NEXT: (i32.const 0) + ;; YESTNH-NEXT: ) + ;; YESTNH-NEXT: ) + ;; YESTNH-NEXT: ) + ;; NO_TNH: (func $toplevel-might-trap (type $0) + ;; NO_TNH-NEXT: (local $0 i32) + ;; NO_TNH-NEXT: (local.set $0 + ;; NO_TNH-NEXT: (i32.load + ;; NO_TNH-NEXT: (i32.const 0) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + (func $toplevel-might-trap + ;; This might trap, and we cannot remove it. In TNH we can ignore that trap, + ;; but we cannot do anything with the knowledge - we still need to emit this + ;; code, as we do not remove local operations in this pass (other passes can + ;; handle it, of course). + (local $0 i32) + (local.set $0 + (i32.load + (i32.const 0) + ) + ) + ) + + ;; YESTNH: (func $drop-loop (type $0) ;; YESTNH-NEXT: (drop ;; YESTNH-NEXT: (loop $loop (result i32) From 18276871c1af92aa6f858fe71c2b7ea7f9fdb63b Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 26 Feb 2026 11:27:38 -0800 Subject: [PATCH 5/7] comment --- src/passes/Vacuum.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/passes/Vacuum.cpp b/src/passes/Vacuum.cpp index 6dabd9c1d7d..bd61013d8cb 100644 --- a/src/passes/Vacuum.cpp +++ b/src/passes/Vacuum.cpp @@ -478,7 +478,8 @@ struct Vacuum : public WalkerPass> { if (!effects.hasUnremovableSideEffects()) { // We can remove these contents. Emit a nop, but not if it might trap - // even in trapsNeverHappen mode, we don't want to turn an unreachable - // into a nop (as the unreachable can be propagated onwards). + // into a nop (as the unreachable can be propagated onwards). (We would + // also need to know that the code *must* trap, not just that it might.) if (!effects.trap) { ExpressionManipulator::nop(curr->body); } From 69818c82f05c83f288e489de94d67daa108687e6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 26 Feb 2026 15:32:11 -0800 Subject: [PATCH 6/7] find explicit unreachables --- src/passes/Vacuum.cpp | 32 +++++++++++++++++++++++++++----- test/lit/passes/vacuum-tnh.wast | 29 +++++++++++++++++------------ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/passes/Vacuum.cpp b/src/passes/Vacuum.cpp index bd61013d8cb..d6a2a4074ac 100644 --- a/src/passes/Vacuum.cpp +++ b/src/passes/Vacuum.cpp @@ -465,6 +465,11 @@ struct Vacuum : public WalkerPass> { } } + // We track if an Unreachable exists, see visitFunction. + bool hasUnreachable = false; + + void visitUnreachable(Unreachable* curr) { hasUnreachable = true; } + void visitFunction(Function* curr) { auto* optimized = optimize(curr->body, curr->getResults() != Type::none, true); @@ -476,11 +481,28 @@ struct Vacuum : public WalkerPass> { if (curr->getResults() == Type::none) { EffectAnalyzer effects(getPassOptions(), *getModule(), curr); if (!effects.hasUnremovableSideEffects()) { - // We can remove these contents. Emit a nop, but not if it might trap - - // even in trapsNeverHappen mode, we don't want to turn an unreachable - // into a nop (as the unreachable can be propagated onwards). (We would - // also need to know that the code *must* trap, not just that it might.) - if (!effects.trap) { + // We can remove these contents. However, there is one situation we want + // to handle here: in trapsNeverHappen mode, we can remove traps, but + // we don't want to remove an actual Unreachable - replacing an + // Unreachable with a Nop is valid, but does not propagate to callers in + // other passes. + // + // To avoid that situation, after finding we can remove the code, we + // also require that no Unreachable exists. Note that this is unoptimal: + // there may be a complex bundle of code whose only effect is to + // potentially trap, and it happens to contain an Unreachable inside + // somewhere, then that would prevent us from nopping the entire thing. + // But we leave untangling such code for other passes. + // + // This is also unoptimal as it is a heuristic: some toolchain might + // emit 0 / 0 for a logical trap, rather than an Unreachable. We would + // remove that 0 / 0 if we saw it, and the trap would not propagate. + // (But other passes would handle it, if they saw it first.) + if (!hasUnreachable) { + // Either trapsNeverHappen and there is no Unreachable (so we are + // only removing implicit traps, which is fine), or traps may happen + // in terms of the flag, but not in this actual code. Either way, we + // can remove all of this. ExpressionManipulator::nop(curr->body); } } diff --git a/test/lit/passes/vacuum-tnh.wast b/test/lit/passes/vacuum-tnh.wast index 6c67c8e1248..cdb72c77a33 100644 --- a/test/lit/passes/vacuum-tnh.wast +++ b/test/lit/passes/vacuum-tnh.wast @@ -64,7 +64,9 @@ ) ) - ;; Ignore unreachable code. + ;; Ignore unreachable code. Note that we leave this dropped unreachable in + ;; the final output - in TNH it is valid to turn it into a nop, but we do + ;; not remove unreachables, to let them propagate to callers. (drop (unreachable) ) @@ -196,11 +198,7 @@ ;; YESTNH: (func $toplevel-might-trap (type $0) ;; YESTNH-NEXT: (local $0 i32) - ;; YESTNH-NEXT: (local.set $0 - ;; YESTNH-NEXT: (i32.load - ;; YESTNH-NEXT: (i32.const 0) - ;; YESTNH-NEXT: ) - ;; YESTNH-NEXT: ) + ;; YESTNH-NEXT: (nop) ;; YESTNH-NEXT: ) ;; NO_TNH: (func $toplevel-might-trap (type $0) ;; NO_TNH-NEXT: (local $0 i32) @@ -211,10 +209,9 @@ ;; NO_TNH-NEXT: ) ;; NO_TNH-NEXT: ) (func $toplevel-might-trap - ;; This might trap, and we cannot remove it. In TNH we can ignore that trap, - ;; but we cannot do anything with the knowledge - we still need to emit this - ;; code, as we do not remove local operations in this pass (other passes can - ;; handle it, of course). + ;; This might trap, but we can still remove it all in TNH mode: the implicit + ;; trap does not inhibit us from removing this code. (If we saw an explicit + ;; unreachable, we would not remove it, as tested above.) (local $0 i32) (local.set $0 (i32.load @@ -578,7 +575,9 @@ ) ;; YESTNH: (func $block-unreachable-all (type $1) (param $p i32) - ;; YESTNH-NEXT: (nop) + ;; YESTNH-NEXT: (drop + ;; YESTNH-NEXT: (local.get $p) + ;; YESTNH-NEXT: ) ;; YESTNH-NEXT: ) ;; NO_TNH: (func $block-unreachable-all (type $2) (param $p i32) ;; NO_TNH-NEXT: (if @@ -602,7 +601,13 @@ (then (block ;; Both stores can be removed, and even the entire if arm and then the - ;; entire if. + ;; entire if. However, we do not manage to do it all in a single + ;; iteration, because of the unreachable below: TNH notices it during + ;; the scan, and is careful not to remove unreachables (as we want + ;; them to propagate). The unreachable vanishes after it is scanned, + ;; so if we re-scanned the body at the end, we could optimize here, + ;; but this rare situation doesn't seem to justify another pass over + ;; the entire function - we leave it to later iterations. (i32.store (i32.const 0) (i32.const 1) From a03b2004b433cd772d9971825af6051d5bd5bf37 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 26 Feb 2026 16:25:00 -0800 Subject: [PATCH 7/7] just FindAll --- src/passes/Vacuum.cpp | 24 +++++++++++++----------- test/lit/passes/vacuum-tnh.wast | 12 ++---------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/passes/Vacuum.cpp b/src/passes/Vacuum.cpp index d6a2a4074ac..2b5ec3f191b 100644 --- a/src/passes/Vacuum.cpp +++ b/src/passes/Vacuum.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -465,11 +466,6 @@ struct Vacuum : public WalkerPass> { } } - // We track if an Unreachable exists, see visitFunction. - bool hasUnreachable = false; - - void visitUnreachable(Unreachable* curr) { hasUnreachable = true; } - void visitFunction(Function* curr) { auto* optimized = optimize(curr->body, curr->getResults() != Type::none, true); @@ -498,13 +494,19 @@ struct Vacuum : public WalkerPass> { // emit 0 / 0 for a logical trap, rather than an Unreachable. We would // remove that 0 / 0 if we saw it, and the trap would not propagate. // (But other passes would handle it, if they saw it first.) - if (!hasUnreachable) { - // Either trapsNeverHappen and there is no Unreachable (so we are - // only removing implicit traps, which is fine), or traps may happen - // in terms of the flag, but not in this actual code. Either way, we - // can remove all of this. - ExpressionManipulator::nop(curr->body); + if (effects.trap) { + // The code is removable, so the trap is the only effect it has, and + // we are considering removing it because TNH is enabled. + assert(getPassOptions().trapsNeverHappen); + if (!FindAll(curr->body).list.empty()) { + return; + } } + // Either trapsNeverHappen and there is no Unreachable (so we are only + // removing implicit traps, which is fine), or traps may happen in terms + // of the flag, but not in this actual code. Either way, we can remove + // all of this. + ExpressionManipulator::nop(curr->body); } } } diff --git a/test/lit/passes/vacuum-tnh.wast b/test/lit/passes/vacuum-tnh.wast index cdb72c77a33..6203c2f3420 100644 --- a/test/lit/passes/vacuum-tnh.wast +++ b/test/lit/passes/vacuum-tnh.wast @@ -575,9 +575,7 @@ ) ;; YESTNH: (func $block-unreachable-all (type $1) (param $p i32) - ;; YESTNH-NEXT: (drop - ;; YESTNH-NEXT: (local.get $p) - ;; YESTNH-NEXT: ) + ;; YESTNH-NEXT: (nop) ;; YESTNH-NEXT: ) ;; NO_TNH: (func $block-unreachable-all (type $2) (param $p i32) ;; NO_TNH-NEXT: (if @@ -601,13 +599,7 @@ (then (block ;; Both stores can be removed, and even the entire if arm and then the - ;; entire if. However, we do not manage to do it all in a single - ;; iteration, because of the unreachable below: TNH notices it during - ;; the scan, and is careful not to remove unreachables (as we want - ;; them to propagate). The unreachable vanishes after it is scanned, - ;; so if we re-scanned the body at the end, we could optimize here, - ;; but this rare situation doesn't seem to justify another pass over - ;; the entire function - we leave it to later iterations. + ;; entire if. (i32.store (i32.const 0) (i32.const 1)