/*
* 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 "ir/type-updating.h"
#include "matchers/effects.h"
#include "parser/wat-parser.h"
#include "support/result.h"
#include "wasm.h"
using namespace wasm;
using namespace testing;
namespace {
class IndirectCallEffectsTest : public Test {
protected:
Module wasm;
PassOptions options;
void SetUp() override {
auto moduleText = R"wasm(
(module
(rec
(type $A (func))
(type $B (func))
(type $C (func))
(type $D (func))
)
(func $f_a (type $A))
(func $f_b (type $B))
(func $f_c (type $C))
(func $f_d (type $D))
)
)wasm";
if (auto err = wasm::WATParser::parseModule(wasm, moduleText).getErr()) {
Fatal() << err->msg;
}
}
// Helper function to test the updating logic for `indirectCallEffects`. Given
// that the original module has `indirectCallEffects` given by `oldEffects`,
// rewrite the module's types using GlobalTypeRewriter::mapTypes according to
// `typeMap` and return the updated `indirectCallEffects` map.
std::unordered_map<std::string, std::shared_ptr<const EffectAnalyzer>>
updateEffects(
const std::unordered_map<std::string,
std::shared_ptr<const EffectAnalyzer>>& oldEffects,
const std::unordered_map<std::string, std::string>& typeMap) {
std::unordered_map<std::string, HeapType> types;
std::unordered_map<HeapType, std::string> typeNames;
for (const auto& [type, name] : wasm.typeNames) {
types[name.name.toString()] = type;
typeNames[type] = name.name.toString();
}
// Type that previously didn't exist in the module.
// This is a new type created by GlobalTypeRewriter.
{
TypeBuilder builder(1);
builder.setHeapType(0, Signature(Type::i32, Type::i32));
HeapType newType = (*builder.build())[0];
types["new type"] = newType;
typeNames[newType] = "new type";
}
for (const auto& [name, effects] : oldEffects) {
wasm.indirectCallEffects[types.at(name)] = effects;
}
GlobalTypeRewriter::TypeMap map;
for (const auto& [oldName, newName] : typeMap) {
HeapType oldType = types.at(oldName);
map[oldType] = types.at(newName);
}
GlobalTypeRewriter rewriter(wasm, WorldMode::Open);
rewriter.mapTypes(map);
std::unordered_map<std::string, std::shared_ptr<const EffectAnalyzer>>
result;
for (const auto& [type, effects] : wasm.indirectCallEffects) {
auto name = typeNames.at(type);
result[name] = effects;
}
return result;
}
};
} // anonymous namespace
TEST_F(IndirectCallEffectsTest, SrcHasUnknownEffects) {
auto effectsB = std::make_shared<EffectAnalyzer>(options, wasm);
effectsB->writesMemory = true;
auto merged =
updateEffects(/*oldEffects=*/{{"B", effectsB}}, /*typeMap=*/{{"A", "B"}});
// $A had unknown effects since it wasn't present in indirectCallEffects. So
// $B's effects become unknown too.
EXPECT_THAT(merged, IsEmpty());
}
TEST_F(IndirectCallEffectsTest, DestHasUnknownEffects) {
auto effectsA = std::make_shared<EffectAnalyzer>(options, wasm);
effectsA->calls = true;
auto merged =
updateEffects(/*oldEffects=*/{{"A", effectsA}}, /*typeMap=*/{{"A", "B"}});
// $B's effects are unknown and $B is not a new type (see the test fixture; $B
// already existed in the module), so when $A is merged with $B, the result
// still has unknown effects.
EXPECT_THAT(merged, IsEmpty());
}
TEST_F(IndirectCallEffectsTest, BothHaveEffects) {
auto effectsA = std::make_shared<EffectAnalyzer>(options, wasm);
effectsA->calls = true;
auto effectsB = std::make_shared<EffectAnalyzer>(options, wasm);
effectsB->writesMemory = true;
auto merged = updateEffects(/*oldEffects=*/{{"A", effectsA}, {"B", effectsB}},
/*typeMap=*/{{"A", "B"}});
// $A is rewritten to $B, so $B gains the effects of $A, and $A no longer has
// effects.
EXPECT_THAT(merged,
UnorderedElementsAre(Pair("B", AllOf(Calls(), WritesMemory()))));
}
TEST_F(IndirectCallEffectsTest, SrcTypeDoesntExist) {
auto effectsA = std::make_shared<EffectAnalyzer>(options, wasm);
effectsA->calls = true;
auto effectsB = std::make_shared<EffectAnalyzer>(options, wasm);
effectsB->writesMemory = true;
// $"new type" doesn't appear in the module but has effects. It may have been
// computed by GlobalEffects earlier and later removed by another pass. Merge
// its effects as normal.
auto merged =
updateEffects(/*oldEffects=*/{{"new type", effectsA}, {"B", effectsB}},
/*typeMap=*/{{"new type", "B"}});
EXPECT_THAT(merged,
UnorderedElementsAre(Pair("B", AllOf(Calls(), WritesMemory()))));
}
TEST_F(IndirectCallEffectsTest, SrcTypeDoesntExistAndNoEffects) {
auto effectsB = std::make_shared<EffectAnalyzer>(options, wasm);
effectsB->writesMemory = true;
auto merged = updateEffects(/*oldEffects=*/{{"B", effectsB}},
/*typeMap=*/{{"new type", "B"}});
// $"new type" doesn't appear in the module and has no effects recorded. This
// is a no-op since $"new type" doesn't appear in the module at all, so it's
// safe to preserve $B's effects rather than removing them from the map.
// EXPECT_THAT(merged, IsEmpty());
EXPECT_THAT(merged, UnorderedElementsAre(Pair("B", effectsB)));
}
TEST_F(IndirectCallEffectsTest, MapToNewType) {
auto effectsA = std::make_shared<EffectAnalyzer>(options, wasm);
effectsA->calls = true;
auto merged = updateEffects(/*oldEffects=*/{{"A", effectsA}},
/*typeMap=*/{{"A", "new type"}});
// $A is rewritten to a new type that didn't previously exist in the module.
// Because the type is brand new, we do *not* treat its absence in
// `indirectCallEffects` as 'unknown effects' like in `DestHasUnknownEffects`.
// Instead $"new type" just gains $A's effects. We do a pointer comparison
// here since the effects object can be reused unchanged.
EXPECT_THAT(merged, UnorderedElementsAre(Pair("new type", effectsA)));
}
TEST_F(IndirectCallEffectsTest, MapToNewTypeUnknownEffects) {
auto merged = updateEffects(/*oldEffects=*/{},
/*typeMap=*/{{"A", "new type"}});
// Like `MapToNewType`, except the source type $A has unknown effects. Because
// $A *is* present in the module, its absence in `indirectCallEffects` does
// mean that its effects are unknown (GlobalEffects never computed it, maybe
// because $A didn't exist at the time that it ran). As a result $"new type"
// has unknown effects.
EXPECT_THAT(merged, IsEmpty());
}
TEST_F(IndirectCallEffectsTest, MergeNewTypeAndExisting) {
auto effectsB = std::make_shared<EffectAnalyzer>(options, wasm);
effectsB->writesMemory = true;
auto merged =
updateEffects(/*oldEffects=*/{{"B", effectsB}},
/*typeMap=*/{{"A", "new type"}, {"B", "new type"}});
// Type A already had unknown effects, so the new type remains unknown.
EXPECT_THAT(merged, IsEmpty());
}
TEST_F(IndirectCallEffectsTest, MergeNewTypeAndExistingWithEffects) {
auto effectsA = std::make_shared<EffectAnalyzer>(options, wasm);
effectsA->calls = true;
auto effectsB = std::make_shared<EffectAnalyzer>(options, wasm);
effectsB->writesMemory = true;
auto merged =
updateEffects(/*oldEffects=*/{{"A", effectsA}, {"B", effectsB}},
/*typeMap=*/{{"A", "new type"}, {"B", "new type"}});
// Like `BothHaveEffects`, except that the two types are both mapped into one
// new type. The new type inherits the effects of both source types.
EXPECT_THAT(
merged,
UnorderedElementsAre(Pair("new type", AllOf(Calls(), WritesMemory()))));
}