Skip to content
Open
114 changes: 109 additions & 5 deletions src/passes/Asyncify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@
// Overall, this should allow good performance with small overhead that is
// mostly noticed at rewind time.
//
// Exceptions handling (-fwasm-exceptions) is partially supported, everything
// except for handling unwinding from within a catch block. If assertions mode
// is enabled then this pass will check for that problem, and if so, throw an
// unreachable exception. (If "ignore unwind from catch" mode is enabled then
// Asyncify will silently skip any unwind call from within catch blocks, see
// below.)
//
// After this pass is run a new i32 global "__asyncify_state" is added, which
// has the following values:
//
Expand Down Expand Up @@ -239,6 +246,12 @@
// an unwind/rewind in an invalid place (this can be helpful for manual
// tweaking of the only-list / remove-list, see later).
//
// --pass-arg=asyncify-ignore-unwind-from-catch
//
// When an unwind operation is triggered from inside a wasm-exceptions
// catch block, which is not supported, silently ignore it rather than
// fail during rewinding later. (This is unsafe in general.)
//
// --pass-arg=asyncify-verbose
//
// Logs out instrumentation decisions to the console. This can help figure
Expand Down Expand Up @@ -1138,6 +1151,18 @@ struct AsyncifyFlow : public Pass {
// here as well.
results.push_back(makeCallSupport(curr));
continue;
} else if (auto* try_ = curr->dynCast<Try>()) {
if (item.phase == Work::Scan) {
work.push_back(Work{curr, Work::Finish});
work.push_back(Work{try_->body, Work::Scan});
// catchBodies are ignored because we assume that pause/resume will
// not happen inside them
continue;
}
try_->body = results.back();
results.pop_back();
results.push_back(try_);
continue;
}
// We must handle all control flow above, and all things that can change
// the state, so there should be nothing that can reach here - add it
Expand Down Expand Up @@ -1316,6 +1341,81 @@ struct AsyncifyAssertInNonInstrumented : public Pass {
Module* module;
};

struct AsyncifyAssertUnwindCorrectness : Pass {
ModuleAnalyzer* analyzer;
Module* module;

AsyncifyAssertUnwindCorrectness(ModuleAnalyzer* analyzer, Module* module) {
this->analyzer = analyzer;
this->module = module;
}

std::unique_ptr<Pass> create() override {
return std::make_unique<AsyncifyAssertUnwindCorrectness>(analyzer, module);
}

bool isFunctionParallel() override { return true; }
Copy link
Member

Choose a reason for hiding this comment

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

As a convention we prefer this at the top of the class, before all other internals.


void runOnFunction(Module*, Function* function) override {
auto builder = std::make_unique<Builder>(*module);
Copy link
Member

Choose a reason for hiding this comment

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

Builders are very cheap to construct - please create one in replaceCallWithCheck, as needed, which would be simpler.


struct UnwindWalker : WalkerPass<ExpressionStackWalker<UnwindWalker>> {
Function* function;
Builder* builder;

// Adds a check for Call that is inside a Catch block (we do not handle unwinding there).
void checkCallInsideCatch(Call* call) {
auto check = builder->makeIf(
builder->makeBinary(NeInt32,
builder->makeGlobalGet(ASYNCIFY_STATE, Type::i32),
builder->makeConst(int32_t(State::Normal))),
builder->makeUnreachable());
if (call->type.isConcrete()) {
auto temp = builder->addVar(function, call->type);
replaceCurrent(builder->makeBlock(
{
builder->makeLocalSet(temp, call),
check,
builder->makeLocalGet(temp, call->type),
},
call->type));
} else {
replaceCurrent(builder->makeBlock(
{
call,
check,
},
call->type));
}
}

void visitCall(Call* curr) {
assert(!expressionStack.empty());
Copy link
Member

Choose a reason for hiding this comment

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

A return_call (curr->isReturn) can be ignored here: It returns first, leaving the Catch, before calling.

// Go up the stack and see if we are in a Catch.
Index i = expressionStack.size() - 1;
while (i > 0) {
auto* expr = expressionStack[i];
if (Try* aTry = expr->template dynCast<Try>()) {
// check if curr is inside body of aTry (which is safe),
// otherwise do replace a call
assert(i + 1 < expressionStack.size());
if (expressionStack[i + 1] != aTry->body) {
replaceCallWithCheck(curr);
}
break;
}
i--;
}
};
};
Copy link
Member

Choose a reason for hiding this comment

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

We should also handle CallIndirect and CallRef here.


UnwindWalker walker;
walker.function = function;
walker.builder = builder.get();
walker.walk(function->body);
}
};

// Instrument local saving/restoring.
struct AsyncifyLocals : public WalkerPass<PostWalker<AsyncifyLocals>> {
bool isFunctionParallel() override { return true; }
Expand Down Expand Up @@ -1759,8 +1859,12 @@ struct Asyncify : public Pass {
// Add asserts in non-instrumented code. Note we do not use an
// instrumented pass runner here as we do want to run on all functions.
PassRunner runner(module);
runner.add(std::make_unique<AsyncifyAssertInNonInstrumented>(
&analyzer, pointerType, asyncifyMemory));
if (asserts) {
Copy link
Member

Choose a reason for hiding this comment

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

This is already inside an if (asserts), line 1860

runner.add(std::make_unique<AsyncifyAssertInNonInstrumented>(
&analyzer, pointerType, asyncifyMemory));
runner.add(
std::make_unique<AsyncifyAssertUnwindCorrectness>(&analyzer, module));
}
runner.setIsNested(true);
runner.setValidateGlobally(false);
runner.run();
Expand All @@ -1786,7 +1890,7 @@ struct Asyncify : public Pass {
}
// Finally, add function support (that should not have been seen by
// the previous passes).
addFunctions(module);
addFunctions(module, asserts);
}

private:
Expand Down Expand Up @@ -1814,14 +1918,14 @@ struct Asyncify : public Pass {
module->addGlobal(std::move(asyncifyData));
}

void addFunctions(Module* module) {
void addFunctions(Module* module, bool asserts) {
Copy link
Member

Choose a reason for hiding this comment

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

New param seems unused?

Builder builder(*module);
auto makeFunction = [&](Name name, bool setData, State state) {
auto* body = builder.makeBlock();
Copy link
Member

Choose a reason for hiding this comment

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

Why move this?

std::vector<Type> params;
if (setData) {
params.push_back(pointerType);
}
auto* body = builder.makeBlock();
body->list.push_back(builder.makeGlobalSet(
ASYNCIFY_STATE, builder.makeConst(int32_t(state))));
if (setData) {
Expand Down
Loading
Loading