Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 79 additions & 69 deletions src/ir/effects.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#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"
Expand Down Expand Up @@ -716,71 +717,47 @@ class EffectAnalyzer {
}

void visitCall(Call* curr) {
Comment thread
stevenfontanella marked this conversation as resolved.
// call.without.effects has no effects.
if (Intrinsics(parent.module).isCallWithoutEffects(curr)) {
return;
}

// Get the target's effects, if they exist. Note that we must handle the
// case of the function not yet existing (we may be executed in the middle
// of a pass, which may have built up calls but not the targets of those
// calls; in such a case, we do not find the targets and therefore assume
// we know nothing about the effects, which is safe).
const EffectAnalyzer* targetEffects = nullptr;
if (auto* target = parent.module.getFunctionOrNull(curr->target)) {
targetEffects = target->effects.get();
const EffectAnalyzer* bodyEffects = nullptr;
if (auto* target = parent.module.getFunctionOrNull(curr->target);
target && target->effects) {
bodyEffects = target->effects.get();
}

if (curr->isReturn) {
parent.branchesOut = true;
// When EH is enabled, any call can throw.
if (parent.features.hasExceptionHandling() &&
(!targetEffects || targetEffects->throws())) {
parent.hasReturnCallThrow = true;
}
}

if (targetEffects) {
// We have effect information for this call target, and can just use
// that. The one change we may want to make is to remove throws_, if the
// target function throws and we know that will be caught anyhow, the
// same as the code below for the general path. We can always filter out
// throws for return calls because they are already more precisely
// captured by `branchesOut`, which models the return, and
// `hasReturnCallThrow`, which models the throw that will happen after
// the return.
if (targetEffects->throws_ && (parent.tryDepth > 0 || curr->isReturn)) {
auto filteredEffects = *targetEffects;
filteredEffects.throws_ = false;
parent.mergeIn(filteredEffects);
} else {
// Just merge in all the effects.
parent.mergeIn(*targetEffects);
}
populateEffectsForCall(curr, bodyEffects);
}
void visitCallIndirect(CallIndirect* curr) {
auto* table = parent.module.getTable(curr->table);
if (!Type::isSubType(Type(curr->heapType, Nullability::Nullable),
table->type)) {
parent.trap = true;
Comment thread
kripken marked this conversation as resolved.
return;
}
if (table->type.isNullable()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we check only this but call trapOnNull on visitCallRef? Should we call trapOnNull on both cases?

parent.implicitTrap = true;
Comment thread
kripken marked this conversation as resolved.
}

parent.calls = true;
// When EH is enabled, any call can throw. Skip this for return calls
// because the throw is already more precisely captured by the combination
// of `hasReturnCallThrow` and `branchesOut`.
if (parent.features.hasExceptionHandling() && parent.tryDepth == 0 &&
!curr->isReturn) {
parent.throws_ = true;
const EffectAnalyzer* bodyEffects = nullptr;
if (auto it = parent.module.typeEffects.find(curr->heapType);
it != parent.module.typeEffects.end() && it->second) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case it->second is null?

bodyEffects = it->second.get();
}
populateEffectsForCall(curr, bodyEffects);
}
void visitCallIndirect(CallIndirect* curr) {
parent.calls = true;
if (curr->isReturn) {
parent.branchesOut = true;
if (parent.features.hasExceptionHandling()) {
parent.hasReturnCallThrow = true;
}
void visitCallRef(CallRef* curr) {
if (trapOnNull(curr->target)) {
return;
}
if (parent.features.hasExceptionHandling() &&
(parent.tryDepth == 0 && !curr->isReturn)) {
parent.throws_ = true;

const EffectAnalyzer* bodyEffects = nullptr;
if (auto it =
parent.module.typeEffects.find(curr->target->type.getHeapType());
it != parent.module.typeEffects.end() && it->second) {
bodyEffects = it->second.get();
}
populateEffectsForCall(curr, bodyEffects);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps processCall or addCallEffects, which are shorter?

}
void visitLocalGet(LocalGet* curr) {
parent.localsRead.insert(curr->index);
Expand Down Expand Up @@ -1038,22 +1015,6 @@ class EffectAnalyzer {
void visitTupleExtract(TupleExtract* curr) {}
void visitRefI31(RefI31* curr) {}
void visitI31Get(I31Get* curr) { trapOnNull(curr->i31); }
void visitCallRef(CallRef* curr) {
if (trapOnNull(curr->target)) {
return;
}
if (curr->isReturn) {
parent.branchesOut = true;
if (parent.features.hasExceptionHandling()) {
parent.hasReturnCallThrow = true;
}
}
parent.calls = true;
if (parent.features.hasExceptionHandling() &&
(parent.tryDepth == 0 && !curr->isReturn)) {
parent.throws_ = true;
}
}
void visitRefTest(RefTest* curr) {}

void visitRefCast(RefCast* curr) {
Expand Down Expand Up @@ -1335,6 +1296,55 @@ class EffectAnalyzer {
parent.throws_ = true;
}
}

private:
// Populate effects of the function's body that were computed from
// GlobalEffects. Note that calls may have other effects that aren't
// captured by the function body of the target (e.g. a call_ref may trap on
// null refs).
template<typename CallType>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
template<typename CallType>
template<typename T>

I believe this is the pattern we use in general.

void populateFunctionBodyEffects(const CallType* curr,
const EffectAnalyzer& funcEffects) {
if (curr->isReturn) {
Copy link
Copy Markdown
Member

@kripken kripken May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

populateFunctionBodyEffects and populateEffectsForCall overlap here, I think? This is adding an effect from the call, not the body. Maybe I don't understand the difference between the functions - perhaps document them more?

if (funcEffects.throws()) {
parent.hasReturnCallThrow = true;
}
}

if (funcEffects.throws_ && (parent.tryDepth > 0 || curr->isReturn)) {
auto filteredEffects = funcEffects;
filteredEffects.throws_ = false;
parent.mergeIn(filteredEffects);
} else {
parent.mergeIn(funcEffects);
}
}

template<typename CallType>
void
populateEffectsForCall(const CallType* curr,
NullablePtr<const EffectAnalyzer*> bodyEffects) {
if (curr->isReturn) {
parent.branchesOut = true;
}

if (bodyEffects) {
populateFunctionBodyEffects(curr, *bodyEffects);
return;
}

parent.calls = true;
// If EH is enabled and we don't have global effects information,
// assume that the call body may throw.
if (parent.features.hasExceptionHandling()) {
if (curr->isReturn) {
parent.hasReturnCallThrow = true;
}
if (parent.tryDepth == 0 && !curr->isReturn) {
parent.throws_ = true;
}
}
}
};

public:
Expand Down
21 changes: 21 additions & 0 deletions src/ir/type-updating.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,27 @@ void GlobalTypeRewriter::mapTypes(const TypeMap& oldToNewTypes) {
for (auto& tag : wasm.tags) {
tag->type = updater.getNew(tag->type);
}

// Update indirect call effects per type.
std::unordered_map<HeapType, std::shared_ptr<const EffectAnalyzer>>
newTypeEffects;
for (auto& [oldType, effects] : wasm.typeEffects) {
if (!effects) {
continue;
}

auto newType = updater.getNew(oldType);
std::shared_ptr<const EffectAnalyzer>& targetEffects =
newTypeEffects[newType];
if (!targetEffects) {
targetEffects = effects;
} else {
auto merged = std::make_shared<EffectAnalyzer>(*targetEffects);
merged->mergeIn(*effects);
targetEffects = merged;
}
}
wasm.typeEffects = std::move(newTypeEffects);
}

void GlobalTypeRewriter::mapTypeNamesAndIndices(const TypeMap& oldToNewTypes) {
Expand Down
36 changes: 25 additions & 11 deletions src/passes/GlobalEffects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "pass.h"
#include "support/graph_traversal.h"
#include "support/strongly_connected_components.h"
#include "support/utilities.h"
#include "wasm.h"

namespace wasm {
Expand Down Expand Up @@ -225,10 +226,13 @@ void mergeMaybeEffects(std::optional<EffectAnalyzer>& dest,
// - Merge all of the effects of functions within the CC
// - Also merge the (already computed) effects of each callee CC
// - Add trap effects for potentially recursive call chains
void propagateEffects(const Module& module,
const PassOptions& passOptions,
std::map<Function*, FuncInfo>& funcInfos,
const CallGraph& callGraph) {
void propagateEffects(
const Module& module,
const PassOptions& passOptions,
std::map<Function*, FuncInfo>& funcInfos,
std::unordered_map<HeapType, std::shared_ptr<const EffectAnalyzer>>&
typeEffects,
const CallGraph& callGraph) {
// We only care about Functions that are roots, not types.
// A type would be a root if a function exists with that type, but no-one
// indirect calls the type.
Expand Down Expand Up @@ -317,12 +321,21 @@ void propagateEffects(const Module& module,
}

// Assign each function's effects to its CC effects.
for (Function* f : ccFuncs) {
if (!ccEffects) {
funcInfos.at(f).effects = UnknownEffects;
} else {
funcInfos.at(f).effects.emplace(*ccEffects);
}
for (auto node : cc) {
std::visit(overloaded{[&](HeapType type) {
if (ccEffects != UnknownEffects) {
typeEffects[type] =
std::make_shared<EffectAnalyzer>(*ccEffects);
}
},
[&](Function* f) {
if (!ccEffects) {
funcInfos.at(f).effects = UnknownEffects;
} else {
funcInfos.at(f).effects.emplace(*ccEffects);
}
}},
node);
}
}
}
Expand All @@ -346,7 +359,8 @@ struct GenerateGlobalEffects : public Pass {
auto callGraph =
buildCallGraph(*module, funcInfos, getPassOptions().closedWorld);

propagateEffects(*module, getPassOptions(), funcInfos, callGraph);
propagateEffects(
*module, getPassOptions(), funcInfos, module->typeEffects, callGraph);

copyEffectsToFunctions(funcInfos);
}
Expand Down
13 changes: 13 additions & 0 deletions src/support/utilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@ class Fatal {
#define WASM_UNREACHABLE(msg) wasm::handle_unreachable()
#endif

// Helper to create an invocable with an overloaded operator(), for use with
// std::visit e.g.
// std::visit(
// overloaded{
// [](const A& a) { ... },
// [](const B& b) { ... }},
// variant)
template<class... Ts> struct overloaded : Ts... {
using Ts::operator()...;
};

template<typename T> using NullablePtr = T;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?


} // namespace wasm

#endif // wasm_support_utilities_h
6 changes: 6 additions & 0 deletions src/wasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -2722,6 +2722,12 @@ class Module {
std::unordered_map<HeapType, TypeNames> typeNames;
std::unordered_map<HeapType, Index> typeIndices;

// Potential effects for bodies of indirect calls to this type.
// TODO: Use Type instead of HeapType to account for nullability and
// exactness.
std::unordered_map<HeapType, std::shared_ptr<const EffectAnalyzer>>
typeEffects;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this managed? Who is responsible for updating it? Please document this here.


MixedArena allocator;

private:
Expand Down
21 changes: 10 additions & 11 deletions test/lit/passes/global-effects-closed-world-tnh.wast
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited.
;; RUN: foreach %s %t wasm-opt -all --closed-world --traps-never-happen --generate-global-effects --vacuum -S -o - | filecheck %s
;; RUN: wasm-opt %s -all --closed-world --traps-never-happen --generate-global-effects --vacuum -S -o - | filecheck %s

;; Tests for aggregating effects from indirect calls in GlobalEffects when
;; --closed-world is true. Continued from global-effects-closed-world.wast.
Expand All @@ -8,6 +8,8 @@
;; CHECK: (type $nopType (func (param i32)))
(type $nopType (func (param i32)))

(table 1 1 funcref)

;; CHECK: (func $nop (type $nopType) (param $0 i32)
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: )
Expand All @@ -16,22 +18,19 @@
)

;; CHECK: (func $calls-nop-via-nullable-ref (type $1) (param $ref (ref null $nopType))
;; CHECK-NEXT: (call_ref $nopType
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: (local.get $ref)
;; CHECK-NEXT: )
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: )
(func $calls-nop-via-nullable-ref (param $ref (ref null $nopType))
;; We would trap if $ref is null, but otherwise this has no effects.
(call_ref $nopType (i32.const 1) (local.get $ref))
)

;; CHECK: (func $f (type $1) (param $ref (ref null $nopType))
;; CHECK: (func $calls-nop-via-ref (type $2)
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: )
(func $f (param $ref (ref null $nopType))
;; The only possible implementation of $nopType has no effects.
;; $calls-nop-via-nullable-ref may trap from a null reference, but
;; --traps-never-happen is enabled, so we're free to optimize this out.
(call $calls-nop-via-nullable-ref (local.get $ref))
(func $calls-nop-via-ref
;; We may trap due to index out of bounds or the function type not matching
;; the table, but otherwise this has no possible effects.
(call_indirect (type $nopType) (i32.const 1) (i32.const 0))
)
)
Loading
Loading