@ 261106.8
current
/*
 * 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