in hphp/hhbbc/analyze.cpp [612:1040]
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;
}