From 9a36dad30f38a0e2124980c896537897d8d54ab9 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 1 Sep 2026 17:18:41 -0700 Subject: [PATCH 1/7] Add a suspends effect This will be necessary for a future optimization that turns resumes of continuations that never suspend into calls. Update the effect analysis of suspends to set the new effect and clobber global state because the suspend handler might do anything before returning. Test that the effects are analyzed as intended and that they work with global effect analysis. --- src/binaryen-c.cpp | 4 + src/binaryen-c.h | 1 + src/ir/effects.cpp | 3 + src/ir/effects.h | 62 +++- src/js/binaryen.js-post.js | 1 + src/passes/GlobalEffects.cpp | 18 +- test/binaryen.js/sideffects.js | 1 + test/binaryen.js/sideffects.js.txt | 3 +- test/gtest/CMakeLists.txt | 1 + test/gtest/effects.cpp | 118 +++++++ test/gtest/matchers/effects.h | 1 + test/lit/passes/global-effects-suspends.wast | 333 +++++++++++++++++++ 12 files changed, 525 insertions(+), 21 deletions(-) create mode 100644 test/gtest/effects.cpp create mode 100644 test/lit/passes/global-effects-suspends.wast diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index 55e6f330724..453b34122db 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -6687,6 +6687,10 @@ BinaryenSideEffects BinaryenSideEffectDanglingPop(void) { return static_cast( EffectAnalyzer::SideEffects::DanglingPop); } +BinaryenSideEffects BinaryenSideEffectSuspends(void) { + return static_cast( + EffectAnalyzer::SideEffects::Suspends); +} BinaryenSideEffects BinaryenSideEffectAny(void) { return static_cast(EffectAnalyzer::SideEffects::Any); } diff --git a/src/binaryen-c.h b/src/binaryen-c.h index a6bb2e0a6a4..844acdc765f 100644 --- a/src/binaryen-c.h +++ b/src/binaryen-c.h @@ -3669,6 +3669,7 @@ BINARYEN_API BinaryenSideEffects BinaryenSideEffectTrapsNeverHappen(void); BINARYEN_API BinaryenSideEffects BinaryenSideEffectIsAtomic(void); BINARYEN_API BinaryenSideEffects BinaryenSideEffectThrows(void); BINARYEN_API BinaryenSideEffects BinaryenSideEffectDanglingPop(void); +BINARYEN_API BinaryenSideEffects BinaryenSideEffectSuspends(void); BINARYEN_API BinaryenSideEffects BinaryenSideEffectAny(void); BINARYEN_API BinaryenSideEffects BinaryenExpressionGetSideEffects( diff --git a/src/ir/effects.cpp b/src/ir/effects.cpp index 017057a1349..cbca3d74408 100644 --- a/src/ir/effects.cpp +++ b/src/ir/effects.cpp @@ -96,6 +96,9 @@ std::ostream& operator<<(std::ostream& o, const EffectAnalyzer& effects) { if (effects.throws_) { o << "throws_\n"; } + if (effects.suspends_) { + o << "suspends_\n"; + } if (effects.tryDepth) { o << "tryDepth\n"; } diff --git a/src/ir/effects.h b/src/ir/effects.h index 40bd93b6ba2..f6326d54c3f 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -44,8 +44,8 @@ class EffectAnalyzer { readsMutableArray(false), writesArray(false), readsSharedMutableArray(false), writesSharedArray(false), trap(false), implicitTrap(false), throws_(false), danglingPop(false), - mayNotReturn(false), hasReturnCallThrow(false), module(module), - features(module.features) {} + mayNotReturn(false), hasReturnCallThrow(false), suspends_(false), + module(module), features(module.features) {} EffectAnalyzer(const PassOptions& passOptions, const Module& module, @@ -138,6 +138,8 @@ class EffectAnalyzer { // more here.) bool hasReturnCallThrow : 1; + bool suspends_ : 1; + const Module& module; FeatureSet features; @@ -228,15 +230,35 @@ class EffectAnalyzer { return calls || readsSharedMutableArray || writesSharedArray; } bool throws() const { return throws_ || !delegateTargets.empty(); } + bool suspends() const { return suspends_; } // Check whether this may transfer control flow to somewhere outside of this - // expression (aside from just flowing out normally). That includes a break - // or a throw (if the throw is not known to be caught inside this expression; + // expression (aside from just flowing out normally). That includes a break, + // a throw (if the throw is not known to be caught inside this expression; // note that if the throw is not caught in this expression then it might be // caught in this function but outside of this expression, or it might not be // caught in the function at all, which would mean control flow cannot be - // transferred inside the function, but this expression does not know that). + // transferred inside the function, but this expression does not know that), + // or a suspension. bool transfersControlFlow() const { - return branchesOut || throws() || hasExternalBreakTargets(); + return branchesOut || throws() || hasExternalBreakTargets() || suspends(); + } + + // Explicitly marks all global mutable state as clobbered (read and written). + void clobbersGlobalState() { + readsMemory = true; + writesMemory = true; + readsSharedMemory = true; + writesSharedMemory = true; + readsTable = true; + writesTable = true; + readsMutableStruct = true; + writesStruct = true; + readsSharedMutableStruct = true; + writesSharedStruct = true; + readsMutableArray = true; + writesArray = true; + readsSharedMutableArray = true; + writesSharedArray = true; } // Changes something in globally-stored state. @@ -264,7 +286,7 @@ class EffectAnalyzer { bool hasNonTrapSideEffects() const { return localsWritten.size() > 0 || danglingPop || writesGlobalState() || throws() || transfersControlFlow() || hasSynchronization() || - mayNotReturn; + mayNotReturn || suspends(); } bool hasSideEffects() const { return trap || hasNonTrapSideEffects(); } @@ -480,6 +502,7 @@ class EffectAnalyzer { danglingPop = danglingPop || other.danglingPop; mayNotReturn = mayNotReturn || other.mayNotReturn; hasReturnCallThrow = hasReturnCallThrow || other.hasReturnCallThrow; + suspends_ = suspends_ || other.suspends_; readOrder = std::max(readOrder, other.readOrder); writeOrder = std::max(writeOrder, other.writeOrder); @@ -1271,9 +1294,10 @@ class EffectAnalyzer { parent.calls = true; } void visitSuspend(Suspend* curr) { - // Similar to resume/call: Suspending means that we execute arbitrary - // other code before we may resume here. - parent.calls = true; + // Suspending transfers control to an enclosing handler and executes + // arbitrary other code before we may resume here. + parent.suspends_ = true; + parent.clobbersGlobalState(); if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) { parent.throws_ = true; } @@ -1370,6 +1394,11 @@ class EffectAnalyzer { parent.throws_ = true; } } + // If stack switching is enabled and we don't have global effects + // information, assume that the call target may suspend. + if (parent.features.hasStackSwitching()) { + parent.suspends_ = true; + } } }; @@ -1407,7 +1436,8 @@ class EffectAnalyzer { Throws = 1 << 12, DanglingPop = 1 << 13, TrapsNeverHappen = 1 << 14, - Any = (1 << 15) - 1 + Suspends = 1 << 15, + Any = (1 << 16) - 1 }; uint32_t getSideEffects() const { uint32_t effects = 0; @@ -1459,12 +1489,15 @@ class EffectAnalyzer { if (danglingPop) { effects |= SideEffects::DanglingPop; } + if (suspends_) { + effects |= SideEffects::Suspends; + } return effects; } - // Ignores all forms of control flow transfers: breaks, returns, and - // exceptions. (Note that traps are not considered relevant here - a trap does - // not just transfer control flow, but can be seen as halting the entire + // Ignores all forms of control flow transfers: breaks, returns, exceptions, + // and suspensions. (Note that traps are not considered relevant here - a trap + // does not just transfer control flow, but can be seen as halting the entire // program.) // // This function matches transfersControlFlow(), that is, after calling this @@ -1474,6 +1507,7 @@ class EffectAnalyzer { breakTargets.clear(); throws_ = false; delegateTargets.clear(); + suspends_ = false; assert(!transfersControlFlow()); } diff --git a/src/js/binaryen.js-post.js b/src/js/binaryen.js-post.js index 1ad2531371d..9f117daa19b 100644 --- a/src/js/binaryen.js-post.js +++ b/src/js/binaryen.js-post.js @@ -666,6 +666,7 @@ function initializeConstants() { 'Throws', 'DanglingPop', 'TrapsNeverHappen', + 'Suspends', 'Any' ].forEach(name => { Module['SideEffects'][name] = Module['_BinaryenSideEffect' + name](); diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index f3313700e8e..ec4ec1dfe03 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -138,10 +138,12 @@ std::map analyzeFuncs(Module& module, // below. funcInfo.effects->calls = false; - // Clear throws as well, as we are "forgetting" calls right now, and - // want to forget their throwing effect as well. If we see something - // else that throws, below, then we'll note that there. + // Clear throws and suspends as well, as we are "forgetting" calls right + // now, and want to forget their throwing and suspending effects as + // well. If we see something else that throws or suspends, below, then + // we'll note that there. funcInfo.effects->throws_ = false; + funcInfo.effects->suspends_ = false; struct CallScanner : public PostWalker analyzeFuncs(Module& module, assert(options.worldMode == WorldMode::Open); funcInfo.effects = std::nullopt; } else { - // No call here, but update throwing if we see it. (Only do so, - // however, if we have effects; if we cleared it - see before - - // then we assume the worst anyhow, and have nothing to update.) + // No call here, but update throwing and suspending if we see it. + // (Only do so, however, if we have effects; if we cleared it - + // see before - then we assume the worst anyhow, and have nothing + // to update.) if (effects.throws_ && funcInfo.effects) { funcInfo.effects->throws_ = true; } + if (effects.suspends_ && funcInfo.effects) { + funcInfo.effects->suspends_ = true; + } } } }; diff --git a/test/binaryen.js/sideffects.js b/test/binaryen.js/sideffects.js index a2ff4778a33..ec627dff42c 100644 --- a/test/binaryen.js/sideffects.js +++ b/test/binaryen.js/sideffects.js @@ -14,6 +14,7 @@ console.log("SideEffects.IsAtomic=" + binaryen.SideEffects.IsAtomic); console.log("SideEffects.Throws=" + binaryen.SideEffects.Throws); console.log("SideEffects.DanglingPop=" + binaryen.SideEffects.DanglingPop); console.log("SideEffects.TrapsNeverHappen=" + binaryen.SideEffects.TrapsNeverHappen); +console.log("SideEffects.Suspends=" + binaryen.SideEffects.Suspends); console.log("SideEffects.Any=" + binaryen.SideEffects.Any); var module = new binaryen.Module(); diff --git a/test/binaryen.js/sideffects.js.txt b/test/binaryen.js/sideffects.js.txt index b582d70bdc0..85fc6bb6b80 100644 --- a/test/binaryen.js/sideffects.js.txt +++ b/test/binaryen.js/sideffects.js.txt @@ -14,4 +14,5 @@ SideEffects.IsAtomic=2048 SideEffects.Throws=4096 SideEffects.DanglingPop=8192 SideEffects.TrapsNeverHappen=16384 -SideEffects.Any=32767 +SideEffects.Suspends=32768 +SideEffects.Any=65535 diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt index c48edef2dd7..4602db36bda 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -15,6 +15,7 @@ set(unittest_SOURCES delta_debugging.cpp dfa_minimization.cpp disjoint_sets.cpp + effects.cpp graph.cpp int128.cpp leaves.cpp diff --git a/test/gtest/effects.cpp b/test/gtest/effects.cpp new file mode 100644 index 00000000000..f959085fea9 --- /dev/null +++ b/test/gtest/effects.cpp @@ -0,0 +1,118 @@ +/* + * Copyright 2026 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "ir/effects.h" +#include "matchers/effects.h" +#include "parser/wat-parser.h" +#include "wasm.h" + +using namespace wasm; +using namespace testing; + +namespace { + +class EffectAnalyzerTest : public Test { +protected: + Module wasm; + PassOptions options; + + void SetUp() override { wasm.features = FeatureSet::All; } +}; + +TEST_F(EffectAnalyzerTest, Suspend) { + auto moduleText = R"wasm( + (module + (tag $tag) + (func $test + (suspend $tag) + ) + ) + )wasm"; + + auto parseResult = WATParser::parseModule(wasm, moduleText); + ASSERT_FALSE(parseResult.getErr()); + + auto* func = wasm.getFunction("test"); + ASSERT_NE(func, nullptr); + + EffectAnalyzer effects(options, wasm, func->body); + + // Suspension detection + EXPECT_TRUE(effects.suspends()); + EXPECT_THAT(&effects, Suspends()); + EXPECT_TRUE(effects.getSideEffects() & EffectAnalyzer::SideEffects::Suspends); + + // Decoupled from call graph edges + EXPECT_FALSE(effects.calls); + EXPECT_THAT(&effects, Not(Calls())); + + // Clobbers all global mutable state + EXPECT_TRUE(effects.readsMemory); + EXPECT_TRUE(effects.writesMemory); + EXPECT_TRUE(effects.readsSharedMemory); + EXPECT_TRUE(effects.writesSharedMemory); + EXPECT_TRUE(effects.readsTable); + EXPECT_TRUE(effects.writesTable); + EXPECT_TRUE(effects.readsMutableStruct); + EXPECT_TRUE(effects.writesStruct); + EXPECT_TRUE(effects.readsSharedMutableStruct); + EXPECT_TRUE(effects.writesSharedStruct); + EXPECT_TRUE(effects.readsMutableArray); + EXPECT_TRUE(effects.writesArray); + EXPECT_TRUE(effects.readsSharedMutableArray); + EXPECT_TRUE(effects.writesSharedArray); + EXPECT_TRUE(effects.writesGlobalState()); + EXPECT_TRUE(effects.readsMutableGlobalState()); + + // Control flow & side effect queries + EXPECT_TRUE(effects.transfersControlFlow()); + EXPECT_TRUE(effects.hasNonTrapSideEffects()); + EXPECT_TRUE(effects.hasSideEffects()); + EXPECT_TRUE(effects.hasUnremovableSideEffects()); +} + +TEST_F(EffectAnalyzerTest, UnknownCall) { + auto moduleText = R"wasm( + (module + (func $callee) + (func $caller + (call $callee) + ) + ) + )wasm"; + + auto parseResult = WATParser::parseModule(wasm, moduleText); + ASSERT_FALSE(parseResult.getErr()); + + auto* caller = wasm.getFunction("caller"); + ASSERT_NE(caller, nullptr); + + // With stack switching enabled, unknown calls conservatively assume + // suspension. + wasm.features.setStackSwitching(true); + EffectAnalyzer effectsWithStackSwitch(options, wasm, caller->body); + EXPECT_TRUE(effectsWithStackSwitch.suspends()); + + // With stack switching disabled, calls do not suspend. + wasm.features.setStackSwitching(false); + EffectAnalyzer effectsWithoutStackSwitch(options, wasm, caller->body); + EXPECT_FALSE(effectsWithoutStackSwitch.suspends()); +} + +} // anonymous namespace diff --git a/test/gtest/matchers/effects.h b/test/gtest/matchers/effects.h index 689850f92dc..b0ed8bb544e 100644 --- a/test/gtest/matchers/effects.h +++ b/test/gtest/matchers/effects.h @@ -45,6 +45,7 @@ MATCHER(Throws, "") { return arg->throws_; } MATCHER(DanglingPop, "") { return arg->danglingPop; } MATCHER(MayNotReturn, "") { return arg->mayNotReturn; } MATCHER(HasReturnCallThrow, "") { return arg->hasReturnCallThrow; } +MATCHER(Suspends, "") { return arg->suspends(); } } // namespace wasm diff --git a/test/lit/passes/global-effects-suspends.wast b/test/lit/passes/global-effects-suspends.wast new file mode 100644 index 00000000000..82e8756e6fc --- /dev/null +++ b/test/lit/passes/global-effects-suspends.wast @@ -0,0 +1,333 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; Test that global effects properly track suspension across functions, +;; call chains, cycles, and indirect calls. + +;; RUN: foreach %s %t wasm-opt -all --generate-global-effects --vacuum -S -o - | filecheck %s --check-prefix VACUUM +;; RUN: foreach %s %t wasm-opt -all --generate-global-effects --closed-world --vacuum -S -o - | filecheck %s --check-prefix CLOSED-VACUUM +;; RUN: foreach %s %t wasm-opt -all --generate-global-effects --simplify-locals -S -o - | filecheck %s --check-prefix SIMPLIFY + +(module + (memory 1 1) + ;; VACUUM: (tag $tag (type $0)) + ;; CLOSED-VACUUM: (tag $tag (type $0)) + ;; SIMPLIFY: (tag $tag (type $0)) + (tag $tag) + + ;; VACUUM: (func $pure (type $0) + ;; VACUUM-NEXT: (nop) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $pure (type $0) + ;; CLOSED-VACUUM-NEXT: (nop) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $pure (type $0) + ;; SIMPLIFY-NEXT: (nop) + ;; SIMPLIFY-NEXT: ) + (func $pure + (nop) + ) + + ;; VACUUM: (func $caller-of-pure (type $0) + ;; VACUUM-NEXT: (nop) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $caller-of-pure (type $0) + ;; CLOSED-VACUUM-NEXT: (nop) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $caller-of-pure (type $0) + ;; SIMPLIFY-NEXT: (call $pure) + ;; SIMPLIFY-NEXT: ) + (func $caller-of-pure + (call $pure) + ) + + ;; VACUUM: (func $suspending (type $0) + ;; VACUUM-NEXT: (suspend $tag) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $suspending (type $0) + ;; CLOSED-VACUUM-NEXT: (suspend $tag) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $suspending (type $0) + ;; SIMPLIFY-NEXT: (suspend $tag) + ;; SIMPLIFY-NEXT: ) + (func $suspending + (suspend $tag) + ) + + ;; VACUUM: (func $caller-of-suspending (type $0) + ;; VACUUM-NEXT: (call $suspending) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $caller-of-suspending (type $0) + ;; CLOSED-VACUUM-NEXT: (call $suspending) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $caller-of-suspending (type $0) + ;; SIMPLIFY-NEXT: (call $suspending) + ;; SIMPLIFY-NEXT: ) + (func $caller-of-suspending + (call $suspending) + ) + + ;; VACUUM: (func $transitive-caller-of-suspending (type $0) + ;; VACUUM-NEXT: (call $caller-of-suspending) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $transitive-caller-of-suspending (type $0) + ;; CLOSED-VACUUM-NEXT: (call $caller-of-suspending) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $transitive-caller-of-suspending (type $0) + ;; SIMPLIFY-NEXT: (call $caller-of-suspending) + ;; SIMPLIFY-NEXT: ) + (func $transitive-caller-of-suspending + (call $caller-of-suspending) + ) + + ;; Cycles + ;; VACUUM: (func $cycle-suspend-a (type $0) + ;; VACUUM-NEXT: (call $cycle-suspend-b) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $cycle-suspend-a (type $0) + ;; CLOSED-VACUUM-NEXT: (call $cycle-suspend-b) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $cycle-suspend-a (type $0) + ;; SIMPLIFY-NEXT: (call $cycle-suspend-b) + ;; SIMPLIFY-NEXT: ) + (func $cycle-suspend-a + (call $cycle-suspend-b) + ) + + ;; VACUUM: (func $cycle-suspend-b (type $0) + ;; VACUUM-NEXT: (call $cycle-suspend-a) + ;; VACUUM-NEXT: (call $suspending) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $cycle-suspend-b (type $0) + ;; CLOSED-VACUUM-NEXT: (call $cycle-suspend-a) + ;; CLOSED-VACUUM-NEXT: (call $suspending) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $cycle-suspend-b (type $0) + ;; SIMPLIFY-NEXT: (call $cycle-suspend-a) + ;; SIMPLIFY-NEXT: (call $suspending) + ;; SIMPLIFY-NEXT: ) + (func $cycle-suspend-b + (call $cycle-suspend-a) + (call $suspending) + ) + + ;; Vacuum tests: pure calls should be eliminated, suspending calls preserved. + ;; VACUUM: (func $test-vacuum (type $0) + ;; VACUUM-NEXT: (call $suspending) + ;; VACUUM-NEXT: (call $caller-of-suspending) + ;; VACUUM-NEXT: (call $transitive-caller-of-suspending) + ;; VACUUM-NEXT: (call $cycle-suspend-a) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $test-vacuum (type $0) + ;; CLOSED-VACUUM-NEXT: (call $suspending) + ;; CLOSED-VACUUM-NEXT: (call $caller-of-suspending) + ;; CLOSED-VACUUM-NEXT: (call $transitive-caller-of-suspending) + ;; CLOSED-VACUUM-NEXT: (call $cycle-suspend-a) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $test-vacuum (type $0) + ;; SIMPLIFY-NEXT: (call $pure) + ;; SIMPLIFY-NEXT: (call $caller-of-pure) + ;; SIMPLIFY-NEXT: (call $suspending) + ;; SIMPLIFY-NEXT: (call $caller-of-suspending) + ;; SIMPLIFY-NEXT: (call $transitive-caller-of-suspending) + ;; SIMPLIFY-NEXT: (call $cycle-suspend-a) + ;; SIMPLIFY-NEXT: ) + (func $test-vacuum + ;; Pure calls eliminated: + (call $pure) + (call $caller-of-pure) + + ;; Direct, transitive, and cyclic calls to suspend preserved: + (call $suspending) + (call $caller-of-suspending) + (call $transitive-caller-of-suspending) + (call $cycle-suspend-a) + ) + + ;; Simplify-locals test: memory access cannot be reordered or eliminated + ;; across a call to a function that directly or transitively suspends. + ;; VACUUM: (func $test-simplify-memory (type $1) (result i32) + ;; VACUUM-NEXT: (local $x i32) + ;; VACUUM-NEXT: (local.set $x + ;; VACUUM-NEXT: (i32.load + ;; VACUUM-NEXT: (i32.const 0) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (call $caller-of-suspending) + ;; VACUUM-NEXT: (local.get $x) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $test-simplify-memory (type $1) (result i32) + ;; CLOSED-VACUUM-NEXT: (local $x i32) + ;; CLOSED-VACUUM-NEXT: (local.set $x + ;; CLOSED-VACUUM-NEXT: (i32.load + ;; CLOSED-VACUUM-NEXT: (i32.const 0) + ;; CLOSED-VACUUM-NEXT: ) + ;; CLOSED-VACUUM-NEXT: ) + ;; CLOSED-VACUUM-NEXT: (call $caller-of-suspending) + ;; CLOSED-VACUUM-NEXT: (local.get $x) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $test-simplify-memory (type $1) (result i32) + ;; SIMPLIFY-NEXT: (local $x i32) + ;; SIMPLIFY-NEXT: (local.set $x + ;; SIMPLIFY-NEXT: (i32.load + ;; SIMPLIFY-NEXT: (i32.const 0) + ;; SIMPLIFY-NEXT: ) + ;; SIMPLIFY-NEXT: ) + ;; SIMPLIFY-NEXT: (call $caller-of-suspending) + ;; SIMPLIFY-NEXT: (local.get $x) + ;; SIMPLIFY-NEXT: ) + (func $test-simplify-memory (result i32) + (local $x i32) + (local.set $x (i32.load (i32.const 0))) + (call $caller-of-suspending) + (local.get $x) + ) + + ;; VACUUM: (func $test-simplify-pure (type $1) (result i32) + ;; VACUUM-NEXT: (local $x i32) + ;; VACUUM-NEXT: (local.set $x + ;; VACUUM-NEXT: (i32.load + ;; VACUUM-NEXT: (i32.const 0) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (local.get $x) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $test-simplify-pure (type $1) (result i32) + ;; CLOSED-VACUUM-NEXT: (local $x i32) + ;; CLOSED-VACUUM-NEXT: (local.set $x + ;; CLOSED-VACUUM-NEXT: (i32.load + ;; CLOSED-VACUUM-NEXT: (i32.const 0) + ;; CLOSED-VACUUM-NEXT: ) + ;; CLOSED-VACUUM-NEXT: ) + ;; CLOSED-VACUUM-NEXT: (local.get $x) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $test-simplify-pure (type $1) (result i32) + ;; SIMPLIFY-NEXT: (local $x i32) + ;; SIMPLIFY-NEXT: (nop) + ;; SIMPLIFY-NEXT: (call $caller-of-pure) + ;; SIMPLIFY-NEXT: (i32.load + ;; SIMPLIFY-NEXT: (i32.const 0) + ;; SIMPLIFY-NEXT: ) + ;; SIMPLIFY-NEXT: ) + (func $test-simplify-pure (result i32) + (local $x i32) + (local.set $x (i32.load (i32.const 0))) + (call $caller-of-pure) + (local.get $x) + ) +) + +(module + ;; VACUUM: (type $sig-suspending (func (param i32))) + + ;; VACUUM: (type $sig-pure (func (param i64))) + + ;; VACUUM: (tag $tag (type $2)) + ;; CLOSED-VACUUM: (type $sig-suspending (func (param i32))) + + ;; CLOSED-VACUUM: (type $sig-pure (func (param i64))) + + ;; CLOSED-VACUUM: (tag $tag (type $2)) + ;; SIMPLIFY: (type $sig-suspending (func (param i32))) + + ;; SIMPLIFY: (type $sig-pure (func (param i64))) + + ;; SIMPLIFY: (tag $tag (type $2)) + (tag $tag) + (type $sig-suspending (func (param i32))) + (type $sig-pure (func (param i64))) + + (table 1 1 funcref) + + ;; VACUUM: (func $target-suspend (type $sig-suspending) (param $0 i32) + ;; VACUUM-NEXT: (suspend $tag) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $target-suspend (type $sig-suspending) (param $0 i32) + ;; CLOSED-VACUUM-NEXT: (suspend $tag) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $target-suspend (type $sig-suspending) (param $0 i32) + ;; SIMPLIFY-NEXT: (suspend $tag) + ;; SIMPLIFY-NEXT: ) + (func $target-suspend (export "target-suspend") (type $sig-suspending) (param i32) + (suspend $tag) + ) + + ;; VACUUM: (func $target-pure (type $sig-pure) (param $0 i64) + ;; VACUUM-NEXT: (nop) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $target-pure (type $sig-pure) (param $0 i64) + ;; CLOSED-VACUUM-NEXT: (nop) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $target-pure (type $sig-pure) (param $0 i64) + ;; SIMPLIFY-NEXT: (nop) + ;; SIMPLIFY-NEXT: ) + (func $target-pure (export "target-pure") (type $sig-pure) (param i64) + (nop) + ) + + ;; VACUUM: (func $test-indirect-suspend (type $3) (param $ref (ref $sig-suspending)) + ;; VACUUM-NEXT: (call_ref $sig-suspending + ;; VACUUM-NEXT: (i32.const 0) + ;; VACUUM-NEXT: (local.get $ref) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $test-indirect-suspend (type $3) (param $ref (ref $sig-suspending)) + ;; CLOSED-VACUUM-NEXT: (call_ref $sig-suspending + ;; CLOSED-VACUUM-NEXT: (i32.const 0) + ;; CLOSED-VACUUM-NEXT: (local.get $ref) + ;; CLOSED-VACUUM-NEXT: ) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $test-indirect-suspend (type $3) (param $ref (ref $sig-suspending)) + ;; SIMPLIFY-NEXT: (call_ref $sig-suspending + ;; SIMPLIFY-NEXT: (i32.const 0) + ;; SIMPLIFY-NEXT: (local.get $ref) + ;; SIMPLIFY-NEXT: ) + ;; SIMPLIFY-NEXT: ) + (func $test-indirect-suspend (param $ref (ref $sig-suspending)) + (call_ref $sig-suspending (i32.const 0) (local.get $ref)) + ) + + ;; VACUUM: (func $test-indirect-pure (type $4) (param $ref (ref $sig-pure)) + ;; VACUUM-NEXT: (call_ref $sig-pure + ;; VACUUM-NEXT: (i64.const 0) + ;; VACUUM-NEXT: (local.get $ref) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $test-indirect-pure (type $4) (param $ref (ref $sig-pure)) + ;; CLOSED-VACUUM-NEXT: (nop) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $test-indirect-pure (type $4) (param $ref (ref $sig-pure)) + ;; SIMPLIFY-NEXT: (call_ref $sig-pure + ;; SIMPLIFY-NEXT: (i64.const 0) + ;; SIMPLIFY-NEXT: (local.get $ref) + ;; SIMPLIFY-NEXT: ) + ;; SIMPLIFY-NEXT: ) + (func $test-indirect-pure (param $ref (ref $sig-pure)) + (call_ref $sig-pure (i64.const 0) (local.get $ref)) + ) + + ;; VACUUM: (func $test-vacuum-indirect (type $5) (param $ref1 (ref $sig-suspending)) (param $ref2 (ref $sig-pure)) + ;; VACUUM-NEXT: (call $test-indirect-pure + ;; VACUUM-NEXT: (local.get $ref2) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (call $test-indirect-suspend + ;; VACUUM-NEXT: (local.get $ref1) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; CLOSED-VACUUM: (func $test-vacuum-indirect (type $5) (param $ref1 (ref $sig-suspending)) (param $ref2 (ref $sig-pure)) + ;; CLOSED-VACUUM-NEXT: (call $test-indirect-suspend + ;; CLOSED-VACUUM-NEXT: (local.get $ref1) + ;; CLOSED-VACUUM-NEXT: ) + ;; CLOSED-VACUUM-NEXT: ) + ;; SIMPLIFY: (func $test-vacuum-indirect (type $5) (param $ref1 (ref $sig-suspending)) (param $ref2 (ref $sig-pure)) + ;; SIMPLIFY-NEXT: (call $test-indirect-pure + ;; SIMPLIFY-NEXT: (local.get $ref2) + ;; SIMPLIFY-NEXT: ) + ;; SIMPLIFY-NEXT: (call $test-indirect-suspend + ;; SIMPLIFY-NEXT: (local.get $ref1) + ;; SIMPLIFY-NEXT: ) + ;; SIMPLIFY-NEXT: ) + (func $test-vacuum-indirect (param $ref1 (ref $sig-suspending)) (param $ref2 (ref $sig-pure)) + ;; The pure indirect call can be eliminated in closed-world, but not the suspending one. + (call $test-indirect-pure (local.get $ref2)) + (call $test-indirect-suspend (local.get $ref1)) + ) +) From f3f4698b697a9593cdcf5dc297b097882b79a255 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 2 Sep 2026 11:34:34 -0700 Subject: [PATCH 2/7] Optimize resume of non-suspending continuations Add a `visitResume` in OptimizeInstructions that does the normal optimizations on null continuations and then tries to turn resumes into calls. We can do this when we are resuming a freshly allocated continuation created with a reference to a known function that GlobalEffects tells us will not suspend. --- src/passes/OptimizeInstructions.cpp | 82 ++ .../passes/optimize-instructions-resume.wast | 1138 +++++++++++++++++ 2 files changed, 1220 insertions(+) create mode 100644 test/lit/passes/optimize-instructions-resume.wast diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index aa093530543..b920f7e8d21 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -1461,6 +1461,88 @@ struct OptimizeInstructions } } + void visitResume(Resume* curr) { + skipNonNullCast(curr->cont, curr); + if (trapOnNull(curr, curr->cont)) { + return; + } + if (curr->type == Type::unreachable) { + return; + } + + // If this resume operates on a freshly-created continuation of an exact + // function that is known never to suspend, we can turn the resumption into + // a direct call, avoiding the continuation allocation and handler overhead. + + // Continuations are single-shot, so resuming a continuation that has + // already been consumed will trap. If traps are assumed never to happen, we + // can assume this continuation was not consumed on another path and look + // through tees and conditional branches. Otherwise, avoid looking through + // them to ensure the continuation cannot be consumed elsewhere. + auto behavior = getPassOptions().trapsNeverHappen + ? Properties::FallthroughBehavior::AllowTeeBrIf + : Properties::FallthroughBehavior::NoTeeBrIf; + + auto* contExpr = Properties::getFallthrough( + curr->cont, getPassOptions(), *getModule(), behavior); + auto* contNew = contExpr->dynCast(); + if (!contNew) { + return; + } + if (contNew->func->type == Type::unreachable) { + return; + } + + auto* funcExpr = Properties::getFallthrough( + contNew->func, getPassOptions(), *getModule(), behavior); + auto* refFunc = funcExpr->dynCast(); + if (!refFunc) { + return; + } + + auto* target = getModule()->getFunctionOrNull(refFunc->func); + if (!target || target->imported()) { + return; + } + + if (!target->effects || target->effects->suspends()) { + return; + } + + Builder builder(*getModule()); + + // If the continuation expression has no side effects, we can eliminate it + // entirely and replace the resume with a direct call. + if (!effects(curr->cont).hasSideEffects()) { + replaceCurrent( + builder.makeCall(target->name, curr->operands, target->getResults())); + return; + } + + // The continuation expression has side effects. In Wasm, resume operands + // are evaluated before the continuation expression. If there are no + // operands, evaluate the continuation (dropped) and then call. + if (curr->operands.empty()) { + replaceCurrent(builder.makeSequence( + builder.makeDrop(curr->cont), + builder.makeCall(target->name, {}, target->getResults()))); + return; + } + + // In the presence of operands, execute the code in curr->cont after the + // operands and before the call happens by spilling the last operand to a + // temporary local. + auto* lastOperand = curr->operands.back(); + auto lastOperandType = lastOperand->type; + Index tempLocal = builder.addVar(getFunction(), lastOperandType); + auto* set = builder.makeLocalSet(tempLocal, lastOperand); + auto* drop = builder.makeDrop(curr->cont); + auto* get = builder.makeLocalGet(tempLocal, lastOperandType); + curr->operands.back() = builder.makeBlock({set, drop, get}); + replaceCurrent( + builder.makeCall(target->name, curr->operands, target->getResults())); + } + // Note on removing casts (which the following utilities, skipNonNullCast and // skipCast do): removing a cast is potentially dangerous, as it removes // information from the IR. For example: diff --git a/test/lit/passes/optimize-instructions-resume.wast b/test/lit/passes/optimize-instructions-resume.wast new file mode 100644 index 00000000000..8ca0f6cadbf --- /dev/null +++ b/test/lit/passes/optimize-instructions-resume.wast @@ -0,0 +1,1138 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; Test that optimize-instructions turns resumptions of continuations that never +;; suspend into direct calls. + +;; RUN: wasm-opt %s --all-features --generate-global-effects --remove-unused-names --optimize-instructions -S -o - | filecheck %s --check-prefix NO-TNH +;; RUN: wasm-opt %s --all-features --generate-global-effects --traps-never-happen --remove-unused-names --optimize-instructions -S -o - | filecheck %s --check-prefix TNH +;; RUN: wasm-opt %s --all-features --generate-global-effects --remove-unused-names --optimize-instructions --vacuum -S -o - | filecheck %s --check-prefix VACUUM + +(module + ;; NO-TNH: (type $sig-none (func)) + + ;; NO-TNH: (type $cont-none (cont $sig-none)) + + ;; NO-TNH: (type $sig-binary (func (param i32 i32) (result i32))) + + ;; NO-TNH: (type $cont-binary (cont $sig-binary)) + + ;; NO-TNH: (type $sig-any (func (param (ref any)) (result (ref any)))) + + ;; NO-TNH: (type $sig-unary (func (param i32) (result i32))) + + ;; NO-TNH: (type $cont-any (cont $sig-any)) + + ;; NO-TNH: (import "env" "imported" (func $imported (type $sig-none))) + ;; TNH: (type $sig-none (func)) + + ;; TNH: (type $cont-none (cont $sig-none)) + + ;; TNH: (type $sig-binary (func (param i32 i32) (result i32))) + + ;; TNH: (type $cont-binary (cont $sig-binary)) + + ;; TNH: (type $sig-any (func (param (ref any)) (result (ref any)))) + + ;; TNH: (type $sig-unary (func (param i32) (result i32))) + + ;; TNH: (type $cont-any (cont $sig-any)) + + ;; TNH: (import "env" "imported" (func $imported (type $sig-none))) + ;; VACUUM: (type $sig-none (func)) + + ;; VACUUM: (type $cont-none (cont $sig-none)) + + ;; VACUUM: (type $sig-binary (func (param i32 i32) (result i32))) + + ;; VACUUM: (type $cont-binary (cont $sig-binary)) + + ;; VACUUM: (type $sig-any (func (param (ref any)) (result (ref any)))) + + ;; VACUUM: (type $sig-unary (func (param i32) (result i32))) + + ;; VACUUM: (type $cont-any (cont $sig-any)) + + ;; VACUUM: (import "env" "imported" (func $imported (type $sig-none))) + (import "env" "imported" (func $imported (type $sig-none))) + + ;; NO-TNH: (global $counter (mut i32) (i32.const 0)) + + ;; NO-TNH: (tag $tag (type $sig-none)) + ;; TNH: (global $counter (mut i32) (i32.const 0)) + + ;; TNH: (tag $tag (type $sig-none)) + ;; VACUUM: (global $counter (mut i32) (i32.const 0)) + + ;; VACUUM: (tag $tag (type $sig-none)) + (tag $tag) + ;; NO-TNH: (tag $tag-i32 (type $9) (param i32)) + ;; TNH: (tag $tag-i32 (type $9) (param i32)) + ;; VACUUM: (tag $tag-i32 (type $9) (param i32)) + (tag $tag-i32 (param i32)) + + (type $sig-none (func)) + (type $sig-binary (func (param i32 i32) (result i32))) + (type $sig-unary (func (param i32) (result i32))) + (type $sig-any (func (param (ref any)) (result (ref any)))) + + (type $cont-none (cont $sig-none)) + (type $cont-binary (cont $sig-binary)) + (type $cont-unary (cont $sig-unary)) + (type $cont-any (cont $sig-any)) + + (global $counter (mut i32) (i32.const 0)) + + ;; NO-TNH: (func $pure (type $sig-none) + ;; NO-TNH-NEXT: (nop) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $pure (type $sig-none) + ;; TNH-NEXT: (nop) + ;; TNH-NEXT: ) + ;; VACUUM: (func $pure (type $sig-none) + ;; VACUUM-NEXT: (nop) + ;; VACUUM-NEXT: ) + (func $pure (type $sig-none) + (nop) + ) + + ;; NO-TNH: (func $pure-binary (type $sig-binary) (param $x i32) (param $y i32) (result i32) + ;; NO-TNH-NEXT: (i32.add + ;; NO-TNH-NEXT: (local.get $x) + ;; NO-TNH-NEXT: (local.get $y) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $pure-binary (type $sig-binary) (param $x i32) (param $y i32) (result i32) + ;; TNH-NEXT: (i32.add + ;; TNH-NEXT: (local.get $x) + ;; TNH-NEXT: (local.get $y) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $pure-binary (type $sig-binary) (param $x i32) (param $y i32) (result i32) + ;; VACUUM-NEXT: (i32.add + ;; VACUUM-NEXT: (local.get $x) + ;; VACUUM-NEXT: (local.get $y) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $pure-binary (type $sig-binary) (param $x i32) (param $y i32) (result i32) + (i32.add (local.get $x) (local.get $y)) + ) + + ;; NO-TNH: (func $pure-any (type $sig-any) (param $x (ref any)) (result (ref any)) + ;; NO-TNH-NEXT: (local.get $x) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $pure-any (type $sig-any) (param $x (ref any)) (result (ref any)) + ;; TNH-NEXT: (local.get $x) + ;; TNH-NEXT: ) + ;; VACUUM: (func $pure-any (type $sig-any) (param $x (ref any)) (result (ref any)) + ;; VACUUM-NEXT: (local.get $x) + ;; VACUUM-NEXT: ) + (func $pure-any (type $sig-any) (param $x (ref any)) (result (ref any)) + (local.get $x) + ) + + ;; NO-TNH: (func $suspending (type $sig-none) + ;; NO-TNH-NEXT: (suspend $tag) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $suspending (type $sig-none) + ;; TNH-NEXT: (suspend $tag) + ;; TNH-NEXT: ) + ;; VACUUM: (func $suspending (type $sig-none) + ;; VACUUM-NEXT: (suspend $tag) + ;; VACUUM-NEXT: ) + (func $suspending (type $sig-none) + (suspend $tag) + ) + + ;; NO-TNH: (func $caller-of-suspending (type $sig-none) + ;; NO-TNH-NEXT: (call $suspending) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $caller-of-suspending (type $sig-none) + ;; TNH-NEXT: (call $suspending) + ;; TNH-NEXT: ) + ;; VACUUM: (func $caller-of-suspending (type $sig-none) + ;; VACUUM-NEXT: (call $suspending) + ;; VACUUM-NEXT: ) + (func $caller-of-suspending (type $sig-none) + (call $suspending) + ) + + ;; NO-TNH: (func $same-sig-pure (type $sig-unary) (param $x i32) (result i32) + ;; NO-TNH-NEXT: (local.get $x) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $same-sig-pure (type $sig-unary) (param $x i32) (result i32) + ;; TNH-NEXT: (local.get $x) + ;; TNH-NEXT: ) + ;; VACUUM: (func $same-sig-pure (type $sig-unary) (param $x i32) (result i32) + ;; VACUUM-NEXT: (local.get $x) + ;; VACUUM-NEXT: ) + (func $same-sig-pure (type $sig-unary) (param $x i32) (result i32) + (local.get $x) + ) + + ;; NO-TNH: (func $same-sig-suspending (type $sig-unary) (param $x i32) (result i32) + ;; NO-TNH-NEXT: (suspend $tag-i32 + ;; NO-TNH-NEXT: (local.get $x) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (local.get $x) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $same-sig-suspending (type $sig-unary) (param $x i32) (result i32) + ;; TNH-NEXT: (suspend $tag-i32 + ;; TNH-NEXT: (local.get $x) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (local.get $x) + ;; TNH-NEXT: ) + ;; VACUUM: (func $same-sig-suspending (type $sig-unary) (param $x i32) (result i32) + ;; VACUUM-NEXT: (suspend $tag-i32 + ;; VACUUM-NEXT: (local.get $x) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (local.get $x) + ;; VACUUM-NEXT: ) + (func $same-sig-suspending (type $sig-unary) (param $x i32) (result i32) + (suspend $tag-i32 (local.get $x)) + (local.get $x) + ) + + ;; NO-TNH: (func $side-effect-1 (type $2) (result i32) + ;; NO-TNH-NEXT: (global.set $counter + ;; NO-TNH-NEXT: (i32.add + ;; NO-TNH-NEXT: (global.get $counter) + ;; NO-TNH-NEXT: (i32.const 1) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (i32.const 10) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $side-effect-1 (type $2) (result i32) + ;; TNH-NEXT: (global.set $counter + ;; TNH-NEXT: (i32.add + ;; TNH-NEXT: (global.get $counter) + ;; TNH-NEXT: (i32.const 1) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (i32.const 10) + ;; TNH-NEXT: ) + ;; VACUUM: (func $side-effect-1 (type $2) (result i32) + ;; VACUUM-NEXT: (global.set $counter + ;; VACUUM-NEXT: (i32.add + ;; VACUUM-NEXT: (global.get $counter) + ;; VACUUM-NEXT: (i32.const 1) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (i32.const 10) + ;; VACUUM-NEXT: ) + (func $side-effect-1 (result i32) + (global.set $counter (i32.add (global.get $counter) (i32.const 1))) + (i32.const 10) + ) + + ;; NO-TNH: (func $side-effect-2 (type $2) (result i32) + ;; NO-TNH-NEXT: (global.set $counter + ;; NO-TNH-NEXT: (i32.add + ;; NO-TNH-NEXT: (global.get $counter) + ;; NO-TNH-NEXT: (i32.const 2) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (i32.const 20) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $side-effect-2 (type $2) (result i32) + ;; TNH-NEXT: (global.set $counter + ;; TNH-NEXT: (i32.add + ;; TNH-NEXT: (global.get $counter) + ;; TNH-NEXT: (i32.const 2) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (i32.const 20) + ;; TNH-NEXT: ) + ;; VACUUM: (func $side-effect-2 (type $2) (result i32) + ;; VACUUM-NEXT: (global.set $counter + ;; VACUUM-NEXT: (i32.add + ;; VACUUM-NEXT: (global.get $counter) + ;; VACUUM-NEXT: (i32.const 2) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (i32.const 20) + ;; VACUUM-NEXT: ) + (func $side-effect-2 (result i32) + (global.set $counter (i32.add (global.get $counter) (i32.const 2))) + (i32.const 20) + ) + + ;; NO-TNH: (func $side-effect-3 (type $sig-none) + ;; NO-TNH-NEXT: (global.set $counter + ;; NO-TNH-NEXT: (i32.add + ;; NO-TNH-NEXT: (global.get $counter) + ;; NO-TNH-NEXT: (i32.const 3) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $side-effect-3 (type $sig-none) + ;; TNH-NEXT: (global.set $counter + ;; TNH-NEXT: (i32.add + ;; TNH-NEXT: (global.get $counter) + ;; TNH-NEXT: (i32.const 3) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $side-effect-3 (type $sig-none) + ;; VACUUM-NEXT: (global.set $counter + ;; VACUUM-NEXT: (i32.add + ;; VACUUM-NEXT: (global.get $counter) + ;; VACUUM-NEXT: (i32.const 3) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $side-effect-3 + (global.set $counter (i32.add (global.get $counter) (i32.const 3))) + ) + + ;; NO-TNH: (func $test-resume-null (type $sig-none) + ;; NO-TNH-NEXT: (unreachable) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-resume-null (type $sig-none) + ;; TNH-NEXT: (unreachable) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-resume-null (type $sig-none) + ;; VACUUM-NEXT: (unreachable) + ;; VACUUM-NEXT: ) + (func $test-resume-null + ;; Resume of null traps: replaced with unreachable. + (resume $cont-none + (ref.null $cont-none) + ) + ) + + ;; NO-TNH: (func $test-resume-null-operands (type $sig-none) + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (block + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (call $side-effect-1) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (call $side-effect-2) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (unreachable) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-resume-null-operands (type $sig-none) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (block + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (call $side-effect-1) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (call $side-effect-2) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (unreachable) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-resume-null-operands (type $sig-none) + ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (block + ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (call $side-effect-1) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (call $side-effect-2) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (unreachable) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-resume-null-operands + ;; Operands are evaluated and dropped before the null trap. + (resume $cont-binary + (call $side-effect-1) + (call $side-effect-2) + (ref.null $cont-binary) + ) + (drop) + ) + + ;; NO-TNH: (func $test-resume-skip-non-null-cast (type $10) (param $c (ref null $cont-none)) + ;; NO-TNH-NEXT: (resume $cont-none + ;; NO-TNH-NEXT: (local.get $c) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-resume-skip-non-null-cast (type $10) (param $c (ref null $cont-none)) + ;; TNH-NEXT: (resume $cont-none + ;; TNH-NEXT: (local.get $c) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-resume-skip-non-null-cast (type $10) (param $c (ref null $cont-none)) + ;; VACUUM-NEXT: (resume $cont-none + ;; VACUUM-NEXT: (local.get $c) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-resume-skip-non-null-cast (param $c (ref null $cont-none)) + ;; Redundant ref.as_non_null on continuation is stripped because resume + ;; traps on null. + (resume $cont-none + (ref.as_non_null + (local.get $c) + ) + ) + ) + + ;; NO-TNH: (func $test-resume-null-arm-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; NO-TNH-NEXT: (resume $cont-none + ;; NO-TNH-NEXT: (if (result (ref null $cont-none)) + ;; NO-TNH-NEXT: (local.get $cond) + ;; NO-TNH-NEXT: (then + ;; NO-TNH-NEXT: (ref.null nocont) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (else + ;; NO-TNH-NEXT: (local.get $c) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-resume-null-arm-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; TNH-NEXT: (resume $cont-none + ;; TNH-NEXT: (block (result (ref $cont-none)) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (local.get $cond) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (local.get $c) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-resume-null-arm-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; VACUUM-NEXT: (resume $cont-none + ;; VACUUM-NEXT: (if (result (ref null $cont-none)) + ;; VACUUM-NEXT: (local.get $cond) + ;; VACUUM-NEXT: (then + ;; VACUUM-NEXT: (ref.null nocont) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (else + ;; VACUUM-NEXT: (local.get $c) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-resume-null-arm-tnh (param $cond i32) (param $c (ref $cont-none)) + ;; Under TNH, if-arms that flow out null are assumed never taken. + (resume $cont-none + (if (result (ref null $cont-none)) + (local.get $cond) + (then + (ref.null $cont-none) + ) + (else + (local.get $c) + ) + ) + ) + ) + + ;; NO-TNH: (func $test-resume-null-select-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; NO-TNH-NEXT: (resume $cont-none + ;; NO-TNH-NEXT: (select (result (ref null $cont-none)) + ;; NO-TNH-NEXT: (ref.null nocont) + ;; NO-TNH-NEXT: (local.get $c) + ;; NO-TNH-NEXT: (local.get $cond) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-resume-null-select-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; TNH-NEXT: (resume $cont-none + ;; TNH-NEXT: (block (result (ref $cont-none)) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (ref.null nocont) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (block (result (ref $cont-none)) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (local.get $cond) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (local.get $c) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-resume-null-select-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; VACUUM-NEXT: (resume $cont-none + ;; VACUUM-NEXT: (select (result (ref null $cont-none)) + ;; VACUUM-NEXT: (ref.null nocont) + ;; VACUUM-NEXT: (local.get $c) + ;; VACUUM-NEXT: (local.get $cond) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-resume-null-select-tnh (param $cond i32) (param $c (ref $cont-none)) + ;; Under TNH, select branches that flow out null are assumed never taken. + (resume $cont-none + (select (result (ref null $cont-none)) + (ref.null $cont-none) + (local.get $c) + (local.get $cond) + ) + ) + ) + + ;; NO-TNH: (func $test-basic (type $sig-none) + ;; NO-TNH-NEXT: (call $pure) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-basic (type $sig-none) + ;; TNH-NEXT: (call $pure) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-basic (type $sig-none) + ;; VACUUM-NEXT: (nop) + ;; VACUUM-NEXT: ) + (func $test-basic + ;; Basic rewrite: resume of non-suspending continuation becomes direct + ;; call. + (resume $cont-none + (cont.new $cont-none (ref.func $pure)) + ) + ) + + ;; NO-TNH: (func $test-binary (type $2) (result i32) + ;; NO-TNH-NEXT: (call $pure-binary + ;; NO-TNH-NEXT: (i32.const 1) + ;; NO-TNH-NEXT: (i32.const 2) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-binary (type $2) (result i32) + ;; TNH-NEXT: (call $pure-binary + ;; TNH-NEXT: (i32.const 1) + ;; TNH-NEXT: (i32.const 2) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-binary (type $2) (result i32) + ;; VACUUM-NEXT: (call $pure-binary + ;; VACUUM-NEXT: (i32.const 1) + ;; VACUUM-NEXT: (i32.const 2) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-binary (result i32) + ;; Operands and results: passed cleanly to the direct call. + (resume $cont-binary + (i32.const 1) + (i32.const 2) + (cont.new $cont-binary (ref.func $pure-binary)) + ) + ) + + ;; NO-TNH: (func $test-fallthrough-block (type $sig-none) + ;; NO-TNH-NEXT: (call $pure) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-fallthrough-block (type $sig-none) + ;; TNH-NEXT: (call $pure) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-fallthrough-block (type $sig-none) + ;; VACUUM-NEXT: (nop) + ;; VACUUM-NEXT: ) + (func $test-fallthrough-block + ;; Fallthrough cases: straight-line blocks and ref.cast. + (resume $cont-none + (block (result (ref $cont-none)) + (cont.new $cont-none + (block (result (ref $sig-none)) + (ref.func $pure) + ) + ) + ) + ) + ) + + ;; NO-TNH: (func $test-fallthrough-cast (type $sig-none) + ;; NO-TNH-NEXT: (call $pure) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-fallthrough-cast (type $sig-none) + ;; TNH-NEXT: (call $pure) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-fallthrough-cast (type $sig-none) + ;; VACUUM-NEXT: (nop) + ;; VACUUM-NEXT: ) + (func $test-fallthrough-cast + (resume $cont-none + (cont.new $cont-none + (ref.cast (ref $sig-none) + (ref.func $pure) + ) + ) + ) + ) + + ;; NO-TNH: (func $test-tee (type $11) (result (ref $cont-none)) + ;; NO-TNH-NEXT: (local $c (ref null $cont-none)) + ;; NO-TNH-NEXT: (resume $cont-none + ;; NO-TNH-NEXT: (local.tee $c + ;; NO-TNH-NEXT: (cont.new $cont-none + ;; NO-TNH-NEXT: (ref.func $pure) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (ref.as_non_null + ;; NO-TNH-NEXT: (local.get $c) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-tee (type $11) (result (ref $cont-none)) + ;; TNH-NEXT: (local $c (ref null $cont-none)) + ;; TNH-NEXT: (block + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (local.tee $c + ;; TNH-NEXT: (cont.new $cont-none + ;; TNH-NEXT: (ref.func $pure) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (call $pure) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (ref.as_non_null + ;; TNH-NEXT: (local.get $c) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-tee (type $11) (result (ref $cont-none)) + ;; VACUUM-NEXT: (local $c (ref null $cont-none)) + ;; VACUUM-NEXT: (resume $cont-none + ;; VACUUM-NEXT: (local.tee $c + ;; VACUUM-NEXT: (cont.new $cont-none + ;; VACUUM-NEXT: (ref.func $pure) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (ref.as_non_null + ;; VACUUM-NEXT: (local.get $c) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-tee (result (ref $cont-none)) + ;; trapsNeverHappen: local.tee is only looked through with TNH. + (local $c (ref null $cont-none)) + (resume $cont-none + (local.tee $c + (cont.new $cont-none (ref.func $pure)) + ) + ) + (ref.as_non_null (local.get $c)) + ) + + ;; NO-TNH: (func $test-tee-params (type $12) (result (ref $cont-binary)) + ;; NO-TNH-NEXT: (local $c (ref null $cont-binary)) + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (resume $cont-binary + ;; NO-TNH-NEXT: (i32.const 1) + ;; NO-TNH-NEXT: (i32.const 2) + ;; NO-TNH-NEXT: (local.tee $c + ;; NO-TNH-NEXT: (cont.new $cont-binary + ;; NO-TNH-NEXT: (ref.func $pure-binary) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (ref.as_non_null + ;; NO-TNH-NEXT: (local.get $c) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-tee-params (type $12) (result (ref $cont-binary)) + ;; TNH-NEXT: (local $c (ref null $cont-binary)) + ;; TNH-NEXT: (local $1 i32) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (call $pure-binary + ;; TNH-NEXT: (i32.const 1) + ;; TNH-NEXT: (block (result i32) + ;; TNH-NEXT: (local.set $1 + ;; TNH-NEXT: (i32.const 2) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (local.tee $c + ;; TNH-NEXT: (cont.new $cont-binary + ;; TNH-NEXT: (ref.func $pure-binary) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (local.get $1) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (ref.as_non_null + ;; TNH-NEXT: (local.get $c) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-tee-params (type $12) (result (ref $cont-binary)) + ;; VACUUM-NEXT: (local $c (ref null $cont-binary)) + ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (resume $cont-binary + ;; VACUUM-NEXT: (i32.const 1) + ;; VACUUM-NEXT: (i32.const 2) + ;; VACUUM-NEXT: (local.tee $c + ;; VACUUM-NEXT: (cont.new $cont-binary + ;; VACUUM-NEXT: (ref.func $pure-binary) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (ref.as_non_null + ;; VACUUM-NEXT: (local.get $c) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-tee-params (result (ref $cont-binary)) + ;; trapsNeverHappen: local.tee with operands is only looked through with + ;; TNH. + (local $c (ref null $cont-binary)) + (drop + (resume $cont-binary + (i32.const 1) + (i32.const 2) + (local.tee $c + (cont.new $cont-binary (ref.func $pure-binary)) + ) + ) + ) + (ref.as_non_null (local.get $c)) + ) + + ;; NO-TNH: (func $test-negative-suspend (type $sig-none) + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (block $on-suspend (result (ref $cont-none)) + ;; NO-TNH-NEXT: (resume $cont-none (on $tag $on-suspend) + ;; NO-TNH-NEXT: (cont.new $cont-none + ;; NO-TNH-NEXT: (ref.func $suspending) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (unreachable) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-negative-suspend (type $sig-none) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (block $on-suspend (result (ref $cont-none)) + ;; TNH-NEXT: (resume $cont-none (on $tag $on-suspend) + ;; TNH-NEXT: (cont.new $cont-none + ;; TNH-NEXT: (ref.func $suspending) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (unreachable) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-negative-suspend (type $sig-none) + ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (block $on-suspend (result (ref $cont-none)) + ;; VACUUM-NEXT: (resume $cont-none (on $tag $on-suspend) + ;; VACUUM-NEXT: (cont.new $cont-none + ;; VACUUM-NEXT: (ref.func $suspending) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (unreachable) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-negative-suspend + ;; Negative test: continuation function that suspends cannot be converted. + (block $on-suspend (result (ref $cont-none)) + (resume $cont-none (on $tag $on-suspend) + (cont.new $cont-none (ref.func $suspending)) + ) + (unreachable) + ) + (drop) + ) + + ;; NO-TNH: (func $test-negative-transitive-suspend (type $sig-none) + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (block $on-suspend (result (ref $cont-none)) + ;; NO-TNH-NEXT: (resume $cont-none (on $tag $on-suspend) + ;; NO-TNH-NEXT: (cont.new $cont-none + ;; NO-TNH-NEXT: (ref.func $caller-of-suspending) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (unreachable) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-negative-transitive-suspend (type $sig-none) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (block $on-suspend (result (ref $cont-none)) + ;; TNH-NEXT: (resume $cont-none (on $tag $on-suspend) + ;; TNH-NEXT: (cont.new $cont-none + ;; TNH-NEXT: (ref.func $caller-of-suspending) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (unreachable) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-negative-transitive-suspend (type $sig-none) + ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (block $on-suspend (result (ref $cont-none)) + ;; VACUUM-NEXT: (resume $cont-none (on $tag $on-suspend) + ;; VACUUM-NEXT: (cont.new $cont-none + ;; VACUUM-NEXT: (ref.func $caller-of-suspending) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (unreachable) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-negative-transitive-suspend + ;; Negative test: continuation function calling a suspending function. + (block $on-suspend (result (ref $cont-none)) + (resume $cont-none (on $tag $on-suspend) + (cont.new $cont-none (ref.func $caller-of-suspending)) + ) + (unreachable) + ) + (drop) + ) + + ;; NO-TNH: (func $test-same-sig (type $2) (result i32) + ;; NO-TNH-NEXT: (call $same-sig-pure + ;; NO-TNH-NEXT: (i32.const 42) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-same-sig (type $2) (result i32) + ;; TNH-NEXT: (call $same-sig-pure + ;; TNH-NEXT: (i32.const 42) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-same-sig (type $2) (result i32) + ;; VACUUM-NEXT: (call $same-sig-pure + ;; VACUUM-NEXT: (i32.const 42) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-same-sig (result i32) + ;; Precision test: function $f does not suspend, but another function with + ;; the same signature does. Verify $f is still converted. + (resume $cont-unary + (i32.const 42) + (cont.new $cont-unary (ref.func $same-sig-pure)) + ) + ) + + ;; NO-TNH: (func $test-eval-order (type $2) (result i32) + ;; NO-TNH-NEXT: (local $0 i32) + ;; NO-TNH-NEXT: (call $pure-binary + ;; NO-TNH-NEXT: (call $side-effect-1) + ;; NO-TNH-NEXT: (block (result i32) + ;; NO-TNH-NEXT: (local.set $0 + ;; NO-TNH-NEXT: (call $side-effect-2) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (block (result (ref $cont-binary)) + ;; NO-TNH-NEXT: (call $side-effect-3) + ;; NO-TNH-NEXT: (cont.new $cont-binary + ;; NO-TNH-NEXT: (ref.func $pure-binary) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (local.get $0) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-eval-order (type $2) (result i32) + ;; TNH-NEXT: (local $0 i32) + ;; TNH-NEXT: (call $pure-binary + ;; TNH-NEXT: (call $side-effect-1) + ;; TNH-NEXT: (block (result i32) + ;; TNH-NEXT: (local.set $0 + ;; TNH-NEXT: (call $side-effect-2) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (block (result (ref $cont-binary)) + ;; TNH-NEXT: (call $side-effect-3) + ;; TNH-NEXT: (cont.new $cont-binary + ;; TNH-NEXT: (ref.func $pure-binary) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (local.get $0) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-eval-order (type $2) (result i32) + ;; VACUUM-NEXT: (local $0 i32) + ;; VACUUM-NEXT: (call $pure-binary + ;; VACUUM-NEXT: (call $side-effect-1) + ;; VACUUM-NEXT: (block (result i32) + ;; VACUUM-NEXT: (local.set $0 + ;; VACUUM-NEXT: (call $side-effect-2) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (block (result (ref (exact $cont-binary))) + ;; VACUUM-NEXT: (call $side-effect-3) + ;; VACUUM-NEXT: (cont.new $cont-binary + ;; VACUUM-NEXT: (ref.func $pure-binary) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (local.get $0) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-eval-order (result i32) + ;; Evaluation order test: side effects in continuation expression execute + ;; after operands and before the direct call. + (resume $cont-binary + (call $side-effect-1) + (call $side-effect-2) + (block (result (ref $cont-binary)) + (call $side-effect-3) + (cont.new $cont-binary (ref.func $pure-binary)) + ) + ) + ) + + ;; NO-TNH: (func $test-handler-elimination (type $sig-none) + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (block $handler (result (ref $cont-none)) + ;; NO-TNH-NEXT: (call $pure) + ;; NO-TNH-NEXT: (return) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-handler-elimination (type $sig-none) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (block $handler (result (ref $cont-none)) + ;; TNH-NEXT: (call $pure) + ;; TNH-NEXT: (return) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-handler-elimination (type $sig-none) + ;; VACUUM-NEXT: (nop) + ;; VACUUM-NEXT: ) + (func $test-handler-elimination + ;; Handler elimination test: dead handler blocks are removed in full + ;; pipeline. + (block $handler (result (ref $cont-none)) + (resume $cont-none (on $tag $handler) + (cont.new $cont-none (ref.func $pure)) + ) + (return) + ) + (drop) + ) + + ;; NO-TNH: (func $test-unreachable-operand (type $sig-none) + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (resume $cont-binary + ;; NO-TNH-NEXT: (unreachable) + ;; NO-TNH-NEXT: (i32.const 1) + ;; NO-TNH-NEXT: (cont.new $cont-binary + ;; NO-TNH-NEXT: (ref.func $pure-binary) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-unreachable-operand (type $sig-none) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (resume $cont-binary + ;; TNH-NEXT: (unreachable) + ;; TNH-NEXT: (i32.const 1) + ;; TNH-NEXT: (cont.new $cont-binary + ;; TNH-NEXT: (ref.func $pure-binary) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-unreachable-operand (type $sig-none) + ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (resume $cont-binary + ;; VACUUM-NEXT: (unreachable) + ;; VACUUM-NEXT: (i32.const 1) + ;; VACUUM-NEXT: (cont.new $cont-binary + ;; VACUUM-NEXT: (ref.func $pure-binary) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-unreachable-operand + ;; Unreachable operands or continuation: resume is unreachable, left for + ;; DCE. + (resume $cont-binary + (unreachable) + (i32.const 1) + (cont.new $cont-binary (ref.func $pure-binary)) + ) + (drop) + ) + + ;; NO-TNH: (func $test-unreachable-cont (type $sig-none) + ;; NO-TNH-NEXT: (block ;; (replaces unreachable Resume we can't emit) + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (unreachable) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (unreachable) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-unreachable-cont (type $sig-none) + ;; TNH-NEXT: (block ;; (replaces unreachable Resume we can't emit) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (unreachable) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (unreachable) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-unreachable-cont (type $sig-none) + ;; VACUUM-NEXT: (block ;; (replaces unreachable Resume we can't emit) + ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (unreachable) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (unreachable) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-unreachable-cont + ;; Unreachable continuation: resume is unreachable, left for DCE. + (resume $cont-none + (unreachable) + ) + ) + + ;; NO-TNH: (func $test-negative-imported (type $sig-none) + ;; NO-TNH-NEXT: (resume $cont-none + ;; NO-TNH-NEXT: (cont.new $cont-none + ;; NO-TNH-NEXT: (ref.func $imported) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-negative-imported (type $sig-none) + ;; TNH-NEXT: (resume $cont-none + ;; TNH-NEXT: (cont.new $cont-none + ;; TNH-NEXT: (ref.func $imported) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-negative-imported (type $sig-none) + ;; VACUUM-NEXT: (resume $cont-none + ;; VACUUM-NEXT: (cont.new $cont-none + ;; VACUUM-NEXT: (ref.func $imported) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-negative-imported + ;; Negative test: continuation function is imported and cannot be proven + ;; non-suspending. + (resume $cont-none + (cont.new $cont-none (ref.func $imported)) + ) + ) + + ;; NO-TNH: (func $test-handler-switch (type $sig-none) + ;; NO-TNH-NEXT: (call $pure) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-handler-switch (type $sig-none) + ;; TNH-NEXT: (call $pure) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-handler-switch (type $sig-none) + ;; VACUUM-NEXT: (nop) + ;; VACUUM-NEXT: ) + (func $test-handler-switch + ;; Handler elimination test: switch handlers are also eliminated when + ;; directizing. + (resume $cont-none (on $tag switch) + (cont.new $cont-none (ref.func $pure)) + ) + ) + + ;; NO-TNH: (func $test-eval-order-no-operands (type $sig-none) + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (block (result (ref $cont-none)) + ;; NO-TNH-NEXT: (call $side-effect-3) + ;; NO-TNH-NEXT: (cont.new $cont-none + ;; NO-TNH-NEXT: (ref.func $pure) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (call $pure) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-eval-order-no-operands (type $sig-none) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (block (result (ref $cont-none)) + ;; TNH-NEXT: (call $side-effect-3) + ;; TNH-NEXT: (cont.new $cont-none + ;; TNH-NEXT: (ref.func $pure) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (call $pure) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-eval-order-no-operands (type $sig-none) + ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (block (result (ref (exact $cont-none))) + ;; VACUUM-NEXT: (call $side-effect-3) + ;; VACUUM-NEXT: (cont.new $cont-none + ;; VACUUM-NEXT: (ref.func $pure) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-eval-order-no-operands + ;; Evaluation order test: side effects in continuation expression execute + ;; before the direct call even when there are no operands. + (resume $cont-none + (block (result (ref $cont-none)) + (call $side-effect-3) + (cont.new $cont-none (ref.func $pure)) + ) + ) + ) + + ;; NO-TNH: (func $test-eval-order-non-nullable (type $sig-any) (param $x (ref any)) (result (ref any)) + ;; NO-TNH-NEXT: (local $1 (ref any)) + ;; NO-TNH-NEXT: (call $pure-any + ;; NO-TNH-NEXT: (block (result (ref any)) + ;; NO-TNH-NEXT: (local.set $1 + ;; NO-TNH-NEXT: (local.get $x) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (block (result (ref $cont-any)) + ;; NO-TNH-NEXT: (call $side-effect-3) + ;; NO-TNH-NEXT: (cont.new $cont-any + ;; NO-TNH-NEXT: (ref.func $pure-any) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (local.get $1) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; TNH: (func $test-eval-order-non-nullable (type $sig-any) (param $x (ref any)) (result (ref any)) + ;; TNH-NEXT: (local $1 (ref any)) + ;; TNH-NEXT: (call $pure-any + ;; TNH-NEXT: (block (result (ref any)) + ;; TNH-NEXT: (local.set $1 + ;; TNH-NEXT: (local.get $x) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (block (result (ref $cont-any)) + ;; TNH-NEXT: (call $side-effect-3) + ;; TNH-NEXT: (cont.new $cont-any + ;; TNH-NEXT: (ref.func $pure-any) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (local.get $1) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; VACUUM: (func $test-eval-order-non-nullable (type $sig-any) (param $x (ref any)) (result (ref any)) + ;; VACUUM-NEXT: (local $1 (ref any)) + ;; VACUUM-NEXT: (call $pure-any + ;; VACUUM-NEXT: (block (result (ref any)) + ;; VACUUM-NEXT: (local.set $1 + ;; VACUUM-NEXT: (local.get $x) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (block (result (ref (exact $cont-any))) + ;; VACUUM-NEXT: (call $side-effect-3) + ;; VACUUM-NEXT: (cont.new $cont-any + ;; VACUUM-NEXT: (ref.func $pure-any) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (local.get $1) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + (func $test-eval-order-non-nullable (param $x (ref any)) (result (ref any)) + ;; Evaluation order test: non-nullable operands are preserved when spilled. + (resume $cont-any + (local.get $x) + (block (result (ref $cont-any)) + (call $side-effect-3) + (cont.new $cont-any (ref.func $pure-any)) + ) + ) + ) + +) From 64465af1c668a628eda883b6fccf1181ec070b0a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 2 Sep 2026 15:30:13 -0700 Subject: [PATCH 3/7] address feedback --- src/ir/effects.cpp | 2 +- src/ir/effects.h | 41 ++++++++++------------------------- src/passes/GlobalEffects.cpp | 6 ++--- test/gtest/effects.cpp | 39 +++++++++++++++------------------ test/gtest/matchers/effects.h | 2 +- 5 files changed, 33 insertions(+), 57 deletions(-) diff --git a/src/ir/effects.cpp b/src/ir/effects.cpp index cbca3d74408..a8d36085f2b 100644 --- a/src/ir/effects.cpp +++ b/src/ir/effects.cpp @@ -96,7 +96,7 @@ std::ostream& operator<<(std::ostream& o, const EffectAnalyzer& effects) { if (effects.throws_) { o << "throws_\n"; } - if (effects.suspends_) { + if (effects.suspends) { o << "suspends_\n"; } if (effects.tryDepth) { diff --git a/src/ir/effects.h b/src/ir/effects.h index f6326d54c3f..674c5036aed 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -23,7 +23,6 @@ #include "ir/intrinsics.h" #include "pass.h" #include "support/name.h" -#include "support/utilities.h" #include "wasm-traversal.h" #include "wasm-type.h" #include "wasm.h" @@ -44,7 +43,7 @@ class EffectAnalyzer { readsMutableArray(false), writesArray(false), readsSharedMutableArray(false), writesSharedArray(false), trap(false), implicitTrap(false), throws_(false), danglingPop(false), - mayNotReturn(false), hasReturnCallThrow(false), suspends_(false), + mayNotReturn(false), hasReturnCallThrow(false), suspends(false), module(module), features(module.features) {} EffectAnalyzer(const PassOptions& passOptions, @@ -138,7 +137,7 @@ class EffectAnalyzer { // more here.) bool hasReturnCallThrow : 1; - bool suspends_ : 1; + bool suspends : 1; const Module& module; FeatureSet features; @@ -230,7 +229,7 @@ class EffectAnalyzer { return calls || readsSharedMutableArray || writesSharedArray; } bool throws() const { return throws_ || !delegateTargets.empty(); } - bool suspends() const { return suspends_; } + // Check whether this may transfer control flow to somewhere outside of this // expression (aside from just flowing out normally). That includes a break, // a throw (if the throw is not known to be caught inside this expression; @@ -240,25 +239,7 @@ class EffectAnalyzer { // transferred inside the function, but this expression does not know that), // or a suspension. bool transfersControlFlow() const { - return branchesOut || throws() || hasExternalBreakTargets() || suspends(); - } - - // Explicitly marks all global mutable state as clobbered (read and written). - void clobbersGlobalState() { - readsMemory = true; - writesMemory = true; - readsSharedMemory = true; - writesSharedMemory = true; - readsTable = true; - writesTable = true; - readsMutableStruct = true; - writesStruct = true; - readsSharedMutableStruct = true; - writesSharedStruct = true; - readsMutableArray = true; - writesArray = true; - readsSharedMutableArray = true; - writesSharedArray = true; + return branchesOut || suspends || throws() || hasExternalBreakTargets(); } // Changes something in globally-stored state. @@ -286,7 +267,7 @@ class EffectAnalyzer { bool hasNonTrapSideEffects() const { return localsWritten.size() > 0 || danglingPop || writesGlobalState() || throws() || transfersControlFlow() || hasSynchronization() || - mayNotReturn || suspends(); + mayNotReturn; } bool hasSideEffects() const { return trap || hasNonTrapSideEffects(); } @@ -502,7 +483,7 @@ class EffectAnalyzer { danglingPop = danglingPop || other.danglingPop; mayNotReturn = mayNotReturn || other.mayNotReturn; hasReturnCallThrow = hasReturnCallThrow || other.hasReturnCallThrow; - suspends_ = suspends_ || other.suspends_; + suspends = suspends || other.suspends; readOrder = std::max(readOrder, other.readOrder); writeOrder = std::max(writeOrder, other.writeOrder); @@ -1296,8 +1277,8 @@ class EffectAnalyzer { void visitSuspend(Suspend* curr) { // Suspending transfers control to an enclosing handler and executes // arbitrary other code before we may resume here. - parent.suspends_ = true; - parent.clobbersGlobalState(); + parent.suspends = true; + parent.calls = true; if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) { parent.throws_ = true; } @@ -1397,7 +1378,7 @@ class EffectAnalyzer { // If stack switching is enabled and we don't have global effects // information, assume that the call target may suspend. if (parent.features.hasStackSwitching()) { - parent.suspends_ = true; + parent.suspends = true; } } }; @@ -1489,7 +1470,7 @@ class EffectAnalyzer { if (danglingPop) { effects |= SideEffects::DanglingPop; } - if (suspends_) { + if (suspends) { effects |= SideEffects::Suspends; } return effects; @@ -1507,7 +1488,7 @@ class EffectAnalyzer { breakTargets.clear(); throws_ = false; delegateTargets.clear(); - suspends_ = false; + suspends = false; assert(!transfersControlFlow()); } diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index ec4ec1dfe03..e4e95a833f4 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -143,7 +143,7 @@ std::map analyzeFuncs(Module& module, // well. If we see something else that throws or suspends, below, then // we'll note that there. funcInfo.effects->throws_ = false; - funcInfo.effects->suspends_ = false; + funcInfo.effects->suspends = false; struct CallScanner : public PostWalker analyzeFuncs(Module& module, if (effects.throws_ && funcInfo.effects) { funcInfo.effects->throws_ = true; } - if (effects.suspends_ && funcInfo.effects) { - funcInfo.effects->suspends_ = true; + if (effects.suspends && funcInfo.effects) { + funcInfo.effects->suspends = true; } } } diff --git a/test/gtest/effects.cpp b/test/gtest/effects.cpp index f959085fea9..4b8ee187e69 100644 --- a/test/gtest/effects.cpp +++ b/test/gtest/effects.cpp @@ -54,31 +54,26 @@ TEST_F(EffectAnalyzerTest, Suspend) { EffectAnalyzer effects(options, wasm, func->body); // Suspension detection - EXPECT_TRUE(effects.suspends()); + EXPECT_TRUE(effects.suspends); EXPECT_THAT(&effects, Suspends()); EXPECT_TRUE(effects.getSideEffects() & EffectAnalyzer::SideEffects::Suspends); - // Decoupled from call graph edges - EXPECT_FALSE(effects.calls); - EXPECT_THAT(&effects, Not(Calls())); - - // Clobbers all global mutable state - EXPECT_TRUE(effects.readsMemory); - EXPECT_TRUE(effects.writesMemory); - EXPECT_TRUE(effects.readsSharedMemory); - EXPECT_TRUE(effects.writesSharedMemory); - EXPECT_TRUE(effects.readsTable); - EXPECT_TRUE(effects.writesTable); - EXPECT_TRUE(effects.readsMutableStruct); - EXPECT_TRUE(effects.writesStruct); - EXPECT_TRUE(effects.readsSharedMutableStruct); - EXPECT_TRUE(effects.writesSharedStruct); - EXPECT_TRUE(effects.readsMutableArray); - EXPECT_TRUE(effects.writesArray); - EXPECT_TRUE(effects.readsSharedMutableArray); - EXPECT_TRUE(effects.writesSharedArray); + // Suspending executes arbitrary other code in the handler before resuming, + // modeled as a call. + EXPECT_TRUE(effects.calls); + EXPECT_THAT(&effects, Calls()); + + // Accesses all global mutable state via calls + EXPECT_TRUE(effects.accessesMemory()); + EXPECT_TRUE(effects.accessesSharedMemory()); + EXPECT_TRUE(effects.accessesTable()); + EXPECT_TRUE(effects.accessesMutableStruct()); + EXPECT_TRUE(effects.accessesSharedMutableStruct()); + EXPECT_TRUE(effects.accessesArray()); + EXPECT_TRUE(effects.accessesSharedArray()); EXPECT_TRUE(effects.writesGlobalState()); EXPECT_TRUE(effects.readsMutableGlobalState()); + EXPECT_TRUE(effects.accessesSharedGlobalState()); // Control flow & side effect queries EXPECT_TRUE(effects.transfersControlFlow()); @@ -107,12 +102,12 @@ TEST_F(EffectAnalyzerTest, UnknownCall) { // suspension. wasm.features.setStackSwitching(true); EffectAnalyzer effectsWithStackSwitch(options, wasm, caller->body); - EXPECT_TRUE(effectsWithStackSwitch.suspends()); + EXPECT_TRUE(effectsWithStackSwitch.suspends); // With stack switching disabled, calls do not suspend. wasm.features.setStackSwitching(false); EffectAnalyzer effectsWithoutStackSwitch(options, wasm, caller->body); - EXPECT_FALSE(effectsWithoutStackSwitch.suspends()); + EXPECT_FALSE(effectsWithoutStackSwitch.suspends); } } // anonymous namespace diff --git a/test/gtest/matchers/effects.h b/test/gtest/matchers/effects.h index b0ed8bb544e..b1bbbad57dc 100644 --- a/test/gtest/matchers/effects.h +++ b/test/gtest/matchers/effects.h @@ -45,7 +45,7 @@ MATCHER(Throws, "") { return arg->throws_; } MATCHER(DanglingPop, "") { return arg->danglingPop; } MATCHER(MayNotReturn, "") { return arg->mayNotReturn; } MATCHER(HasReturnCallThrow, "") { return arg->hasReturnCallThrow; } -MATCHER(Suspends, "") { return arg->suspends(); } +MATCHER(Suspends, "") { return arg->suspends; } } // namespace wasm From cb448562d255838ba8f074dda24f96e20518b1b0 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 2 Sep 2026 17:42:48 -0700 Subject: [PATCH 4/7] disable stack switching on affected tests --- scripts/test/wasm2js.py | 19 +++++++++++-------- ...tion-handling_disable-stack-switching.txt} | 0 ...ion-handling_disable-stack-switching.wast} | 0 3 files changed, 11 insertions(+), 8 deletions(-) rename test/passes/{simplify-locals_all-features_disable-exception-handling.txt => simplify-locals_all-features_disable-exception-handling_disable-stack-switching.txt} (100%) rename test/passes/{simplify-locals_all-features_disable-exception-handling.wast => simplify-locals_all-features_disable-exception-handling_disable-stack-switching.wast} (100%) diff --git a/scripts/test/wasm2js.py b/scripts/test/wasm2js.py index 76b788841e9..41f53f7722d 100644 --- a/scripts/test/wasm2js.py +++ b/scripts/test/wasm2js.py @@ -88,10 +88,11 @@ def test_wasm2js_output(): for module, asserts in support.split_wast(t): support.write_wast('split.wast', module, asserts) - # wasm2js does not yet support EH, and enabling it can reduce - # optimization opportunities + # wasm2js does not yet support EH or stack switching, and + # enabling them can reduce optimization opportunities cmd = shared.WASM2JS + ['split.wast', '-all', - '--disable-exception-handling'] + '--disable-exception-handling', + '--disable-stack-switching'] if opt: cmd += ['-O'] if 'emscripten' in basename: @@ -150,7 +151,8 @@ def test_asserts_output(): wasm = os.path.join(shared.get_test_dir('wasm2js'), wasm) cmd = shared.WASM2JS + [wasm, '--allow-asserts', '-all', - '--disable-exception-handling'] + '--disable-exception-handling', + '--disable-stack-switching'] out = support.run_command(cmd) shared.fail_if_not_identical_to_file(out, asserts_expected_file) @@ -201,10 +203,11 @@ def update_wasm2js_tests(): for module, asserts in support.split_wast(t): support.write_wast('split.wast', module, asserts) - # wasm2js does not yet support EH, and enable it can reduce - # optimization opportunities + # wasm2js does not yet support EH or stack switching, and + # enabling them can reduce optimization opportunities cmd = shared.WASM2JS + ['split.wast', '-all', - '--disable-exception-handling'] + '--disable-exception-handling', + '--disable-stack-switching'] if opt: cmd += ['-O'] if 'emscripten' in basename: @@ -225,7 +228,7 @@ def update_wasm2js_tests(): asserts_expected_file = os.path.join(shared.options.binaryen_test, 'wasm2js', asserts) traps_expected_file = os.path.join(shared.options.binaryen_test, 'wasm2js', traps) - cmd = shared.WASM2JS + [os.path.join(shared.get_test_dir('wasm2js'), wasm), '--allow-asserts', '-all', '--disable-exception-handling'] + cmd = shared.WASM2JS + [os.path.join(shared.get_test_dir('wasm2js'), wasm), '--allow-asserts', '-all', '--disable-exception-handling', '--disable-stack-switching'] out = support.run_command(cmd) with open(asserts_expected_file, 'w') as o: o.write(out) diff --git a/test/passes/simplify-locals_all-features_disable-exception-handling.txt b/test/passes/simplify-locals_all-features_disable-exception-handling_disable-stack-switching.txt similarity index 100% rename from test/passes/simplify-locals_all-features_disable-exception-handling.txt rename to test/passes/simplify-locals_all-features_disable-exception-handling_disable-stack-switching.txt diff --git a/test/passes/simplify-locals_all-features_disable-exception-handling.wast b/test/passes/simplify-locals_all-features_disable-exception-handling_disable-stack-switching.wast similarity index 100% rename from test/passes/simplify-locals_all-features_disable-exception-handling.wast rename to test/passes/simplify-locals_all-features_disable-exception-handling_disable-stack-switching.wast From de31b495efa99a699c5dbe723e2ae692a27b2007 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 2 Sep 2026 18:22:00 -0700 Subject: [PATCH 5/7] update binaryenjs test --- test/binaryen.js/sideffects.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/test/binaryen.js/sideffects.js b/test/binaryen.js/sideffects.js index ec627dff42c..050918e369c 100644 --- a/test/binaryen.js/sideffects.js +++ b/test/binaryen.js/sideffects.js @@ -115,7 +115,7 @@ assert( ); // If exception handling feature is enabled, calls can throw -module.setFeatures(binaryen.Features.All); +module.setFeatures(binaryen.Features.ExceptionHandling); assert( binaryen.getSideEffects( module.call("test", [], binaryen.i32), @@ -125,6 +125,28 @@ assert( (binaryen.SideEffects.Calls | binaryen.SideEffects.Throws) ); +// If stack switching feature is enabled, calls can suspend +module.setFeatures(binaryen.Features.StackSwitching); +assert( + binaryen.getSideEffects( + module.call("test", [], binaryen.i32), + module + ) + == + (binaryen.SideEffects.Calls | binaryen.SideEffects.Suspends) +); + +// If all features are enabled, calls can throw and suspend +module.setFeatures(binaryen.Features.All); +assert( + binaryen.getSideEffects( + module.call("test", [], binaryen.i32), + module + ) + == + (binaryen.SideEffects.Calls | binaryen.SideEffects.Throws | binaryen.SideEffects.Suspends) +); + assert( binaryen.getSideEffects( module.drop(module.i32.pop()), From e64929f21a8f1e36626b3b6efee16b0a27f87834 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 3 Sep 2026 11:19:25 -0700 Subject: [PATCH 6/7] more test comments --- test/lit/passes/global-effects-suspends.wast | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/lit/passes/global-effects-suspends.wast b/test/lit/passes/global-effects-suspends.wast index 82e8756e6fc..00c53ee09d2 100644 --- a/test/lit/passes/global-effects-suspends.wast +++ b/test/lit/passes/global-effects-suspends.wast @@ -175,6 +175,8 @@ ;; SIMPLIFY-NEXT: (local.get $x) ;; SIMPLIFY-NEXT: ) (func $test-simplify-memory (result i32) + ;; Simplify-locals cannot move the load across a possibly-suspending call + ;; because the handler might modify memory before resuming. (local $x i32) (local.set $x (i32.load (i32.const 0))) (call $caller-of-suspending) @@ -208,6 +210,8 @@ ;; SIMPLIFY-NEXT: ) ;; SIMPLIFY-NEXT: ) (func $test-simplify-pure (result i32) + ;; When global effects tells us the callee will never suspend (or have other + ;; effects), simplify-locals can safely move a load across it. (local $x i32) (local.set $x (i32.load (i32.const 0))) (call $caller-of-pure) @@ -282,6 +286,7 @@ ;; SIMPLIFY-NEXT: ) ;; SIMPLIFY-NEXT: ) (func $test-indirect-suspend (param $ref (ref $sig-suspending)) + ;; This has the suspend effect, so it cannot be vacuumed away. (call_ref $sig-suspending (i32.const 0) (local.get $ref)) ) @@ -301,6 +306,8 @@ ;; SIMPLIFY-NEXT: ) ;; SIMPLIFY-NEXT: ) (func $test-indirect-pure (param $ref (ref $sig-pure)) + ;; With a closed world, global effects tells us that functions of this type + ;; never suspend (or have other side effects), so we can vacuum this away. (call_ref $sig-pure (i64.const 0) (local.get $ref)) ) From a3ffe83a30319665e5d495121495bc3e7d4e1390 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 3 Sep 2026 11:40:38 -0700 Subject: [PATCH 7/7] use ChildLocalizer --- src/passes/OptimizeInstructions.cpp | 39 +-- .../passes/optimize-instructions-resume.wast | 223 +++++++++--------- 2 files changed, 119 insertions(+), 143 deletions(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index 4b41b8db675..2ea7b323a27 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -1509,38 +1509,15 @@ struct OptimizeInstructions return; } + auto* block = + ChildLocalizer(curr, getFunction(), *getModule(), getPassOptions()) + .getChildrenReplacement(); Builder builder(*getModule()); - - // If the continuation expression has no side effects, we can eliminate it - // entirely and replace the resume with a direct call. - if (!effects(curr->cont).hasSideEffects()) { - replaceCurrent( - builder.makeCall(target->name, curr->operands, target->getResults())); - return; - } - - // The continuation expression has side effects. In Wasm, resume operands - // are evaluated before the continuation expression. If there are no - // operands, evaluate the continuation (dropped) and then call. - if (curr->operands.empty()) { - replaceCurrent(builder.makeSequence( - builder.makeDrop(curr->cont), - builder.makeCall(target->name, {}, target->getResults()))); - return; - } - - // In the presence of operands, execute the code in curr->cont after the - // operands and before the call happens by spilling the last operand to a - // temporary local. - auto* lastOperand = curr->operands.back(); - auto lastOperandType = lastOperand->type; - Index tempLocal = builder.addVar(getFunction(), lastOperandType); - auto* set = builder.makeLocalSet(tempLocal, lastOperand); - auto* drop = builder.makeDrop(curr->cont); - auto* get = builder.makeLocalGet(tempLocal, lastOperandType); - curr->operands.back() = builder.makeBlock({set, drop, get}); - replaceCurrent( - builder.makeCall(target->name, curr->operands, target->getResults())); + Type results = target->getResults(); + block->list.push_back( + builder.makeCall(target->name, curr->operands, results)); + block->type = results; + replaceCurrent(block); } // Note on removing casts (which the following utilities, skipNonNullCast and diff --git a/test/lit/passes/optimize-instructions-resume.wast b/test/lit/passes/optimize-instructions-resume.wast index 8ca0f6cadbf..f6376ccdeef 100644 --- a/test/lit/passes/optimize-instructions-resume.wast +++ b/test/lit/passes/optimize-instructions-resume.wast @@ -17,10 +17,10 @@ ;; NO-TNH: (type $sig-any (func (param (ref any)) (result (ref any)))) - ;; NO-TNH: (type $sig-unary (func (param i32) (result i32))) - ;; NO-TNH: (type $cont-any (cont $sig-any)) + ;; NO-TNH: (type $sig-unary (func (param i32) (result i32))) + ;; NO-TNH: (import "env" "imported" (func $imported (type $sig-none))) ;; TNH: (type $sig-none (func)) @@ -32,10 +32,10 @@ ;; TNH: (type $sig-any (func (param (ref any)) (result (ref any)))) - ;; TNH: (type $sig-unary (func (param i32) (result i32))) - ;; TNH: (type $cont-any (cont $sig-any)) + ;; TNH: (type $sig-unary (func (param i32) (result i32))) + ;; TNH: (import "env" "imported" (func $imported (type $sig-none))) ;; VACUUM: (type $sig-none (func)) @@ -47,10 +47,10 @@ ;; VACUUM: (type $sig-any (func (param (ref any)) (result (ref any)))) - ;; VACUUM: (type $sig-unary (func (param i32) (result i32))) - ;; VACUUM: (type $cont-any (cont $sig-any)) + ;; VACUUM: (type $sig-unary (func (param i32) (result i32))) + ;; VACUUM: (import "env" "imported" (func $imported (type $sig-none))) (import "env" "imported" (func $imported (type $sig-none))) @@ -373,7 +373,7 @@ ) ) - ;; NO-TNH: (func $test-resume-null-arm-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; NO-TNH: (func $test-resume-null-arm-tnh (type $8) (param $cond i32) (param $c (ref $cont-none)) ;; NO-TNH-NEXT: (resume $cont-none ;; NO-TNH-NEXT: (if (result (ref null $cont-none)) ;; NO-TNH-NEXT: (local.get $cond) @@ -386,7 +386,7 @@ ;; NO-TNH-NEXT: ) ;; NO-TNH-NEXT: ) ;; NO-TNH-NEXT: ) - ;; TNH: (func $test-resume-null-arm-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; TNH: (func $test-resume-null-arm-tnh (type $8) (param $cond i32) (param $c (ref $cont-none)) ;; TNH-NEXT: (resume $cont-none ;; TNH-NEXT: (block (result (ref $cont-none)) ;; TNH-NEXT: (drop @@ -396,7 +396,7 @@ ;; TNH-NEXT: ) ;; TNH-NEXT: ) ;; TNH-NEXT: ) - ;; VACUUM: (func $test-resume-null-arm-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; VACUUM: (func $test-resume-null-arm-tnh (type $8) (param $cond i32) (param $c (ref $cont-none)) ;; VACUUM-NEXT: (resume $cont-none ;; VACUUM-NEXT: (if (result (ref null $cont-none)) ;; VACUUM-NEXT: (local.get $cond) @@ -424,7 +424,7 @@ ) ) - ;; NO-TNH: (func $test-resume-null-select-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; NO-TNH: (func $test-resume-null-select-tnh (type $8) (param $cond i32) (param $c (ref $cont-none)) ;; NO-TNH-NEXT: (resume $cont-none ;; NO-TNH-NEXT: (select (result (ref null $cont-none)) ;; NO-TNH-NEXT: (ref.null nocont) @@ -433,7 +433,7 @@ ;; NO-TNH-NEXT: ) ;; NO-TNH-NEXT: ) ;; NO-TNH-NEXT: ) - ;; TNH: (func $test-resume-null-select-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; TNH: (func $test-resume-null-select-tnh (type $8) (param $cond i32) (param $c (ref $cont-none)) ;; TNH-NEXT: (resume $cont-none ;; TNH-NEXT: (block (result (ref $cont-none)) ;; TNH-NEXT: (drop @@ -448,7 +448,7 @@ ;; TNH-NEXT: ) ;; TNH-NEXT: ) ;; TNH-NEXT: ) - ;; VACUUM: (func $test-resume-null-select-tnh (type $7) (param $cond i32) (param $c (ref $cont-none)) + ;; VACUUM: (func $test-resume-null-select-tnh (type $8) (param $cond i32) (param $c (ref $cont-none)) ;; VACUUM-NEXT: (resume $cont-none ;; VACUUM-NEXT: (select (result (ref null $cont-none)) ;; VACUUM-NEXT: (ref.null nocont) @@ -568,8 +568,9 @@ ;; NO-TNH-NEXT: ) ;; TNH: (func $test-tee (type $11) (result (ref $cont-none)) ;; TNH-NEXT: (local $c (ref null $cont-none)) + ;; TNH-NEXT: (local $1 (ref null $cont-none)) ;; TNH-NEXT: (block - ;; TNH-NEXT: (drop + ;; TNH-NEXT: (local.set $1 ;; TNH-NEXT: (local.tee $c ;; TNH-NEXT: (cont.new $cont-none ;; TNH-NEXT: (ref.func $pure) @@ -625,22 +626,19 @@ ;; NO-TNH-NEXT: ) ;; TNH: (func $test-tee-params (type $12) (result (ref $cont-binary)) ;; TNH-NEXT: (local $c (ref null $cont-binary)) - ;; TNH-NEXT: (local $1 i32) + ;; TNH-NEXT: (local $1 (ref null $cont-binary)) ;; TNH-NEXT: (drop - ;; TNH-NEXT: (call $pure-binary - ;; TNH-NEXT: (i32.const 1) - ;; TNH-NEXT: (block (result i32) - ;; TNH-NEXT: (local.set $1 - ;; TNH-NEXT: (i32.const 2) - ;; TNH-NEXT: ) - ;; TNH-NEXT: (drop - ;; TNH-NEXT: (local.tee $c - ;; TNH-NEXT: (cont.new $cont-binary - ;; TNH-NEXT: (ref.func $pure-binary) - ;; TNH-NEXT: ) + ;; TNH-NEXT: (block (result i32) + ;; TNH-NEXT: (local.set $1 + ;; TNH-NEXT: (local.tee $c + ;; TNH-NEXT: (cont.new $cont-binary + ;; TNH-NEXT: (ref.func $pure-binary) ;; TNH-NEXT: ) ;; TNH-NEXT: ) - ;; TNH-NEXT: (local.get $1) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (call $pure-binary + ;; TNH-NEXT: (i32.const 1) + ;; TNH-NEXT: (i32.const 2) ;; TNH-NEXT: ) ;; TNH-NEXT: ) ;; TNH-NEXT: ) @@ -801,63 +799,72 @@ ;; NO-TNH: (func $test-eval-order (type $2) (result i32) ;; NO-TNH-NEXT: (local $0 i32) - ;; NO-TNH-NEXT: (call $pure-binary + ;; NO-TNH-NEXT: (local $1 i32) + ;; NO-TNH-NEXT: (local $2 (ref $cont-binary)) + ;; NO-TNH-NEXT: (local.set $0 ;; NO-TNH-NEXT: (call $side-effect-1) - ;; NO-TNH-NEXT: (block (result i32) - ;; NO-TNH-NEXT: (local.set $0 - ;; NO-TNH-NEXT: (call $side-effect-2) - ;; NO-TNH-NEXT: ) - ;; NO-TNH-NEXT: (drop - ;; NO-TNH-NEXT: (block (result (ref $cont-binary)) - ;; NO-TNH-NEXT: (call $side-effect-3) - ;; NO-TNH-NEXT: (cont.new $cont-binary - ;; NO-TNH-NEXT: (ref.func $pure-binary) - ;; NO-TNH-NEXT: ) - ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (local.set $1 + ;; NO-TNH-NEXT: (call $side-effect-2) + ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (local.set $2 + ;; NO-TNH-NEXT: (block (result (ref $cont-binary)) + ;; NO-TNH-NEXT: (call $side-effect-3) + ;; NO-TNH-NEXT: (cont.new $cont-binary + ;; NO-TNH-NEXT: (ref.func $pure-binary) ;; NO-TNH-NEXT: ) - ;; NO-TNH-NEXT: (local.get $0) ;; NO-TNH-NEXT: ) ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (call $pure-binary + ;; NO-TNH-NEXT: (local.get $0) + ;; NO-TNH-NEXT: (local.get $1) + ;; NO-TNH-NEXT: ) ;; NO-TNH-NEXT: ) ;; TNH: (func $test-eval-order (type $2) (result i32) ;; TNH-NEXT: (local $0 i32) - ;; TNH-NEXT: (call $pure-binary + ;; TNH-NEXT: (local $1 i32) + ;; TNH-NEXT: (local $2 (ref $cont-binary)) + ;; TNH-NEXT: (local.set $0 ;; TNH-NEXT: (call $side-effect-1) - ;; TNH-NEXT: (block (result i32) - ;; TNH-NEXT: (local.set $0 - ;; TNH-NEXT: (call $side-effect-2) - ;; TNH-NEXT: ) - ;; TNH-NEXT: (drop - ;; TNH-NEXT: (block (result (ref $cont-binary)) - ;; TNH-NEXT: (call $side-effect-3) - ;; TNH-NEXT: (cont.new $cont-binary - ;; TNH-NEXT: (ref.func $pure-binary) - ;; TNH-NEXT: ) - ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (local.set $1 + ;; TNH-NEXT: (call $side-effect-2) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (local.set $2 + ;; TNH-NEXT: (block (result (ref $cont-binary)) + ;; TNH-NEXT: (call $side-effect-3) + ;; TNH-NEXT: (cont.new $cont-binary + ;; TNH-NEXT: (ref.func $pure-binary) ;; TNH-NEXT: ) - ;; TNH-NEXT: (local.get $0) ;; TNH-NEXT: ) ;; TNH-NEXT: ) + ;; TNH-NEXT: (call $pure-binary + ;; TNH-NEXT: (local.get $0) + ;; TNH-NEXT: (local.get $1) + ;; TNH-NEXT: ) ;; TNH-NEXT: ) ;; VACUUM: (func $test-eval-order (type $2) (result i32) ;; VACUUM-NEXT: (local $0 i32) - ;; VACUUM-NEXT: (call $pure-binary + ;; VACUUM-NEXT: (local $1 i32) + ;; VACUUM-NEXT: (local $2 (ref $cont-binary)) + ;; VACUUM-NEXT: (local.set $0 ;; VACUUM-NEXT: (call $side-effect-1) - ;; VACUUM-NEXT: (block (result i32) - ;; VACUUM-NEXT: (local.set $0 - ;; VACUUM-NEXT: (call $side-effect-2) - ;; VACUUM-NEXT: ) - ;; VACUUM-NEXT: (drop - ;; VACUUM-NEXT: (block (result (ref (exact $cont-binary))) - ;; VACUUM-NEXT: (call $side-effect-3) - ;; VACUUM-NEXT: (cont.new $cont-binary - ;; VACUUM-NEXT: (ref.func $pure-binary) - ;; VACUUM-NEXT: ) - ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (local.set $1 + ;; VACUUM-NEXT: (call $side-effect-2) + ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (local.set $2 + ;; VACUUM-NEXT: (block (result (ref (exact $cont-binary))) + ;; VACUUM-NEXT: (call $side-effect-3) + ;; VACUUM-NEXT: (cont.new $cont-binary + ;; VACUUM-NEXT: (ref.func $pure-binary) ;; VACUUM-NEXT: ) - ;; VACUUM-NEXT: (local.get $0) ;; VACUUM-NEXT: ) ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (call $pure-binary + ;; VACUUM-NEXT: (local.get $0) + ;; VACUUM-NEXT: (local.get $1) + ;; VACUUM-NEXT: ) ;; VACUUM-NEXT: ) (func $test-eval-order (result i32) ;; Evaluation order test: side effects in continuation expression execute @@ -875,7 +882,9 @@ ;; NO-TNH: (func $test-handler-elimination (type $sig-none) ;; NO-TNH-NEXT: (drop ;; NO-TNH-NEXT: (block $handler (result (ref $cont-none)) - ;; NO-TNH-NEXT: (call $pure) + ;; NO-TNH-NEXT: (block + ;; NO-TNH-NEXT: (call $pure) + ;; NO-TNH-NEXT: ) ;; NO-TNH-NEXT: (return) ;; NO-TNH-NEXT: ) ;; NO-TNH-NEXT: ) @@ -883,7 +892,9 @@ ;; TNH: (func $test-handler-elimination (type $sig-none) ;; TNH-NEXT: (drop ;; TNH-NEXT: (block $handler (result (ref $cont-none)) - ;; TNH-NEXT: (call $pure) + ;; TNH-NEXT: (block + ;; TNH-NEXT: (call $pure) + ;; TNH-NEXT: ) ;; TNH-NEXT: (return) ;; TNH-NEXT: ) ;; TNH-NEXT: ) @@ -1025,7 +1036,8 @@ ) ;; NO-TNH: (func $test-eval-order-no-operands (type $sig-none) - ;; NO-TNH-NEXT: (drop + ;; NO-TNH-NEXT: (local $0 (ref $cont-none)) + ;; NO-TNH-NEXT: (local.set $0 ;; NO-TNH-NEXT: (block (result (ref $cont-none)) ;; NO-TNH-NEXT: (call $side-effect-3) ;; NO-TNH-NEXT: (cont.new $cont-none @@ -1036,7 +1048,8 @@ ;; NO-TNH-NEXT: (call $pure) ;; NO-TNH-NEXT: ) ;; TNH: (func $test-eval-order-no-operands (type $sig-none) - ;; TNH-NEXT: (drop + ;; TNH-NEXT: (local $0 (ref $cont-none)) + ;; TNH-NEXT: (local.set $0 ;; TNH-NEXT: (block (result (ref $cont-none)) ;; TNH-NEXT: (call $side-effect-3) ;; TNH-NEXT: (cont.new $cont-none @@ -1047,7 +1060,8 @@ ;; TNH-NEXT: (call $pure) ;; TNH-NEXT: ) ;; VACUUM: (func $test-eval-order-no-operands (type $sig-none) - ;; VACUUM-NEXT: (drop + ;; VACUUM-NEXT: (local $0 (ref $cont-none)) + ;; VACUUM-NEXT: (local.set $0 ;; VACUUM-NEXT: (block (result (ref (exact $cont-none))) ;; VACUUM-NEXT: (call $side-effect-3) ;; VACUUM-NEXT: (cont.new $cont-none @@ -1068,61 +1082,46 @@ ) ;; NO-TNH: (func $test-eval-order-non-nullable (type $sig-any) (param $x (ref any)) (result (ref any)) - ;; NO-TNH-NEXT: (local $1 (ref any)) - ;; NO-TNH-NEXT: (call $pure-any - ;; NO-TNH-NEXT: (block (result (ref any)) - ;; NO-TNH-NEXT: (local.set $1 - ;; NO-TNH-NEXT: (local.get $x) - ;; NO-TNH-NEXT: ) - ;; NO-TNH-NEXT: (drop - ;; NO-TNH-NEXT: (block (result (ref $cont-any)) - ;; NO-TNH-NEXT: (call $side-effect-3) - ;; NO-TNH-NEXT: (cont.new $cont-any - ;; NO-TNH-NEXT: (ref.func $pure-any) - ;; NO-TNH-NEXT: ) - ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (local $1 (ref $cont-any)) + ;; NO-TNH-NEXT: (local.set $1 + ;; NO-TNH-NEXT: (block (result (ref $cont-any)) + ;; NO-TNH-NEXT: (call $side-effect-3) + ;; NO-TNH-NEXT: (cont.new $cont-any + ;; NO-TNH-NEXT: (ref.func $pure-any) ;; NO-TNH-NEXT: ) - ;; NO-TNH-NEXT: (local.get $1) ;; NO-TNH-NEXT: ) ;; NO-TNH-NEXT: ) + ;; NO-TNH-NEXT: (call $pure-any + ;; NO-TNH-NEXT: (local.get $x) + ;; NO-TNH-NEXT: ) ;; NO-TNH-NEXT: ) ;; TNH: (func $test-eval-order-non-nullable (type $sig-any) (param $x (ref any)) (result (ref any)) - ;; TNH-NEXT: (local $1 (ref any)) - ;; TNH-NEXT: (call $pure-any - ;; TNH-NEXT: (block (result (ref any)) - ;; TNH-NEXT: (local.set $1 - ;; TNH-NEXT: (local.get $x) - ;; TNH-NEXT: ) - ;; TNH-NEXT: (drop - ;; TNH-NEXT: (block (result (ref $cont-any)) - ;; TNH-NEXT: (call $side-effect-3) - ;; TNH-NEXT: (cont.new $cont-any - ;; TNH-NEXT: (ref.func $pure-any) - ;; TNH-NEXT: ) - ;; TNH-NEXT: ) + ;; TNH-NEXT: (local $1 (ref $cont-any)) + ;; TNH-NEXT: (local.set $1 + ;; TNH-NEXT: (block (result (ref $cont-any)) + ;; TNH-NEXT: (call $side-effect-3) + ;; TNH-NEXT: (cont.new $cont-any + ;; TNH-NEXT: (ref.func $pure-any) ;; TNH-NEXT: ) - ;; TNH-NEXT: (local.get $1) ;; TNH-NEXT: ) ;; TNH-NEXT: ) + ;; TNH-NEXT: (call $pure-any + ;; TNH-NEXT: (local.get $x) + ;; TNH-NEXT: ) ;; TNH-NEXT: ) ;; VACUUM: (func $test-eval-order-non-nullable (type $sig-any) (param $x (ref any)) (result (ref any)) - ;; VACUUM-NEXT: (local $1 (ref any)) - ;; VACUUM-NEXT: (call $pure-any - ;; VACUUM-NEXT: (block (result (ref any)) - ;; VACUUM-NEXT: (local.set $1 - ;; VACUUM-NEXT: (local.get $x) - ;; VACUUM-NEXT: ) - ;; VACUUM-NEXT: (drop - ;; VACUUM-NEXT: (block (result (ref (exact $cont-any))) - ;; VACUUM-NEXT: (call $side-effect-3) - ;; VACUUM-NEXT: (cont.new $cont-any - ;; VACUUM-NEXT: (ref.func $pure-any) - ;; VACUUM-NEXT: ) - ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (local $1 (ref $cont-any)) + ;; VACUUM-NEXT: (local.set $1 + ;; VACUUM-NEXT: (block (result (ref (exact $cont-any))) + ;; VACUUM-NEXT: (call $side-effect-3) + ;; VACUUM-NEXT: (cont.new $cont-any + ;; VACUUM-NEXT: (ref.func $pure-any) ;; VACUUM-NEXT: ) - ;; VACUUM-NEXT: (local.get $1) ;; VACUUM-NEXT: ) ;; VACUUM-NEXT: ) + ;; VACUUM-NEXT: (call $pure-any + ;; VACUUM-NEXT: (local.get $x) + ;; VACUUM-NEXT: ) ;; VACUUM-NEXT: ) (func $test-eval-order-non-nullable (param $x (ref any)) (result (ref any)) ;; Evaluation order test: non-nullable operands are preserved when spilled.