hphp/runtime/vm/jit/frame-state.cpp (1,675 lines of code) (raw):
/*
+----------------------------------------------------------------------+
| HipHop for PHP |
+----------------------------------------------------------------------+
| Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com) |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/
#include "hphp/runtime/vm/jit/frame-state.h"
#include "hphp/runtime/vm/jit/alias-class.h"
#include "hphp/runtime/vm/jit/analysis.h"
#include "hphp/runtime/vm/jit/cfg.h"
#include "hphp/runtime/vm/jit/ir-instruction.h"
#include "hphp/runtime/vm/jit/irgen-call.h"
#include "hphp/runtime/vm/jit/location.h"
#include "hphp/runtime/vm/jit/memory-effects.h"
#include "hphp/runtime/vm/jit/simplify.h"
#include "hphp/runtime/vm/jit/ssa-tmp.h"
#include "hphp/runtime/vm/jit/stack-offsets.h"
#include "hphp/runtime/vm/jit/translator.h"
#include "hphp/runtime/vm/jit/type-array-elem.h"
#include "hphp/runtime/vm/resumable.h"
#include "hphp/util/dataflow-worklist.h"
#include "hphp/util/match.h"
#include "hphp/util/trace.h"
#include <boost/range/adaptor/reversed.hpp>
#include <algorithm>
TRACE_SET_MOD(hhir_fsm);
namespace HPHP::jit::irgen {
namespace {
using Trace::Indent;
///////////////////////////////////////////////////////////////////////////////
/*
* Helper that sets a value to a new value and also returns whether it changed.
*/
template<class T>
bool merge_util(T& oldVal, const T& newVal) {
auto changed = oldVal != newVal;
oldVal = newVal;
return changed;
}
/*
* Merge TypeSourceSets, returning whether anything changed.
*
* TypeSourceSets are merged a join points by unioning the type sources. The
* reason for this is that if a type is constrained, we need to be able to find
* "all" possible sources of the type and constrain them.
*/
bool merge_into(TypeSourceSet& dst, const TypeSourceSet& src) {
auto changed = false;
for (auto x : src) changed = dst.insert(x).second || changed;
return changed;
}
Type bound_type(Type type, Type limit) {
type &= limit;
if (!(type <= limit)) type = limit;
return type;
}
// Merge a SSATmp and Type pair, which are meant to stay in sync
bool merge_value_and_type(SSATmp*& dstValue, SSATmp* srcValue,
Type& dstType, const Type& srcType) {
auto changed = false;
changed |= merge_util(dstType, dstType | srcType);
// Get the least common ancestor across both states.
changed |= merge_util(dstValue, least_common_ancestor(dstValue, srcValue));
// Certain unions of types (involving interfaces) may widen the type
// beyond that of the known value. Clamp the type to the known value
// in that case. Alternately, we could drop the known value, but
// that's usually more valuable.
if (dstValue != nullptr && !(dstType <= dstValue->type())) {
dstType = bound_type(dstType, dstValue->type());
changed = true;
}
return changed;
}
/*
* Merge LocationStates, returning whether anything changed.
*/
template<LTag lt, LTag rt>
bool merge_into(LocationState<lt>& dst, const LocationState<rt>& src) {
auto changed = false;
changed |= merge_value_and_type(dst.value, src.value, dst.type, src.type);
changed |= merge_into(dst.typeSrcs, src.typeSrcs);
if (!dst.maybeChanged && src.maybeChanged) {
dst.maybeChanged = true;
changed = true;
}
return changed;
}
bool merge_memory_stack_into(StackStateMap& dst, const StackStateMap& src) {
auto changed = false;
// Throw away any information only known in dst.
for (auto& [dIdx, dState] : dst) {
if (src.count(dIdx) == 0) {
dState = StackState{};
changed = true;
}
}
// Merge the information from src into dst.
for (auto& [sIdx, sState] : src) {
changed |= merge_into(dst[sIdx], sState);
}
return changed;
}
/*
* Merge one FrameState into another, returning whether it changed. Frame
* pointers and stack depth must match. If the stack pointer tmps are
* different, clear the tracked value (we can make a new one, given fp and
* irSPOff).
*/
bool merge_into(FrameState& dst, const FrameState& src) {
auto changed = false;
// Cannot merge irSPOff state, so assert they match.
always_assert(dst.irSPOff == src.irSPOff);
always_assert(dst.curFunc == src.curFunc);
// The only thing that can change the FP is inlining, but we can't have one
// of the predecessors in an inlined callee while the other isn't.
always_assert(dst.fpValue == src.fpValue);
always_assert(dst.fixupFPValue == src.fixupFPValue);
// We must always have the same spValue.
always_assert(dst.spValue == src.spValue);
// We must always have the same stublogue mode.
always_assert(dst.stublogue == src.stublogue);
changed |= merge_value_and_type(
dst.ctx, src.ctx,
dst.ctxType, src.ctxType
);
changed |= merge_value_and_type(
dst.mbr.ptr, src.mbr.ptr,
dst.mbr.ptrType, src.mbr.ptrType
);
changed |= merge_util(dst.mbr.pointee, dst.mbr.pointee | src.mbr.pointee);
changed |= merge_into(dst.mbase, src.mbase);
changed |= merge_value_and_type(
dst.mTempBase.value, src.mTempBase.value,
dst.mTempBase.type, src.mTempBase.type
);
changed |= merge_util(dst.mROProp, dst.mROProp | src.mROProp);
changed |= merge_util(dst.mbaseLocalType,
dst.mbaseLocalType | src.mbaseLocalType);
changed |= merge_util(dst.mbaseStackType,
dst.mbaseStackType | src.mbaseStackType);
changed |= merge_util(dst.mbaseTempType,
dst.mbaseTempType | src.mbaseTempType);
// If we clamped the mbase type above, we might need to clamp the
// location specific types as well.
if (!(dst.mbaseLocalType <= dst.mbase.type)) {
dst.mbaseLocalType = bound_type(dst.mbaseLocalType, dst.mbase.type);
changed = true;
}
if (!(dst.mbaseStackType <= dst.mbase.type)) {
dst.mbaseStackType = bound_type(dst.mbaseStackType, dst.mbase.type);
changed = true;
}
if (!(dst.mbaseTempType <= dst.mbase.type)) {
dst.mbaseTempType = bound_type(dst.mbaseTempType, dst.mbase.type);
changed = true;
}
// Throw away any local information only known at dst.
for (auto& it : dst.locals) {
auto const id = it.first;
auto& dState = it.second;
if (src.locals.count(id) == 0) {
dState = LocalState{};
changed = true;
}
}
// Merge the information from src into dst.
for (auto& it : src.locals) {
auto const srcId = it.first;
auto const& srcState = it.second;
changed |= merge_into(dst.locals[srcId], srcState);
}
changed |= merge_memory_stack_into(dst.stack, src.stack);
changed |= merge_util(dst.stackModified,
dst.stackModified || src.stackModified);
changed |= merge_util(dst.localsCleared,
dst.localsCleared || src.localsCleared);
// Eval stack depth should be the same at merge points.
always_assert(dst.bcSPOff == src.bcSPOff);
assertx(dst.checkInvariants());
return changed;
}
/*
* Merge two state-stacks. The stacks must have the same depth. Returns
* whether any states changed.
*/
bool merge_into(jit::vector<FrameState>& dst,
const jit::vector<FrameState>& src) {
always_assert(src.size() == dst.size());
auto changed = false;
for (auto idx = uint32_t{0}; idx < dst.size(); ++idx) {
changed |= merge_into(dst[idx], src[idx]);
}
return changed;
}
///////////////////////////////////////////////////////////////////////////////
}
///////////////////////////////////////////////////////////////////////////////
FrameState::FrameState(const Func* func)
: curFunc(func)
{}
FrameStateMgr::FrameStateMgr(const Func* func)
: m_stack{FrameState(func)}
{}
void FrameStateMgr::update(const IRInstruction* inst) {
ITRACE(3, "FrameStateMgr::update processing {}\n", *inst);
Indent _i;
if (auto const taken = inst->taken()) {
// If CheckMROProp fails, we know the bit is false, so propagate
// that to the taken edge.
auto const oldMROProp = cur().mROProp;
if (inst->is(CheckMROProp)) cur().mROProp = TriBool::No;
/*
* TODO(#4323657): we should make this assertion for all non-empty blocks
* (exits in addition to catches). It would fail right now for exit
* traces.
*
* If you hit this assertion: you've created a catch block and then
* modified tracked state, then generated a potentially exception-throwing
* instruction using that catch block as a target. This is not allowed.
*/
if (debug && taken->isCatch()) {
auto const tmp = save(taken);
always_assert_flog(
!tmp,
"catch block B{} had non-matching in state",
taken->id()
);
}
// When we're building the IR, we append a conditional jump after
// generating its target block: see emitJmpCondHelper, where we
// call makeExit() before gen(JmpZero). It doesn't make sense to
// update the target block state at this point, so don't. The
// state doesn't have this problem during optimization passes,
// because we'll always process the jump before the target block.
if (taken->empty()) save(taken);
// Restore the original value since we're going to process the
// next edge now.
cur().mROProp = oldMROProp;
}
auto const killIterLocals = [&](const std::initializer_list<uint32_t>& ids) {
for (auto id : ids) {
setValueAndSyncMBase(loc(id), nullptr, false);
}
};
auto const setFrameCtx = [&] (const SSATmp* fp, SSATmp* ctx) {
for (auto& frame : m_stack) {
if (frame.fpValue != fp) continue;
frame.ctx = ctx;
frame.ctxType = ctx->type();
}
};
switch (inst->op()) {
case BeginInlining: trackBeginInlining(inst); break;
case EndInlining: trackEndInlining(); break;
case InlineCall: trackInlineCall(inst); break;
case StFrameCtx:
setFrameCtx(inst->src(0), inst->src(1));
break;
case Call:
{
assertx(cur().checkMInstrStateDead());
auto const extra = inst->extra<Call>();
// Remove tracked state for the slots for args and the actrec.
uint32_t numCells = kNumActRecCells + extra->numInputs();
for (auto i = uint32_t{0}; i < numCells; ++i) {
setValue(stk(extra->spOffset + i), nullptr);
}
// Mark out parameter locations as being at least InitCell
auto const callee = inst->src(2)->hasConstVal(TFunc)
? inst->src(2)->funcVal() : nullptr;
auto const base = extra->spOffset + numCells;
for (auto i = uint32_t{0}; i < extra->numOut; ++i) {
auto const ty = callee && callee->takesInOutParams()
? irgen::callOutType(callee, i)
: TInitCell;
setType(stk(base + i), ty);
}
// We consider popping an ActRec and args to be synced to memory.
assertx(cur().bcSPOff == inst->marker().bcSPOff());
cur().bcSPOff -= numCells;
}
break;
case ContEnter:
assertx(cur().checkMInstrStateDead());
break;
case DefFP:
case DefFuncEntryFP:
cur().fpValue = inst->dst();
cur().fixupFPValue = inst->dst();
break;
case InitFrame:
/* The last opcode of prologues. Does not modify any defined frame. */
break;
case EnterPrologue:
cur().stublogue = true;
break;
case ExitPrologue:
cur().stublogue = false;
break;
case RetCtrl:
uninitStack();
cur().fpValue = nullptr;
cur().fixupFPValue = nullptr;
break;
case DefFrameRelSP:
case DefRegSP: {
auto const data = inst->extra<DefStackData>();
initStack(inst->dst(), data->irSPOff, data->bcSPOff);
break;
}
case LdMem:
pointerLoad(inst->src(0), inst->dst());
break;
case StMem:
case StMemMeta:
pointerStore(inst->src(0), inst->src(1));
break;
case LdStk: {
auto const offset = inst->extra<LdStk>()->offset;
// Nearly all callers of setValue() for stack slots represent a
// modification of the stack, so it sets stackModified. LdStk is the one
// exception, so we compensate for that here.
auto const oldModified = cur().stackModified;
setValueAndSyncMBase(stk(offset), inst->dst(), true);
cur().stackModified = oldModified;
break;
}
case StStk:
case StStkMeta:
setValueAndSyncMBase(
stk(inst->extra<IRSPRelOffsetData>()->offset),
inst->src(1),
false
);
break;
case CheckType:
case AssertType: {
auto const oldVal = inst->src(0);
auto const newVal = inst->dst();
for (auto& frame : m_stack) {
for (auto& it : frame.locals) {
refineValue(it.second, oldVal, newVal);
}
for (auto& it : frame.stack) {
refineValue(it.second, oldVal, newVal);
}
if (frame.ctx && canonical(frame.ctx) == canonical(inst->src(0))) {
frame.ctx = inst->dst();
frame.ctxType = inst->dst()->type();
}
}
// MInstrState can only be live for the current frame.
refineMBaseValue(oldVal, newVal);
auto const canonOldVal = canonical(oldVal);
if (cur().mTempBase.value &&
canonOldVal == canonical(cur().mTempBase.value)) {
setMTempBase(newVal);
}
if (cur().mbr.ptr && canonOldVal == canonical(cur().mbr.ptr)) {
setMBR(newVal, true);
}
break;
}
case CheckTypeMem:
pointerRefine(inst->src(0), inst->typeParam());
break;
case CheckInitMem:
pointerRefine(inst->src(0), TInitCell);
break;
case AssertLoc:
case CheckLoc: {
auto const id = inst->extra<LocalId>()->locId;
refineTypeAndSyncMBase(
loc(id),
inst->typeParam(),
TypeSource::makeGuard(inst)
);
break;
}
case AssertStk:
case CheckStk:
refineTypeAndSyncMBase(
stk(inst->extra<IRSPRelOffsetData>()->offset),
inst->typeParam(),
TypeSource::makeGuard(inst)
);
break;
case AssertMBase:
refineTypeAndSyncMBase(
Location::MBase{},
inst->typeParam(),
TypeSource::makeGuard(inst)
);
break;
case CheckMBase:
setMBR(inst->src(0), true);
refineTypeAndSyncMBase(
Location::MBase{},
inst->typeParam(),
TypeSource::makeGuard(inst)
);
break;
case StLoc:
case StLocMeta:
setValueAndSyncMBase(
loc(inst->extra<LocalId>()->locId),
inst->src(1),
false
);
break;
case LdLoc: {
auto const id = inst->extra<LdLoc>()->locId;
setValueAndSyncMBase(loc(id), inst->dst(), true);
break;
}
case EndCatch:
/*
* Hitting this means we've messed up with syncing the stack in a catch
* trace. If the stack isn't clean or doesn't match the marker's irSPOff,
* the unwinder won't see what we expect.
*/
always_assert_flog(
inst->extra<EndCatch>()->offset.to<SBInvOffset>(cur().irSPOff) ==
inst->marker().bcSPOff(),
"EndCatch stack didn't seem right:\n"
" spOff: {}\n"
" EndCatch offset: {}\n"
" marker's spOff: {}\n",
cur().irSPOff.offset,
inst->extra<EndCatch>()->offset.offset,
inst->marker().bcSPOff().offset
);
break;
case InterpOne:
case InterpOneCF: {
auto const& extra = *inst->extra<InterpOneData>();
assertx(!extra.smashesAllLocals || extra.nChangedLocals == 0);
if (extra.smashesAllLocals) {
clearLocals();
} else {
auto it = extra.changedLocals;
auto const end = it + extra.nChangedLocals;
for (; it != end; ++it) {
auto& local = *it;
setType(loc(local.id), local.type);
}
}
// Offset of the bytecode stack top relative to the IR stack pointer.
auto const bcSPOff = extra.spOffset;
// Clear tracked information for slots pushed and popped.
for (auto i = uint32_t{0}; i < extra.cellsPopped; ++i) {
setValue(stk(bcSPOff + i), nullptr);
}
for (auto i = uint32_t{0}; i < extra.cellsPushed; ++i) {
setValue(stk(bcSPOff + extra.cellsPopped - 1 - i), nullptr);
}
auto adjustedTop = bcSPOff + extra.cellsPopped - extra.cellsPushed;
switch (extra.opcode) {
case Op::CGetL2:
setType(stk(adjustedTop + 1), inst->typeParam());
break;
default:
// We don't track cells pushed by interp one except the top of the
// stack, aside from the above special cases.
if (inst->hasTypeParam()) {
auto const instrInfo = getInstrInfo(extra.opcode);
if (instrInfo.out & InstrFlags::Stack1) {
setType(stk(adjustedTop), inst->typeParam());
}
}
break;
}
cur().bcSPOff += extra.cellsPushed;
cur().bcSPOff -= extra.cellsPopped;
// Be conservative and drop minstr state
clearMInstr();
break;
}
case IterInit:
case LIterInit:
case IterNext:
case LIterNext: {
auto const& args = inst->extra<IterData>()->args;
assertx(!args.hasKey());
killIterLocals({safe_cast<uint32_t>(args.valId)});
break;
}
case IterInitK:
case LIterInitK:
case IterNextK:
case LIterNextK: {
auto const& args = inst->extra<IterData>()->args;
assertx(args.hasKey());
killIterLocals({safe_cast<uint32_t>(args.keyId),
safe_cast<uint32_t>(args.valId)});
break;
}
case LdMBase:
setMBR(inst->dst(), true);
break;
case StMBase:
setMBR(inst->src(0), false);
break;
case StMROProp:
cur().mROProp = inst->src(0)->hasConstVal()
? yesOrNo(inst->src(0)->boolVal())
: TriBool::Maybe;
break;
case CheckMROProp:
cur().mROProp = TriBool::Yes;
break;
case FinishMemberOp:
clearMInstr();
break;
case CallBuiltin:
// CallBuiltin uses the same memory as mTempBase, so it had better
// be dead.
assertx(cur().checkMInstrStateDead());
break;
case PropX:
case PropDX:
case PropQ:
cur().mROProp |= TriBool::Yes;
if (!inst->src(2)->isA(TNullptr)) {
assertx(inst->src(2)->isA(TPtrToMISTemp));
pointerStore(inst->src(2), nullptr);
}
break;
default:
// Use precise Minstr effects if we can
if (hasMInstrBaseEffects(*inst)) {
handleMInstr(inst);
} else {
// Handle everything else conservatively according to its memory
// effects
handleConservatively(inst);
}
break;
}
assertx(checkInvariants());
}
// Handle instructions which modify tracked state in a way we can
// track more precisely than handleConservatively.
void FrameStateMgr::handleMInstr(const IRInstruction* inst) {
auto const basePtr = inst->src(0);
assertx(basePtr->isA(TLval));
auto const acls = canonicalize(pointee(basePtr));
ITRACE(4, "FrameStateMgr::handleMInstr: {}\n", jit::show(acls));
Indent _i;
// mInstrBaseEffects tells us what the base will become after this
// instruction. We need to apply it to any tracked location which
// might intersect with the Lval passed to the instruction:
auto const update = [&] (Location l, bool widen) {
if (auto const u = mInstrBaseEffects(*inst, typeOf(l))) {
widen ? widenType(l, *u) : setType(l, *u);
}
};
if (auto const updateMBase = isMBase(basePtr, acls);
updateMBase != TriBool::No) {
auto const widen = updateMBase == TriBool::Maybe;
update(Location::MBase{}, widen);
// Adjust the mbase location specific types as well:
auto const spec = [&] (Type& t) {
auto const u = mInstrBaseEffects(*inst, t);
if (!u) return;
t = widen ? (t | *u) : *u;
};
if (acls.maybe(ALocalAny)) spec(cur().mbaseLocalType);
if (acls.maybe(AStackAny)) spec(cur().mbaseStackType);
if (acls.maybe(AMIStateTempBase)) spec(cur().mbaseTempType);
}
// If it's a single location, we can change the known type to that
// type precisely.
if (acls.isSingleLocation()) {
if (auto const l = acls.is_local()) {
update(loc(l->ids.singleValue()), false);
} else if (auto const s = acls.is_stack()) {
assertx(s->size() == 1);
if (auto const l = optStk(s->low)) update(*l, false);
} else if (acls <= AMIStateTempBase) {
if (auto const u = mInstrBaseEffects(*inst, mTempBase().type)) {
setMTempBaseType(*u);
}
}
} else {
// Otherwise it might be one of multiple locations, so we need to
// widen in the type (not replace).
if (acls.maybe(ALocalAny)) {
for (auto const& local : cur().locals) {
if (!acls.maybe(ALocal{fp(), local.first})) continue;
update(loc(local.first), true);
}
// This instruction could also affect locals that aren't being
// tracked. Be conservative and assume that they could be
// affected.
cur().localsCleared = true;
}
if (acls.maybe(AStackAny)) {
for (auto const& stack : cur().stack) {
auto const offset = stack.first.to<IRSPRelOffset>(irSPOff());
if (!acls.maybe(AStack::at(offset))) continue;
update(stk(stack.first), true);
}
}
if (acls.maybe(AMIStateTempBase)) {
if (auto const u = mInstrBaseEffects(*inst, mTempBase().type)) {
widenMTempBase(*u);
}
}
}
}
// Update tracked state conservatively according to the instruction's
// memory effects.
void FrameStateMgr::handleConservatively(const IRInstruction* inst) {
auto const store = [&] (const AliasClass& stores) {
if (stores.maybe(AMIStateBase)) {
setMBR(nullptr, false);
} else if (isMBase(nullptr, stores) != TriBool::No) {
setValue(Location::MBase{}, nullptr);
if (stores.maybe(ALocalAny)) cur().mbaseLocalType = TCell;
if (stores.maybe(AStackAny)) cur().mbaseStackType = TCell;
if (stores.maybe(AMIStateTempBase)) cur().mbaseTempType = TCell;
}
if (stores.maybe(ALocalAny)) {
for (auto const& local : cur().locals) {
if (!stores.maybe(ALocal{fp(), local.first})) continue;
setValue(loc(local.first), nullptr);
}
// This instruction could also affect locals that aren't being
// tracked. Be conservative and assume that they could be
// affected.
cur().localsCleared = true;
}
if (stores.maybe(AStackAny)) {
for (auto const& stack : cur().stack) {
auto const offset = stack.first.to<IRSPRelOffset>(irSPOff());
if (!stores.maybe(AStack::at(offset))) continue;
setValue(stk(stack.first), nullptr);
}
}
if (stores.maybe(AMIStateTempBase)) setMTempBase(nullptr);
if (stores.maybe(AMIStateROProp)) cur().mROProp = TriBool::Maybe;
};
auto const effects = memory_effects(*inst);
ITRACE(4, "FrameStateMgr::handleConservatively: {}\n", jit::show(effects));
Indent _i;
match<void>(
effects,
[&] (GeneralEffects x) {
store(x.stores);
store(x.inout);
},
[&] (CallEffects x) {
store(x.actrec);
store(x.outputs);
},
[&] (PureStore x) { store(x.dst); },
[&] (PureInlineCall x) { store(x.base); },
[&] (UnknownEffects) { store(AUnknown); },
[&] (ReturnEffects x) {},
[&] (ExitEffects x) {},
[&] (PureLoad) {},
[&] (IrrelevantEffects) {}
);
}
///////////////////////////////////////////////////////////////////////////////
/*
* Collects the post-conditions associated with the current state, which is
* essentially a list of local/stack locations and their known types at the end
* of a block.
*/
PostConditions FrameStateMgr::collectPostConds() {
PostConditions postConds;
if (sp() != nullptr) {
for (auto& it : cur().stack) {
auto const sbRel = it.first;
auto& state = it.second;
auto const type = state.type;
auto const changed = state.maybeChanged;
if (changed || type < TCell) {
ITRACE(1, "Stack({}, {}): {} ({})\n",
sbRel.to<BCSPRelOffset>(bcSPOff()).offset, sbRel.offset,
type, changed ? "changed" : "refined");
auto& vec = changed ? postConds.changed : postConds.refined;
vec.push_back({ Location::Stack{sbRel}, type });
}
}
}
if (fp() != nullptr) {
for (auto& l : cur().locals) {
auto id = l.first;
auto& state = l.second;
auto const type = state.type;
auto const changed = state.maybeChanged;
if (changed || type < TCell) {
ITRACE(1, "Local {}: {} ({})\n", id, type.toString(),
changed ? "changed" : "refined");
auto& vec = changed ? postConds.changed : postConds.refined;
vec.push_back({ Location::Local{id}, type });
}
}
}
auto const ty = mbase().type;
auto const changed = mbase().maybeChanged;
if (changed || ty < TCell) {
ITRACE(1, "MBase{{}}: {} ({})\n", ty, changed ? "changed" : "refined");
auto& vec = changed ? postConds.changed : postConds.refined;
vec.push_back({ Location::MBase{}, ty });
}
return postConds;
}
///////////////////////////////////////////////////////////////////////////////
// Per-block state.
bool FrameStateMgr::hasStateFor(Block* block) const {
return m_states.count(block);
}
void FrameStateMgr::startBlock(Block* block, bool hasUnprocessedPred) {
ITRACE(3, "FrameStateMgr::startBlock: {}\n", block->id());
auto const it = m_states.find(block);
auto const end = m_states.end();
if (it != end) {
always_assert_flog(
block->empty(),
"tried to startBlock a non-empty block while building"
);
m_stack = it->second.in;
ITRACE(4, "Loading state for B{}:\n{}\n", block->id(), show());
always_assert_flog(
!m_stack.empty(),
"invalid startBlock for B{}",
block->id()
);
} else if (debug) {
// NOTE: Highly suspect; different debug vs. non-debug behavior.
save(block, nullptr);
}
assertx(!m_stack.empty());
// Reset state if the block has any predecessor that we haven't processed yet.
if (hasUnprocessedPred) {
Indent _;
ITRACE(4, "B{} is a loop header; resetting state\n", block->id());
clearForUnprocessedPred();
// pointee() won't do the right thing if a DefLabel isn't fully
// formed
always_assert_flog(
block->empty() || !block->front().is(DefLabel),
"B{} has a DefLabel with an unprocessed pred. Frame-state can't "
"handle this, and it shouldn't happen with how we do irgen.",
block->id()
);
}
}
bool FrameStateMgr::finishBlock(Block* block) {
assertx(block->back().isTerminal() == !block->next());
if (block->isExitNoThrow()) {
m_exitPostConds[block] = collectPostConds();
ITRACE(2, "PostConditions for exit Block {}:\n{}\n",
block->id(), jit::show(m_exitPostConds[block]));
}
assertx(hasStateFor(block));
if (m_states[block].out) {
assertx(m_states[block].out->empty());
m_states[block].out = m_stack;
}
auto changed = false;
if (!block->back().isTerminal()) changed |= save(block->next());
return changed;
}
void FrameStateMgr::setSaveOutState(Block* block) {
assertx(hasStateFor(block));
assertx(!m_states[block].out || m_states[block].out->empty());
m_states[block].out.emplace();
}
void FrameStateMgr::pauseBlock(Block* block) {
// Note: this can't use std::move, because pauseBlock must leave the current
// state alone so startBlock can use it as the in state for another block.
m_states[block].paused = m_stack;
}
void FrameStateMgr::unpauseBlock(Block* block) {
assertx(hasStateFor(block));
m_stack = *m_states[block].paused;
}
void FrameStateMgr::resetBlock(Block* block, Block* pred) {
assertx(m_states[pred].out && !m_states[pred].out->empty());
m_states[block].in = *m_states[pred].out;
}
const PostConditions& FrameStateMgr::postConds(Block* exitBlock) const {
assertx(exitBlock->isExitNoThrow());
auto it = m_exitPostConds.find(exitBlock);
assertx(it != m_exitPostConds.end());
auto& pconds = it->second;
if (debug) {
for (DEBUG_ONLY auto& c : pconds.changed) {
for (DEBUG_ONLY auto& r : pconds.refined) {
assert_flog(c.location != r.location,
"Location {} in both changed and refined sets",
jit::show(c.location));
}
}
}
return pconds;
}
/*
* Save the current state as the in-state for `block'.
*
* If this is the first time saving state for `block', create a new snapshot
* using either the current state or the out-state of `pred' if we are given
* one. Otherwise merge the current state into the existing snapshot.
*/
bool FrameStateMgr::save(Block* block, Block* pred) {
ITRACE(4, "Saving current state to B{}:\n{}\n", block->id(), show());
// If the destination block is unreachable, there's no need to merge in the
// frame state.
if (block->isUnreachable()) return false;
auto const it = m_states.find(block);
auto changed = true;
if (it != m_states.end()) {
changed = merge_into(it->second.in, m_stack);
ITRACE(4, "Merged state:\n{}\n", show());
} else {
if (pred) {
assertx(hasStateFor(pred));
assertx(m_states[pred].out);
assertx(!m_states[pred].out->empty());
m_states[block].in = *m_states[pred].out;
} else {
assertx(!m_stack.empty());
m_states[block].in = m_stack;
}
}
return changed;
}
/*
* Modify state to conservative values given an unprocessed predecessor.
*
* The fpValue, irSPOff, and curFunc are not cleared because they must agree
* at bytecode-level control-flow merge points (which can be either merge
* points at the bytecode or due to retranslated blocks).
*/
void FrameStateMgr::clearForUnprocessedPred() {
ITRACE(1, "clearForUnprocessedPred\n");
// Forget any information about stack values in memory.
for (auto& it : cur().stack) {
it.second = StackState{};
}
// These values must go toward their conservative state.
clearLocals();
clearMInstr();
}
///////////////////////////////////////////////////////////////////////////////
void FrameStateMgr::initStack(SSATmp* sp, SBInvOffset irSPOff,
SBInvOffset bcSPOff) {
cur().spValue = sp;
cur().irSPOff = irSPOff;
cur().bcSPOff = bcSPOff;
cur().stack.clear();
}
void FrameStateMgr::uninitStack() {
cur().spValue = nullptr;
cur().irSPOff = SBInvOffset{0};
cur().bcSPOff = SBInvOffset{0};
cur().stack.clear();
}
void FrameStateMgr::trackBeginInlining(const IRInstruction* inst) {
assertx(inst->is(BeginInlining));
assertx(cur().checkMInstrStateDead());
auto const extra = inst->extra<BeginInlining>();
auto const callee = extra->func;
auto const spOffset = extra->spOffset;
assertx(cur().bcSPOff == spOffset.to<SBInvOffset>(irSPOff()));
// Remove space from callee's frame from the caller's stack.
for (auto i = uint32_t{0}; i < kNumActRecCells; ++i) {
setValue(stk(spOffset + i), nullptr);
}
cur().bcSPOff -= kNumActRecCells;
if (callee->isCPPBuiltin()) {
auto const inout = callee->numInOutParams();
for (auto i = uint32_t{0}; i < inout; ++i) {
auto const type = irgen::callOutType(callee, i);
setType(stk(extra->spOffset + kNumActRecCells + i), type);
}
}
// Push a new state for the inlined callee; saving the state we'll need to
// pop on return.
m_stack.emplace_back(FrameState{callee});
// Set up the callee's frame.
cur().fpValue = inst->dst();
cur().fixupFPValue = caller().fixupFPValue;
/*
* Set up the callee's stack.
*
* We need to calculate the new `irSPOff`, which is an offset of `spValue`
* from the new stack base. It consists of two parts:
*
* - the inverse offset of the new `fpValue` from the new stack base, given by
* `-callee->numSlotsInFrame()`
* - the inverse offset of `spValue` from the new `fpValue`, which is the same
* numeric value as a regular offset of `fpValue` from `spValue`, given by
* `spOffset`
*
* The callee's stack starts empty, so `bcSPOff` is zero.
*/
auto const irSPOff = SBInvOffset{spOffset.offset - callee->numSlotsInFrame()};
auto const bcSPOff = SBInvOffset{0};
initStack(caller().spValue, irSPOff, bcSPOff);
ITRACE(6, "BeginInlining setting irSPOff: {}\n", irSPOff.offset);
}
void FrameStateMgr::trackEndInlining() {
// EndInlining is not allowed after InlineCall
assertx(caller().fixupFPValue == cur().fixupFPValue);
// Inlining may not change spValue
assertx(caller().spValue == cur().spValue);
// Pop the inlined frame.
m_stack.pop_back();
assertx(!m_stack.empty());
assertx(cur().checkMInstrStateDead());
}
void FrameStateMgr::trackInlineCall(const IRInstruction* inst) {
assertx(cur().fixupFPValue == inst->src(1));
cur().fixupFPValue = inst->src(0);
}
///////////////////////////////////////////////////////////////////////////////
bool FrameState::checkInvariants() const {
for (auto const& it : locals) {
auto const id = it.first;
auto const& local = it.second;
always_assert_flog(
local.value == nullptr || local.type <= local.value->type(),
"local {} had incompatible type {} with value {}",
id,
local.type,
local.value->toString()
);
}
for (auto const& it : stack) {
auto const id = it.first;
auto const& stk = it.second;
always_assert_flog(
stk.value == nullptr || stk.type <= stk.value->type(),
"stack {} had incompatible type {} with value {}",
id.offset,
stk.type,
stk.value->toString()
);
}
always_assert_flog(
mbase.value == nullptr || mbase.type <= mbase.value->type(),
"mbase had incompatible type {} with value {}",
mbase.type,
mbase.value->toString()
);
always_assert_flog(
mbr.ptr == nullptr || mbr.ptrType <= mbr.ptr->type(),
"MBR had incompatible type {} with value {}",
mbr.ptrType,
mbr.ptr->toString()
);
always_assert_flog(
mbr.ptrType <= TLval,
"MBR contains a {}, not a Lval like it should",
mbr.ptrType
);
always_assert_flog(
mbr.pointee <= AUnknownTV,
"MBR's pointee {} is wider than AUnknownTV",
jit::show(mbr.pointee)
);
always_assert_flog(
mbr.ptr == nullptr || mbr.pointee <= canonicalize(pointee(mbr.ptr->type())),
"MBR's pointee {} is wider than it's ptr implies {}",
jit::show(mbr.pointee),
jit::show(canonicalize(pointee(mbr.ptr->type())))
);
always_assert_flog(
mbaseStackType <= mbase.type,
"mbase's stack-only type {} is wider than it's general type {}",
mbaseStackType,
mbase.type
);
always_assert_flog(
mbaseLocalType <= mbase.type,
"mbase's local-only type {} is wider than it's general type {}",
mbaseLocalType,
mbase.type
);
always_assert_flog(
mbaseTempType <= mbase.type,
"mbase's temp-base-only type {} is wider than it's general type {}",
mbaseTempType,
mbase.type
);
always_assert_flog(
IMPLIES(!mbr.pointee.maybe(AStackAny), mbaseStackType == TBottom),
"mbase has a stack-only type {} when it can't be the stack",
mbaseStackType
);
always_assert_flog(
IMPLIES(!mbr.pointee.maybe(ALocalAny), mbaseLocalType == TBottom),
"mbase has a local-only type {} when it can't be a local",
mbaseLocalType
);
always_assert_flog(
IMPLIES(!mbr.pointee.maybe(AMIStateTempBase), mbaseTempType == TBottom),
"mbase has a temp-base-only type {} when it can't be the MTempBase",
mbaseTempType
);
if (auto const l = mbr.pointee.is_local()) {
if (l->ids.hasSingleValue()) {
auto const localId = l->ids.singleValue();
if (auto const it = locals.find(localId); it != locals.end()) {
auto const& state = it->second;
always_assert_flog(
mbase.value == state.value &&
mbase.type == state.type,
"MBase and local {} do not agree on state ({} {} != {} {})",
localId,
mbase.value ? mbase.value->toString() : "<>",
mbase.type,
state.value ? state.value->toString() : "<>",
state.type
);
always_assert_flog(
mbase.type == mbaseLocalType,
"mbase has an incompatible local-only type ({} and {}) "
"when it must be a local",
mbase.type,
mbaseLocalType
);
}
}
} else if (auto const s = mbr.pointee.is_stack()) {
if (s->size() == 1) {
auto const offset = s->low.to<SBInvOffset>(irSPOff);
if (auto const it = stack.find(offset); it != stack.end()) {
auto const& state = it->second;
always_assert_flog(
mbase.value == state.value &&
mbase.type == state.type,
"MBase and stack {} do not agree on state ({} {} != {} {})",
offset.offset,
mbase.value ? mbase.value->toString() : "<>",
mbase.type,
state.value ? state.value->toString() : "<>",
state.type
);
always_assert_flog(
mbase.type == mbaseStackType,
"mbase has an incompatible stack-only type ({} and {}) "
"when it must be a stack slot",
mbase.type,
mbaseStackType
);
}
}
} else if (mbr.pointee <= AMIStateTempBase) {
always_assert_flog(
mbase.value == mTempBase.value &&
mbase.type == mTempBase.type,
"MBase and MTempBase do not agree on state ({} {} != {} {})",
mbase.value ? mbase.value->toString() : "<>",
mbase.type,
mTempBase.value ? mTempBase.value->toString() : "<>",
mTempBase.type
);
always_assert_flog(
mbase.type == mbaseTempType,
"mbase has an incompatible temp-base-only type ({} and {}) "
"when it must be the MTempBase",
mbase.type,
mbaseTempType
);
}
// Make sure the stack is either initialized or read-only empty.
always_assert_flog(
spValue != nullptr || (irSPOff.offset == 0 && bcSPOff.offset == 0),
"incorrectly initialized stack"
);
return true;
}
bool FrameStateMgr::checkInvariants() const {
for (auto const& state : m_stack) {
// Every state except the current one should have a dead minstr
// state.
if (&state != &m_stack.back()) always_assert(state.checkMInstrStateDead());
always_assert(state.checkInvariants());
}
return true;
}
bool FrameState::checkMInstrStateDead() const {
always_assert_flog(
!mbr.ptr && mbr.ptrType == TLval && !(mbr.pointee < AUnknownTV),
"MBR is not dead when it should be ({} {} {})",
mbr.ptr ? mbr.ptr->toString() : "<>",
mbr.ptrType.toString(),
jit::show(mbr.pointee)
);
always_assert_flog(
!mbase.value && mbase.type == TCell,
"MBase is not dead when it should be ({} {})",
mbase.value ? mbase.value->toString() : "<>",
mbase.type.toString()
);
always_assert_flog(
!mTempBase.value && mTempBase.type == TCell,
"MTempBase is not dead when it should be ({} {})",
mTempBase.value ? mTempBase.value->toString() : "<>",
mTempBase.type.toString()
);
always_assert_flog(
mbaseStackType == TCell &&
mbaseLocalType == TCell &&
mbaseTempType == TCell,
"MBase has location specific types ({} {} {}) when it should be dead",
mbaseStackType,
mbaseLocalType,
mbaseTempType
);
always_assert_flog(
mROProp == TriBool::Maybe,
"MROProp has a known value {} when it should be dead",
HPHP::show(mROProp)
);
return true;
}
///////////////////////////////////////////////////////////////////////////////
/*
* Wrap a local or stack ID into a Location.
*/
Location FrameStateMgr::loc(uint32_t id) const {
return Location::Local { id };
}
Location FrameStateMgr::stk(SBInvOffset off) const {
return Location::Stack { off };
}
Location FrameStateMgr::stk(IRSPRelOffset off) const {
auto const sbRel = off.to<SBInvOffset>(irSPOff());
return Location::Stack { sbRel };
}
Optional<Location> FrameStateMgr::optStk(IRSPRelOffset off) const {
return optStk(off.to<SBInvOffset>(irSPOff()));
}
Optional<Location> FrameStateMgr::optStk(SBInvOffset off) const {
if (off.offset < 1) return std::nullopt;
return Location::Stack { off };
}
LocalState& FrameStateMgr::localState(uint32_t id) {
assertx(id < cur().curFunc->numLocals());
return cur().locals[id];
}
LocalState& FrameStateMgr::localState(Location l) {
assertx(l.tag() == LTag::Local);
return localState(l.localId());
}
StackState& FrameStateMgr::stackState(IRSPRelOffset spRel) {
auto const sbRel = spRel.to<SBInvOffset>(irSPOff());
return stackState(sbRel);
}
StackState& FrameStateMgr::stackState(SBInvOffset sbRel) {
always_assert_flog(
sbRel.offset >= 1,
"stack sbRel.offset went below 1: irSPOff: {}, sbRel: {}\n",
cur().irSPOff.offset,
sbRel.offset
);
return cur().stack[sbRel];
}
StackState& FrameStateMgr::stackState(Location l) {
assertx(l.tag() == LTag::Stack);
return stackState(l.stackIdx());
}
LocalState FrameStateMgr::local(uint32_t id) const {
auto const& locals = cur().locals;
auto const it = locals.find(id);
return it != locals.end() ? it->second : LocalState{};
}
bool FrameStateMgr::tracked(Location l) const {
switch (l.tag()) {
case LTag::Local:
return cur().locals.count(l.localId()) > 0;
case LTag::Stack: {
auto const sbRel = l.stackIdx();
return cur().stack.count(sbRel) > 0;
}
case LTag::MBase:
return true;
}
not_reached();
}
bool FrameStateMgr::validStackOffset(IRSPRelOffset off) const {
return optStk(off).has_value();
}
/*
* We consider it logically const to extend with default-constructed stack
* values.
*/
StackState FrameStateMgr::stack(IRSPRelOffset offset) const {
auto const sbRel = offset.to<SBInvOffset>(irSPOff());
return stack(sbRel);
}
StackState FrameStateMgr::stack(SBInvOffset offset) const {
auto const& curStack = cur().stack;
auto const it = curStack.find(offset);
return it != curStack.end() ? it->second : StackState{};
}
#define IMPL_MEMBER_OF(type_t, name) \
type_t FrameStateMgr::name##Of(Location l) const { \
return [&]() -> type_t { \
switch (l.tag()) { \
case LTag::Local: return local(l.localId()).name; \
case LTag::Stack: return stack(l.stackIdx()).name; \
case LTag::MBase: return mbase().name; \
} \
not_reached(); \
}(); \
}
IMPL_MEMBER_OF(SSATmp*, value)
IMPL_MEMBER_OF(Type, type)
IMPL_MEMBER_OF(TypeSourceSet, typeSrcs)
#undef IMPL_MEMBER_AT
///////////////////////////////////////////////////////////////////////////////
/*
* Conservatively calculate the type of a location solely from the
* AliasClass.
*/
Type FrameStateMgr::typeFromAliasClass(const AliasClass& acls) const {
// If it's a single one of our tracked locations, we can use what we
// know of that location.
if (auto const l = acls.is_local()) {
if (l->ids.hasSingleValue()) return typeOf(loc(l->ids.singleValue()));
} else if (auto const s = acls.is_stack()) {
if (s->size() == 1) {
if (auto const l = optStk(s->low)) return typeOf(*l);
}
} else if (acls <= AMIStateTempBase) {
return cur().mTempBase.type;
}
// Otherwise we don't know
return TCell;
}
/*
* Calculate the type off a pointer's pointee using it's
* definitions. If the calculated type exceeds `limit`, stop and
* return the type so far.
*/
Type FrameStateMgr::typeOfPointeeFromDefs(SSATmp* ptr, Type limit) const {
assertx(ptr->isA(TMem));
// Visit every defining instruction, and use what we know of that
// particular instruction to calculate the type. They all get
// unioned together.
auto t = TBottom;
auto const visit = [&] (const IRInstruction* inst, const SSATmp*) {
t |= [&] {
switch (inst->op()) {
// Use our tracked state for these:
case LdLocAddr:
return typeOf(loc(inst->extra<LdLocAddr>()->locId));
case LdStkAddr:
return typeOf(stk(inst->extra<LdStkAddr>()->offset));
case LdMIStateTempBaseAddr:
return cur().mTempBase.type;
case LdMBase:
// Laundering the pointer through the MBR destroys our def
// information. Be conservative and use what we know from the
// alias classes.
return typeFromAliasClass(inst->extra<LdMBase>()->acls);
case DefConst: {
auto const constTy = inst->typeParam();
assertx(constTy.hasConstVal(TMem));
auto const p = constTy.ptrVal();
// It's tempting to just dereference the constant ptr and get
// it's type. However there's no guarantee it's type is
// constant, nor even that the pointer is even
// dereferencable. Instead compare the address with known
// constants.
if (p == &immutable_null_base) return TInitNull;
if (p == &immutable_uninit_base) return TUninit;
return TCell;
}
case LdPropAddr:
case LdInitPropAddr:
case LdClsPropAddrOrNull:
case LdClsPropAddrOrRaise:
// These types are invariant and calculated when the IR op is
// emitted.
assertx(inst->typeParam() <= TCell);
return inst->typeParam();
case LdRDSAddr:
case LdInitRDSAddr:
// These too are calculated when emitted (and won't change).
return inst->extra<RDSHandleAndType>()->type;
case ElemDictD:
case ElemDictU:
case BespokeElem: {
// These can be calculated depending on the inputs. The type
// param tells us the known type of the base (which is
// provided via pointer).
assertx(inst->typeParam() <= TArrLike);
auto elem = arrLikeElemType(
inst->typeParam(),
inst->src(1)->type(),
inst->ctx()
);
if (!elem.second) {
if (inst->is(ElemDictU) ||
(inst->is(BespokeElem) && !inst->src(2)->boolVal())) {
elem.first |= TInitNull;
}
}
assertx(elem.first != TBottom);
return elem.first;
}
case ElemDictK:
// These require no type params as there's no pointers
// involved.
return arrLikePosType(
inst->src(3)->type(),
inst->src(2)->type(),
false,
inst->ctx()
);
case LdVecElemAddr:
return arrLikeElemType(
inst->src(2)->type(),
inst->src(1)->type(),
inst->ctx()
).first;
case StructDictElemAddr: {
auto const arrType = inst->src(3)->type();
auto elem = arrLikeElemType(
arrType,
inst->src(1)->type(),
inst->ctx()
);
auto const& layout = arrType.arrSpec().layout();
if (!elem.second && !layout.slotAlwaysPresent(inst->src(2)->type())) {
elem.first |= TUninit;
}
return elem.first;
}
default:
// Otherwise something we can't say anything about.
return TCell;
}
}();
return t < limit;
};
visitEveryDefiningInst(ptr, visit);
// Anything other than a Bottom should point at something, and thus
// we should get some type for it.
assertx(ptr->isA(TBottom) || t != TBottom);
return t;
}
Type FrameStateMgr::typeOfPointee(SSATmp* ptr, Type limit) const {
assertx(ptr->isA(TMem));
auto const acls = canonicalize(pointee(ptr));
if (isMBase(ptr, acls) == TriBool::Yes) return mbase().type;
return typeOfPointeeFromDefs(ptr, limit);
}
/*
* Return any SSATmp representing the value of the given pointer's
* pointee.
*/
SSATmp* FrameStateMgr::valueOfPointee(SSATmp* ptr) const {
assertx(ptr->isA(TMem));
auto const acls = canonicalize(pointee(ptr));
if (isMBase(ptr, acls) == TriBool::Yes) return mbase().value;
if (!acls.isSingleLocation()) return nullptr;
if (auto const l = acls.is_local()) {
assertx(l->ids.hasSingleValue());
return valueOf(loc(l->ids.singleValue()));
} else if (auto const s = acls.is_stack()) {
assertx(s->size() == 1);
if (auto const l = optStk(s->low)) return valueOf(*l);
} else if (acls <= AMIStateTempBase) {
return cur().mTempBase.value;
}
return nullptr;
}
///////////////////////////////////////////////////////////////////////////////
AliasClass FrameStateMgr::locationToAliasClass(Location l) const {
switch (l.tag()) {
case LTag::Local:
return ALocal{canonical(fp()), l.localId()};
case LTag::Stack:
return AStack::at(l.stackIdx().to<IRSPRelOffset>(irSPOff()));
case LTag::MBase:
return AMIStateTempBase;
}
not_reached();
}
// Determine if the given TMem/AliasClass might be the current
// MBase. Ptr is optional. If given we can do more precise (and
// faster) checks.
TriBool FrameStateMgr::isMBase(SSATmp* ptr, const AliasClass& acls) const {
if (ptr) {
// If the ptr is the known mbr, then it obviously is the mbase. If
// the two pointers aren't even compatible, it can't be.
assertx(ptr->isA(TMem));
if (canonical(ptr) == canonical(mbr().ptr)) return TriBool::Yes;
// NB: One could be a Ptr and the other a Lval, so don't use type
// comparison. Compare the locations directly.
if (!ptr_location_t(ptr->type().ptrLocation() &
mbr().ptrType.ptrLocation())) {
return TriBool::No;
}
}
// If their known alias classes don't even overlap, they can't be
// the same.
if (!acls.maybe(mbr().pointee)) return TriBool::No;
// If the known types at both locations are disjoint, they can't be
// the same.
auto const knownType = [&] {
if (ptr) return typeOfPointeeFromDefs(ptr, TCell);
return typeFromAliasClass(acls);
}();
auto const mbrType = [&] {
if (acls <= (ALocalAny|AStackAny|AMIStateTempBase)) {
auto ty = TBottom;
if (acls.maybe(ALocalAny)) ty |= cur().mbaseLocalType;
if (acls.maybe(AStackAny)) ty |= cur().mbaseStackType;
if (acls.maybe(AMIStateTempBase)) ty |= cur().mbaseTempType;
return ty;
}
return cur().mbase.type;
}();
if (!knownType.maybe(mbrType)) return TriBool::No;
// If either represents multiple locations, we can't say for sure.
if (!acls.isSingleLocation() || !mbr().pointee.isSingleLocation()) {
return TriBool::Maybe;
}
// The alias classes are the same, and they're both single
// locations, so it's a definite match.
if (acls == mbr().pointee) return TriBool::Yes;
// If not, however, they still might be. There's alias classes which
// can be single locations, and not equal to each other, but still
// might be each other (props, for example).
return TriBool::Maybe;
}
TriBool FrameStateMgr::isMBase(SSATmp* ptr) const {
assertx(ptr->isA(TMem));
return isMBase(ptr, canonicalize(pointee(ptr)));
}
///////////////////////////////////////////////////////////////////////////////
// Update tracked state to reflect a load (into dst) through the given
// pointer.
void FrameStateMgr::pointerLoad(SSATmp* ptr, SSATmp* dst) {
assertx(ptr->isA(TMem));
auto const acls = canonicalize(pointee(ptr));
// These updates are optional, since they are only used to give us a
// known SSATmp for the location. We can only update them if we know
// for sure what location is being affected.
if (isMBase(ptr, acls) == TriBool::Yes) {
setValue(Location::MBase{}, dst);
if (acls.maybe(ALocalAny)) cur().mbaseLocalType = dst->type();
if (acls.maybe(AStackAny)) cur().mbaseStackType = dst->type();
if (acls.maybe(AMIStateTempBase)) cur().mbaseTempType = dst->type();
}
if (acls.isSingleLocation()) {
auto const oldModified = cur().stackModified;
setValue(acls, dst);
cur().stackModified = oldModified;
}
}
// Update tracked state to reflect a store of the given value through
// the given pointer.
void FrameStateMgr::pointerStore(SSATmp* ptr, SSATmp* value) {
assertx(ptr->isA(TMem));
auto const acls = canonicalize(pointee(ptr));
// First sync mbase state. If the pointer might point at the mbase,
// update it's known value/type.
auto prevLocalType = TCell;
auto prevStackType = TCell;
auto prevTempType = TCell;
switch (isMBase(ptr, acls)) {
case TriBool::Yes: {
prevLocalType = cur().mbaseLocalType;
prevStackType = cur().mbaseStackType;
prevTempType = cur().mbaseTempType;
setValue(Location::MBase{}, value);
auto const type = value ? value->type() : TCell;
if (acls.maybe(ALocalAny)) cur().mbaseLocalType = type;
if (acls.maybe(AStackAny)) cur().mbaseStackType = type;
if (acls.maybe(AMIStateTempBase)) cur().mbaseTempType = type;
break;
}
case TriBool::Maybe:
prevLocalType = prevStackType = prevTempType =
typeOfPointeeFromDefs(ptr, TCell);
if (value) {
widenType(Location::MBase{}, value->type());
if (acls.maybe(ALocalAny)) cur().mbaseLocalType |= value->type();
if (acls.maybe(AStackAny)) cur().mbaseStackType |= value->type();
if (acls.maybe(AMIStateTempBase)) cur().mbaseTempType |= value->type();
} else {
setValue(Location::MBase{}, nullptr);
if (acls.maybe(ALocalAny)) cur().mbaseLocalType = TCell;
if (acls.maybe(AStackAny)) cur().mbaseStackType = TCell;
if (acls.maybe(AMIStateTempBase)) cur().mbaseTempType = TCell;
}
break;
case TriBool::No:
prevLocalType = prevStackType = prevTempType =
typeOfPointeeFromDefs(ptr, TCell);
break;
}
// If the alias class is a single location, we can precisely change
// the state.
if (acls.isSingleLocation()) {
setValue(acls, value);
} else {
// Otherwise union in the state with anything it might be
if (acls.maybe(ALocalAny)) {
for (auto const& local : cur().locals) {
if (!prevLocalType.maybe(local.second.type)) continue;
if (!acls.maybe(ALocal{fp(), local.first})) continue;
value
? widenType(loc(local.first), value->type())
: setValue(loc(local.first), nullptr);
}
// This instruction could also affect locals that aren't being
// tracked. Be conservative and assume that they could be
// affected.
cur().localsCleared = true;
}
if (acls.maybe(AStackAny)) {
for (auto const& stack : cur().stack) {
if (!prevStackType.maybe(stack.second.type)) continue;
auto const offset = stack.first.to<IRSPRelOffset>(irSPOff());
if (!acls.maybe(AStack::at(offset))) continue;
value
? widenType(stk(stack.first), value->type())
: setValue(stk(stack.first), nullptr);
}
}
if (acls.maybe(AMIStateTempBase)) {
if (prevTempType.maybe(cur().mTempBase.type)) {
value ? widenMTempBase(value->type()) : setMTempBase(nullptr);
}
}
}
}
void FrameStateMgr::pointerRefine(SSATmp* ptr, Type type) {
assertx(ptr->isA(TMem));
auto const acls = canonicalize(pointee(ptr));
if (isMBase(ptr, acls) == TriBool::Yes) {
refineType(Location::MBase{}, type, std::nullopt);
cur().mbaseStackType &= type;
cur().mbaseLocalType &= type;
cur().mbaseTempType &= type;
}
if (acls.isSingleLocation()) refineType(acls, type, std::nullopt);
}
///////////////////////////////////////////////////////////////////////////////
template<LTag tag>
static void setValueImpl(Location l,
LocationState<tag>& state,
SSATmp* value) {
ITRACE(2, "{} := {}\n", jit::show(l), value ? value->toString() : "<>");
state.value = value;
state.type = value ? value->type() : LocationState<tag>::default_type();
state.maybeChanged = true;
state.typeSrcs.clear();
if (value) {
state.typeSrcs.insert(TypeSource::makeValue(value));
}
}
/*
* Update the value (and type) for `l'.
*/
void FrameStateMgr::setValue(Location l, SSATmp* value) {
switch (l.tag()) {
case LTag::Local:
return setValueImpl(l, localState(l), value);
case LTag::Stack:
cur().stackModified = true;
return setValueImpl(l, stackState(l), value);
case LTag::MBase:
return setValueImpl(l, cur().mbase, value);
}
not_reached();
}
void FrameStateMgr::setValue(const AliasClass& pointee, SSATmp* value) {
assertx(pointee.isSingleLocation());
if (auto const l = pointee.is_local()) {
setValue(loc(l->ids.singleValue()), value);
} else if (auto const s = pointee.is_stack()) {
assertx(s->size() == 1);
if (auto const l = optStk(s->low)) setValue(*l, value);
} else if (pointee <= AMIStateTempBase) {
setMTempBase(value);
}
}
/*
* Like setValue, but also updates the MBase state appropriately if
* the MBase might be the given location.
*/
void FrameStateMgr::setValueAndSyncMBase(Location l,
SSATmp* value,
bool forLoad) {
assertx(l.tag() != LTag::MBase);
switch (isMBase(nullptr, locationToAliasClass(l))) {
case TriBool::No:
break;
case TriBool::Yes:
setValue(Location::MBase{}, value);
if (l.tag() == LTag::Stack) {
cur().mbaseStackType = value ? value->type() : TCell;
} else if (l.tag() == LTag::Local) {
cur().mbaseLocalType = value ? value->type() : TCell;
}
break;
case TriBool::Maybe:
if (forLoad) break;
if (value) {
widenType(Location::MBase{}, value->type());
} else {
setValue(Location::MBase{}, nullptr);
}
if (l.tag() == LTag::Stack) {
cur().mbaseStackType |= value ? value->type() : TCell;
} else if (l.tag() == LTag::Local) {
cur().mbaseLocalType |= value ? value->type() : TCell;
}
break;
}
// NB: Do this after the mbase check, since it might consult the
// type in the location being modified.
setValue(l, value);
}
template<LTag tag>
static void setTypeImpl(Location l, LocationState<tag>& state, Type type) {
ITRACE(2, "{} :: {} -> {}\n", jit::show(l), state.type, type);
state.value = nullptr;
state.type = type;
state.maybeChanged = true;
state.typeSrcs.clear();
}
/*
* Update the type for `l' to reflect a possible change in the value---but when
* we don't have that value.
*
* Setting the type clears the typeSrcs, so the new type may not be derived
* from the old type in any way.
*/
void FrameStateMgr::setType(Location l, Type type) {
switch (l.tag()) {
case LTag::Local:
return setTypeImpl(l, localState(l), type);
case LTag::Stack:
cur().stackModified = true;
return setTypeImpl(l, stackState(l), type);
case LTag::MBase:
return setTypeImpl(l, cur().mbase, type);
}
not_reached();
}
template<LTag tag>
static void widenTypeImpl(Location l, LocationState<tag>& state, Type type) {
ITRACE(2, "{} :: {} -> {}\n", jit::show(l), state.type, state.type | type);
state.value = nullptr;
state.type |= type;
state.maybeChanged = true;
}
/*
* Update the type for `l' as a result of an operation that might change the
* value.
*
* This is just like setType() except that typeSrcs are preserved, so the new
* type may be derived from the old type.
*/
void FrameStateMgr::widenType(Location l, Type type) {
switch (l.tag()) {
case LTag::Local:
return widenTypeImpl(l, localState(l), type);
case LTag::Stack:
cur().stackModified = true;
return widenTypeImpl(l, stackState(l), type);
case LTag::MBase:
return widenTypeImpl(l, cur().mbase, type);
}
not_reached();
}
template<LTag tag>
static void refineTypeImpl(Location l, LocationState<tag>& state,
Type type, Optional<TypeSource> typeSrc) {
auto const refined = state.type & type;
ITRACE(2, "{} :: {} -> {} (via {})\n",
jit::show(l), state.type, refined, type);
state.type = refined;
state.typeSrcs.clear();
if (typeSrc) state.typeSrcs.insert(*typeSrc);
}
/*
* Update the type for `l' to reflect new information that we've obtained from
* guards, assertions, or the like.
*
* A type refinement does /not/ indicate a change in value, so the various
* changed flags are not touched.
*/
void FrameStateMgr::refineType(Location l,
Type type,
Optional<TypeSource> typeSrc) {
switch (l.tag()) {
case LTag::Local: return refineTypeImpl(l, localState(l), type, typeSrc);
case LTag::Stack: return refineTypeImpl(l, stackState(l), type, typeSrc);
case LTag::MBase: return refineTypeImpl(l, cur().mbase, type, typeSrc);
}
not_reached();
}
void FrameStateMgr::refineType(const AliasClass& pointee, Type type,
Optional<TypeSource> typeSrc) {
assertx(pointee.isSingleLocation());
if (auto const l = pointee.is_local()) {
refineType(loc(l->ids.singleValue()), type, typeSrc);
} else if (auto const s = pointee.is_stack()) {
assertx(s->size() == 1);
if (auto const l = optStk(s->low)) refineType(*l, type, typeSrc);
} else if (pointee <= AMIStateTempBase) {
refineMTempBase(type);
}
}
/*
* Like refineType, but also keeps state synced between the MBase and
* the location it might represent.
*/
void FrameStateMgr::refineTypeAndSyncMBase(Location l,
Type type,
TypeSource typeSrc) {
if (l.tag() == LTag::MBase) {
// We're updating the MBase. Try to find what tracked locations
// the MBase might be and update those too. We only do this if we
// definitely know the location.
auto const& p = cur().mbr.pointee;
if (auto const local = p.is_local()) {
if (local->ids.hasSingleValue()) {
refineType(loc(local->ids.singleValue()), type, typeSrc);
}
} else if (auto const stack = p.is_stack()) {
if (stack->size() == 1) {
if (auto const os = optStk(stack->low)) refineType(*os, type, typeSrc);
}
} else if (p <= AMIStateTempBase) {
refineMTempBase(type);
}
cur().mbaseStackType &= type;
cur().mbaseLocalType &= type;
cur().mbaseTempType &= type;
} else {
// Only refine the MBase if it's definitely this location
auto const acls = locationToAliasClass(l);
if (isMBase(nullptr, acls) == TriBool::Yes) {
refineType(Location::MBase{}, type, typeSrc);
if (l.tag() == LTag::Stack) {
cur().mbaseStackType &= type;
} else if (l.tag() == LTag::Local) {
cur().mbaseLocalType &= type;
}
}
}
refineType(l, type, typeSrc);
}
/*
* Refine the value for `state' to `newVal' if it was set to `oldVal'.
*/
template<LTag tag>
bool FrameStateMgr::refineValue(LocationState<tag>& state,
SSATmp* oldVal, SSATmp* newVal) {
if (!state.value || canonical(state.value) != canonical(oldVal)) {
return false;
}
ITRACE(2, "refining value: {} -> {}\n", *state.value, *newVal);
state.value = newVal;
state.type = newVal->type();
state.typeSrcs.clear();
state.typeSrcs.insert(TypeSource::makeValue(newVal));
return true;
}
void FrameStateMgr::refineMBaseValue(SSATmp* oldVal, SSATmp* newVal) {
if (!refineValue(cur().mbase, oldVal, newVal)) return;
cur().mbaseStackType &= newVal->type();
cur().mbaseLocalType &= newVal->type();
cur().mbaseTempType &= newVal->type();
}
///////////////////////////////////////////////////////////////////////////////
void FrameStateMgr::clearLocals() {
ITRACE(2, "clearLocals\n");
for (auto& it : cur().locals) {
auto const id = it.first;
setValue(loc(id), nullptr);
}
cur().localsCleared = true;
if (mbr().pointee.maybe(ALocalAny)) {
setValue(Location::MBase{}, nullptr);
cur().mbaseLocalType = TCell;
}
}
void FrameStateMgr::clearMInstr() {
ITRACE(2, "clearMInstr\n");
auto& c = cur();
c.mbr = MBRState{};
c.mbase = MBaseState{};
c.mTempBase = MTempBaseState{};
c.mROProp = TriBool::Maybe;
c.mbaseStackType = TCell;
c.mbaseLocalType = TCell;
c.mbaseTempType = TCell;
}
///////////////////////////////////////////////////////////////////////////////
void FrameStateMgr::setMBR(SSATmp* mbr, bool forLoad) {
assertx(!mbr || mbr->isA(TMem));
if (mbr && mbr == cur().mbr.ptr) return;
auto const mbrPointee = mbr ? canonicalize(pointee(mbr)) : AUnknownTV;
assertx(mbrPointee <= AUnknownTV);
assertx(!mbr || mbrPointee <= canonicalize(pointee(mbr->type())));
ITRACE(2, "MBR := {}\n", mbr ? mbr->toString() : "<>");
cur().mbr.ptr = mbr;
cur().mbr.ptrType = mbr ? mbr->type() : TLval;
cur().mbr.pointee = mbrPointee;
if (forLoad) return;
// Since we changed the MBR, the MBase might be a different location
// now. Try to find the new location and sync the MBase state to
// match it. We only do this if we definitely know the location.
auto const set = [&] (SSATmp* val, Type type) {
ITRACE(2, "MBase := {} ({})\n", val ? val->toString() : "<>", type);
cur().mbase.value = val;
cur().mbase.type = type;
cur().mbase.maybeChanged = true;
cur().mbase.typeSrcs.clear();
};
cur().mbaseLocalType = TBottom;
cur().mbaseStackType = TBottom;
cur().mbaseTempType = TBottom;
if (auto const l = mbrPointee.is_local()) {
if (l->ids.hasSingleValue()) {
auto const& state = localState(l->ids.singleValue());
cur().mbaseLocalType = state.type;
return set(state.value, state.type);
}
} else if (auto const s = mbrPointee.is_stack()) {
if (s->size() == 1) {
if (auto const l = optStk(s->low)) {
auto const& state = stackState(*l);
cur().mbaseStackType = state.type;
return set(state.value, state.type);
}
}
} else if (mbrPointee <= AMIStateTempBase) {
cur().mbaseTempType = cur().mTempBase.type;
return set(cur().mTempBase.value, cur().mTempBase.type);
}
// It isn't known to be one of our tracked locations. Try to
// calculate the type from it's definitions.
auto const type = mbr ? typeOfPointeeFromDefs(mbr, TCell) : TCell;
if (mbrPointee.maybe(ALocalAny)) cur().mbaseLocalType = type;
if (mbrPointee.maybe(AStackAny)) cur().mbaseStackType = type;
if (mbrPointee.maybe(AMIStateTempBase)) cur().mbaseTempType = type;
set(nullptr, type);
}
///////////////////////////////////////////////////////////////////////////////
void FrameStateMgr::setMTempBase(SSATmp* value) {
ITRACE(2, "MTempBase := {}\n", value ? value->toString() : "<>");
cur().mTempBase.value = value;
cur().mTempBase.type = value ? value->type() : TCell;
}
void FrameStateMgr::setMTempBaseType(Type type) {
ITRACE(2, "MTempBase :: {} -> {}\n", cur().mTempBase.type, type);
cur().mTempBase.value = nullptr;
cur().mTempBase.type = type;
}
void FrameStateMgr::widenMTempBase(Type type) {
ITRACE(2, "MTempBase :: {} -> {}\n",
cur().mTempBase.type, cur().mTempBase.type | type);
cur().mTempBase.value = nullptr;
cur().mTempBase.type |= type;
}
void FrameStateMgr::refineMTempBase(Type type) {
auto const refined = cur().mTempBase.type & type;
ITRACE(2, "MTempBase :: {} -> {} (via {})\n",
cur().mTempBase.type, refined, type);
cur().mTempBase.type = refined;
}
///////////////////////////////////////////////////////////////////////////////
std::string FrameState::show() const {
auto const optTmp = [] (SSATmp* t) -> std::string {
if (t) return t->toString();
return "<>";
};
std::string out;
folly::format(
&out,
"Func: {}\n"
" fp: {}, sp: {}, ctx: {}, fpFixup: {}\n"
" irSPOff: {}, bcSPOff: {}, stublogue: {}\n"
" stack modified: {}, locals cleared: {}\n"
" read-only prop: {}\n",
curFunc->fullName(),
optTmp(fpValue),
optTmp(spValue),
optTmp(ctx),
optTmp(fixupFPValue),
irSPOff.offset,
bcSPOff.offset,
stublogue ? "yes" : "no",
stackModified ? "yes" : "no",
localsCleared ? "yes" : "no",
HPHP::show(mROProp)
);
if (!locals.empty()) {
folly::format(&out, "{:-^70}\n", "");
for (auto const& local : locals) {
folly::format(
&out, " Local #{}: {} {}{}\n",
local.first,
optTmp(local.second.value),
local.second.type,
local.second.maybeChanged ? " (changed)" : ""
);
}
}
if (!stack.empty()) {
folly::format(&out, "{:-^70}\n", "");
for (auto const& stk : stack) {
folly::format(
&out, " Stack #{}: {} {}{}\n",
stk.first.offset,
optTmp(stk.second.value),
stk.second.type,
stk.second.maybeChanged ? " (changed)" : ""
);
}
}
if (mTempBase.value || mTempBase.type < TCell) {
folly::format(
&out, "{:-^70}\n MTempBase: {} {}\n",
"",
optTmp(mTempBase.value),
mTempBase.type
);
}
if (mbr.ptr || mbr.ptrType < TLval ||
mbr.pointee < AUnknownTV) {
folly::format(
&out, "{:-^70}\n MBR: {} {} {}\n",
"",
optTmp(mbr.ptr),
mbr.ptrType,
jit::show(mbr.pointee)
);
}
if (mbase.value ||
mbase.type < TCell ||
mbaseStackType < TCell ||
mbaseLocalType < TCell ||
mbaseTempType < TCell) {
folly::format(
&out, "{:-^70}\n MBase: {} {}{} [L:{}, S:{}, T:{}]\n",
"",
optTmp(mbase.value),
mbase.type,
mbase.maybeChanged ? " (changed)" : "",
mbaseLocalType,
mbaseStackType,
mbaseTempType
);
}
return out;
}
std::string FrameStateMgr::show() const {
std::string out;
for (auto const& state : m_stack) {
folly::format(&out, "{:=^70}\n{}", "", state.show());
}
folly::format(&out, "{:=^70}\n", "");
return out;
}
///////////////////////////////////////////////////////////////////////////////
}