blob: c0eb6f2dcf0c2c31e32632fa780e7b5337c66a1d [file]
/*
* 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