hphp/hhbbc/analyze.cpp (788 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/hhbbc/analyze.h"
#include <cstdint>
#include <cstdio>
#include <set>
#include <algorithm>
#include <string>
#include <vector>
#include "hphp/util/trace.h"
#include "hphp/util/dataflow-worklist.h"
#include "hphp/hhbbc/interp-state.h"
#include "hphp/hhbbc/interp.h"
#include "hphp/hhbbc/index.h"
#include "hphp/hhbbc/options.h"
#include "hphp/hhbbc/representation.h"
#include "hphp/hhbbc/cfg.h"
#include "hphp/hhbbc/unit-util.h"
#include "hphp/hhbbc/cfg-opts.h"
#include "hphp/hhbbc/class-util.h"
#include "hphp/hhbbc/func-util.h"
#include "hphp/hhbbc/options-util.h"
#include "hphp/runtime/vm/reified-generics.h"
namespace HPHP::HHBBC {
namespace {
TRACE_SET_MOD(hhbbc);
struct KnownArgs {
Type context;
const CompactVector<Type>& args;
};
//////////////////////////////////////////////////////////////////////
const StaticString s_Closure("Closure");
//////////////////////////////////////////////////////////////////////
/*
* Short-hand to get the rpoId of a block in a given FuncAnalysis. (The RPO
* ids are re-assigned per analysis.)
*/
uint32_t rpoId(const FuncAnalysis& ai, BlockId blk) {
return ai.bdata[blk].rpoId;
}
const StaticString s_reified_generics_var("0ReifiedGenerics");
const StaticString s_coeffects_var("0Coeffects");
State entry_state(const Index& index, const Context& ctx,
const KnownArgs* knownArgs) {
auto ret = State{};
ret.initialized = true;
ret.thisType = [&] {
if (!ctx.cls) return TNull;
if (knownArgs && !knownArgs->context.subtypeOf(BBottom)) {
if (knownArgs->context.subtypeOf(BOptObj)) {
return setctx(knownArgs->context);
}
if (is_specialized_cls(knownArgs->context)) {
auto const dcls = dcls_of(knownArgs->context);
return setctx(dcls.type == DCls::Exact ?
objExact(dcls.cls) : subObj(dcls.cls));
}
}
auto const maybeThisType = thisType(index, ctx);
auto thisType = maybeThisType ? *maybeThisType : TObj;
if (index.lookup_this_available(ctx.func)) return thisType;
return opt(std::move(thisType));
}();
ret.locals.resize(ctx.func->locals.size());
ret.iters.resize(ctx.func->numIters);
auto locId = uint32_t{0};
for (; locId < ctx.func->params.size(); ++locId) {
if (knownArgs) {
if (locId < knownArgs->args.size()) {
if (ctx.func->params[locId].isVariadic) {
std::vector<Type> pack(knownArgs->args.begin() + locId,
knownArgs->args.end());
for (auto& p : pack) p = unctx(std::move(p));
ret.locals[locId] = vec(std::move(pack));
} else {
ret.locals[locId] = unctx(knownArgs->args[locId]);
}
} else {
ret.locals[locId] = ctx.func->params[locId].isVariadic ? TVec : TUninit;
}
continue;
}
auto const& param = ctx.func->params[locId];
if (ctx.func->isMemoizeImpl) {
auto const& constraint = param.typeConstraint;
if (constraint.hasConstraint() && !constraint.isTypeVar() &&
!constraint.isTypeConstant()) {
ret.locals[locId] = index.lookup_constraint(ctx, constraint);
continue;
}
}
// Because we throw a non-recoverable error for having fewer than the
// required number of args, all function parameters must be initialized.
ret.locals[locId] = ctx.func->params[locId].isVariadic ? TVec : TInitCell;
}
// Closures have use vars, we need to look up their types from the index.
auto const useVars = ctx.func->isClosureBody
? index.lookup_closure_use_vars(ctx.func)
: CompactVector<Type>{};
/*
* Reified functions have a hidden local that's always the first
* (non-parameter) local, which stores reified generics.
*/
if (ctx.func->isReified) {
// Currently closures cannot be reified
assertx(!ctx.func->isClosureBody);
assertx(locId < ret.locals.size());
assertx(ctx.func->locals[locId].name->same(s_reified_generics_var.get()));
ret.locals[locId++] = get_type_of_reified_list(ctx.func->userAttributes);
}
/*
* Functions with coeffect rules have a hidden local that's always the first
* (non-parameter) local (after reified generics, if exists),
* which stores the ambient coeffects.
*/
if (has_coeffects_local(ctx.func)) {
assertx(locId < ret.locals.size());
assertx(ctx.func->locals[locId].name->same(s_coeffects_var.get()));
ret.locals[locId++] = TInt;
}
auto afterParamsLocId = uint32_t{0};
for (; locId < ctx.func->locals.size(); ++locId, ++afterParamsLocId) {
/*
* Some of the closure locals are mapped to used variables. The types of
* use vars are looked up from the index.
*/
if (ctx.func->isClosureBody) {
if (afterParamsLocId < useVars.size()) {
ret.locals[locId] = useVars[afterParamsLocId];
continue;
}
}
// Otherwise the local will start uninitialized, like normal.
ret.locals[locId] = TUninit;
}
// Finally, make sure any volatile locals are set to Gen, even if they are
// parameters.
for (auto locId = uint32_t{0}; locId < ctx.func->locals.size(); ++locId) {
if (is_volatile_local(ctx.func, locId)) {
ret.locals[locId] = TCell;
}
}
return ret;
}
/*
* Helper for do_analyze to initialize the states for all function entries
* (i.e. each dv init and the main entry), and all of them count as places the
* function could be entered, so they all must be visited at least once.
*
* If we're entering at a DV-init, all higher parameter locals must be
* Uninit, with the possible exception of a final variadic param
* (which will be an array). It is also possible that the DV-init is
* reachable from within the function with these parameter locals
* already initialized (although the normal php emitter can't do
* this), but that case will be discovered when iterating.
*/
dataflow_worklist<uint32_t>
prepare_incompleteQ(const Index& index,
FuncAnalysis& ai,
const KnownArgs* knownArgs) {
auto incompleteQ = dataflow_worklist<uint32_t>(ai.rpoBlocks.size());
auto const ctx = ai.ctx;
auto const numParams = ctx.func->params.size();
auto const entryState = entry_state(index, ctx, knownArgs);
if (knownArgs) {
// When we have known args, we only need to add one of the entry points to
// the initial state, since we know how many arguments were passed.
auto const useDvInit = [&] {
if (knownArgs->args.size() >= numParams) return false;
for (auto i = knownArgs->args.size(); i < numParams; ++i) {
auto const dv = ctx.func->params[i].dvEntryPoint;
if (dv != NoBlockId) {
ai.bdata[dv].stateIn.copy_from(entryState);
incompleteQ.push(rpoId(ai, dv));
return true;
}
}
return false;
}();
if (!useDvInit) {
ai.bdata[ctx.func->mainEntry].stateIn.copy_from(entryState);
incompleteQ.push(rpoId(ai, ctx.func->mainEntry));
}
return incompleteQ;
}
for (auto paramId = uint32_t{0}; paramId < numParams; ++paramId) {
auto const dv = ctx.func->params[paramId].dvEntryPoint;
if (dv != NoBlockId) {
ai.bdata[dv].stateIn.copy_from(entryState);
incompleteQ.push(rpoId(ai, dv));
for (auto locId = paramId; locId < numParams; ++locId) {
ai.bdata[dv].stateIn.locals[locId] =
ctx.func->params[locId].isVariadic ? TVec : TUninit;
}
}
}
ai.bdata[ctx.func->mainEntry].stateIn.copy_from(entryState);
incompleteQ.push(rpoId(ai, ctx.func->mainEntry));
return incompleteQ;
}
/*
* Closures inside of classes are analyzed in the context they are
* created in (this affects accessibility rules, access to privates,
* etc).
*
* Note that in the interpreter code, ctx.func->cls is not
* necessarily the same as ctx.cls because of closures.
*/
AnalysisContext adjust_closure_context(AnalysisContext ctx) {
if (ctx.cls && ctx.cls->closureContextCls) {
ctx.cls = ctx.cls->closureContextCls;
}
return ctx;
}
FuncAnalysis do_analyze_collect(const Index& index,
const AnalysisContext& ctx,
CollectedInfo& collect,
const KnownArgs* knownArgs) {
assertx(ctx.cls == adjust_closure_context(ctx).cls);
auto ai = FuncAnalysis { ctx };
SCOPE_ASSERT_DETAIL("do-analyze-collect-2") {
std::string ret;
for (auto bid : ctx.func.blockRange()) {
folly::format(&ret,
"block #{}\nin-{}\n{}",
bid,
state_string(*ctx.func, ai.bdata[bid].stateIn, collect),
show(*ctx.func, *ctx.func.blocks()[bid])
);
}
return ret;
};
SCOPE_ASSERT_DETAIL("do-analyze-collect-1") {
return "Analyzing: " + show(ctx);
};
auto const bump = trace_bump_for(ctx.cls, ctx.func);
Trace::Bump bumper1{Trace::hhbbc, bump};
Trace::Bump bumper2{Trace::hhbbc_cfg, bump};
Trace::Bump bumper3{Trace::hhbbc_index, bump};
if (knownArgs) {
FTRACE(2, "{:.^70}\n", "Inline Interp");
}
SCOPE_EXIT {
if (knownArgs) {
FTRACE(2, "{:.^70}\n", "End Inline Interp");
}
};
FTRACE(2, "{:-^70}\n-- {}\n", "Analyze", show(ctx));
/*
* Set of RPO ids that still need to be visited.
*
* Initially, we need each entry block in this list. As we visit
* blocks, we propagate states to their successors and across their
* back edges---when state merges cause a change to the block
* stateIn, we will add it to this queue so it gets visited again.
*/
auto incompleteQ = prepare_incompleteQ(index, ai, knownArgs);
/*
* There are potentially infinitely growing types when we're using union_of to
* merge states, so occasionally we need to apply a widening operator.
*
* Currently this is done by having a straight-forward hueristic: if you visit
* a block too many times, we'll start doing all the merges with the widening
* operator. We must then continue iterating in case the actual fixed point is
* higher than the result of widening. Likewise if we loop too much because of
* local static types changing, we'll widen those.
*
* Termination is guaranteed because the widening operator has only finite
* chains in the type lattice.
*/
auto totalVisits = std::vector<uint32_t>(ctx.func.blocks().size());
// For debugging, count how many times basic blocks get interpreted.
auto interp_counter = uint32_t{0};
hphp_fast_map<BlockId, BlockUpdateInfo> blockUpdates;
/*
* Iterate until a fixed point.
*
* Each time a stateIn for a block changes, we re-insert the block's
* rpo ID in incompleteQ. Since incompleteQ is ordered, we'll
* always visit blocks with earlier RPO ids first, which hopefully
* means less iterations.
*/
do {
while (!incompleteQ.empty()) {
auto const bid = ai.rpoBlocks[incompleteQ.pop()];
totalVisits[bid]++;
FTRACE(2, "block #{}\nin {}{}", bid,
state_string(*ctx.func, ai.bdata[bid].stateIn, collect),
property_state_string(collect.props));
++interp_counter;
auto propagate = [&] (BlockId target, const State* st) {
if (!st) {
FTRACE(2, " Force reprocess: {}\n", target);
incompleteQ.push(rpoId(ai, target));
return;
}
auto const needsWiden =
totalVisits[target] >= options.analyzeFuncWideningLimit;
FTRACE(2, " {}-> {}\n", needsWiden ? "widening " : "", target);
FTRACE(4, "target old {}",
state_string(*ctx.func, ai.bdata[target].stateIn, collect));
auto const changed =
needsWiden ? widen_into(ai.bdata[target].stateIn, *st)
: merge_into(ai.bdata[target].stateIn, *st);
if (changed) {
incompleteQ.push(rpoId(ai, target));
}
FTRACE(4, "target new {}",
state_string(*ctx.func, ai.bdata[target].stateIn, collect));
};
auto const blk = ctx.func.blocks()[bid].get();
auto stateOut = ai.bdata[bid].stateIn;
auto interp = Interp { index, ctx, collect, bid, blk, stateOut };
auto flags = run(interp, ai.bdata[bid].stateIn, propagate);
if (any(collect.opts & CollectionOpts::EffectFreeOnly) &&
!collect.effectFree) {
break;
}
if (flags.updateInfo.replacedBcs.size() ||
flags.updateInfo.unchangedBcs != blk->hhbcs.size() ||
flags.updateInfo.fallthrough != blk->fallthrough) {
blockUpdates[bid] = std::move(flags.updateInfo);
} else {
blockUpdates.erase(bid);
}
if (flags.returned) {
ai.inferredReturn |= std::move(*flags.returned);
if (flags.retParam == NoLocalId) {
ai.retParam = NoLocalId;
} else if (ai.retParam != flags.retParam) {
if (ai.retParam != MaxLocalId) {
ai.retParam = NoLocalId;
} else {
ai.retParam = flags.retParam;
}
}
}
ai.bdata[bid].noThrow = flags.noThrow;
}
if (any(collect.opts & CollectionOpts::EffectFreeOnly) &&
!collect.effectFree) {
break;
}
} while (!incompleteQ.empty());
ai.closureUseTypes = std::move(collect.closureUseTypes);
ai.effectFree = collect.effectFree;
ai.hasInvariantIterBase = collect.hasInvariantIterBase;
ai.unfoldableFuncs = collect.unfoldableFuncs;
ai.usedParams = collect.usedParams;
ai.publicSPropMutations = std::move(collect.publicSPropMutations);
for (auto& elm : blockUpdates) {
ai.blockUpdates.emplace_back(elm.first, std::move(elm.second));
}
index.fixup_return_type(ctx.func, ai.inferredReturn);
/*
* If inferredReturn is TBottom, the callee didn't execute a return
* at all. (E.g. it unconditionally throws, or is an abstract
* function body.)
*
* In this case, we leave the return type as TBottom, to indicate
* the same to callers.
*/
assertx(ai.inferredReturn.subtypeOf(BCell));
// For debugging, print the final input states for each block.
FTRACE(2, "{}", [&] {
auto const bsep = std::string(60, '=') + "\n";
auto const sep = std::string(60, '-') + "\n";
auto ret = folly::format(
"{}function {} ({} block interps):\n{}",
bsep,
show(ctx),
interp_counter,
bsep
).str();
for (auto& bd : ai.bdata) {
folly::format(
&ret,
"{}block {}{}:\nin {}",
sep,
ai.rpoBlocks[bd.rpoId],
bd.noThrow ? " (no throw)" : "",
state_string(*ctx.func, bd.stateIn, collect)
);
}
ret += sep + bsep;
folly::format(&ret, "Inferred return type: {}\n", show(ai.inferredReturn));
ret += bsep;
return ret;
}());
return ai;
}
FuncAnalysis do_analyze(const Index& index,
const AnalysisContext& inputCtx,
ClassAnalysis* clsAnalysis,
const KnownArgs* knownArgs = nullptr,
CollectionOpts opts = CollectionOpts{}) {
auto const ctx = adjust_closure_context(inputCtx);
auto collect = CollectedInfo { index, ctx, clsAnalysis, opts };
auto ret = do_analyze_collect(index, ctx, collect, knownArgs);
if (ctx.func->name == s_86cinit.get() && !knownArgs) {
// We need to try to resolve any dynamic constants
size_t idx = 0;
for (auto const& c : ctx.cls->constants) {
if (c.val && c.val->m_type == KindOfUninit) {
auto const fa = analyze_func_inline(index, ctx, TCls, { sval(c.name) });
ret.resolvedConstants.emplace_back(idx, unctx(fa.inferredReturn));
}
++idx;
}
}
return ret;
}
//////////////////////////////////////////////////////////////////////
/*
* In the case of HNI builtin classes, private properties are
* allowed to be mutated by native code, so we may not see all the
* modifications.
*
* We are allowed to assume the type annotation on the property is
* accurate, although nothing is currently checking that this is the
* case. We handle this right now by doing inference as if it
* couldn't be affected by native code, then assert the inferred
* type is at least a subtype of the annotated type, and expanding
* it to be the annotated type if it is bigger.
*/
void expand_hni_prop_types(ClassAnalysis& clsAnalysis) {
auto relax_prop = [&] (const php::Prop& prop, PropState& propState) {
auto it = propState.find(prop.name);
if (it == end(propState)) return;
it->second.everModified = true;
/*
* When any functions are interceptable, we don't require the constraints to
* actually match, and relax all the HNI types to Gen.
*
* This is because with any interceptable functions, it's quite
* possible that some function calls in systemlib might not be
* known to return things matching the property type hints for
* some properties, or not to take their arguments by reference.
*/
auto const hniTy = from_hni_constraint(prop.userType);
if (it->second.ty.subtypeOf(hniTy)) {
it->second.ty = hniTy;
return;
}
always_assert_flog(
false,
"HNI class {}::{} inferred property type ({}) doesn't "
"match annotation ({})\n",
clsAnalysis.ctx.cls->name,
prop.name,
show(it->second.ty),
show(hniTy)
);
};
for (auto& prop : clsAnalysis.ctx.cls->properties) {
relax_prop(prop, clsAnalysis.privateProperties);
relax_prop(prop, clsAnalysis.privateStatics);
}
}
//////////////////////////////////////////////////////////////////////
}
//////////////////////////////////////////////////////////////////////
const php::Func* ClassAnalysisWorklist::next() {
if (worklist.empty()) return nullptr;
auto f = worklist.front();
inWorklist.erase(f);
worklist.pop_front();
return f;
}
bool ClassAnalysisWorklist::schedule(const php::Func& f) {
auto const insert = inWorklist.emplace(&f);
if (!insert.second) return false;
worklist.emplace_back(&f);
return true;
}
void ClassAnalysisWorklist::scheduleForProp(SString name) {
auto const it = propDeps.find(name);
if (it == propDeps.end()) return;
for (auto const f : it->second) schedule(*f);
}
void ClassAnalysisWorklist::scheduleForPropMutate(SString name) {
auto const it = propMutateDeps.find(name);
if (it == propMutateDeps.end()) return;
for (auto const f : it->second) schedule(*f);
}
void ClassAnalysisWorklist::scheduleForReturnType(const php::Func& callee) {
auto const it = returnTypeDeps.find(&callee);
if (it == returnTypeDeps.end()) return;
for (auto const f : it->second) schedule(*f);
}
//////////////////////////////////////////////////////////////////////
FuncAnalysisResult::FuncAnalysisResult(AnalysisContext ctx)
: ctx(ctx)
, inferredReturn(TBottom)
{
}
FuncAnalysis::FuncAnalysis(AnalysisContext ctx)
: FuncAnalysisResult{ctx}
, rpoBlocks{rpoSortAddDVs(ctx.func)}
, bdata{ctx.func.blocks().size()}
{
for (auto rpoId = size_t{0}; rpoId < rpoBlocks.size(); ++rpoId) {
bdata[rpoBlocks[rpoId]].rpoId = rpoId;
}
}
FuncAnalysis analyze_func(const Index& index, const AnalysisContext& ctx,
CollectionOpts opts) {
return do_analyze(index, ctx, nullptr, nullptr, opts);
}
FuncAnalysis analyze_func_inline(const Index& index,
const AnalysisContext& ctx,
const Type& thisType,
const CompactVector<Type>& args,
CollectionOpts opts) {
assertx(!ctx.func->isClosureBody);
auto const knownArgs = KnownArgs { thisType, args };
return do_analyze(index, ctx, nullptr, &knownArgs,
opts | CollectionOpts::Inlining);
}
ClassAnalysis analyze_class(const Index& index, const Context& ctx) {
assertx(ctx.cls && !ctx.func && !is_used_trait(*ctx.cls));
{
Trace::Bump bumper{Trace::hhbbc, kSystemLibBump,
is_systemlib_part(*ctx.unit)};
FTRACE(2, "{:#^70}\n", "Class");
}
ClassAnalysis clsAnalysis(ctx);
auto const associatedClosures = index.lookup_closures(ctx.cls);
auto const associatedMethods = index.lookup_extra_methods(ctx.cls);
auto const isHNIBuiltin = ctx.cls->attrs & AttrBuiltin;
/*
* Initialize inferred private property types to their in-class
* initializers.
*
* We need to loosen_all on instance properties, because the class could be
* unserialized, which we don't guarantee preserves those aspects of the
* type.
*
* Also, set Uninit properties to TBottom, so that analysis
* of 86pinit methods sets them to the correct type.
*/
for (auto& prop : const_cast<php::Class*>(ctx.cls)->properties) {
auto const cellTy = from_cell(prop.val);
if (prop_might_have_bad_initial_value(index, *ctx.cls, prop)) {
prop.attrs = (Attr)(prop.attrs & ~AttrInitialSatisfiesTC);
// If Uninit, it will be determined in the 86[s,p]init function.
if (!cellTy.subtypeOf(BUninit)) clsAnalysis.badPropInitialValues = true;
} else {
prop.attrs |= AttrInitialSatisfiesTC;
}
if (!(prop.attrs & AttrPrivate)) continue;
if (isHNIBuiltin) {
auto const hniTy = from_hni_constraint(prop.userType);
if (!cellTy.subtypeOf(hniTy)) {
always_assert_flog(
false,
"hni {}::{} has impossible type. "
"The annotation says it is type ({}) "
"but the default value is type ({}).\n",
ctx.cls->name,
prop.name,
show(hniTy),
show(cellTy)
);
}
}
if (!(prop.attrs & AttrStatic)) {
auto t = loosen_this_prop_for_serialization(*ctx.cls, prop.name, cellTy);
if (!is_closure(*ctx.cls) && t.subtypeOf(BUninit)) {
/*
* For non-closure classes, a property of type KindOfUninit
* means that it has non-scalar initializer which will be set
* by a 86pinit method. For these classes, we want the
* initial type of the property to be the type set by the
* 86pinit method, so we set the type to TBottom.
*
* Closures will not have an 86pinit body, but still may have
* properties of kind KindOfUninit (they will later contain
* used variables). We don't want to touch those.
*/
t = TBottom;
} else if (!(prop.attrs & AttrSystemInitialValue)) {
t = adjust_type_for_prop(index, *ctx.cls, &prop.typeConstraint, t);
}
auto& elem = clsAnalysis.privateProperties[prop.name];
elem.ty = std::move(t);
elem.tc = &prop.typeConstraint;
elem.attrs = prop.attrs;
elem.everModified = false;
} else {
// Same thing as the above regarding TUninit and TBottom.
// Static properties don't need to exclude closures for this,
// though---we use instance properties for the closure use vars.
auto t = cellTy.subtypeOf(BUninit)
? TBottom
: (prop.attrs & AttrSystemInitialValue)
? cellTy
: adjust_type_for_prop(index, *ctx.cls, &prop.typeConstraint, cellTy);
auto& elem = clsAnalysis.privateStatics[prop.name];
elem.ty = std::move(t);
elem.tc = &prop.typeConstraint;
elem.attrs = prop.attrs;
elem.everModified = false;
}
}
/*
* For builtins, we assume the runtime can write to the properties
* in un-analyzable ways (but won't violate their type-hint). So,
* expand the analyzed types to at least include the type-hint.
*/
if (isHNIBuiltin) expand_hni_prop_types(clsAnalysis);
/*
* For classes with non-scalar initializers, the 86pinit, 86sinit,
* 86linit, 86cinit, and 86reifiedinit methods are guaranteed to run
* before any other method, and are never called afterwards. Thus,
* we can analyze these methods first to determine the initial types
* of properties with non-scalar initializers, and these need not be
* be run again as part of the fixedpoint computation.
*/
CompactVector<FuncAnalysis> initResults;
auto analyze_86init = [&](const StaticString &name) {
if (auto func = find_method(ctx.cls, name.get())) {
auto const wf = php::WideFunc::cns(func);
auto const context = AnalysisContext { ctx.unit, wf, ctx.cls };
initResults.push_back(do_analyze(index, context, &clsAnalysis));
}
};
analyze_86init(s_86pinit);
analyze_86init(s_86sinit);
analyze_86init(s_86linit);
analyze_86init(s_86cinit);
analyze_86init(s_86reifiedinit);
// NB: Properties can still be TBottom at this point if their initial values
// cannot possibly satisfy their type-constraints. The classes of such
// properties cannot be instantiated.
/*
* Similar to the function case in do_analyze, we have to handle the
* fact that there are infinitely growing chains in our type lattice
* under union_of.
*
* So if we've visited a func some number of times and still aren't
* at a fixed point, we'll set the property state to the result of
* widening the old state with the new state, and then reset the
* counter. This guarantees eventual termination.
*/
ClassAnalysisWork work;
clsAnalysis.work = &work;
clsAnalysis.methods.reserve(initResults.size() + ctx.cls->methods.size());
for (auto& m : initResults) {
clsAnalysis.methods.emplace_back(std::move(m));
}
if (associatedClosures) {
clsAnalysis.closures.reserve(associatedClosures->size());
}
auto const startPrivateProperties = clsAnalysis.privateProperties;
auto const startPrivateStatics = clsAnalysis.privateStatics;
struct FuncMeta {
const php::Unit* unit;
const php::Class* cls;
CompactVector<FuncAnalysisResult>* output;
size_t startReturnRefinements;
size_t localReturnRefinements = 0;
int outputIdx = -1;
size_t visits = 0;
};
hphp_fast_map<const php::Func*, FuncMeta> funcMeta;
auto const getMeta = [&] (const php::Func& f) -> FuncMeta& {
auto metaIt = funcMeta.find(&f);
assertx(metaIt != funcMeta.end());
return metaIt->second;
};
// Build up the initial worklist:
for (auto const& f : ctx.cls->methods) {
if (f->name->isame(s_86pinit.get()) ||
f->name->isame(s_86sinit.get()) ||
f->name->isame(s_86linit.get()) ||
f->name->isame(s_86cinit.get()) ||
f->name->isame(s_86reifiedinit.get())) {
continue;
}
auto const DEBUG_ONLY inserted = work.worklist.schedule(*f);
assertx(inserted);
auto [type, refinements] = index.lookup_return_type_raw(f.get());
work.returnTypes.emplace(f.get(), std::move(type));
funcMeta.emplace(
f.get(),
FuncMeta{ctx.unit, ctx.cls, &clsAnalysis.methods, refinements}
);
}
if (associatedClosures) {
for (auto const c : *associatedClosures) {
auto const f = c->methods[0].get();
auto const DEBUG_ONLY inserted = work.worklist.schedule(*f);
assertx(inserted);
auto [type, refinements] = index.lookup_return_type_raw(f);
work.returnTypes.emplace(f, std::move(type));
funcMeta.emplace(
f, FuncMeta{ctx.unit, c, &clsAnalysis.closures, refinements}
);
}
}
if (associatedMethods) {
for (auto const m : *associatedMethods) {
auto const DEBUG_ONLY inserted = work.worklist.schedule(*m);
assertx(inserted);
funcMeta.emplace(m, FuncMeta{m->unit, ctx.cls, nullptr, 0, 0});
}
}
// Keep analyzing until we have more functions scheduled (the fixed
// point).
while (!work.worklist.empty()) {
// First analyze funcs until we hit a fixed point for the
// properties. Until we reach that, the return types are *not*
// guaranteed to be correct.
while (auto const f = work.worklist.next()) {
auto& meta = getMeta(*f);
auto const wf = php::WideFunc::cns(f);
auto const context = AnalysisContext { meta.unit, wf, meta.cls };
auto results = do_analyze(index, context, &clsAnalysis);
if (meta.output) {
if (meta.outputIdx < 0) {
meta.outputIdx = meta.output->size();
meta.output->emplace_back(std::move(results));
} else {
(*meta.output)[meta.outputIdx] = std::move(results);
}
}
if (meta.visits++ >= options.analyzeClassWideningLimit) {
for (auto& prop : clsAnalysis.privateProperties) {
auto wide = widen_type(prop.second.ty);
if (prop.second.ty.strictlyMoreRefined(wide)) {
prop.second.ty = std::move(wide);
work.worklist.scheduleForProp(prop.first);
}
}
for (auto& prop : clsAnalysis.privateStatics) {
auto wide = widen_type(prop.second.ty);
if (prop.second.ty.strictlyMoreRefined(wide)) {
prop.second.ty = std::move(wide);
work.worklist.scheduleForProp(prop.first);
}
}
}
}
// We've hit a fixed point for the properties. Other local
// information (such as return type information) is now correct
// (but might not be optimal).
auto bail = false;
// Reflect any improved return types into the results. This will
// make them available for local analysis and they'll eventually
// be written back into the Index.
for (auto& kv : funcMeta) {
auto const f = kv.first;
auto& meta = kv.second;
if (!meta.output) continue;
assertx(meta.outputIdx >= 0);
auto& results = (*meta.output)[meta.outputIdx];
auto const oldTypeIt = work.returnTypes.find(f);
assertx(oldTypeIt != work.returnTypes.end());
auto& oldType = oldTypeIt->second;
results.inferredReturn =
loosen_interfaces(std::move(results.inferredReturn));
// Heed the return type refinement limit
if (results.inferredReturn.strictlyMoreRefined(oldType)) {
if (meta.startReturnRefinements + meta.localReturnRefinements
< options.returnTypeRefineLimit) {
oldType = results.inferredReturn;
work.worklist.scheduleForReturnType(*f);
} else if (meta.localReturnRefinements > 0) {
results.inferredReturn = oldType;
}
++meta.localReturnRefinements;
} else if (!more_refined_for_index(results.inferredReturn, oldType)) {
// If we have a monotonicity violation, bail out immediately
// and let the Index complain.
bail = true;
}
results.localReturnRefinements = meta.localReturnRefinements;
if (results.localReturnRefinements > 0) --results.localReturnRefinements;
}
if (bail) break;
hphp_fast_set<const php::Func*> changed;
// We've made the return types available for local analysis. Now
// iterate again and see if we can improve them.
while (auto const f = work.worklist.next()) {
auto& meta = getMeta(*f);
auto const wf = php::WideFunc::cns(f);
auto const context = AnalysisContext { meta.unit, wf, meta.cls };
work.propsRefined = false;
auto results = do_analyze(index, context, &clsAnalysis);
assertx(!work.propsRefined);
if (!meta.output) continue;
auto returnTypeIt = work.returnTypes.find(f);
assertx(returnTypeIt != work.returnTypes.end());
auto& oldReturn = returnTypeIt->second;
results.inferredReturn =
loosen_interfaces(std::move(results.inferredReturn));
// Heed the return type refinement limit
if (results.inferredReturn.strictlyMoreRefined(oldReturn)) {
if (meta.startReturnRefinements + meta.localReturnRefinements
< options.returnTypeRefineLimit) {
oldReturn = results.inferredReturn;
work.worklist.scheduleForReturnType(*f);
changed.emplace(f);
} else if (meta.localReturnRefinements > 0) {
results.inferredReturn = oldReturn;
}
++meta.localReturnRefinements;
} else if (!more_refined_for_index(results.inferredReturn, oldReturn)) {
// If we have a monotonicity violation, bail out immediately
// and let the Index complain.
bail = true;
}
results.localReturnRefinements = meta.localReturnRefinements;
if (results.localReturnRefinements > 0) --results.localReturnRefinements;
assertx(meta.outputIdx >= 0);
(*meta.output)[meta.outputIdx] = std::move(results);
}
if (bail) break;
// Return types have reached a fixed point. However, this means
// that we might be able to further improve property types. So, if
// a method has an improved return return, examine the methods
// which depend on that return type. Drop any property info for
// properties those methods write to. Reschedule any methods which
// or write to those properties. The idea is we want to re-analyze
// all mutations of those properties again, since the refined
// returned types may result in better property types. This
// process may repeat multiple times, but will eventually reach a
// fixed point.
if (!work.propMutators.empty()) {
auto const resetProp = [&] (SString name,
const PropState& src,
PropState& dst) {
auto dstIt = dst.find(name);
auto const srcIt = src.find(name);
if (dstIt == dst.end()) {
assertx(srcIt == src.end());
return;
}
assertx(srcIt != src.end());
dstIt->second.ty = srcIt->second.ty;
dstIt->second.everModified = srcIt->second.everModified;
};
hphp_fast_set<SString> retryProps;
for (auto const f : changed) {
auto const deps = work.worklist.depsForReturnType(*f);
if (!deps) continue;
for (auto const dep : *deps) {
auto const propsIt = work.propMutators.find(dep);
if (propsIt == work.propMutators.end()) continue;
for (auto const prop : propsIt->second) retryProps.emplace(prop);
}
}
// Schedule the funcs which mutate the props before the ones
// that read them.
for (auto const prop : retryProps) {
resetProp(prop, startPrivateProperties,
clsAnalysis.privateProperties);
resetProp(prop, startPrivateStatics,
clsAnalysis.privateStatics);
work.worklist.scheduleForPropMutate(prop);
}
for (auto const prop : retryProps) {
work.worklist.scheduleForProp(prop);
}
}
// This entire loop will eventually terminate when we cannot
// improve properties nor return types.
}
Trace::Bump bumper{Trace::hhbbc, kSystemLibBump,
is_systemlib_part(*ctx.unit)};
// For debugging, print the final state of the class analysis.
FTRACE(2, "{}", [&] {
auto const bsep = std::string(60, '+') + "\n";
auto ret = folly::format(
"{}class {}:\n{}",
bsep,
ctx.cls->name,
bsep
).str();
for (auto& kv : clsAnalysis.privateProperties) {
ret += folly::format(
"private ${: <14} :: {}\n",
kv.first,
show(kv.second.ty)
).str();
}
for (auto& kv : clsAnalysis.privateStatics) {
ret += folly::format(
"private static ${: <14} :: {}\n",
kv.first,
show(kv.second.ty)
).str();
}
ret += bsep;
return ret;
}());
clsAnalysis.work = nullptr;
return clsAnalysis;
}
//////////////////////////////////////////////////////////////////////
PropagatedStates::PropagatedStates(State&& state, StateMutationUndo undos)
: m_locals{std::move(state.locals)}
, m_undos{std::move(undos)}
{
for (size_t i = 0; i < state.stack.size(); ++i) {
m_stack.emplace_back(std::move(state.stack[i].type));
}
}
void PropagatedStates::next() {
// The undo log shouldn't be empty, and we should be at a mark
// (which marks instruction boundary).
assertx(!m_undos.events.empty());
assertx(boost::get<StateMutationUndo::Mark>(&m_undos.events.back()));
m_lastPush.reset();
m_afterLocals.clear();
m_undos.events.pop_back();
// Use the undo log to "unwind" the current state.
while (true) {
assertx(!m_undos.events.empty());
auto const stop = match<bool>(
m_undos.events.back(),
[] (const StateMutationUndo::Mark&) { return true; },
[this] (StateMutationUndo::Push) {
assertx(!m_stack.empty());
if (!m_lastPush) m_lastPush.emplace(std::move(m_stack.back()));
m_stack.pop_back();
return false;
},
[this] (StateMutationUndo::Pop& p) {
m_stack.emplace_back(std::move(p.t));
return false;
},
[this] (StateMutationUndo::Stack& s) {
assertx(s.idx < m_stack.size());
m_stack[s.idx] = std::move(s.t);
return false;
},
[this] (StateMutationUndo::Local& l) {
assertx(l.id < m_locals.size());
auto& old = m_locals[l.id];
m_afterLocals.emplace_back(std::make_pair(l.id, std::move(old)));
old = std::move(l.t);
return false;
}
);
if (stop) break;
m_undos.events.pop_back();
}
}
PropagatedStates
locally_propagated_states(const Index& index,
const AnalysisContext& ctx,
CollectedInfo& collect,
BlockId bid,
State state) {
Trace::Bump bumper{Trace::hhbbc, 10};
auto const blk = ctx.func.blocks()[bid].get();
// Set up the undo log for the interp. We reserve it using this size
// heuristic which captures typical undo log sizes.
StateMutationUndo undos;
undos.events.reserve((blk->hhbcs.size() + 1) * 4);
auto interp = Interp { index, ctx, collect, bid, blk, state, &undos };
for (auto const& op : blk->hhbcs) {
auto const markIdx = undos.events.size();
// Record instruction boundary
undos.events.emplace_back(StateMutationUndo::Mark{});
// Interpret it. This appends more info to the undo log.
auto const stepFlags = step(interp, op);
// Store the step flags in the mark we recorded before the
// interpret.
auto& mark = boost::get<StateMutationUndo::Mark>(undos.events[markIdx]);
mark.wasPEI = stepFlags.wasPEI;
mark.mayReadLocalSet = stepFlags.mayReadLocalSet;
mark.unreachable = state.unreachable;
state.stack.compact();
}
// Add a final mark to maintain invariants (this will be popped
// immediately when the first next() is called).
undos.events.emplace_back(StateMutationUndo::Mark{});
return PropagatedStates{std::move(state), std::move(undos)};
}
State locally_propagated_bid_state(const Index& index,
const AnalysisContext& ctx,
CollectedInfo& collect,
BlockId bid,
State state,
BlockId targetBid) {
Trace::Bump bumper{Trace::hhbbc, 10};
if (!state.initialized) return {};
auto const originalState = state;
auto const blk = ctx.func.blocks()[bid].get();
auto interp = Interp { index, ctx, collect, bid, blk, state };
State ret{};
auto const propagate = [&] (BlockId target, const State* st) {
if (target == targetBid) merge_into(ret, *st);
};
run(interp, originalState, propagate);
ret.stack.compact();
return ret;
}
//////////////////////////////////////////////////////////////////////
}