blob: 14f2fe8dacf8f5d30fd2c2b3706c6e840f86544f [file] [edit]
/*
* Copyright 2019 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.
*/
//
// Turn indirect calls into direct calls. This is possible if we know
// the table cannot change, and if we see a constant argument for the
// indirect call's index.
//
// If called with
//
// --pass-arg=directize-initial-contents-immutable
//
// then the initial tables' contents are assumed to be immutable (see
// TableUtils::TableInfo).
//
#include <unordered_map>
#include "call-utils.h"
#include "ir/drop.h"
#include "ir/eh-utils.h"
#include "ir/find_all.h"
#include "ir/table-utils.h"
#include "ir/utils.h"
#include "pass.h"
#include "wasm-builder.h"
#include "wasm-traversal.h"
#include "wasm.h"
namespace wasm {
namespace {
struct FunctionDirectizer : public WalkerPass<PostWalker<FunctionDirectizer>> {
bool isFunctionParallel() override { return true; }
std::unique_ptr<Pass> create() override {
return std::make_unique<FunctionDirectizer>(tables);
}
FunctionDirectizer(const TableUtils::TableInfoMap& tables) : tables(tables) {}
bool optimized = false;
void visitCallIndirect(CallIndirect* curr) {
auto& table = tables.at(curr->table);
if (!table.canOptimizeByEntry()) {
return;
}
// If the target is constant, we can emit a direct call.
if (curr->target->is<Const>()) {
std::vector<Expression*> operands(curr->operands.begin(),
curr->operands.end());
makeDirectCall(operands, curr->target, table, curr);
optimized = true;
return;
}
// Emit direct calls for things like a select over constants.
if (auto* calls = CallUtils::convertToDirectCalls(
curr,
[&](Expression* target) {
return getTargetInfo(target, table, curr);
},
*getFunction(),
*getModule())) {
replaceCurrent(calls);
optimized = true;
// Note that types may have changed, as the utility here can add locals
// which require fixups if they are non-nullable, for example.
changedTypes = true;
return;
}
}
bool hasTry = false;
void visitTry(Try* curr) { hasTry = true; }
void doWalkFunction(Function* func) {
WalkerPass<PostWalker<FunctionDirectizer>>::doWalkFunction(func);
if (optimized && hasTry) {
EHUtils::handleBlockNestedPops(func, *getModule());
}
if (changedTypes) {
ReFinalize().walkFunctionInModule(func, getModule());
}
}
private:
const TableUtils::TableInfoMap& tables;
bool changedTypes = false;
// Given an expression that we will use as the target of an indirect call,
// analyze it and return one of the results of CallUtils::IndirectCallInfo,
// that is, whether we know a direct call target, or we know it will trap, or
// if we know nothing.
CallUtils::IndirectCallInfo getTargetInfo(Expression* target,
const TableUtils::TableInfo& info,
CallIndirect* original) {
auto* c = target->dynCast<Const>();
if (!c) {
return CallUtils::Unknown{};
}
Address index = c->value.getUnsigned();
// We'll check if we know what is called from the table's initial content,
// but we can only do this if the initial contents are immutable, or if
// there is no writing to the table at all.
if (!info.initialContentsImmutable && info.hasSet) {
return CallUtils::Unknown{};
}
auto* table = getModule()->getTable(original->table);
Name calledName;
auto& flatTable = *info.flatTable;
if (index < flatTable.names.size()) {
calledName = flatTable.names[index];
}
if (!calledName) {
// We did not see a value there, but the table might have a default
// value.
if (table->imported()) {
// An imported table might have a default value, and we can't tell.
return CallUtils::Unknown{};
}
if (table->init) {
if (index < table->initial) {
// This is in bounds, so the default value of the table is called.
if (auto* refFunc = table->init->dynCast<RefFunc>()) {
calledName = refFunc->func;
} else {
// There is an initial value, but it is unknown, like a
// global.get. We can infer nothing, not even a trap.
return CallUtils::Unknown{};
}
} else {
// We are beyond the initial table size, and can't infer anything,
// unless we are in the simple case of no growth, in which case we
// trap.
if (!info.hasGrow) {
return CallUtils::Trap{};
} else {
return CallUtils::Unknown{};
}
}
}
}
// If we found no data, and the table init did not change anything, then
// we trap.
if (!calledName) {
// But we must not read a place where grow can write to - we must either
// have no grow, or have an index below where grow writes to.
if (!info.hasGrow || index < table->initial) {
return CallUtils::Trap{};
}
// Otherwise, give up.
return CallUtils::Unknown{};
}
// We know what is called, but it must have the right type.
auto* func = getModule()->getFunction(calledName);
if (!HeapType::isSubType(func->type.getHeapType(), original->heapType)) {
return CallUtils::Trap{};
}
// We know exactly what is called, and it does not trap.
return CallUtils::Known{calledName};
}
// Create a direct call for a given list of operands, an expression which is
// known to contain a constant indicating the table offset, and the relevant
// table, if we can. If we can see that the call will trap, instead replace
// with an unreachable.
void makeDirectCall(const std::vector<Expression*>& operands,
Expression* c,
const TableUtils::TableInfo& table,
CallIndirect* original) {
auto info = getTargetInfo(c, table, original);
if (std::get_if<CallUtils::Unknown>(&info)) {
// We don't know anything here.
return;
}
// If the index is invalid, or the type is wrong, we can skip the call and
// emit an unreachable here (with dropped children as needed), since in
// Binaryen it is ok to reorder/replace traps when optimizing (but never to
// remove them, at least not by default).
if (std::get_if<CallUtils::Trap>(&info)) {
replaceCurrent(
getDroppedChildrenAndAppend(original,
*getModule(),
getPassOptions(),
Builder(*getModule()).makeUnreachable(),
DropMode::IgnoreParentEffects));
changedTypes = true;
return;
}
// Everything looks good!
auto name = std::get<CallUtils::Known>(info).target;
auto results = getModule()->getFunction(name)->getResults();
replaceCurrent(Builder(*getModule())
.makeCall(name, operands, results, original->isReturn));
// When we call a function of a subtype of the call_indirect's call type, we
// may be refining results.
if (results != original->type) {
changedTypes = true;
}
}
};
struct Directize : public Pass {
void run(Module* module) override {
if (module->tables.empty()) {
return;
}
// TODO: consider a per-table option here
auto initialContentsImmutable =
hasArgument("directize-initial-contents-immutable");
auto tables =
TableUtils::computeTableInfo(*module, initialContentsImmutable);
// Stop if we cannot optimize anything.
auto hasOptimizableTable = false;
for (auto& [_, info] : tables) {
if (info.canOptimizeByEntry()) {
hasOptimizableTable = true;
break;
}
}
if (!hasOptimizableTable) {
return;
}
// We can optimize!
FunctionDirectizer(tables).run(getPassRunner(), module);
}
};
} // anonymous namespace
Pass* createDirectizePass() { return new Directize(); }
} // namespace wasm