| /* |
| * 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); |
| |
| // 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()); |
| 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); |
| } |
| |
| TEST_F(EffectAnalyzerTest, MayNotReturnOrdering) { |
| auto moduleText = R"wasm( |
| (module |
| (memory 1 1 shared) |
| (global $g (mut i32) (i32.const 0)) |
| (func $callee) |
| (func $test (param $x i32) (param $y i32) |
| (loop $l1 |
| (br_if $l1 (local.get $x)) |
| ) |
| (loop $l2 |
| (br_if $l2 (local.get $y)) |
| ) |
| (drop (i32.div_s (i32.const 1) (local.get $y))) |
| (call $callee) |
| (global.set $g (i32.const 1)) |
| (local.set $y (i32.const 2)) |
| (drop (memory.atomic.wait32 (i32.const 0) (i32.const 0) (i64.const -1))) |
| ) |
| ) |
| )wasm"; |
| |
| auto parseResult = WATParser::parseModule(wasm, moduleText); |
| ASSERT_FALSE(parseResult.getErr()); |
| |
| auto* func = wasm.getFunction("test"); |
| ASSERT_NE(func, nullptr); |
| auto* block = func->body->cast<Block>(); |
| ASSERT_EQ(block->list.size(), 7u); |
| |
| auto* loop1 = block->list[0]; |
| auto* loop2 = block->list[1]; |
| auto* trapExpr = block->list[2]; |
| auto* callExpr = block->list[3]; |
| auto* globalSetExpr = block->list[4]; |
| auto* localSetExpr = block->list[5]; |
| auto* waitExpr = block->list[6]; |
| |
| EffectAnalyzer loop1Effects(options, wasm, loop1); |
| EffectAnalyzer loop2Effects(options, wasm, loop2); |
| EffectAnalyzer trapEffects(options, wasm, trapExpr); |
| EffectAnalyzer callEffects(options, wasm, callExpr); |
| EffectAnalyzer globalSetEffects(options, wasm, globalSetExpr); |
| EffectAnalyzer localSetEffects(options, wasm, localSetExpr); |
| EffectAnalyzer waitEffects(options, wasm, waitExpr); |
| |
| EXPECT_THAT(&loop1Effects, MayNotReturn()); |
| EXPECT_FALSE(loop1Effects.transfersControlFlow()); |
| EXPECT_THAT(&waitEffects, MayNotReturn()); |
| |
| // Cannot reorder mayNotReturn with traps, calls, or global writes. |
| EXPECT_TRUE(loop1Effects.orderedBefore(trapEffects)); |
| EXPECT_TRUE(trapEffects.orderedBefore(loop1Effects)); |
| EXPECT_FALSE(EffectAnalyzer::canReorder(options, wasm, loop1, trapExpr)); |
| EXPECT_FALSE(EffectAnalyzer::canReorder(options, wasm, trapExpr, loop1)); |
| |
| // Even with trapsNeverHappen, mayNotReturn (including atomic.wait) cannot be |
| // reordered with a trap. |
| PassOptions tnhOptions = options; |
| tnhOptions.trapsNeverHappen = true; |
| EXPECT_FALSE(EffectAnalyzer::canReorder(tnhOptions, wasm, loop1, trapExpr)); |
| EXPECT_FALSE(EffectAnalyzer::canReorder(tnhOptions, wasm, trapExpr, loop1)); |
| EXPECT_FALSE( |
| EffectAnalyzer::canReorder(tnhOptions, wasm, waitExpr, trapExpr)); |
| EXPECT_FALSE( |
| EffectAnalyzer::canReorder(tnhOptions, wasm, trapExpr, waitExpr)); |
| |
| EXPECT_TRUE(loop1Effects.orderedBefore(callEffects)); |
| EXPECT_TRUE(callEffects.orderedBefore(loop1Effects)); |
| EXPECT_FALSE(EffectAnalyzer::canReorder(options, wasm, loop1, callExpr)); |
| EXPECT_FALSE(EffectAnalyzer::canReorder(options, wasm, callExpr, loop1)); |
| |
| EXPECT_TRUE(loop1Effects.orderedBefore(globalSetEffects)); |
| EXPECT_TRUE(globalSetEffects.orderedBefore(loop1Effects)); |
| EXPECT_FALSE(EffectAnalyzer::canReorder(options, wasm, loop1, globalSetExpr)); |
| EXPECT_FALSE(EffectAnalyzer::canReorder(options, wasm, globalSetExpr, loop1)); |
| |
| // Can reorder mayNotReturn with unrelated local writes or another pure |
| // mayNotReturn. |
| EXPECT_FALSE(loop1Effects.orderedBefore(localSetEffects)); |
| EXPECT_FALSE(localSetEffects.orderedBefore(loop1Effects)); |
| EXPECT_TRUE(EffectAnalyzer::canReorder(options, wasm, loop1, localSetExpr)); |
| EXPECT_TRUE(EffectAnalyzer::canReorder(options, wasm, localSetExpr, loop1)); |
| |
| EXPECT_FALSE(loop1Effects.orderedBefore(loop2Effects)); |
| EXPECT_FALSE(loop2Effects.orderedBefore(loop1Effects)); |
| EXPECT_TRUE(EffectAnalyzer::canReorder(options, wasm, loop1, loop2)); |
| EXPECT_TRUE(EffectAnalyzer::canReorder(options, wasm, loop2, loop1)); |
| } |
| |
| } // anonymous namespace |