hphp/runtime/vm/jit/irgen-builtin.cpp (1,982 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/irgen-builtin.h" #include "hphp/runtime/base/array-init.h" #include "hphp/runtime/base/bespoke-array.h" #include "hphp/runtime/base/collections.h" #include "hphp/runtime/base/enum-cache.h" #include "hphp/runtime/base/file-util.h" #include "hphp/runtime/base/tv-refcount.h" #include "hphp/runtime/base/type-variant.h" #include "hphp/runtime/vm/func.h" #include "hphp/runtime/vm/repo-global-data.h" #include "hphp/runtime/vm/vm-regs.h" #include "hphp/runtime/vm/jit/analysis.h" #include "hphp/runtime/vm/jit/extra-data.h" #include "hphp/runtime/vm/jit/guard-constraint.h" #include "hphp/runtime/vm/jit/type.h" #include "hphp/runtime/vm/jit/vm-protect.h" #include "hphp/runtime/vm/jit/irgen-call.h" #include "hphp/runtime/vm/jit/irgen-control.h" #include "hphp/runtime/vm/jit/irgen-exit.h" #include "hphp/runtime/vm/jit/irgen-inlining.h" #include "hphp/runtime/vm/jit/irgen-internal.h" #include "hphp/runtime/vm/jit/irgen-interpone.h" #include "hphp/runtime/vm/jit/irgen-minstr.h" #include "hphp/runtime/vm/jit/irgen-ret.h" #include "hphp/runtime/vm/jit/irgen-types.h" #include "hphp/runtime/ext/collections/ext_collections-map.h" #include "hphp/runtime/ext/collections/ext_collections-set.h" #include "hphp/runtime/ext/collections/ext_collections-vector.h" #include "hphp/runtime/ext/hh/ext_hh.h" #include "hphp/runtime/ext/std/ext_std_errorfunc.h" #include "hphp/util/text-util.h" namespace HPHP::jit::irgen { namespace { ////////////////////////////////////////////////////////////////////// struct ParamPrep { explicit ParamPrep(size_t count, const Func* callee) : info{count} {} struct Info { SSATmp* value{nullptr}; // The value with the appropriate ref-iness to pass to the C++ builtin SSATmp* argValue{nullptr}; bool passByAddr{false}; bool needsConversion{false}; bool isInOut{false}; }; const Info& operator[](size_t idx) const { return info[idx]; } Info& operator[](size_t idx) { return info[idx]; } size_t size() const { return info.size(); } // For free/class/instance methods, ctx is null/Class*/Object* respectively. SSATmp* ctx{nullptr}; jit::vector<Info> info; uint32_t numByAddr{0}; }; ////////////////////////////////////////////////////////////////////// // Will turn into either an int or a double in zend_convert_scalar_to_number. bool type_converts_to_number(Type ty) { return ty.subtypeOfAny(TDbl, TInt, TNull, TObj, TRes, TStr, TBool); } ////////////////////////////////////////////////////////////////////// SSATmp* is_a_impl(IRGS& env, const ParamPrep& params, bool subclassOnly) { auto const nparams = params.size(); if (nparams != 2 && nparams != 3) return nullptr; auto const obj = params[0].value; auto const cls = params[1].value; auto const allowClass = nparams == 3 ? params[2].value : cns(env, false); if (!obj->type().subtypeOfAny(TCls, TObj) || !cls->type().subtypeOfAny(TCls, TLazyCls, TStr) || !allowClass->isA(TBool)) { return nullptr; } auto const lhs = obj->isA(TObj) ? gen(env, LdObjClass, obj) : obj; auto const rhs = [&] { if (cls->isA(TStr)) { return gen(env, LookupClsRDS, cls); } if (cls->isA(TLazyCls)) { auto const cname = gen(env, LdLazyClsName, cls); return gen(env, LookupClsRDS, cname); } return cls; }(); return cond( env, [&](Block* taken) { return gen(env, CheckNonNull, taken, rhs); }, [&](SSATmp* rhs) { // is_a() finishes here. if (!subclassOnly) return gen(env, InstanceOf, lhs, rhs); // is_subclass_of() also needs to check that LHS and RHS don't match. return cond( env, [&](Block* match) { auto const eq = gen(env, EqCls, lhs, rhs); gen(env, JmpNZero, match, eq); }, [&]{ return gen(env, InstanceOf, lhs, rhs); }, [&]{ return cns(env, false); } ); }, [&]{ return cns(env, false); } ); } SSATmp* opt_is_a(IRGS& env, const ParamPrep& params) { return is_a_impl(env, params, false /* subclassOnly */); } SSATmp* opt_is_subclass_of(IRGS& env, const ParamPrep& params) { return is_a_impl(env, params, true /* subclassOnly */); } SSATmp* opt_method_exists(IRGS& env, const ParamPrep& params) { if (params.size() != 2) return nullptr; auto const meth = params[1].value; auto const obj = params[0].value; if (!obj->isA(TObj) || !meth->isA(TStr)) return nullptr; auto const cls = gen(env, LdObjClass, obj); return gen(env, MethodExists, cls, meth); } const StaticString s_conv_clsmeth_to_vec("Implicit clsmeth to vec conversion"); SSATmp* opt_count(IRGS& env, const ParamPrep& params) { if (params.size() != 2) return nullptr; auto const mode = params[1].value; auto const val = params[0].value; // Bail if we're trying to do a recursive count() if (!mode->hasConstVal(0)) return nullptr; // Count may throw return gen(env, Count, val); } SSATmp* opt_sizeof(IRGS& env, const ParamPrep& params) { return opt_count(env, params); } SSATmp* opt_ord(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const arg = params[0].value; auto const arg_type = arg->type(); if (arg_type <= TStr) { return gen(env, OrdStr, arg); } return nullptr; } SSATmp* opt_chr(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const arg = params[0].value; auto const arg_type = arg->type(); if (arg_type <= TInt) { return gen(env, ChrInt, arg); } return nullptr; } SSATmp* opt_hphp_debug_caller_info(IRGS& env, const ParamPrep& params) { if (params.size() != 0) return nullptr; Array result = empty_dict_array(); auto skipped = false; auto found = true; for (auto i = env.inlineState.depth; i > 0; i--) { auto const sk = env.inlineState.bcStateStack[i - 1]; found = hphp_debug_caller_info_impl( result, skipped, sk.func(), sk.offset()); if (found) break; } if (!found) return nullptr; auto const layout = ArrayLayout::FromArray(result.get()); auto const vad = ArrayData::GetScalarArray(std::move(result)); auto const bad = layout.apply(vad); return cns(env, bad); } SSATmp* opt_ini_get(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; // Only generate the optimized version if the argument passed in is a // static string with a constant literal value so we can get the string value // at JIT time. auto const argType = params[0].value->type(); if (!(argType.hasConstVal(TStaticStr))) { return nullptr; } // We can only optimize settings that are system wide since user level // settings can be overridden during the execution of a request. // // TODO: the above is true for settings whose value we burn directly into the // TC, but for non-system settings, we can optimize them as a load from the // known static address or thread-local address of where the setting lives. auto const settingName = params[0].value->strVal()->toCppString(); IniSetting::Mode mode = IniSetting::PHP_INI_NONE; if (!IniSetting::GetMode(settingName, mode)) { return nullptr; } if (mode & ~IniSetting::PHP_INI_SYSTEM) { return nullptr; } if (mode == IniSetting::PHP_INI_ALL) { /* PHP_INI_ALL has a weird encoding */ return nullptr; } Variant value; IniSetting::Get(settingName, value); // All scalar values are cast to a string before being returned. if (value.isString()) { return cns(env, makeStaticString(value.toString())); } if (value.isInteger()) { return cns(env, makeStaticString(folly::to<std::string>(value.toInt64()))); } if (value.isBoolean()) { static auto const s_one = makeStaticString("1"); return cns(env, value.toBoolean() ? s_one : staticEmptyString()); } // ini_get() is now enhanced to return more than strings. // Get out of here if we are something else like an array. return nullptr; } /* * Transforms in_array with a static haystack argument into an AKExistsKeyset. */ SSATmp* opt_in_array(IRGS& env, const ParamPrep& params) { if (params.size() != 3 && params.size() != 2) return nullptr; // We will restrict this optimization to needles that are strings, and // haystacks that have only non-numeric string keys. This avoids a bunch of // complication around numeric-string array-index semantics. auto const needle = params[0].value; if (!(needle->type() <= TStr)) { return nullptr; } auto const haystackType = params[1].value->type(); if (!haystackType.hasConstVal(TArrLike)) { // Haystack isn't statically known return nullptr; } auto const haystack = haystackType.arrLikeVal(); if (haystack->size() == 0) { return cns(env, false); } KeysetInit flipped{haystack->size()}; bool failed{false}; IterateV( haystack, [&](TypedValue key) { if (!isStringType(type(key)) || val(key).pstr->isNumeric()) { // Numeric strings will complicate matters because the loose comparisons // done with array keys are not quite the same as loose comparisons done // by in_array. For example: in_array('0', array('0000')) is true, but // doing array('0000' => true)['0'] will say "undefined index". // This seems unlikely to affect real-world usage. failed = true; return true; } flipped.add(val(key).pstr); return false; } ); if (failed) { return nullptr; } auto const ad = ArrayData::GetScalarArray(flipped.toArray()); return gen(env, AKExistsKeyset, cns(env, ad), needle); } SSATmp* opt_get_class(IRGS& env, const ParamPrep& params) { auto const curCls = curClass(env); auto const curName = [&] { return curCls != nullptr ? cns(env, curCls->name()) : nullptr; }; if (params.size() == 0 && RuntimeOption::EvalGetClassBadArgument == 0) { return curName(); } if (params.size() != 1) return nullptr; auto const val = params[0].value; auto const ty = val->type(); if (ty <= TNull && RuntimeOption::EvalGetClassBadArgument == 0) { return curName(); } if (ty <= TObj) { auto const cls = gen(env, LdObjClass, val); return gen(env, LdClsName, cls); } return nullptr; } SSATmp* opt_class_get_class_name(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const value = params[0].value; if (value->type() <= TCls) { return gen(env, LdClsName, value); } if (value->type() <= TLazyCls) { return gen(env, LdLazyClsName, value); } return nullptr; } SSATmp* opt_sqrt(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const val = params[0].value; auto const ty = val->type(); if (ty <= TDbl) return gen(env, Sqrt, val); return nullptr; } SSATmp* opt_strlen(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const val = params[0].value; auto const ty = val->type(); if (ty <= TStr) { return gen(env, LdStrLen, val); } return nullptr; } SSATmp* opt_clock_gettime_ns(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const val = params[0].value; // CLOCK_THREAD_CPUTIME_ID needs special handling if (val->hasConstVal(TInt) && val->intVal() != CLOCK_THREAD_CPUTIME_ID) { return gen(env, GetTimeNs, val); } return nullptr; } SSATmp* opt_microtime(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const val = params[0].value; if (val->hasConstVal(true)) { return gen(env, GetTime); } return nullptr; } SSATmp* minmax(IRGS& env, const ParamPrep& params, const bool is_max) { auto const val1 = params[1].value; auto const ty1 = val1->type(); auto const val2 = params[0].value; auto const ty2 = val2->type(); // this optimization is only for 2 ints/doubles if (!(ty1 <= TInt || ty1 <= TDbl) || !(ty2 <= TInt || ty2 <= TDbl)) return nullptr; auto const cmp = [&]{ if (ty1 <= TInt && ty2 <= TInt) { return gen(env, is_max ? GtInt : LtInt, val1, val2); } else { auto conv1 = (ty1 <= TDbl) ? val1 : gen(env, ConvIntToDbl, val1); auto conv2 = (ty2 <= TDbl) ? val2 : gen(env, ConvIntToDbl, val2); return gen(env, is_max ? GtDbl : LtDbl, conv1, conv2); } }(); return gen(env, Select, cmp, val1, val2); } SSATmp* opt_max2(IRGS& env, const ParamPrep& params) { // max2 is only called for 2 operands return params.size() == 2 ? minmax(env, params, true) : nullptr; } SSATmp* opt_min2(IRGS& env, const ParamPrep& params) { // min2 is only called for 2 operands return params.size() == 2 ? minmax(env, params, false) : nullptr; } SSATmp* opt_ceil(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; if (!folly::CpuId().sse41()) return nullptr; auto const val = params[0].value; if (!type_converts_to_number(val->type())) return nullptr; // May throw auto const dbl = gen(env, ConvTVToDbl, val); return gen(env, Ceil, dbl); } SSATmp* opt_floor(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; if (!folly::CpuId().sse41()) return nullptr; auto const val = params[0].value; if (!type_converts_to_number(val->type())) return nullptr; // May throw auto const dbl = gen(env, ConvTVToDbl, val); return gen(env, Floor, dbl); } SSATmp* opt_abs(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const value = params[0].value; if (value->type() <= TInt) { // compute integer absolute value ((src>>63) ^ src) - (src>>63) auto const t1 = gen(env, Shr, value, cns(env, 63)); auto const t2 = gen(env, XorInt, t1, value); return gen(env, SubInt, t2, t1); } if (value->type() <= TDbl) return gen(env, AbsDbl, value); if (value->type() <= TArrLike) return cns(env, false); return nullptr; } SSATmp* opt_array_key_cast(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const value = params[0].value; if (value->isA(TInt)) return value; if (value->isA(TNull)) return cns(env, staticEmptyString()); if (value->isA(TBool)) return gen(env, ConvBoolToInt, value); if (value->isA(TDbl)) return gen(env, ConvDblToInt, value); if (value->isA(TRes)) return gen(env, ConvResToInt, value); if (value->isA(TStr)) return gen(env, StrictlyIntegerConv, value); if (value->isA(TLazyCls)) { if (RuntimeOption::EvalRaiseClassConversionWarning) { gen( env, RaiseWarning, cns(env, makeStaticString(Strings::CLASS_TO_STRING)) ); } return gen(env, LdLazyClsName, value); } if (value->isA(TCls)) { if (RuntimeOption::EvalRaiseClassConversionWarning) { gen( env, RaiseWarning, cns(env, makeStaticString(Strings::CLASS_TO_STRING)) ); } return gen(env, LdClsName, value); } return nullptr; } SSATmp* impl_opt_type_structure(IRGS& env, const ParamPrep& params, bool getName, bool noThrow) { if (params.size() != 2) return nullptr; auto const clsNameTmp = params[0].value; auto const cnsNameTmp = params[1].value; if (!cnsNameTmp->hasConstVal(TStaticStr)) return nullptr; auto const cnsName = cnsNameTmp->strVal(); auto const clsTmp = [&] () -> SSATmp* { if (clsNameTmp->isA(TCls)) return clsNameTmp; if (clsNameTmp->isA(TObj)) return gen(env, LdObjClass, clsNameTmp); if (clsNameTmp->inst()->is(LdClsName) || clsNameTmp->inst()->is(LdLazyCls)) { return clsNameTmp->inst()->src(0); } if (clsNameTmp->type().subtypeOfAny(TStr, TLazyCls)) { return ldCls(env, clsNameTmp); } return nullptr; }(); if (!clsTmp) return nullptr; if (!clsTmp->type().clsSpec()) return nullptr; auto const cls = clsTmp->type().clsSpec().cls(); // Slot indices aren't invariant for non-normal classes. if (!isNormalClass(cls)) return nullptr; auto const cnsSlot = cls->clsCnsSlot(cnsName, ConstModifiers::Kind::Type, true); if (cnsSlot == kInvalidSlot) return nullptr; if (!getName) { return cond( env, [&] (Block* taken) { return gen( env, LdResolvedTypeCns, taken, ClsCnsSlotData { cnsName, cnsSlot }, clsTmp ); }, [&] (SSATmp* cns) { return cns; }, [&] { hint(env, Block::Hint::Unlikely); return gen( env, noThrow ? LdTypeCnsNoThrow : LdTypeCns, clsTmp, cnsNameTmp ); } ); } assertx(!noThrow); return cond( env, [&] (Block* taken) { auto const classname = gen( env, LdResolvedTypeCnsClsName, ClsCnsSlotData { cnsName, cnsSlot }, clsTmp ); return gen(env, CheckNonNull, taken, classname); }, [&] (SSATmp* s) { return s; }, [&] { hint(env, Block::Hint::Unlikely); return gen( env, LdTypeCnsClsName, clsTmp, cnsNameTmp ); } ); } SSATmp* opt_type_structure(IRGS& env, const ParamPrep& params) { return impl_opt_type_structure(env, params, false, false); } SSATmp* opt_type_structure_no_throw(IRGS& env, const ParamPrep& params) { return impl_opt_type_structure(env, params, false, true); } SSATmp* opt_type_structure_classname(IRGS& env, const ParamPrep& params) { return impl_opt_type_structure(env, params, true, false); } SSATmp* opt_is_list_like(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const type = params[0].value->type(); if (!type.maybe(TArrLike)) return cns(env, false); if (type <= TVec) return cns(env, true); return nullptr; } SSATmp* opt_is_vec_or_varray(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const type = params[0].value->type(); if (type <= TVec) { return cns(env, true); } return nullptr; } SSATmp* opt_is_dict_or_darray(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const type = params[0].value->type(); if (type <= TDict) { return cns(env, true); } if (!type.maybe(TDict)) { return cns(env, false); } return nullptr; } SSATmp* opt_foldable(IRGS& env, const Func* func, const ParamPrep& params) { if (!func->isFoldable()) return nullptr; const Class* cls = nullptr; if (func->isMethod()) { if (!params.ctx || !func->isStatic()) return nullptr; cls = params.ctx->type().clsSpec().exactCls(); if (!cls) return nullptr; } ArrayData* variadicArgs = nullptr; uint32_t numVariadicArgs = 0; auto numNonDefaultArgs = params.size(); if (numNonDefaultArgs > func->numNonVariadicParams()) { assertx(params.size() == func->numParams()); auto const variadic = params.info.back().value; if (!variadic->type().hasConstVal(TVec)) return nullptr; variadicArgs = variadic->variantVal().asCArrRef().get(); numVariadicArgs = variadicArgs->size(); if (numVariadicArgs && !variadicArgs->isVecType()) return nullptr; assertx(variadicArgs->isStatic()); numNonDefaultArgs = func->numNonVariadicParams(); } // Don't pop the args yet---if the builtin throws at compile time (because // it would raise a warning or something at runtime) we're going to leave // the call alone. VecInit args(numNonDefaultArgs + numVariadicArgs); for (auto i = 0; i < numNonDefaultArgs; ++i) { auto const t = params[i].value->type(); if (!t.hasConstVal() && !t.subtypeOfAny(TUninit, TInitNull, TNullptr)) { return nullptr; } else { args.append(params[i].value->variantVal()); } } if (variadicArgs) { for (auto i = 0; i < numVariadicArgs; i++) { args.append(variadicArgs->get(i)); } } VMProtect::Pause deprot; always_assert(regState() == VMRegState::CLEAN); // Even though regState() is marked clean, vmpc() has not necessarily been // set to anything valid, so we need to do so here (for assertions and // backtraces in the invocation, among other things). auto const savedPC = vmpc(); vmpc() = vmfp() ? vmfp()->func()->entry() : nullptr; SCOPE_EXIT{ vmpc() = savedPC; }; try { // We don't know if notices would be enabled or not when this function // would normally get called, so be safe and don't optimize any calls that // COULD generate notices. ThrowAllErrorsSetter taes; assertx(!RID().getJitFolding()); RID().setJitFolding(true); SCOPE_EXIT{ RID().setJitFolding(false); }; auto retVal = g_context->invokeFunc(func, args.toArray(), nullptr, const_cast<Class*>(cls), RuntimeCoeffects::fixme(), false); SCOPE_EXIT { tvDecRefGen(retVal); }; assertx(tvIsPlausible(retVal)); auto scalar_array = [&] { auto& a = val(retVal).parr; ArrayData::GetScalarArray(&a); return a; }; switch (retVal.m_type) { case KindOfNull: case KindOfBoolean: case KindOfInt64: case KindOfDouble: return cns(env, retVal); case KindOfPersistentString: case KindOfString: return cns(env, makeStaticString(retVal.m_data.pstr)); case KindOfPersistentVec: case KindOfVec: return cns(env, make_tv<KindOfPersistentVec>(scalar_array())); case KindOfPersistentDict: case KindOfDict: return cns(env, make_tv<KindOfPersistentDict>(scalar_array())); case KindOfPersistentKeyset: case KindOfKeyset: return cns(env, make_tv<KindOfPersistentKeyset>(scalar_array())); case KindOfLazyClass: return cns(env, retVal.m_data.plazyclass.name()); case KindOfUninit: case KindOfObject: case KindOfResource: case KindOfRFunc: // TODO (T29639296) case KindOfFunc: case KindOfClass: case KindOfClsMeth: case KindOfRClsMeth: return nullptr; } } catch (...) { // If an exception or notice occurred, don't optimize } return nullptr; } /* * Container intrinsic for HH\traversable */ SSATmp* opt_container_first(IRGS& env, const ParamPrep& params) { if (params.size() != 1) { return nullptr; } auto const value = params[0].value; auto const type = value->type(); if (type <= TVec) { auto const r = gen(env, VecFirst, value); gen(env, IncRef, r); return r; } else if (type <= TDict) { auto const r = gen(env, DictFirst, value); gen(env, IncRef, r); return r; } else if (type <= TKeyset) { auto const r = gen(env, KeysetFirst, value); gen(env, IncRef, r); return r; } return nullptr; } SSATmp* opt_container_last(IRGS& env, const ParamPrep& params) { if (params.size() != 1) { return nullptr; } auto const value = params[0].value; auto const type = value->type(); if (type <= TVec) { auto const r = gen(env, VecLast, value); gen(env, IncRef, r); return r; } else if (type <= TDict) { auto const r = gen(env, DictLast, value); gen(env, IncRef, r); return r; } else if (type <= TKeyset) { auto const r = gen(env, KeysetLast, value); gen(env, IncRef, r); return r; } return nullptr; } SSATmp* opt_container_first_key(IRGS& env, const ParamPrep& params) { if (params.size() != 1) { return nullptr; } auto const value = params[0].value; auto const type = value->type(); if (type <= TVec) { return cond( env, [&](Block* taken) { auto const length = gen(env, CountVec, value); gen(env, JmpZero, taken, length); }, [&] { return cns(env, 0); }, [&] { return cns(env, TInitNull); } ); } else if (type <= TDict) { auto const r = gen(env, DictFirstKey, value); gen(env, IncRef, r); return r; } else if (type <= TKeyset) { auto const r = gen(env, KeysetFirst, value); gen(env, IncRef, r); return r; } return nullptr; } SSATmp* opt_container_last_key(IRGS& env, const ParamPrep& params) { if (params.size() != 1) { return nullptr; } auto const value = params[0].value; auto const type = value->type(); if (type <= TVec) { return cond( env, [&](Block* taken) { auto const length = gen(env, CountVec, value); gen(env, JmpZero, taken, length); return length; }, [&] (SSATmp* next) { return gen(env, SubInt, next, cns(env, 1)); }, [&] { return cns(env, TInitNull); } ); } else if (type <= TDict) { auto const r = gen(env, DictLastKey, value); gen(env, IncRef, r); return r; } else if (type <= TKeyset) { auto const r = gen(env, KeysetLast, value); gen(env, IncRef, r); return r; } return nullptr; } namespace { const StaticString s_MCHELPER_ON_GET_CLS("MethCallerHelper is used on meth_caller_get_class()"), s_MCHELPER_ON_GET_METH( "MethCallerHelper is used on meth_caller_get_method()"), s_BAD_ARG_ON_MC_GET_CLS( "Argument 1 passed to meth_caller_get_class() must be a MethCaller"), s_BAD_ARG_ON_MC_GET_METH( "Argument 1 passed to meth_caller_get_method() must be a MethCaller"), s_meth_caller_cls("__SystemLib\\MethCallerHelper"), s_dyn_meth_caller_cls("__SystemLib\\DynMethCallerHelper"), s_cls_prop("class"), s_meth_prop("method"); const Slot s_cls_idx{0}; const Slot s_meth_idx{1}; SSATmp* opt_fun_get_function(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const value = params[0].value; auto const type = value->type(); if (type <= TFunc) { return gen(env, LdFuncName, value); } else if (type <= TRFunc) { return gen(env, LdFuncName, gen(env, LdFuncFromRFunc, value)); } return nullptr; } DEBUG_ONLY bool meth_caller_has_expected_prop(const Class *mcCls) { return mcCls->lookupDeclProp(s_cls_prop.get()) == s_cls_idx && mcCls->lookupDeclProp(s_meth_prop.get()) == s_meth_idx && mcCls->declPropTypeConstraint(s_cls_idx).isString() && mcCls->declPropTypeConstraint(s_meth_idx).isString(); } template<bool isCls> SSATmp* meth_caller_get_name(IRGS& env, SSATmp *value) { if (value->isA(TFunc)) { return cond( env, [&] (Block* taken) { auto const attr = AttrData { AttrIsMethCaller }; auto isMC = gen(env, FuncHasAttr, attr, value); gen(env, JmpZero, taken, isMC); }, [&] { return gen(env, LdMethCallerName, MethCallerData{isCls}, value); }, [&] { // Taken: src is not a meth_caller hint(env, Block::Hint::Unlikely); updateMarker(env); env.irb->exceptionStackBoundary(); gen(env, RaiseError, cns(env, isCls ? s_BAD_ARG_ON_MC_GET_CLS.get() : s_BAD_ARG_ON_MC_GET_METH.get())); // Dead-code, but needed to satisfy cond(). return cns(env, staticEmptyString()); } ); } if (value->isA(TObj)) { auto loadProp = [&] (Class* cls, bool isGetCls, SSATmp* obj) { auto const slot = isGetCls ? s_cls_idx : s_meth_idx; auto const prop = ldPropAddr(env, obj, nullptr, cls, slot, TStr); auto const ret = gen(env, LdMem, TStr, prop); gen(env, IncRef, ret); return ret; }; auto const check = [&] (const Class* cls, Block* taken) -> SSATmp* { auto isMC = gen(env, EqCls, cns(env, cls), gen(env, LdObjClass, value)); gen(env, JmpZero, taken, isMC); return nullptr; }; auto const mcCls = Class::lookup(s_meth_caller_cls.get()); auto const dynMcCls = Class::lookup(s_dyn_meth_caller_cls.get()); assertx(mcCls && meth_caller_has_expected_prop(mcCls)); assertx(dynMcCls && meth_caller_has_expected_prop(dynMcCls)); MultiCond mc{env}; mc.ifThen( [&] (Block* taken) { return check(dynMcCls, taken); }, [&] (SSATmp*) { return loadProp(dynMcCls, isCls, value); } ); mc.ifThen( [&] (Block* taken) { return check(mcCls, taken); }, [&] (SSATmp*) { if (RO::EvalEmitMethCallerFuncPointers && RO::EvalNoticeOnMethCallerHelperUse) { updateMarker(env); env.irb->exceptionStackBoundary(); auto const msg = cns(env, isCls ? s_MCHELPER_ON_GET_CLS.get() : s_MCHELPER_ON_GET_METH.get()); gen(env, RaiseNotice, msg); } return loadProp(mcCls, isCls, value); } ); return mc.elseDo( [&] { // src is not a meth_caller hint(env, Block::Hint::Unlikely); updateMarker(env); env.irb->exceptionStackBoundary(); gen(env, RaiseError, cns(env, isCls ? s_BAD_ARG_ON_MC_GET_CLS.get() : s_BAD_ARG_ON_MC_GET_METH.get())); // Dead-code, but needed to satisfy cond(). return cns(env, staticEmptyString()); } ); } return nullptr; } } SSATmp* opt_class_meth_get_class(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const value = params[0].value; if (value->type() <= TClsMeth) { return gen(env, LdClsName, gen(env, LdClsFromClsMeth, value)); } else if (value->type() <= TRClsMeth) { return gen(env, LdClsName, gen(env, LdClsFromRClsMeth, value)); } return nullptr; } SSATmp* opt_class_meth_get_method(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const value = params[0].value; if (value->type() <= TClsMeth) { return gen(env, LdFuncName, gen(env, LdFuncFromClsMeth, value)); } else if (value->type() <= TRClsMeth) { return gen(env, LdFuncName, gen(env, LdFuncFromRClsMeth, value)); } return nullptr; } const EnumValues* getEnumValues(IRGS& env, const ParamPrep& params) { if (!(params.ctx && params.ctx->hasConstVal(TCls))) return nullptr; auto const cls = params.ctx->clsVal(); if (!(isEnum(cls) && classHasPersistentRDS(cls))) return nullptr; return EnumCache::getValuesStatic(cls); } SSATmp* opt_enum_names(IRGS& env, const ParamPrep& params) { if (params.size() != 0) return nullptr; auto const enum_values = getEnumValues(env, params); return enum_values ? cns(env, enum_values->names.get()) : nullptr; } SSATmp* opt_enum_values(IRGS& env, const ParamPrep& params) { if (params.size() != 0) return nullptr; auto const enum_values = getEnumValues(env, params); return enum_values ? cns(env, enum_values->values.get()) : nullptr; } SSATmp* opt_enum_is_valid(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const origVal = params[0].value; if (!origVal->type().isKnownDataType()) return nullptr; auto const value = convertClassKey(env, origVal); auto const enum_values = getEnumValues(env, params); if (!enum_values) return nullptr; auto const ad = VanillaDict::as(enum_values->names.get()); if (value->isA(TInt)) { if (ad->keyTypes().mustBeStrs()) return cns(env, false); return gen(env, AKExistsDict, cns(env, ad->asArrayData()), value); } else if (value->isA(TStr)) { // We're not doing intish-casts here, so we bail if ad has any int keys. if (!ad->keyTypes().mustBeStrs()) return nullptr; return gen(env, AKExistsDict, cns(env, ad->asArrayData()), value); } return cns(env, false); } SSATmp* opt_enum_coerce(IRGS& env, const ParamPrep& params) { auto const valid = opt_enum_is_valid(env, params); if (!valid) return nullptr; return cond(env, [&](Block* taken) { gen(env, JmpZero, taken, valid); }, [&]{ // We never need to coerce strs to ints here, but we may need to coerce // ints to strs if the enum is a string type with intish values. auto const value = params[0].value; auto const isstr = isStringType(params.ctx->clsVal()->enumBaseTy()); if (value->isA(TInt) && isstr) return gen(env, ConvIntToStr, value); gen(env, IncRef, value); return value; }, [&]{ return cns(env, TInitNull); } ); } SSATmp* opt_is_meth_caller(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; auto const value = params[0].value; if (value->isA(TFunc)) { return gen(env, FuncHasAttr, AttrData { AttrIsMethCaller }, value); } if (value->isA(TObj)) { auto const mcCls = Class::lookup(s_meth_caller_cls.get()); auto const dynMcCls = Class::lookup(s_dyn_meth_caller_cls.get()); assertx(mcCls); assertx(dynMcCls); auto const cls = gen(env, LdObjClass, value); auto const isMC = gen(env, EqCls, cns(env, mcCls), cls); auto const isDMC = gen(env, EqCls, cns(env, dynMcCls), cls); return gen(env, XorBool, isMC, isDMC); } return nullptr; } SSATmp* opt_meth_caller_get_class(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; return meth_caller_get_name<true>(env, params[0].value); } SSATmp* opt_meth_caller_get_method(IRGS& env, const ParamPrep& params) { if (params.size() != 1) return nullptr; return meth_caller_get_name<false>(env, params[0].value); } ////////////////////////////////////////////////////////////////////// // Whitelists of builtins that we have optimized HHIR emitters for. // The first whitelist here simply lets us look up the functions above. using OptEmitFn = SSATmp* (*)(IRGS& env, const ParamPrep& params); const hphp_fast_string_imap<OptEmitFn> s_opt_emit_fns{ {"is_a", opt_is_a}, {"is_subclass_of", opt_is_subclass_of}, {"method_exists", opt_method_exists}, {"count", opt_count}, {"sizeof", opt_sizeof}, {"ini_get", opt_ini_get}, {"in_array", opt_in_array}, {"get_class", opt_get_class}, {"sqrt", opt_sqrt}, {"strlen", opt_strlen}, {"clock_gettime_ns", opt_clock_gettime_ns}, {"microtime", opt_microtime}, {"__SystemLib\\max2", opt_max2}, {"__SystemLib\\min2", opt_min2}, {"ceil", opt_ceil}, {"floor", opt_floor}, {"abs", opt_abs}, {"ord", opt_ord}, {"chr", opt_chr}, {"hphp_debug_caller_info", opt_hphp_debug_caller_info}, {"hh\\array_key_cast", opt_array_key_cast}, {"hh\\type_structure", opt_type_structure}, {"hh\\type_structure_no_throw", opt_type_structure_no_throw}, {"hh\\type_structure_classname", opt_type_structure_classname}, {"hh\\is_list_like", opt_is_list_like}, {"HH\\is_dict_or_darray", opt_is_dict_or_darray}, {"HH\\is_vec_or_varray", opt_is_vec_or_varray}, {"HH\\Lib\\_Private\\Native\\first", opt_container_first}, {"HH\\Lib\\_Private\\Native\\last", opt_container_last}, {"HH\\Lib\\_Private\\Native\\first_key", opt_container_first_key}, {"HH\\Lib\\_Private\\Native\\last_key", opt_container_last_key}, {"HH\\fun_get_function", opt_fun_get_function}, {"HH\\class_meth_get_class", opt_class_meth_get_class}, {"HH\\class_meth_get_method", opt_class_meth_get_method}, {"HH\\class_get_class_name", opt_class_get_class_name}, {"HH\\BuiltinEnum::getNames", opt_enum_names}, {"HH\\BuiltinEnum::getValues", opt_enum_values}, {"HH\\BuiltinEnum::coerce", opt_enum_coerce}, {"HH\\BuiltinEnum::isValid", opt_enum_is_valid}, {"HH\\is_meth_caller", opt_is_meth_caller}, {"HH\\meth_caller_get_class", opt_meth_caller_get_class}, {"HH\\meth_caller_get_method", opt_meth_caller_get_method}, }; // This second whitelist, a subset of the first, records which parameter // (if any) we need a vanilla input for to generate optimized HHIR. const hphp_fast_string_imap<int> s_vanilla_params{ {"HH\\Lib\\_Private\\Native\\first", 0}, {"HH\\Lib\\_Private\\Native\\last", 0}, {"HH\\Lib\\_Private\\Native\\first_key", 0}, {"HH\\Lib\\_Private\\Native\\last_key", 0}, }; ////////////////////////////////////////////////////////////////////// // If we encounter an inlined NativeImpls in an optimized region, we can't use // a layout-sensitive implementation for the bytecode, today, because we don't // support arbitrary control flow with an inlined NativeImpl. bool skipLayoutSensitiveNativeImpl(IRGS& env, const StringData* fname) { return allowBespokeArrayLikes() && s_vanilla_params.find(fname->data()) != s_vanilla_params.end(); } SSATmp* ldOutAddr(IRGS& env, uint32_t inOutIndex) { if (isInlining(env)) { auto const func = curFunc(env); return gen( env, LdStkAddr, IRSPRelOffsetData {offsetFromIRSP(env, SBInvOffset { - func->numSlotsInFrame() - (int)kNumActRecCells - (int)inOutIndex })}, sp(env) ); } return gen( env, LdOutAddr, IndexData(inOutIndex), fp(env) ); } SSATmp* optimizedFCallBuiltin(IRGS& env, const Func* func, const ParamPrep& params) { auto const result = [&]() -> SSATmp* { auto const fname = func->fullName(); if (auto const retVal = opt_foldable(env, func, params)) { // Check if any of the parameters are in-out. If not, we don't // need any special handling. auto const numInOut = std::count_if( params.info.begin(), params.info.end(), [] (const ParamPrep::Info& i) { return i.isInOut; } ); if (!numInOut) return retVal; // Otherwise, the return value is actually a tuple containing // all of the results. We need to unpack the tuple and write the // contents to their proper place on the stack. auto const ad = retVal->arrLikeVal(); assertx(ad->isStatic()); assertx(ad->isVecType()); assertx(ad->size() == numInOut + 1); uint32_t inOutIndex = 0; for (auto const& param : params.info) { if (!param.isInOut) continue; // NB: The parameters to the builtin have already been popped // at this point, so we don't need to account for them when // calculating the stack offset. auto const val = cns(env, ad->nvGetVal(inOutIndex + 1)); auto const addr = ldOutAddr(env, inOutIndex); gen(env, StMem, addr, val); ++inOutIndex; } // The first element of the tuple is always the actual function // return. return cns(env, ad->nvGetVal(0)); } if (skipLayoutSensitiveNativeImpl(env, fname)) { return nullptr; } auto const it = s_opt_emit_fns.find(fname->data()); if (it != s_opt_emit_fns.end()) return it->second(env, params); return nullptr; }(); if (result == nullptr) return nullptr; // We don't constrain types when loading parameters whose typehints don't // imply any checks. However, optimized codegen for a builtin generally // requires specific input types (and uses them to produce specific outputs). // // As a result, if we're returning optimized builtin codegen, we also need // to constrain our input parameters as well. // // To play well with assumptions in tracelet region selection, our optimized // codegen must obey the following restriction: // - IF we relax the inputs for an optimized builtin to DataTypeSpecific // - THEN the output must have its current type relaxed to DataTypeSpecific // // Here's a breaking example: a builtin that returns an int for one string // input and null for a different string input. If any builtin behaves like // this, it should place additional constraints on its inputs. (No current // builtins need to do so - DataTypeSpecific is a good default.) for (auto const& param : params.info) { env.irb->constrainValue(param.argValue, DataTypeSpecific); } return result; } ////////////////////////////////////////////////////////////////////// /* * Return the target type of a parameter to a builtin function. * * If the builtin parameter has no type hints to cause coercion, this function * returns TBottom. */ Type param_target_type(const Func* callee, uint32_t paramIdx) { auto const& pi = callee->params()[paramIdx]; auto const& tc = pi.typeConstraint; if (tc.isNullable()) { auto const dt = tc.underlyingDataType(); if (!dt) return TBottom; return TNull | Type(*dt); } if (!pi.builtinType) { return tc.isVecOrDict() ? TVec|TDict : TBottom; } if (pi.builtinType == KindOfObject && pi.defaultValue.m_type == KindOfNull) { return TNullableObj; } return Type(*pi.builtinType); } ////////////////////////////////////////////////////////////////////// /* * Collect parameters for a call to a builtin. Also determine which ones will * need to be passed through the eval stack, and which ones will need * conversions. */ ParamPrep prepare_params(IRGS& env, const Func* callee, SSATmp* ctx, uint32_t numArgs) { auto ret = ParamPrep{numArgs, callee}; ret.ctx = ctx; // Fill in in reverse order, since they may come from popC's (depending on // what loadParam wants to do). for (auto offset = uint32_t{numArgs}; offset-- > 0;) { auto const ty = param_target_type(callee, offset); auto& cur = ret[offset]; auto& pi = callee->params()[offset]; // If ty > TBottom, it had some kind of type hint. cur.needsConversion = ty > TBottom; cur.isInOut = callee->isInOut(offset); cur.value = ldLoc(env, offset, DataTypeSpecific); // We do actually mean exact type equality here. We're only capable of // passing the following primitives through registers; everything else goes // by address unless its flagged "NativeArg". auto const nonrefType = !pi.isTakenAsVariant() && (ty == TBool || ty == TInt || ty == TDbl || pi.isNativeArg() || pi.isTakenAsTypedValue()); if (cur.isInOut || nonrefType) { cur.argValue = cur.value; continue; } cur.argValue = gen(env, LdLocAddr, LocalId(offset), fp(env)); ++ret.numByAddr; cur.passByAddr = true; } return ret; } ////////////////////////////////////////////////////////////////////// /* * Take the value in param, apply any needed conversions * and return the value to be passed to CallBuiltin. * * checkType(ty, fail): * verify that the param is of type ty, and branch to fail * if not. If it results in a new SSATmp*, (as eg CheckType * would), then that should be returned; otherwise it should * return nullptr; * convertParam(ty): * convert the param to ty; failure should be handled by * CatchMaker::makeParamCoerceCatch, and it should return * a new SSATmp* (if appropriate) or nullptr. * realize(): * return the SSATmp* needed by CallBuiltin for this parameter. * if checkType and convertParam returned non-null values, * param.argValue will have been updated with a phi of their results. */ template<class V, class C, class R> SSATmp* realize_param(IRGS& env, ParamPrep::Info& param, const Func* callee, Type targetTy, V checkType, C convertParam, R realize) { if (param.needsConversion) { auto const baseTy = targetTy - TNull; assertx(baseTy.isKnownDataType() || baseTy == (TVec|TDict)); if (auto const value = cond( env, [&] (Block* convert) -> SSATmp* { if (targetTy == baseTy) { return checkType(baseTy, convert); } return cond( env, [&] (Block* fail) { return checkType(baseTy, fail); }, [&] (SSATmp* v) { return v; }, [&] { return checkType(TInitNull, convert); }); }, [&] (SSATmp* v) { return v; }, [&] () -> SSATmp* { return convertParam(baseTy); } )) { // Heads up on non-local state here: we have to update // the values inside ParamPrep so that the CatchMaker // functions know about new potentially refcounted types // to decref, or values that were already decref'd and // replaced with things like ints. param.argValue = value; } } return realize(); } template<class U, class F> SSATmp* maybeCoerceValue( IRGS& env, SSATmp* val, Type target, uint32_t id, const Func* func, U update, F fail ) { assertx(target.isKnownDataType() || (target == (TVec|TDict))); auto bail = [&] { fail(); return cns(env, TBottom); }; if (target <= TStr) { if (!val->type().maybe(TLazyCls | TCls)) return bail(); auto castW = [&] (SSATmp* val){ if (RuntimeOption::EvalClassStringHintNotices) { gen( env, RaiseNotice, cns(env, makeStaticString(Strings::CLASS_TO_STRING_IMPLICIT)) ); } return update(val); }; MultiCond mc{env}; mc.ifTypeThen(val, TLazyCls, [&](SSATmp* lcval) { return castW(gen(env, LdLazyClsName, lcval)); }); mc.ifTypeThen(val, TCls, [&](SSATmp* cval) { return castW(gen(env, LdClsName, cval)); }); return mc.elseDo([&] { hint(env, Block::Hint::Unlikely); return bail(); }); } return bail(); } StaticString s_varray_or_darray("varray_or_darray"), s_vec_or_dict("vec_or_dict"); /* * Prepare the actual arguments to the CallBuiltin instruction, by converting a * ParamPrep into a vector of SSATmps to pass to CallBuiltin. If any of the * parameters needed type conversions, we need to do that here too. */ jit::vector<SSATmp*> realize_params(IRGS& env, const Func* callee, ParamPrep& params) { auto const cbNumArgs = 2 + params.size() + (params.ctx ? 1 : 0); auto ret = jit::vector<SSATmp*>(cbNumArgs); auto argIdx = uint32_t{0}; ret[argIdx++] = fixupFP(env); ret[argIdx++] = sp(env); if (params.ctx) ret[argIdx++] = params.ctx; auto const genFail = [&](uint32_t param, SSATmp* val) { auto const expected_type = [&]{ auto const& tc = callee->params()[param].typeConstraint; if (tc.isVecOrDict()) return s_vec_or_dict.get(); auto const dt = param_target_type(callee, param) - TNull; return getDataTypeString(dt.toDataType()).get(); }(); auto const data = FuncArgTypeData { callee, param + 1, expected_type }; gen(env, ThrowParameterWrongType, data, val); }; for (auto paramIdx = uint32_t{0}; paramIdx < params.size(); ++paramIdx) { auto& param = params[paramIdx]; auto const targetTy = param_target_type(callee, paramIdx); if (param.passByAddr) { assertx(param.argValue->type() <= TMem); ret[argIdx++] = realize_param( env, param, callee, targetTy, [&] (const Type& ty, Block* fail) -> SSATmp* { gen(env, CheckTypeMem, ty, fail, param.argValue); return nullptr; }, [&] (const Type& ty) -> SSATmp* { hint(env, Block::Hint::Unlikely); auto val = gen(env, LdMem, TCell, param.argValue); maybeCoerceValue( env, val, ty, paramIdx, callee, [&] (SSATmp* val) { gen(env, StLoc, LocalId{paramIdx}, fp(env), val); return val; }, [&] { genFail(paramIdx, val); } ); return nullptr; }, [&] { return param.argValue; }); } else { assertx(param.argValue->type() <= TCell); auto const oldVal = params[paramIdx].argValue; ret[argIdx++] = realize_param( env, param, callee, targetTy, [&] (const Type& ty, Block* fail) { auto ret = gen(env, CheckType, ty, fail, param.argValue); env.irb->constrainValue(ret, DataTypeSpecific); return ret; }, [&] (const Type& ty) -> SSATmp* { hint(env, Block::Hint::Unlikely); return maybeCoerceValue( env, param.argValue, ty, paramIdx, callee, [&] (SSATmp* val) { return val; }, [&] { genFail(paramIdx, oldVal); } ); }, [&] { return param.argValue; }); } } assertx(argIdx == cbNumArgs); return ret; } ////////////////////////////////////////////////////////////////////// SSATmp* builtinInValue(IRGS& env, const Func* builtin, uint32_t i) { auto const tv = Native::builtinInValue(builtin, i); if (!tv) return nullptr; return cns(env, *tv); } SSATmp* builtinCall(IRGS& env, const Func* callee, ParamPrep& params) { assertx(callee->nativeFuncPtr()); if (isInlining(env)) { // For FCallBuiltin, params are TypedValues, while for NativeImpl, they're // pointers to these values on the frame. We only optimize native calls // when we have the values. auto const opt = optimizedFCallBuiltin(env, callee, params); if (opt) return opt; env.irb->exceptionStackBoundary(); } // Collect the realized parameters. auto realized = realize_params(env, callee, params); // Store the inout parameters into their out locations. if (callee->takesInOutParams()) { int32_t idx = 0; uint32_t aoff = params.ctx ? 3 : 2; for (auto i = uint32_t{0}; i < params.size(); ++i) { if (!params[i].isInOut) continue; auto ty = [&] () -> Optional<Type> { auto const r = builtinOutType(callee, i); if (r.isKnownDataType()) return r; return {}; }(); if (auto const iv = builtinInValue(env, callee, i)) { decRef(env, realized[i + aoff], static_cast<DecRefProfileId>(i)); realized[i + aoff] = iv; ty = iv->type(); if (ty->maybe(TPersistentVec)) *ty |= TVec; if (ty->maybe(TPersistentDict)) *ty |= TDict; if (ty->maybe(TPersistentKeyset)) *ty |= TKeyset; if (ty->maybe(TPersistentStr)) *ty |= TStr; } // Move the value to the caller stack to avoid an extra ref-count gen(env, StLoc, LocalId{i}, fp(env), cns(env, TInitNull)); auto const addr = ldOutAddr(env, idx++); gen(env, StMem, ty, addr, realized[i + aoff]); realized[i + aoff] = addr; } env.irb->exceptionStackBoundary(); } // Make the actual call. SSATmp** const decayedPtr = &realized[0]; auto const ret = gen( env, CallBuiltin, CallBuiltinData { spOffBCFromIRSP(env), callee, }, std::make_pair(realized.size(), decayedPtr) ); return ret; } ////////////////////////////////////////////////////////////////////// } ////////////////////////////////////////////////////////////////////// const StaticString s_isObjectMethCaller("is_object() called on MethCaller"); SSATmp* optimizedCallIsObject(IRGS& env, SSATmp* src) { auto notice = [&] (StringData* sd) { gen(env, RaiseNotice, cns(env, sd)); }; if (src->isA(TObj) && src->type().clsSpec()) { auto const cls = src->type().clsSpec().cls(); if (!env.irb->constrainValue(src, GuardConstraint(cls).setWeak())) { if (RO::EvalNoticeOnMethCallerHelperIsObject) { if (cls == SystemLib::s_MethCallerHelperClass) { notice(s_isObjectMethCaller.get()); } } // If we know the class without having to specialize a guard // any further, use it. return cns(env, cls != SystemLib::s___PHP_Incomplete_ClassClass); } } if (!src->type().maybe(TObj)) { return cns(env, false); } auto checkClass = [&] (SSATmp* obj) { auto cls = gen(env, LdObjClass, obj); if (RO::EvalNoticeOnMethCallerHelperIsObject) { auto const c = SystemLib::s_MethCallerHelperClass; ifThen( env, [&] (Block* taken) { gen(env, JmpNZero, taken, gen(env, EqCls, cls, cns(env, c))); }, [&] { notice(s_isObjectMethCaller.get()); } ); } auto testCls = SystemLib::s___PHP_Incomplete_ClassClass; auto eq = gen(env, EqCls, cls, cns(env, testCls)); return gen(env, XorBool, eq, cns(env, true)); }; return cond( env, [&] (Block* taken) { auto isObj = gen(env, IsType, TObj, src); gen(env, JmpZero, taken, isObj); }, [&] { // Next: src is an object auto obj = gen(env, AssertType, TObj, src); return checkClass(obj); }, [&] { // Taken: src is not an object return cns(env, false); } ); } ////////////////////////////////////////////////////////////////////// void emitNativeImpl(IRGS& env) { auto const callee = curFunc(env); auto genericNativeImpl = [&]() { gen(env, NativeImpl, fp(env), sp(env)); auto const retVal = gen(env, LdRetVal, callReturnType(callee), fp(env)); auto const spAdjust = offsetToReturnSlot(env); auto const data = RetCtrlData { spAdjust, false, AuxUnion{0} }; gen(env, RetCtrl, data, sp(env), fp(env), retVal); }; if (!callee->nativeFuncPtr()) { genericNativeImpl(); return; } auto ctx = callee->isMethod() ? ldCtx(env) : nullptr; auto params = prepare_params( env, callee, ctx, callee->numParams() ); auto const ret = builtinCall(env, callee, params); push(env, ret); emitRetC(env); } ////////////////////////////////////////////////////////////////////// namespace { const StaticString s_add("add"); const StaticString s_addall("addall"); const StaticString s_append("append"); const StaticString s_clear("clear"); const StaticString s_remove("remove"); const StaticString s_removeall("removeall"); const StaticString s_removekey("removekey"); const StaticString s_set("set"); const StaticString s_setall("setall"); // Whitelist of known collection methods that always return $this (ignoring // parameter coercion failure issues). bool collectionMethodReturnsThis(const Func* callee) { auto const cls = callee->implCls(); if (cls == c_Vector::classof()) { return callee->name()->isame(s_add.get()) || callee->name()->isame(s_addall.get()) || callee->name()->isame(s_append.get()) || callee->name()->isame(s_clear.get()) || callee->name()->isame(s_removekey.get()) || callee->name()->isame(s_set.get()) || callee->name()->isame(s_setall.get()); } if (cls == c_Map::classof()) { return callee->name()->isame(s_add.get()) || callee->name()->isame(s_addall.get()) || callee->name()->isame(s_clear.get()) || callee->name()->isame(s_remove.get()) || callee->name()->isame(s_set.get()) || callee->name()->isame(s_setall.get()); } if (cls == c_Set::classof()) { return callee->name()->isame(s_add.get()) || callee->name()->isame(s_addall.get()) || callee->name()->isame(s_clear.get()) || callee->name()->isame(s_remove.get()) || callee->name()->isame(s_removeall.get()); } return false; } } Type builtinOutType(const Func* builtin, uint32_t i) { assertx(builtin->isCPPBuiltin()); assertx(builtin->isInOut(i)); auto const& pinfo = builtin->params()[i]; auto const& tc = pinfo.typeConstraint; if (auto const dt = Native::builtinOutType(tc, pinfo.userAttributes)) { const auto ty = Type{*dt}; return pinfo.typeConstraint.isNullable() ? TInitNull | ty : ty; } if (tc.isSoft() || tc.isMixed()) return TInitCell; auto ty = [&] () -> Type { switch (tc.metaType()) { case AnnotMetaType::Precise: return Type{*tc.underlyingDataType()}; case AnnotMetaType::Mixed: return TInitCell; case AnnotMetaType::Callable: return TInitCell; case AnnotMetaType::Number: return TInt | TDbl; case AnnotMetaType::ArrayKey: return TInt | TStr; case AnnotMetaType::This: return TObj; case AnnotMetaType::VecOrDict: return TVec|TDict; case AnnotMetaType::ArrayLike: return TArrLike; case AnnotMetaType::Classname: assertx(RO::EvalClassPassesClassname); return TStr | TCls | TLazyCls; case AnnotMetaType::Nonnull: case AnnotMetaType::NoReturn: case AnnotMetaType::Nothing: return TInitCell; } not_reached(); }(); return tc.isNullable() ? ty | TInitNull : ty; } Type builtinReturnType(const Func* builtin) { // Why do we recalculate the type here than just using HHBBC's inferred type? // Unlike for regular PHP functions, we have access to all the same // information that HHBBC does, and the JIT type-system is slightly more // expressive. So, by doing it ourself, we can derive a slightly more precise // type. assertx(builtin->isCPPBuiltin()); // NB: It is *not* safe to be pessimistic here and return TCell (or any other // approximation). The builtin's return type inferred here is used to control // code-gen when lowering the builtin call to vasm and must be no more general // than the HNI declaration (if present). auto type = [&]{ // If this is a collection method which returns $this, use that fact to // infer the exact returning type. Otherwise try to use HNI declaration. if (collectionMethodReturnsThis(builtin)) { assertx(builtin->hniReturnType() == KindOfObject); return Type::ExactObj(builtin->implCls()); } if (auto const hniType = builtin->hniReturnType()) { return Type{*hniType}; } return TInitCell; }(); // Allow builtins to return bespoke array likes if the flag is set. assertx(!type.arrSpec().vanilla()); if (!allowBespokeArrayLikes()) type = type.narrowToVanilla(); // "Reference" types (types represented by a pointer) can always be null. if (type.isReferenceType()) { type |= TInitNull; } else { assertx(type == TInitCell || type.isSimpleType()); } return type & TInitCell; } ///////////////////////////////////////////////////////////////////// namespace { void implVecIdx(IRGS& env, SSATmp* loaded_collection_vec) { auto const def = popC(env); auto const key = popC(env); auto const stack_base = popC(env); auto const finish = [&](SSATmp* elem) { pushIncRef(env, elem); decRef(env, def, DecRefProfileId::IdxDef); decRef(env, key, DecRefProfileId::IdxKey); decRef(env, stack_base, DecRefProfileId::IdxBase); }; if (key->isA(TNull | TStr)) return finish(def); if (!key->isA(TInt)) { // TODO(T11019533): Fix the underlying issue with unreachable code rather // than papering over it by pushing an unused value here. finish(def); updateMarker(env); env.irb->exceptionStackBoundary(); gen(env, ThrowInvalidArrayKey, stack_base, key); return; } auto const use_base = loaded_collection_vec ? loaded_collection_vec : stack_base; assertx(use_base->isA(TVec)); auto const elem = cond( env, [&] (Block* taken) { gen(env, CheckVecBounds, taken, use_base, key); }, [&] { return gen(env, LdVecElem, use_base, key); }, [&] { return def; } ); auto const pelem = profiledType(env, elem, [&] { finish(elem); } ); finish(pelem); } void implDictKeysetIdx(IRGS& env, bool is_dict, SSATmp* loaded_collection_dict) { auto const def = topC(env, BCSPRelOffset{0}); auto const origKey = topC(env, BCSPRelOffset{1}); if (!origKey->type().isKnownDataType()) PUNT(Idx-KeyNotKnown); auto const key = convertClassKey(env, origKey); auto const stack_base = topC(env, BCSPRelOffset{2}); auto const finish = [&](SSATmp* elem) { popC(env); popC(env); popC(env); pushIncRef(env, elem); decRef(env, def, DecRefProfileId::IdxDef); decRef(env, key, DecRefProfileId::IdxKey); decRef(env, stack_base, DecRefProfileId::IdxBase); }; if (key->isA(TNull)) return finish(def); if (!key->isA(TInt) && !key->isA(TStr)) { // TODO(T11019533): Fix the underlying issue with unreachable code rather // than papering over it by pushing an unused value here. finish(def); updateMarker(env); env.irb->exceptionStackBoundary(); gen(env, ThrowInvalidArrayKey, stack_base, key); return; } assertx(is_dict || !loaded_collection_dict); auto const use_base = loaded_collection_dict ? loaded_collection_dict : stack_base; assertx(use_base->isA(is_dict ? TDict : TKeyset)); auto const elem = profiledArrayAccess( env, use_base, key, MOpMode::None, [&] (SSATmp* base, SSATmp* key, SSATmp* pos) { return gen(env, is_dict ? DictGetK : KeysetGetK, base, key, pos); }, [&] (SSATmp*) { return def; }, [&] (SSATmp* key, SizeHintData data) { return is_dict ? gen(env, DictIdx, data, use_base, key, def) : gen(env, KeysetIdx, use_base, key, def); }, finish ); auto const pelem = profiledType(env, elem, [&] { finish(elem); }); finish(pelem); } /* * Return the GuardConstraint that should be used to constrain baseType for an * Idx bytecode. */ GuardConstraint idxBaseConstraint(Type baseType, Type keyType, bool& useVec, bool& useDict) { if (baseType < TObj && baseType.clsSpec()) { auto const cls = baseType.clsSpec().cls(); // Vector is only usable with int keys, so we can only optimize for // Vector if the key is an Int useVec = (collections::isType(cls, CollectionType::Vector) || collections::isType(cls, CollectionType::ImmVector)) && keyType <= TInt; useDict = collections::isType(cls, CollectionType::Map) || collections::isType(cls, CollectionType::ImmMap) || collections::isType(cls, CollectionType::Set) || collections::isType(cls, CollectionType::ImmSet); if (useVec || useDict) return GuardConstraint(cls); } useVec = useDict = false; return DataTypeSpecific; } ////////////////////////////////////////////////////////////////////// } namespace { void implArrayMarkLegacy(IRGS& env, bool legacy) { auto const recursive = topC(env, BCSPRelOffset{0}); auto const value = topC(env, BCSPRelOffset{1}); if (!recursive->isA(TBool)) { PUNT(ArrayMarkLegacy-RecursiveMustBeBool); } else if (!value->type().isKnownDataType()) { PUNT(ArrayMarkLegacy-ValueMustBeKnown); } if (!value->isA(TVec|TDict)) { discard(env); return; } auto const result = cond( env, [&](Block* taken) { gen(env, JmpZero, taken, recursive); }, [&]{ auto const op = legacy ? ArrayMarkLegacyRecursive : ArrayUnmarkLegacyRecursive; return gen(env, op, value); }, [&]{ auto const op = legacy ? ArrayMarkLegacyShallow : ArrayUnmarkLegacyShallow; return gen(env, op, value); } ); discard(env, 2); push(env, result); } } void emitArrayMarkLegacy(IRGS& env) { implArrayMarkLegacy(env, true); } void emitArrayUnmarkLegacy(IRGS& env) { implArrayMarkLegacy(env, false); } void emitArrayIdx(IRGS& env) { auto const arrType = topC(env, BCSPRelOffset{2}, DataTypeGeneric)->type(); if (arrType <= TVec) return implVecIdx(env, nullptr); if (arrType <= TDict) return implDictKeysetIdx(env, true, nullptr); if (arrType <= TKeyset) return implDictKeysetIdx(env, false, nullptr); if (arrType <= TClsMeth) PUNT(ArrayIdx_clsmeth); interpOne(env, TCell, 3); } void emitIdx(IRGS& env) { auto const key = topC(env, BCSPRelOffset{1}, DataTypeGeneric); auto const base = topC(env, BCSPRelOffset{2}, DataTypeGeneric); auto const keyType = key->type(); auto const baseType = base->type(); if (baseType <= TVec) return implVecIdx(env, nullptr); if (baseType <= TDict) return implDictKeysetIdx(env, true, nullptr); if (baseType <= TKeyset) return implDictKeysetIdx(env, false, nullptr); if (keyType <= TNull || !baseType.maybe(TArrLike | TObj | TStr)) { auto const def = popC(env, DataTypeGeneric); popC(env, keyType <= TNull ? DataTypeSpecific : DataTypeGeneric); popC(env, keyType <= TNull ? DataTypeGeneric : DataTypeSpecific); push(env, def); decRef(env, base, DecRefProfileId::IdxDef); decRef(env, key, DecRefProfileId::IdxKey); return; } if (!(keyType <= TInt || keyType <= TStr)) { interpOne(env, TCell, 3); return; } bool useVec, useDict; auto const tc = idxBaseConstraint(baseType, keyType, useVec, useDict); if (useVec || useDict) { env.irb->constrainValue(base, tc); env.irb->constrainValue(key, DataTypeSpecific); if (useVec) { auto const vec = gen(env, LdColVec, base); implVecIdx(env, vec); } else { auto const dict = gen(env, LdColDict, base); implDictKeysetIdx(env, true, dict); } return; } interpOne(env, TCell, 3); } void emitAKExists(IRGS& env) { auto const arr = topC(env); auto const origKey = topC(env, BCSPRelOffset{ 1 }); if (!origKey->type().isKnownDataType()) PUNT(AKExists-KeyNotKnown); auto const key = convertClassKey(env, origKey); if (key->isA(TFunc)) PUNT(AKExists_func_key); if (!arr->type().subtypeOfAny(TVec, TDict, TKeyset, TObj)) { PUNT(AKExists_unknown_array_or_obj_type); } auto const throwBadKey = [&] { // TODO(T11019533): Fix the underlying issue with unreachable code rather // than papering over it by pushing an unused value here. discard(env, 2); push(env, cns(env, false)); decRef(env, arr, DecRefProfileId::AKExistsArr); decRef(env, key, DecRefProfileId::AKExistsKey); updateMarker(env); env.irb->exceptionStackBoundary(); gen(env, ThrowInvalidArrayKey, arr, key); }; if (arr->isA(TVec)) { if (key->isA(TStr)) { discard(env, 2); push(env, cns(env, false)); decRef(env, arr, DecRefProfileId::AKExistsArr); decRef(env, key, DecRefProfileId::AKExistsKey); return; } if (key->isA(TInt)) { auto const result = cond( env, [&](Block* taken) { gen(env, CheckVecBounds, taken, arr, key); }, [&] { return cns(env, true); }, [&] { return cns(env, false); } ); discard(env, 2); push(env, result); decRef(env, arr, DecRefProfileId::AKExistsArr); return; } return throwBadKey(); } if (arr->type().subtypeOfAny(TDict, TKeyset)) { if (!key->type().subtypeOfAny(TInt, TStr)) { return throwBadKey(); } auto const finish = [&] (SSATmp* present) { discard(env, 2); push(env, present); decRef(env, arr, DecRefProfileId::AKExistsArr); decRef(env, key, DecRefProfileId::AKExistsKey); }; auto const present = profiledArrayAccess( env, arr, key, MOpMode::None, [&] (SSATmp*, SSATmp*, SSATmp*) { return cns(env, true); }, [&] (SSATmp*) { return cns(env, false); }, [&] (SSATmp* key, SizeHintData) { auto const op = arr->isA(TKeyset) ? AKExistsKeyset : AKExistsDict; return gen(env, op, arr, key); }, finish ); finish(present); return; } if (!key->isA(TStr) && !key->isA(TInt)) PUNT(AKExists_badKey); if (arr->isA(TObj) && key->isA(TInt) && collections::isType(arr->type().clsSpec().cls(), CollectionType::Vector, CollectionType::ImmVector)) { auto const val = gen(env, CheckRange, key, gen(env, CountCollection, arr)); discard(env, 2); push(env, val); decRef(env, arr, DecRefProfileId::AKExistsArr); return; } auto const val = gen(env, AKExistsObj, arr, key); discard(env, 2); push(env, val); decRef(env, arr, DecRefProfileId::AKExistsArr); decRef(env, key, DecRefProfileId::AKExistsKey); } ////////////////////////////////////////////////////////////////////// void emitGetMemoKeyL(IRGS& env, NamedLocal loc) { DEBUG_ONLY auto const func = curFunc(env); assertx(func->isMemoizeWrapper()); auto const value = ldLocWarn(env, loc, DataTypeSpecific); // Use the generic scheme, which is implemented by GetMemoKey. The simplifier // will catch any additional special cases. push(env, gen(env, GetMemoKey, value)); } namespace { void memoGetImpl(IRGS& env, Offset notfoundOff, Offset suspendedOff, LocalRange keys) { assertx(curFunc(env)->isMemoizeWrapper()); assertx(keys.first + keys.count <= curFunc(env)->numLocals()); assertx(suspendedOff == kInvalidOffset || curFunc(env)->isAsyncFunction()); CompactVector<bool> types; for (auto i = keys.count; i > 0; --i) { auto const type = env.irb->local(keys.first + i - 1, DataTypeSpecific).type; if (type <= TStr) { types.emplace_back(true); } else if (type <= TInt) { types.emplace_back(false); } else { // Let it fatal from the interpreter PUNT(MemoGet); } } auto const notFound = getBlock(env, bcOff(env) + notfoundOff); assertx(notFound != nullptr); auto const func = curFunc(env); auto const loadAux = suspendedOff != kInvalidOffset; auto const val = [&]{ // Any value we get from memoization must be the same type we return from // this function. If we need to load the aux field, force the type to be // InitCell so that we actually load the type. We'll assert the proper type // once we've checked aux. auto const retTy = loadAux ? TInitCell : typeFromRAT(func->repoReturnType(), curClass(env)) & TInitCell; if (func->isMethod() && !func->isStatic()) { auto const cls = func->cls(); assertx(cls != nullptr); assertx(cls->hasMemoSlots()); auto const this_ = checkAndLoadThis(env); if (!this_->isA(Type::SubObj(cls))) PUNT(MemoGet); auto const memoInfo = cls->memoSlotForFunc(func->getFuncId()); if (keys.count == 0 && !memoInfo.second) { return gen( env, MemoGetInstanceValue, MemoValueInstanceData { memoInfo.first, func, std::nullopt, loadAux }, notFound, retTy, this_ ); } return gen( env, MemoGetInstanceCache, MemoCacheInstanceData { memoInfo.first, keys, types.data(), func, memoInfo.second, std::nullopt, loadAux }, notFound, retTy, fp(env), this_ ); } if (func->isMemoizeWrapperLSB()) { /* For LSB memoization, we need the LSB class */ auto const lsbCls = ldCtxCls(env); if (keys.count > 0) { return gen( env, MemoGetLSBCache, MemoCacheStaticData { func, keys, types.data(), std::nullopt, loadAux }, notFound, retTy, fp(env), lsbCls ); } return gen( env, MemoGetLSBValue, MemoValueStaticData { func, std::nullopt, loadAux }, notFound, retTy, lsbCls ); } /* Static (non-LSB) Memoization */ if (keys.count > 0) { return gen( env, MemoGetStaticCache, MemoCacheStaticData { func, keys, types.data(), std::nullopt, loadAux }, notFound, retTy, fp(env) ); } return gen( env, MemoGetStaticValue, MemoValueStaticData { func, std::nullopt, loadAux }, notFound, retTy ); }(); if (!loadAux) { pushIncRef(env, val); return; } ifThenElse( env, [&] (Block* taken) { auto const aux = gen(env, LdTVAux, LdTVAuxData {}, val); auto const tst = gen(env, AndInt, aux, cns(env, 1u << 31)); gen(env, JmpZero, taken, tst); }, [&] { pushIncRef( env, gen( env, AssertType, typeFromRAT(func->repoAwaitedReturnType(), curClass(env)) & TInitCell, val ) ); }, [&] { hint(env, Block::Hint::Unlikely); pushIncRef( env, gen( env, AssertType, typeFromRAT(func->repoReturnType(), curClass(env)) & TInitCell, val ) ); jmpImpl(env, bcOff(env) + suspendedOff); } ); } } void emitMemoGet(IRGS& env, Offset notfoundOff, LocalRange keys) { memoGetImpl(env, notfoundOff, kInvalidOffset, keys); } void emitMemoGetEager(IRGS& env, Offset notfoundOff, Offset suspendedOff, LocalRange keys) { assertx(curFunc(env)->isAsyncFunction()); assertx(resumeMode(env) == ResumeMode::None); memoGetImpl(env, notfoundOff, suspendedOff, keys); } namespace { void memoSetImpl(IRGS& env, LocalRange keys, bool eager) { assertx(curFunc(env)->isMemoizeWrapper()); assertx(keys.first + keys.count <= curFunc(env)->numLocals()); assertx(!eager || curFunc(env)->isAsyncFunction()); CompactVector<bool> types; for (auto i = keys.count; i > 0; --i) { auto const type = env.irb->local(keys.first + i - 1, DataTypeSpecific).type; if (type <= TStr) { types.emplace_back(true); } else if (type <= TInt) { types.emplace_back(false); } else { // Let it fatal from the interpreter PUNT(MemoSet); } } auto const ldVal = [&] (DataTypeCategory tc) { return gen( env, AssertType, TInitCell, topC(env, BCSPRelOffset{ 0 }, tc) ); }; auto const func = curFunc(env); auto const asyncEager = [&] () -> Optional<bool> { if (!func->isAsyncFunction()) return std::nullopt; return eager; }(); if (func->isMethod() && !func->isStatic()) { auto const cls = func->cls(); assertx(cls != nullptr); assertx(cls->hasMemoSlots()); auto const this_ = checkAndLoadThis(env); if (!this_->isA(Type::SubObj(cls))) PUNT(MemoSet); auto const memoInfo = cls->memoSlotForFunc(func->getFuncId()); if (keys.count == 0 && !memoInfo.second) { gen( env, MemoSetInstanceValue, MemoValueInstanceData { memoInfo.first, func, asyncEager, false }, this_, ldVal(DataTypeGeneric) ); return; } gen( env, MemoSetInstanceCache, MemoCacheInstanceData { memoInfo.first, keys, types.data(), func, memoInfo.second, asyncEager, false }, fp(env), this_, ldVal(DataTypeGeneric) ); return; } if (func->isMemoizeWrapperLSB()) { /* For LSB memoization, we need the LSB class */ auto const lsbCls = ldCtxCls(env); if (keys.count > 0) { gen( env, MemoSetLSBCache, MemoCacheStaticData { func, keys, types.data(), asyncEager, false }, fp(env), lsbCls, ldVal(DataTypeGeneric) ); return; } gen( env, MemoSetLSBValue, MemoValueStaticData { func, asyncEager, false }, ldVal(DataTypeGeneric), lsbCls ); return; } if (keys.count > 0) { gen( env, MemoSetStaticCache, MemoCacheStaticData { func, keys, types.data(), asyncEager, false }, fp(env), ldVal(DataTypeGeneric) ); return; } gen( env, MemoSetStaticValue, MemoValueStaticData { func, asyncEager, false }, ldVal(DataTypeGeneric) ); } } void emitMemoSet(IRGS& env, LocalRange keys) { memoSetImpl(env, keys, false); } void emitMemoSetEager(IRGS& env, LocalRange keys) { assertx(curFunc(env)->isAsyncFunction()); assertx(resumeMode(env) == ResumeMode::None); memoSetImpl(env, keys, true); } ////////////////////////////////////////////////////////////////////// void emitSilence(IRGS& env, Id localId, SilenceOp subop) { switch (subop) { case SilenceOp::Start: // We assume that whatever is in the local is dead and doesn't need to be // refcounted before being overwritten. gen(env, AssertLoc, TUncounted, LocalId(localId), fp(env)); gen(env, StLoc, LocalId(localId), fp(env), gen(env, ZeroErrorLevel)); break; case SilenceOp::End: { gen(env, AssertLoc, TInt, LocalId(localId), fp(env)); auto const level = ldLoc(env, localId, DataTypeGeneric); gen(env, RestoreErrorLevel, level); } break; } } void emitSetImplicitContextByValue(IRGS& env) { if (!RO::EvalEnableImplicitContext) { popDecRef(env, DecRefProfileId::Default); push(env, cns(env, make_tv<KindOfNull>())); return; } auto const tv = topC(env); auto const prev_ctx = cond( env, [&] (Block* taken) { return gen(env, CheckType, TInitNull, taken, tv); }, [&] (SSATmp* null) { auto const prev = gen(env, LdImplicitContext); gen(env, StImplicitContext, null); return prev; }, [&] (Block* taken) { return gen(env, CheckType, TObj, taken, tv); }, [&] (SSATmp* obj) { auto const prev = gen(env, LdImplicitContext); gen(env, StImplicitContext, obj); return prev; }, [&] { hint(env, Block::Hint::Unlikely); interpOne(env); return cns(env, TBottom); } ); popC(env); push(env, prev_ctx); } ////////////////////////////////////////////////////////////////////// }