Jit/hir/ssa.cpp (748 lines of code) (raw):
// Copyright (c) Facebook, Inc. and its affiliates. (http://www.facebook.com)
#include "Jit/hir/ssa.h"
#include "Jit/bitvector.h"
#include "Jit/hir/analysis.h"
#include "Jit/hir/hir.h"
#include "Jit/hir/printer.h"
#include "Jit/hir/type.h"
#include "Jit/log.h"
#include <fmt/ostream.h>
#include <ostream>
#include <queue>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace jit {
namespace hir {
namespace {
struct CheckEnv {
CheckEnv(const Function& func, std::ostream& err)
: func{func}, err{err}, assign{func, true} {
assign.Run();
}
const Function& func;
std::ostream& err;
bool ok{true};
// Definite assignment analysis. Used to ensure all uses of a register are
// dominated by its definition.
AssignmentAnalysis assign;
// Flow-insensitive map from register definitions to the source
// block. Tracked separately from `assign` to ensure no register is defined
// twice, even if the first definition doesn't dominate the second.
std::unordered_map<const Register*, const BasicBlock*> defs;
// Current set of defined registers.
RegisterSet defined;
// Current block and instruction.
const BasicBlock* block{nullptr};
const Instr* instr{nullptr};
};
// Verify the following:
// - All blocks reachable from the entry block are part of this CFG.
// - The CFG's block list contains no unreachable blocks.
// - No reachable blocks have any unreachable predecessors.
// - No blocks have > 1 edge from the same predecessor
bool checkCFG(const Function& func, std::ostream& err) {
std::queue<const BasicBlock*> queue;
std::unordered_set<const BasicBlock*> reachable;
queue.push(func.cfg.entry_block);
reachable.insert(func.cfg.entry_block);
while (!queue.empty()) {
auto block = queue.front();
queue.pop();
if (block->cfg != &func.cfg) {
fmt::print(err, "ERROR: Reachable bb {} isn't part of CFG\n", block->id);
return false;
}
for (auto edge : block->out_edges()) {
auto succ = edge->to();
if (reachable.emplace(succ).second) {
queue.push(succ);
}
}
}
for (auto& block : func.cfg.blocks) {
if (!reachable.count(&block)) {
fmt::print(err, "ERROR: CFG contains unreachable bb {}\n", block.id);
return false;
}
std::unordered_set<BasicBlock*> seen;
for (auto edge : block.in_edges()) {
auto pred = edge->from();
if (!reachable.count(pred)) {
fmt::print(
err,
"ERROR: bb {} has unreachable predecessor bb {}\n",
block.id,
pred->id);
return false;
}
if (seen.count(pred)) {
fmt::print(
err,
"ERROR: bb {} has > 1 edge from predecessor bb {}\n",
block.id,
pred->id);
return false;
}
seen.insert(pred);
}
}
return true;
}
void checkPhi(CheckEnv& env) {
auto& phi = static_cast<const Phi&>(*env.instr);
auto block = phi.block();
std::unordered_set<const BasicBlock*> preds;
for (auto edge : block->in_edges()) {
preds.emplace(edge->from());
}
for (auto phi_block : phi.basic_blocks()) {
if (preds.count(phi_block) == 0) {
fmt::print(
env.err,
"ERROR: Instruction '{}' in bb {} references bb {}, which isn't a "
"predecessor\n",
phi,
block->id,
phi_block->id);
env.ok = false;
}
}
}
void checkTerminator(CheckEnv& env) {
auto is_last = env.instr == &env.block->back();
if (env.instr->IsTerminator() && !is_last) {
fmt::print(
env.err,
"ERROR: bb {} contains terminator '{}' in non-terminal position\n",
env.block->id,
*env.instr);
env.ok = false;
}
if (is_last && !env.instr->IsTerminator()) {
fmt::print(
env.err, "ERROR: bb {} has no terminator at end\n", env.block->id);
env.ok = false;
}
}
void checkRegisters(CheckEnv& env) {
if (env.instr->IsPhi()) {
auto phi = static_cast<const Phi*>(env.instr);
for (size_t i = 0; i < phi->NumOperands(); ++i) {
auto operand = phi->GetOperand(i);
if (!env.assign.IsAssignedOut(phi->basic_blocks()[i], operand)) {
fmt::print(
env.err,
"ERROR: Phi input '{}' to instruction '{}' in bb {} not "
"defined at end of bb {}\n",
operand->name(),
*phi,
env.block->id,
phi->basic_blocks()[i]->id);
env.ok = false;
}
}
} else {
for (size_t i = 0, n = env.instr->NumOperands(); i < n; ++i) {
auto operand = env.instr->GetOperand(i);
if (!env.defined.count(operand)) {
fmt::print(
env.err,
"ERROR: Operand '{}' of instruction '{}' not defined at use in "
"bb {}\n",
operand->name(),
*env.instr,
env.block->id);
env.ok = false;
}
}
}
if (auto output = env.instr->GetOutput()) {
if (output->instr() != env.instr) {
fmt::print(
env.err,
"ERROR: {}'s instr is not '{}', which claims to define it\n",
output->name(),
*env.instr);
env.ok = false;
}
auto pair = env.defs.emplace(output, env.block);
if (!pair.second) {
fmt::print(
env.err,
"ERROR: {} redefined in bb {}; previous definition was in bb {}\n",
output->name(),
env.block->id,
pair.first->second->id);
env.ok = false;
}
env.defined.insert(output);
}
}
} // namespace
// Verify the following properties:
//
// - The CFG is well-formed (see checkCFG() for details).
// - Every block has exactly one terminator instruction, as its final
// instruction. This implies that blocks cannot be empty, which is also
// verified.
// - Phi instructions do not appear after any non-Phi instructions in their
// block.
// - Phi instructions only reference direct predecessors.
// - No register is assigned to by more than one instruction.
// - Every register has a link to its defining instruction.
// - All uses of a register are dominated by its definition.
bool checkFunc(const Function& func, std::ostream& err) {
if (!checkCFG(func, err)) {
return false;
}
CheckEnv env{func, err};
for (auto& block : func.cfg.blocks) {
env.block = █
env.defined = env.assign.GetIn(&block);
if (block.empty()) {
fmt::print(err, "ERROR: bb {} has no instructions\n", block.id);
env.ok = false;
continue;
}
bool phi_section = true;
bool allow_prologue_loads = env.block == func.cfg.entry_block;
for (auto& instr : block) {
env.instr = &instr;
if (instr.IsPhi()) {
if (!phi_section) {
fmt::print(
err,
"ERROR: '{}' in bb {} comes after non-Phi instruction\n",
instr,
block.id);
env.ok = false;
continue;
}
checkPhi(env);
} else {
phi_section = false;
}
if (instr.IsLoadArg() || instr.IsLoadCurrentFunc()) {
if (!allow_prologue_loads) {
fmt::print(
err,
"ERROR: '{}' in bb {} comes after non-LoadArg instruction\n",
instr,
block.id);
env.ok = false;
}
} else {
allow_prologue_loads = false;
}
checkTerminator(env);
checkRegisters(env);
}
}
return env.ok;
}
Type outputType(
const Instr& instr,
const std::function<Type(std::size_t)>& get_op_type) {
switch (instr.opcode()) {
case Opcode::kCallEx:
case Opcode::kCallExKw:
case Opcode::kCallMethod:
case Opcode::kCompare:
case Opcode::kBinaryOp:
case Opcode::kFillTypeAttrCache:
case Opcode::kGetIter:
case Opcode::kImportFrom:
case Opcode::kImportName:
case Opcode::kInPlaceOp:
case Opcode::kInvokeIterNext:
case Opcode::kInvokeMethod:
case Opcode::kLoadAttr:
case Opcode::kLoadAttrSpecial:
case Opcode::kLoadAttrSuper:
case Opcode::kLoadGlobal:
case Opcode::kLoadMethod:
case Opcode::kLoadMethodSuper:
case Opcode::kLoadTupleItem:
case Opcode::kUnaryOp:
case Opcode::kVectorCall:
case Opcode::kVectorCallKW:
case Opcode::kVectorCallStatic:
case Opcode::kWaitHandleLoadCoroOrResult:
return TObject;
case Opcode::kBuildString:
return TMortalUnicode;
// Many opcodes just return a possibly-null PyObject*. Some of these will
// be further specialized based on the input types in the hopefully near
// future.
case Opcode::kCallCFunc:
case Opcode::kLoadCellItem:
case Opcode::kLoadGlobalCached:
case Opcode::kStealCellItem:
case Opcode::kWaitHandleLoadWaiter:
case Opcode::kYieldAndYieldFrom:
case Opcode::kYieldFrom:
case Opcode::kYieldValue:
return TOptObject;
case Opcode::kFormatValue:
return TUnicode;
case Opcode::kLoadVarObjectSize:
return TCInt64;
case Opcode::kInvokeStaticFunction:
return static_cast<const InvokeStaticFunction&>(instr).ret_type();
case Opcode::kLoadArrayItem:
return static_cast<const LoadArrayItem&>(instr).type();
case Opcode::kLoadField:
return static_cast<const LoadField&>(instr).type();
case Opcode::kLoadFieldAddress:
return TCPtr;
case Opcode::kCallStatic: {
auto& call = static_cast<const CallStatic&>(instr);
return call.ret_type();
}
case Opcode::kIntConvert: {
auto& conv = static_cast<const IntConvert&>(instr);
return conv.type();
}
case Opcode::kIntBinaryOp: {
auto& binop = static_cast<const IntBinaryOp&>(instr);
if (binop.op() == BinaryOpKind::kPower ||
binop.op() == BinaryOpKind::kPowerUnsigned) {
return TCDouble;
}
return binop.left()->type().unspecialized();
}
case Opcode::kDoubleBinaryOp: {
return TCDouble;
}
case Opcode::kPrimitiveCompare:
return TCBool;
case Opcode::kPrimitiveUnaryOp:
// TODO if we have a specialized input type we should really be
// constant-folding
if (static_cast<const PrimitiveUnaryOp&>(instr).op() ==
PrimitiveUnaryOpKind::kNotInt) {
return TCBool;
}
return get_op_type(0).unspecialized();
// Some return something slightly more interesting.
case Opcode::kBuildSlice:
return TMortalSlice;
case Opcode::kGetTuple:
return TTupleExact;
case Opcode::kInitialYield:
return TOptNoneType;
case Opcode::kLoadArg: {
auto& loadarg = static_cast<const LoadArg&>(instr);
Type typ = loadarg.type();
return typ <= TCEnum ? TCInt64 : typ;
}
case Opcode::kLoadCurrentFunc:
return TFunc;
case Opcode::kLoadEvalBreaker:
return TCInt32;
case Opcode::kMakeCell:
return TMortalCell;
case Opcode::kMakeDict:
return TMortalDict;
case Opcode::kMakeCheckedDict: {
auto& makechkdict = static_cast<const MakeCheckedDict&>(instr);
return makechkdict.type();
}
case Opcode::kMakeCheckedList: {
auto& makechklist = static_cast<const MakeCheckedList&>(instr);
return makechklist.type();
}
case Opcode::kMakeFunction:
return TMortalFunc;
case Opcode::kMakeSet:
return TMortalSet;
case Opcode::kLongBinaryOp: {
auto& binop = static_cast<const LongBinaryOp&>(instr);
if (binop.op() == BinaryOpKind::kTrueDivide) {
return TFloatExact;
}
return TLongExact;
}
case Opcode::kLongCompare:
case Opcode::kRunPeriodicTasks:
return TBool;
// TODO(bsimmers): These wrap C functions that return 0 for success and -1
// for an error, which is converted into Py_None or nullptr,
// respectively. At some point we should get rid of this extra layer and
// deal with the int return value directly.
case Opcode::kListExtend:
case Opcode::kMergeDictUnpack:
case Opcode::kStoreAttr:
return TNoneType;
case Opcode::kListAppend:
case Opcode::kMergeSetUnpack:
case Opcode::kSetSetItem:
case Opcode::kSetDictItem:
case Opcode::kStoreSubscr:
return TCInt32;
case Opcode::kIsErrStopAsyncIteration:
return TCInt32;
case Opcode::kIsNegativeAndErrOccurred:
return TCInt64;
// Some compute their output type from either their inputs or some other
// source.
// Executing LoadTypeAttrCacheItem<cache_id, 1> is only legal if
// appropriately guarded by LoadTypeAttrCacheItem<cache_id, 0>, and the
// former will always produce a non-null object.
//
// TODO(bsimmers): We should probably split this into two instructions
// rather than changing the output type based on the item index.
case Opcode::kLoadTypeAttrCacheItem: {
auto item = static_cast<const LoadTypeAttrCacheItem&>(instr).item_idx();
return item == 1 ? TObject : TOptObject;
}
case Opcode::kAssign:
return get_op_type(0);
case Opcode::kLoadConst: {
return static_cast<const LoadConst&>(instr).type();
}
case Opcode::kMakeListTuple: {
auto is_tuple = static_cast<const MakeListTuple&>(instr).is_tuple();
return is_tuple ? TMortalTupleExact : TMortalListExact;
}
case Opcode::kMakeTupleFromList:
case Opcode::kUnpackExToTuple:
return TMortalTupleExact;
case Opcode::kPhi: {
auto ty = TBottom;
for (std::size_t i = 0, n = instr.NumOperands(); i < n; ++i) {
ty |= get_op_type(i);
}
return ty;
}
case Opcode::kCheckSequenceBounds: {
return TCInt64;
}
// 1 if comparison is true, 0 if not, -1 on error
case Opcode::kCompareBool:
case Opcode::kIsInstance:
// 1 if is subtype, 0 if not
case Opcode::kIsSubtype:
// 1, 0 if the value is truthy, not truthy
case Opcode::kIsTruthy: {
return TCInt32;
}
case Opcode::kLoadFunctionIndirect: {
return TObject;
}
case Opcode::kRepeatList: {
return TListExact;
}
case Opcode::kRepeatTuple: {
return TTupleExact;
}
case Opcode::kPrimitiveBox: {
// This duplicates the logic in Type::asBoxed(), but it has enough
// special cases (for exactness/optionality/nullptr) that it's not worth
// trying to reuse it here.
auto& pb = static_cast<const PrimitiveBox&>(instr);
if (pb.type() <= TCEnum) {
// Calling an enum type in JITRT_BoxEnum can raise an exception
return TOptObject;
}
if (pb.value()->type() <= TCDouble) {
return TOptFloatExact;
}
if (pb.value()->type() <= (TCUnsigned | TCSigned | TNullptr)) {
// Special Nullptr case for an uninitialized variable; load zero.
return TOptLongExact;
}
if (pb.value()->type() <= TCBool) {
// JITRT_BoxBool cannot fail since it returns one of two globals and
// does not allocate.
return TBool;
}
JIT_CHECK(
false,
"only primitive numeric types should be boxed. type verification"
"missed an unexpected type %s",
pb.value()->type());
}
case Opcode::kPrimitiveUnbox: {
auto& unbox = static_cast<const PrimitiveUnbox&>(instr);
Type typ = unbox.type();
return typ <= TCEnum ? TCInt64 : typ;
}
// Check opcodes return a copy of their input that is statically known to
// not be null.
case Opcode::kCheckExc:
case Opcode::kCheckField:
case Opcode::kCheckFreevar:
case Opcode::kCheckNeg:
case Opcode::kCheckVar: {
return get_op_type(0) - TNullptr;
}
case Opcode::kGuardIs: {
return Type::fromObject(static_cast<const GuardIs&>(instr).target());
}
case Opcode::kCast: {
auto& cast = static_cast<const Cast&>(instr);
Type to_type = Type::fromType(cast.pytype()) |
(cast.optional() ? TNoneType : TBottom);
return to_type;
}
case Opcode::kTpAlloc: {
auto& tp_alloc = static_cast<const TpAlloc&>(instr);
Type alloc_type = Type::fromTypeExact(tp_alloc.pytype());
return alloc_type;
}
// Refine type gives us more information about the type of its input.
case Opcode::kRefineType: {
auto type = static_cast<const RefineType&>(instr).type();
return get_op_type(0) & type;
}
case Opcode::kGuardType: {
auto type = static_cast<const GuardType&>(instr).target();
return get_op_type(0) & type;
}
// Finally, some opcodes have no destination.
case Opcode::kBatchDecref:
case Opcode::kBeginInlinedFunction:
case Opcode::kBranch:
case Opcode::kCallStaticRetVoid:
case Opcode::kClearError:
case Opcode::kCondBranch:
case Opcode::kCondBranchCheckType:
case Opcode::kCondBranchIterNotDone:
case Opcode::kDecref:
case Opcode::kDeleteAttr:
case Opcode::kDeleteSubscr:
case Opcode::kDeopt:
case Opcode::kDeoptPatchpoint:
case Opcode::kEndInlinedFunction:
case Opcode::kGuard:
case Opcode::kHintType:
case Opcode::kIncref:
case Opcode::kInitFunction:
case Opcode::kInitListTuple:
case Opcode::kRaise:
case Opcode::kRaiseAwaitableError:
case Opcode::kRaiseStatic:
case Opcode::kReturn:
case Opcode::kSetCurrentAwaiter:
case Opcode::kSetCellItem:
case Opcode::kSetFunctionAttr:
case Opcode::kSnapshot:
case Opcode::kStoreArrayItem:
case Opcode::kStoreField:
case Opcode::kUseType:
case Opcode::kWaitHandleRelease:
case Opcode::kXDecref:
case Opcode::kXIncref:
JIT_CHECK(false, "Opcode %s has no output", instr.opname());
}
JIT_CHECK(false, "Bad opcode %d", static_cast<int>(instr.opcode()));
}
Type outputType(const Instr& instr) {
return outputType(
instr, [&](std::size_t ind) { return instr.GetOperand(ind)->type(); });
}
void reflowTypes(Environment* env, BasicBlock* start) {
// First, reset all types to Bottom so Phi inputs from back edges don't
// contribute to the output type of the Phi until they've been processed.
for (auto& pair : env->GetRegisters()) {
pair.second->set_type(TBottom);
}
// Next, flow types forward, iterating to a fixed point.
auto rpo_blocks = CFG::GetRPOTraversal(start);
for (bool changed = true; changed;) {
changed = false;
for (auto block : rpo_blocks) {
for (auto& instr : *block) {
if (instr.opcode() == Opcode::kReturn) {
Type type = static_cast<const Return&>(instr).type();
JIT_DCHECK(
instr.GetOperand(0)->type() <= type,
"bad return type %s, expected %s in %s",
instr.GetOperand(0)->type(),
type,
*start->cfg);
}
auto dst = instr.GetOutput();
if (dst == nullptr) {
continue;
}
auto new_ty = outputType(instr);
if (new_ty == dst->type()) {
continue;
}
dst->set_type(new_ty);
changed = true;
}
}
}
}
void reflowTypes(Function& func) {
reflowTypes(&func.env, func.cfg.entry_block);
}
void SSAify::Run(Function& irfunc) {
Run(irfunc.cfg.entry_block, &irfunc.env);
}
// This implements the algorithm outlined in "Simple and Efficient Construction
// of Static Single Assignment Form"
// https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf
void SSAify::Run(BasicBlock* start, Environment* env) {
env_ = env;
auto blocks = CFG::GetRPOTraversal(start);
auto ssa_basic_blocks = initSSABasicBlocks(blocks);
reg_replacements_.clear();
phi_uses_.clear();
for (auto& block : blocks) {
auto ssablock = ssa_basic_blocks.at(block);
for (auto& instr : *block) {
JIT_CHECK(!instr.IsPhi(), "SSAify does not support Phis in its input");
instr.visitUses([&](Register*& reg) {
JIT_CHECK(
reg != nullptr, "Instructions should not have nullptr operands.")
reg = getDefine(ssablock, reg);
return true;
});
auto out_reg = instr.GetOutput();
if (out_reg != nullptr) {
auto new_reg = env_->AllocateRegister();
instr.SetOutput(new_reg);
ssablock->local_defs[out_reg] = new_reg;
}
}
for (auto& succ : ssablock->succs) {
succ->unsealed_preds--;
if (succ->unsealed_preds > 0) {
continue;
}
fixIncompletePhis(succ);
}
}
fixRegisters(ssa_basic_blocks);
// realize phi functions
for (auto& bb : ssa_basic_blocks) {
auto block = bb.first;
auto ssablock = bb.second;
for (auto& pair : ssablock->phi_nodes) {
block->push_front(pair.second);
}
delete ssablock;
}
reflowTypes(env, start);
}
Register* SSAify::getDefine(SSABasicBlock* ssablock, Register* reg) {
auto iter = ssablock->local_defs.find(reg);
if (iter != ssablock->local_defs.end()) {
// If defined locally, just return
return iter->second;
}
if (ssablock->preds.size() == 0) {
// If we made it back to the entry block and didn't find a definition, use
// a Nullptr from LoadConst. Place it after the initialization of the args
// which explicitly come first.
if (null_reg_ == nullptr) {
auto it = ssablock->block->begin();
while (it != ssablock->block->end() &&
(it->IsLoadArg() || it->IsLoadCurrentFunc())) {
++it;
}
null_reg_ = env_->AllocateRegister();
auto loadnull = LoadConst::create(null_reg_, TNullptr);
loadnull->copyBytecodeOffset(*it);
loadnull->InsertBefore(*it);
}
ssablock->local_defs.emplace(reg, null_reg_);
return null_reg_;
}
if (ssablock->unsealed_preds > 0) {
// If we haven't visited all our predecessors, they can't provide
// definitions for us to look up. We'll place an incomplete phi that will
// be resolved once we've visited all predecessors.
auto phi_output = env_->AllocateRegister();
ssablock->incomplete_phis.emplace_back(reg, phi_output);
ssablock->local_defs.emplace(reg, phi_output);
return phi_output;
}
if (ssablock->preds.size() == 1) {
// If we only have a single predecessor, use its value
auto new_reg = getDefine(*ssablock->preds.begin(), reg);
ssablock->local_defs.emplace(reg, new_reg);
return new_reg;
}
// We have multiple predecessors and may need to create a phi.
auto new_reg = env_->AllocateRegister();
// Adding a phi may loop back to our block if there is a loop in the CFG. We
// update our local_defs before adding the phi to terminate the recursion
// rather than looping infinitely.
ssablock->local_defs.emplace(reg, new_reg);
maybeAddPhi(ssablock, reg, new_reg);
return ssablock->local_defs.at(reg);
}
void SSAify::maybeAddPhi(
SSABasicBlock* ssa_block,
Register* reg,
Register* out) {
std::unordered_map<BasicBlock*, Register*> pred_defs;
for (auto& pred : ssa_block->preds) {
auto pred_reg = getDefine(pred, reg);
if (auto replacement = getReplacement(pred_reg)) {
pred_reg = replacement;
}
pred_defs.emplace(pred->block, pred_reg);
}
Register* replacement = getCommonPredValue(out, pred_defs);
if (replacement != nullptr) {
removeTrivialPhi(ssa_block, reg, out, replacement);
} else {
auto bc_off = ssa_block->block->begin()->bytecodeOffset();
auto phi = Phi::create(out, pred_defs);
phi->setBytecodeOffset(bc_off);
ssa_block->phi_nodes.emplace(out, phi);
for (auto& def_pair : pred_defs) {
phi_uses_[def_pair.second].emplace(phi, ssa_block);
}
}
}
void SSAify::removeTrivialPhi(
SSABasicBlock* ssa_block,
Register* reg,
Register* from,
Register* to) {
// Update our local definition for reg if it was provided by the phi.
auto it = ssa_block->local_defs.find(reg);
if (it->second == from) {
it->second = to;
}
// If we're removing a phi that was realized, delete the corresponding
// instruction
auto phi_it = ssa_block->phi_nodes.find(from);
if (phi_it != ssa_block->phi_nodes.end()) {
Phi* phi = phi_it->second;
for (std::size_t i = 0; i < phi->NumOperands(); i++) {
phi_uses_[phi->GetOperand(i)].erase(phi);
}
ssa_block->phi_nodes.erase(phi_it);
delete phi;
}
// We need to replace all uses of the value the phi would have produced with
// the replacement. This is where our implementation diverges from the
// paper. We record that non-phi uses of the original value should be
// replaced with the new value. Once we've finished processing the CFG we
// will go through and fix up all uses as a final step.
reg_replacements_[from] = to;
// Finally, we eagerly update all phis that used the original value since
// some of them may become trivial. This process is repeated recursively
// until no more trivial phis can be removed.
auto use_it = phi_uses_.find(from);
if (use_it == phi_uses_.end()) {
return;
}
for (auto& use : use_it->second) {
Phi* phi = use.first;
phi->ReplaceUsesOf(from, to);
phi_uses_[to][phi] = use.second;
Register* trivial_out = phi->isTrivial();
if (trivial_out != nullptr) {
removeTrivialPhi(use.second, reg, phi->GetOutput(), trivial_out);
}
}
phi_uses_.erase(from);
}
Register* SSAify::getCommonPredValue(
const Register* out_reg,
const std::unordered_map<BasicBlock*, Register*>& defs) {
Register* other_reg = nullptr;
for (auto& def_pair : defs) {
auto def = def_pair.second;
if (def == out_reg) {
continue;
}
if (other_reg != nullptr && def != other_reg) {
return nullptr;
}
other_reg = def;
}
return other_reg;
}
void SSAify::fixIncompletePhis(SSABasicBlock* ssa_block) {
for (auto& pi : ssa_block->incomplete_phis) {
maybeAddPhi(ssa_block, pi.first, pi.second);
}
}
std::unordered_map<BasicBlock*, SSABasicBlock*> SSAify::initSSABasicBlocks(
std::vector<BasicBlock*>& blocks) {
std::unordered_map<BasicBlock*, SSABasicBlock*> ssa_basic_blocks;
auto get_or_create_ssa_block =
[&ssa_basic_blocks](BasicBlock* block) -> SSABasicBlock* {
auto iter = ssa_basic_blocks.find(block);
if (iter == ssa_basic_blocks.end()) {
auto ssablock = new SSABasicBlock(block);
ssa_basic_blocks.emplace(block, ssablock);
return ssablock;
}
return iter->second;
};
for (auto& block : blocks) {
auto ssablock = get_or_create_ssa_block(block);
for (auto& edge : block->out_edges()) {
auto succ = edge->to();
auto succ_ssa_block = get_or_create_ssa_block(succ);
auto p = succ_ssa_block->preds.insert(ssablock);
if (p.second) {
// It's possible that we have multiple outgoing edges to the same
// successor. Since we only care about the number of unsealed
// predecessor *nodes*, only update if this is the first time we're
// processing this predecessor.
succ_ssa_block->unsealed_preds++;
ssablock->succs.insert(succ_ssa_block);
}
}
}
return ssa_basic_blocks;
}
void SSAify::fixRegisters(
std::unordered_map<BasicBlock*, SSABasicBlock*>& ssa_basic_blocks) {
for (auto& bb : ssa_basic_blocks) {
auto ssa_block = bb.second;
for (auto& instr : *(ssa_block->block)) {
instr.visitUses([&](Register*& reg) {
if (auto replacement = getReplacement(reg)) {
reg = replacement;
}
return true;
});
}
}
}
Register* SSAify::getReplacement(Register* reg) {
Register* replacement = reg;
std::vector<std::unordered_map<Register*, Register*>::iterator> chain;
auto prev_it = reg_replacements_.end();
while (true) {
auto it = reg_replacements_.find(replacement);
if (it == reg_replacements_.end()) {
break;
}
replacement = it->second;
if (prev_it != reg_replacements_.end()) {
chain.emplace_back(prev_it);
}
prev_it = it;
}
for (auto& it : chain) {
it->second = replacement;
}
return replacement == reg ? nullptr : replacement;
}
} // namespace hir
} // namespace jit