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
195 changes: 114 additions & 81 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 @@ -670,27 +671,34 @@ class EffectAnalyzer {
}
}

// Handle effects due to a null type arriving in a place where a null input
// causes trapping. That is, handle the case of the type proving that the
// input is null.
// Returns true iff there is no need to consider further effects.
bool trapOnNull(Type type) {
if (type == Type::unreachable) {
return true;
}
assert(type.isRef());
if (type.isNull()) {
parent.trap = true;
return true;
}
if (type.isNullable()) {
parent.implicitTrap = true;
}

return false;
}

// Handle effects due to an explicit null check of the operands in `exprs`.
// Returns true iff there is no need to consider further effects.
bool trapOnNull(std::initializer_list<Expression*> exprs) {
for (auto* expr : exprs) {
if (expr && expr->type == Type::unreachable) {
if (expr && trapOnNull(expr->type)) {
return true;
}
}
for (auto* expr : exprs) {
assert(!expr || expr->type.isRef());
if (expr && expr->type.isNull()) {
parent.trap = true;
return true;
}
}
for (auto* expr : exprs) {
if (expr && expr->type.isNullable()) {
parent.implicitTrap = true;
break;
}
}
return false;
}

Expand All @@ -716,71 +724,52 @@ class EffectAnalyzer {
}

void visitCall(Call* curr) {
// 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* callTargetEffects = nullptr;
if (auto* target = parent.module.getFunctionOrNull(curr->target);
target && target->effects) {
callTargetEffects = 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;
}
addCallEffects(curr, callTargetEffects);
}
void visitCallIndirect(CallIndirect* curr) {
auto* table = parent.module.getTable(curr->table);
if (trapOnNull(table->type)) {
return;
}

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);
}
if (!Type::isSubType(Type(curr->heapType, Nullability::NonNullable),
table->type)) {
parent.trap = true;
return;
}

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;
// Due to index out of bounds. Type-related traps are handled above and
// may set either implicitTrap or trap (or neither).
parent.implicitTrap = true;

const EffectAnalyzer* callTargetEffects = nullptr;
if (auto it = parent.module.indirectCallEffects.find(curr->heapType);
it != parent.module.indirectCallEffects.end()) {
callTargetEffects = it->second.get();
}
addCallEffects(curr, callTargetEffects);
}
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* callTargetEffects = nullptr;
if (auto it = parent.module.indirectCallEffects.find(
curr->target->type.getHeapType());
it != parent.module.indirectCallEffects.end()) {
callTargetEffects = it->second.get();
}
addCallEffects(curr, callTargetEffects);
}
void visitLocalGet(LocalGet* curr) {
parent.localsRead.insert(curr->index);
Expand Down Expand Up @@ -1038,22 +1027,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 +1308,66 @@ class EffectAnalyzer {
parent.throws_ = true;
}
}

private:
// Populate a call's effects using effects 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>
void addCallEffectsFromGlobalEffects(const CallType* curr,
const EffectAnalyzer& funcEffects) {
if (curr->isReturn) {
if (funcEffects.throws()) {
parent.hasReturnCallThrow = true;
}
}

if (funcEffects.throws_ && (parent.tryDepth > 0 || curr->isReturn)) {
// We can ignore a throw here, as the parent catches it.
//
// Also, we can 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.
//
// TODO: we check for throws_ here instead of throws() and only clear
// throws_. So throws() can remain true in the case that this expression
// is the target of a delegate statement. We should consider clearing
// filteredEffects.delegateTargets here.
auto filteredEffects = funcEffects;
filteredEffects.throws_ = false;
parent.mergeIn(filteredEffects);
} else {
parent.mergeIn(funcEffects);
}
}

// Common effects logic for the 3 types of call: `call`, `call_indirect`,
// and `call_ref`.
template<typename CallType>
void addCallEffects(const CallType* curr,
const EffectAnalyzer* callTargetEffects) {
if (curr->isReturn) {
parent.branchesOut = true;
}

if (callTargetEffects) {
addCallEffectsFromGlobalEffects(curr, *callTargetEffects);
return;
}

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

public:
Expand Down
59 changes: 38 additions & 21 deletions src/ir/linear-execution.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ struct LinearExecutionWalker : public PostWalker<SubType, VisitorType> {
static void scan(SubType* self, Expression** currp) {
Expression* curr = *currp;

auto handleCall = [&](bool mayThrow, bool isReturn) {
auto handleCall = [&](bool isReturn, const EffectAnalyzer* effects) {
bool refutesThrowEffect = effects && !effects->throws_;
bool mayThrow = !self->getModule() ||
self->getModule()->features.hasExceptionHandling();
mayThrow = mayThrow && !refutesThrowEffect;

if (!self->connectAdjacentBlocks) {
// Control is nonlinear if we return or throw. Traps don't need to be
// taken into account since they don't break control flow in a way
Expand Down Expand Up @@ -156,40 +161,52 @@ struct LinearExecutionWalker : public PostWalker<SubType, VisitorType> {
case Expression::Id::CallId: {
auto* call = curr->cast<Call>();

bool mayThrow = !self->getModule() ||
self->getModule()->features.hasExceptionHandling();
if (mayThrow && self->getModule()) {
auto* effects =
self->getModule()->getFunction(call->target)->effects.get();

if (effects && !effects->throws_) {
mayThrow = false;
const EffectAnalyzer* effects = nullptr;
if (self->getModule()) {
auto* func = self->getModule()->getFunctionOrNull(call->target);
if (func) {
effects = func->effects.get();
Comment on lines +166 to +168
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.

I would expect us to be able to assume that the target function exists, given that it would be invalid for it not to exist.

Suggested change
auto* func = self->getModule()->getFunctionOrNull(call->target);
if (func) {
effects = func->effects.get();
auto* func = self->getModule()->getFunction(call->target);
effects = func->effects.get();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I thought so too, but when we run with --asyncify it causes us to fail when trying to lookup __asyncify_get_call_index. Maybe it's a problem with when the function is generated?

(lldb) f 1
frame #1: 0x00007ffff636e8a7 libbinaryen.so`wasm::LinearExecutionWalker<wasm::SimplifyLocals<true, false, true>, wasm::Visitor<wasm::SimplifyLocals<true, false, true>, void>>::scan(self=0x00007ffdc4009e50, currp=0x00007ffdb00019a8) at linear-execution.h:168:40
   165           if (self->getModule()) {
   166             auto* func = self->getModule()->getFunctionOrNull(call->target);
   167             if (true || func) {
-> 168               effects = func->effects.get();
   169             }
   170           }
   171
(lldb) p call->target
(wasm::Name) {
  wasm::IString = {
    str = (internal = "__asyncify_get_call_index")
  }
}

The repro is from test/lit/passes/asyncify_optimize-level=1.wast. The earlier code assumed that call targets exist but looks like it never hit an error before by luck.

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.

if (true || func) 😆 what is going on there?

It looks like Asyncify is temporarily adding calls to "intrinsics" that it later replaces with real code sequences. I think it should be fixed to add its "intrinsics" as actual function imports to avoid running other passes on invalid IR. But maybe that can be done as a follow-up. I'd be happy with a TODO here pointing to the problem.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

if (true || func) 😆 what is going on there?

I just quickly added that as an equivalent to your snippet to test out!

Sounds good, will add a todo here and follow up.

}
}

handleCall(mayThrow, call->isReturn);
handleCall(call->isReturn, effects);
break;
}
case Expression::Id::CallRefId: {
auto* callRef = curr->cast<CallRef>();

// TODO: Effect analysis for indirect calls isn't implemented yet.
// Assume any indirect call may throw for now.
bool mayThrow = !self->getModule() ||
self->getModule()->features.hasExceptionHandling();
const EffectAnalyzer* effects = [&]() -> const EffectAnalyzer* {
if (!self->getModule()) {
return nullptr;
}
if (!callRef->target->type.isRef()) {
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.

Might as well filter out nullfuncref and (ref nofunc) here.

Suggested change
if (!callRef->target->type.isRef()) {
if (!callRef->target->type.isSignature()) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I hadn't thought of that, but I think we don't want to do that because that means that we'd fail to lookup effects for calls to these types and be overly conservative as a result. We want the lookup to succeed and see empty effects which is what happens today:

// Add the key to ensure the lookup doesn't fail for indirect calls to
// uninhabited types.
callGraph[calleeType];
. Let me add a test for this case.

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.

I would expect these to get a trap effect and nothing else. Since the result is known, we shouldn't need a map lookup (unless it makes the code much simpler).

return nullptr;
}

handleCall(mayThrow, callRef->isReturn);
auto* effects_ptr =
Comment thread
stevenfontanella marked this conversation as resolved.
find_or_null(self->getModule()->indirectCallEffects,
callRef->target->type.getHeapType());
if (!effects_ptr) {
return nullptr;
}
return effects_ptr->get();
}();

handleCall(callRef->isReturn, effects);
break;
}
case Expression::Id::CallIndirectId: {
auto* callIndirect = curr->cast<CallIndirect>();

// TODO: Effect analysis for indirect calls isn't implemented yet.
// Assume any indirect call may throw for now.
bool mayThrow = !self->getModule() ||
self->getModule()->features.hasExceptionHandling();

handleCall(mayThrow, callIndirect->isReturn);
const EffectAnalyzer* effects = nullptr;
if (self->getModule()) {
if (const auto& effects_ptr =
find_or_null(self->getModule()->indirectCallEffects,
callIndirect->heapType)) {
effects = effects_ptr->get();
}
}
handleCall(callIndirect->isReturn, effects);
break;
}
case Expression::Id::TryId: {
Expand Down
23 changes: 23 additions & 0 deletions src/ir/type-updating.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,29 @@ void GlobalTypeRewriter::mapTypes(const TypeMap& oldToNewTypes) {
for (auto& tag : wasm.tags) {
tag->type = updater.getNew(tag->type);
}

// Update indirect call effects per type.
// When A is rewritten to B, B inherits the effects of A and A loses its
// effects.
std::unordered_map<HeapType, std::shared_ptr<const EffectAnalyzer>>
newTypeEffects;
for (auto& [oldType, oldEffects] : wasm.indirectCallEffects) {
if (!oldEffects) {
continue;
}

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

void GlobalTypeRewriter::mapTypeNamesAndIndices(const TypeMap& oldToNewTypes) {
Expand Down
Loading
Loading