| /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- |
| * vim: set ts=8 sts=4 et sw=4 tw=99: |
| * This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
| |
| #include "jit/IonBuilder.h" |
| |
| #include "mozilla/DebugOnly.h" |
| #include "mozilla/SizePrintfMacros.h" |
| |
| #include "builtin/Eval.h" |
| #include "builtin/TypedObject.h" |
| #include "frontend/SourceNotes.h" |
| #include "jit/BaselineFrame.h" |
| #include "jit/BaselineInspector.h" |
| #include "jit/Ion.h" |
| #include "jit/IonOptimizationLevels.h" |
| #include "jit/JitSpewer.h" |
| #include "jit/Lowering.h" |
| #include "jit/MIRGraph.h" |
| #include "vm/ArgumentsObject.h" |
| #include "vm/Opcodes.h" |
| #include "vm/RegExpStatics.h" |
| #include "vm/TraceLogging.h" |
| |
| #include "jsopcodeinlines.h" |
| #include "jsscriptinlines.h" |
| |
| #include "jit/CompileInfo-inl.h" |
| #include "jit/shared/Lowering-shared-inl.h" |
| #include "vm/NativeObject-inl.h" |
| #include "vm/ObjectGroup-inl.h" |
| #include "vm/ScopeObject-inl.h" |
| #include "vm/UnboxedObject-inl.h" |
| |
| using namespace js; |
| using namespace js::jit; |
| |
| using mozilla::AssertedCast; |
| using mozilla::DebugOnly; |
| using mozilla::Maybe; |
| |
| using JS::TrackedStrategy; |
| using JS::TrackedOutcome; |
| using JS::TrackedTypeSite; |
| |
| class jit::BaselineFrameInspector |
| { |
| public: |
| TypeSet::Type thisType; |
| JSObject* singletonScopeChain; |
| |
| Vector<TypeSet::Type, 4, JitAllocPolicy> argTypes; |
| Vector<TypeSet::Type, 4, JitAllocPolicy> varTypes; |
| |
| explicit BaselineFrameInspector(TempAllocator* temp) |
| : thisType(TypeSet::UndefinedType()), |
| singletonScopeChain(nullptr), |
| argTypes(*temp), |
| varTypes(*temp) |
| {} |
| }; |
| |
| BaselineFrameInspector* |
| jit::NewBaselineFrameInspector(TempAllocator* temp, BaselineFrame* frame, CompileInfo* info) |
| { |
| MOZ_ASSERT(frame); |
| |
| BaselineFrameInspector* inspector = temp->lifoAlloc()->new_<BaselineFrameInspector>(temp); |
| if (!inspector) |
| return nullptr; |
| |
| // Note: copying the actual values into a temporary structure for use |
| // during compilation could capture nursery pointers, so the values' types |
| // are recorded instead. |
| |
| if (frame->isNonEvalFunctionFrame()) |
| inspector->thisType = TypeSet::GetMaybeUntrackedValueType(frame->thisArgument()); |
| |
| if (frame->scopeChain()->isSingleton()) |
| inspector->singletonScopeChain = frame->scopeChain(); |
| |
| JSScript* script = frame->script(); |
| |
| if (script->functionNonDelazifying()) { |
| if (!inspector->argTypes.reserve(frame->numFormalArgs())) |
| return nullptr; |
| for (size_t i = 0; i < frame->numFormalArgs(); i++) { |
| if (script->formalIsAliased(i)) { |
| inspector->argTypes.infallibleAppend(TypeSet::UndefinedType()); |
| } else if (!script->argsObjAliasesFormals()) { |
| TypeSet::Type type = |
| TypeSet::GetMaybeUntrackedValueType(frame->unaliasedFormal(i)); |
| inspector->argTypes.infallibleAppend(type); |
| } else if (frame->hasArgsObj()) { |
| TypeSet::Type type = |
| TypeSet::GetMaybeUntrackedValueType(frame->argsObj().arg(i)); |
| inspector->argTypes.infallibleAppend(type); |
| } else { |
| inspector->argTypes.infallibleAppend(TypeSet::UndefinedType()); |
| } |
| } |
| } |
| |
| if (!inspector->varTypes.reserve(frame->script()->nfixed())) |
| return nullptr; |
| for (size_t i = 0; i < frame->script()->nfixed(); i++) { |
| if (info->isSlotAliasedAtOsr(i + info->firstLocalSlot())) { |
| inspector->varTypes.infallibleAppend(TypeSet::UndefinedType()); |
| } else { |
| TypeSet::Type type = TypeSet::GetMaybeUntrackedValueType(frame->unaliasedLocal(i)); |
| inspector->varTypes.infallibleAppend(type); |
| } |
| } |
| |
| return inspector; |
| } |
| |
| IonBuilder::IonBuilder(JSContext* analysisContext, CompileCompartment* comp, |
| const JitCompileOptions& options, TempAllocator* temp, |
| MIRGraph* graph, CompilerConstraintList* constraints, |
| BaselineInspector* inspector, CompileInfo* info, |
| const OptimizationInfo* optimizationInfo, |
| BaselineFrameInspector* baselineFrame, size_t inliningDepth, |
| uint32_t loopDepth) |
| : MIRGenerator(comp, options, temp, graph, info, optimizationInfo), |
| backgroundCodegen_(nullptr), |
| actionableAbortScript_(nullptr), |
| actionableAbortPc_(nullptr), |
| actionableAbortMessage_(nullptr), |
| analysisContext(analysisContext), |
| baselineFrame_(baselineFrame), |
| constraints_(constraints), |
| analysis_(*temp, info->script()), |
| thisTypes(nullptr), |
| argTypes(nullptr), |
| typeArray(nullptr), |
| typeArrayHint(0), |
| bytecodeTypeMap(nullptr), |
| loopDepth_(loopDepth), |
| trackedOptimizationSites_(*temp), |
| lexicalCheck_(nullptr), |
| callerResumePoint_(nullptr), |
| callerBuilder_(nullptr), |
| cfgStack_(*temp), |
| loops_(*temp), |
| switches_(*temp), |
| labels_(*temp), |
| iterators_(*temp), |
| loopHeaders_(*temp), |
| inspector(inspector), |
| inliningDepth_(inliningDepth), |
| inlinedBytecodeLength_(0), |
| numLoopRestarts_(0), |
| failedBoundsCheck_(info->script()->failedBoundsCheck()), |
| failedShapeGuard_(info->script()->failedShapeGuard()), |
| failedLexicalCheck_(info->script()->failedLexicalCheck()), |
| nonStringIteration_(false), |
| lazyArguments_(nullptr), |
| inlineCallInfo_(nullptr), |
| maybeFallbackFunctionGetter_(nullptr) |
| { |
| script_ = info->script(); |
| scriptHasIonScript_ = script_->hasIonScript(); |
| pc = info->startPC(); |
| abortReason_ = AbortReason_Disable; |
| |
| MOZ_ASSERT(script()->hasBaselineScript() == (info->analysisMode() != Analysis_ArgumentsUsage)); |
| MOZ_ASSERT(!!analysisContext == (info->analysisMode() == Analysis_DefiniteProperties)); |
| |
| if (!info->isAnalysis()) |
| script()->baselineScript()->setIonCompiledOrInlined(); |
| } |
| |
| void |
| IonBuilder::clearForBackEnd() |
| { |
| MOZ_ASSERT(!analysisContext); |
| baselineFrame_ = nullptr; |
| |
| // The caches below allocate data from the malloc heap. Release this before |
| // later phases of compilation to avoid leaks, as the top level IonBuilder |
| // is not explicitly destroyed. Note that builders for inner scripts are |
| // constructed on the stack and will release this memory on destruction. |
| gsn.purge(); |
| scopeCoordinateNameCache.purge(); |
| } |
| |
| bool |
| IonBuilder::abort(const char* message, ...) |
| { |
| // Don't call PCToLineNumber in release builds. |
| #ifdef JS_JITSPEW |
| va_list ap; |
| va_start(ap, message); |
| abortFmt(message, ap); |
| va_end(ap); |
| # ifdef DEBUG |
| JitSpew(JitSpew_IonAbort, "aborted @ %s:%d", script()->filename(), PCToLineNumber(script(), pc)); |
| # else |
| JitSpew(JitSpew_IonAbort, "aborted @ %s", script()->filename()); |
| # endif |
| #endif |
| trackActionableAbort(message); |
| return false; |
| } |
| |
| IonBuilder* |
| IonBuilder::outermostBuilder() |
| { |
| IonBuilder* builder = this; |
| while (builder->callerBuilder_) |
| builder = builder->callerBuilder_; |
| return builder; |
| } |
| |
| void |
| IonBuilder::trackActionableAbort(const char* message) |
| { |
| if (!isOptimizationTrackingEnabled()) |
| return; |
| |
| IonBuilder* topBuilder = outermostBuilder(); |
| if (topBuilder->hadActionableAbort()) |
| return; |
| |
| topBuilder->actionableAbortScript_ = script(); |
| topBuilder->actionableAbortPc_ = pc; |
| topBuilder->actionableAbortMessage_ = message; |
| } |
| |
| void |
| IonBuilder::spew(const char* message) |
| { |
| // Don't call PCToLineNumber in release builds. |
| #ifdef DEBUG |
| JitSpew(JitSpew_IonMIR, "%s @ %s:%d", message, script()->filename(), PCToLineNumber(script(), pc)); |
| #endif |
| } |
| |
| static inline int32_t |
| GetJumpOffset(jsbytecode* pc) |
| { |
| MOZ_ASSERT(CodeSpec[JSOp(*pc)].type() == JOF_JUMP); |
| return GET_JUMP_OFFSET(pc); |
| } |
| |
| IonBuilder::CFGState |
| IonBuilder::CFGState::If(jsbytecode* join, MTest* test) |
| { |
| CFGState state; |
| state.state = IF_TRUE; |
| state.stopAt = join; |
| state.branch.ifFalse = test->ifFalse(); |
| state.branch.test = test; |
| return state; |
| } |
| |
| IonBuilder::CFGState |
| IonBuilder::CFGState::IfElse(jsbytecode* trueEnd, jsbytecode* falseEnd, MTest* test) |
| { |
| MBasicBlock* ifFalse = test->ifFalse(); |
| |
| CFGState state; |
| // If the end of the false path is the same as the start of the |
| // false path, then the "else" block is empty and we can devolve |
| // this to the IF_TRUE case. We handle this here because there is |
| // still an extra GOTO on the true path and we want stopAt to point |
| // there, whereas the IF_TRUE case does not have the GOTO. |
| state.state = (falseEnd == ifFalse->pc()) |
| ? IF_TRUE_EMPTY_ELSE |
| : IF_ELSE_TRUE; |
| state.stopAt = trueEnd; |
| state.branch.falseEnd = falseEnd; |
| state.branch.ifFalse = ifFalse; |
| state.branch.test = test; |
| return state; |
| } |
| |
| IonBuilder::CFGState |
| IonBuilder::CFGState::AndOr(jsbytecode* join, MBasicBlock* lhs) |
| { |
| CFGState state; |
| state.state = AND_OR; |
| state.stopAt = join; |
| state.branch.ifFalse = lhs; |
| state.branch.test = nullptr; |
| return state; |
| } |
| |
| IonBuilder::CFGState |
| IonBuilder::CFGState::TableSwitch(jsbytecode* exitpc, MTableSwitch* ins) |
| { |
| CFGState state; |
| state.state = TABLE_SWITCH; |
| state.stopAt = exitpc; |
| state.tableswitch.exitpc = exitpc; |
| state.tableswitch.breaks = nullptr; |
| state.tableswitch.ins = ins; |
| state.tableswitch.currentBlock = 0; |
| return state; |
| } |
| |
| JSFunction* |
| IonBuilder::getSingleCallTarget(TemporaryTypeSet* calleeTypes) |
| { |
| if (!calleeTypes) |
| return nullptr; |
| |
| JSObject* obj = calleeTypes->maybeSingleton(); |
| if (!obj || !obj->is<JSFunction>()) |
| return nullptr; |
| |
| return &obj->as<JSFunction>(); |
| } |
| |
| bool |
| IonBuilder::getPolyCallTargets(TemporaryTypeSet* calleeTypes, bool constructing, |
| ObjectVector& targets, uint32_t maxTargets) |
| { |
| MOZ_ASSERT(targets.empty()); |
| |
| if (!calleeTypes) |
| return true; |
| |
| if (calleeTypes->baseFlags() != 0) |
| return true; |
| |
| unsigned objCount = calleeTypes->getObjectCount(); |
| |
| if (objCount == 0 || objCount > maxTargets) |
| return true; |
| |
| if (!targets.reserve(objCount)) |
| return false; |
| for (unsigned i = 0; i < objCount; i++) { |
| JSObject* obj = calleeTypes->getSingleton(i); |
| if (obj) { |
| MOZ_ASSERT(obj->isSingleton()); |
| } else { |
| ObjectGroup* group = calleeTypes->getGroup(i); |
| if (!group) |
| continue; |
| |
| obj = group->maybeInterpretedFunction(); |
| if (!obj) { |
| targets.clear(); |
| return true; |
| } |
| |
| MOZ_ASSERT(!obj->isSingleton()); |
| } |
| |
| // Don't optimize if the callee is not callable or constructable per |
| // the manner it is being invoked, so that CallKnown does not have to |
| // handle these cases (they will always throw). |
| if (constructing ? !obj->isConstructor() : !obj->isCallable()) { |
| targets.clear(); |
| return true; |
| } |
| |
| targets.infallibleAppend(obj); |
| } |
| |
| return true; |
| } |
| |
| IonBuilder::InliningDecision |
| IonBuilder::DontInline(JSScript* targetScript, const char* reason) |
| { |
| if (targetScript) { |
| JitSpew(JitSpew_Inlining, "Cannot inline %s:%" PRIuSIZE ": %s", |
| targetScript->filename(), targetScript->lineno(), reason); |
| } else { |
| JitSpew(JitSpew_Inlining, "Cannot inline: %s", reason); |
| } |
| |
| return InliningDecision_DontInline; |
| } |
| |
| /* |
| * |hasCommonInliningPath| determines whether the current inlining path has been |
| * seen before based on the sequence of scripts in the chain of |IonBuilder|s. |
| * |
| * An inlining path for a function |f| is the sequence of functions whose |
| * inlinings precede |f| up to any previous occurrences of |f|. |
| * So, if we have the chain of inlinings |
| * |
| * f1 -> f2 -> f -> f3 -> f4 -> f5 -> f |
| * -------- -------------- |
| * |
| * the inlining paths for |f| are [f2, f1] and [f5, f4, f3]. |
| * When attempting to inline |f|, we find all existing inlining paths for |f| |
| * and check whether they share a common prefix with the path created were |f| |
| * inlined. |
| * |
| * For example, given mutually recursive functions |f| and |g|, a possible |
| * inlining is |
| * |
| * +---- Inlining stopped here... |
| * | |
| * v |
| * a -> f -> g -> f \ -> g -> f -> g -> ... |
| * |
| * where the vertical bar denotes the termination of inlining. |
| * Inlining is terminated because we have already observed the inlining path |
| * [f] when inlining function |g|. Note that this will inline recursive |
| * functions such as |fib| only one level, as |fib| has a zero length inlining |
| * path which trivially prefixes all inlining paths. |
| * |
| */ |
| bool |
| IonBuilder::hasCommonInliningPath(const JSScript* scriptToInline) |
| { |
| // Find all previous inlinings of the |scriptToInline| and check for common |
| // inlining paths with the top of the inlining stack. |
| for (IonBuilder* it = this->callerBuilder_; it; it = it->callerBuilder_) { |
| if (it->script() != scriptToInline) |
| continue; |
| |
| // This only needs to check the top of each stack for a match, |
| // as a match of length one ensures a common prefix. |
| IonBuilder* path = it->callerBuilder_; |
| if (!path || this->script() == path->script()) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| IonBuilder::InliningDecision |
| IonBuilder::canInlineTarget(JSFunction* target, CallInfo& callInfo) |
| { |
| if (!optimizationInfo().inlineInterpreted()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineGeneric); |
| return InliningDecision_DontInline; |
| } |
| |
| if (TraceLogTextIdEnabled(TraceLogger_InlinedScripts)) { |
| return DontInline(nullptr, "Tracelogging of inlined scripts is enabled" |
| "but Tracelogger cannot do that yet."); |
| } |
| |
| if (!target->isInterpreted()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineNotInterpreted); |
| return DontInline(nullptr, "Non-interpreted target"); |
| } |
| |
| if (info().analysisMode() != Analysis_DefiniteProperties) { |
| // If |this| or an argument has an empty resultTypeSet, don't bother |
| // inlining, as the call is currently unreachable due to incomplete type |
| // information. This does not apply to the definite properties analysis, |
| // in that case we want to inline anyway. |
| |
| if (callInfo.thisArg()->emptyResultTypeSet()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineUnreachable); |
| return DontInline(nullptr, "Empty TypeSet for |this|"); |
| } |
| |
| for (size_t i = 0; i < callInfo.argc(); i++) { |
| if (callInfo.getArg(i)->emptyResultTypeSet()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineUnreachable); |
| return DontInline(nullptr, "Empty TypeSet for argument"); |
| } |
| } |
| } |
| |
| // Allow constructing lazy scripts when performing the definite properties |
| // analysis, as baseline has not been used to warm the caller up yet. |
| if (target->isInterpreted() && info().analysisMode() == Analysis_DefiniteProperties) { |
| RootedScript script(analysisContext, target->getOrCreateScript(analysisContext)); |
| if (!script) |
| return InliningDecision_Error; |
| |
| if (!script->hasBaselineScript() && script->canBaselineCompile()) { |
| MethodStatus status = BaselineCompile(analysisContext, script); |
| if (status == Method_Error) |
| return InliningDecision_Error; |
| if (status != Method_Compiled) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineNoBaseline); |
| return InliningDecision_DontInline; |
| } |
| } |
| } |
| |
| if (!target->hasScript()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineLazy); |
| return DontInline(nullptr, "Lazy script"); |
| } |
| |
| JSScript* inlineScript = target->nonLazyScript(); |
| if (callInfo.constructing() && !target->isConstructor()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineNotConstructor); |
| return DontInline(inlineScript, "Callee is not a constructor"); |
| } |
| |
| if (!callInfo.constructing() && target->isClassConstructor()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineClassConstructor); |
| return DontInline(inlineScript, "Not constructing class constructor"); |
| } |
| |
| AnalysisMode analysisMode = info().analysisMode(); |
| if (!CanIonCompile(inlineScript, analysisMode)) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineDisabledIon); |
| return DontInline(inlineScript, "Disabled Ion compilation"); |
| } |
| |
| // Don't inline functions which don't have baseline scripts. |
| if (!inlineScript->hasBaselineScript()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineNoBaseline); |
| return DontInline(inlineScript, "No baseline jitcode"); |
| } |
| |
| if (TooManyFormalArguments(target->nargs())) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineTooManyArgs); |
| return DontInline(inlineScript, "Too many args"); |
| } |
| |
| // We check the number of actual arguments against the maximum number of |
| // formal arguments as we do not want to encode all actual arguments in the |
| // callerResumePoint. |
| if (TooManyFormalArguments(callInfo.argc())) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineTooManyArgs); |
| return DontInline(inlineScript, "Too many actual args"); |
| } |
| |
| if (hasCommonInliningPath(inlineScript)) { |
| trackOptimizationOutcome(TrackedOutcome::HasCommonInliningPath); |
| return DontInline(inlineScript, "Common inlining path"); |
| } |
| |
| if (inlineScript->uninlineable()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineGeneric); |
| return DontInline(inlineScript, "Uninlineable script"); |
| } |
| |
| if (inlineScript->needsArgsObj()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineNeedsArgsObj); |
| return DontInline(inlineScript, "Script that needs an arguments object"); |
| } |
| |
| if (inlineScript->isDebuggee()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineDebuggee); |
| return DontInline(inlineScript, "Script is debuggee"); |
| } |
| |
| TypeSet::ObjectKey* targetKey = TypeSet::ObjectKey::get(target); |
| if (targetKey->unknownProperties()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineUnknownProps); |
| return DontInline(inlineScript, "Target type has unknown properties"); |
| } |
| |
| return InliningDecision_Inline; |
| } |
| |
| void |
| IonBuilder::popCfgStack() |
| { |
| if (cfgStack_.back().isLoop()) |
| loops_.popBack(); |
| if (cfgStack_.back().state == CFGState::LABEL) |
| labels_.popBack(); |
| cfgStack_.popBack(); |
| } |
| |
| bool |
| IonBuilder::analyzeNewLoopTypes(MBasicBlock* entry, jsbytecode* start, jsbytecode* end) |
| { |
| // The phi inputs at the loop head only reflect types for variables that |
| // were present at the start of the loop. If the variable changes to a new |
| // type within the loop body, and that type is carried around to the loop |
| // head, then we need to know about the new type up front. |
| // |
| // Since SSA information hasn't been constructed for the loop body yet, we |
| // need a separate analysis to pick out the types that might flow around |
| // the loop header. This is a best-effort analysis that may either over- |
| // or under-approximate the set of such types. |
| // |
| // Over-approximating the types may lead to inefficient generated code, and |
| // under-approximating the types will cause the loop body to be analyzed |
| // multiple times as the correct types are deduced (see finishLoop). |
| |
| // If we restarted processing of an outer loop then get loop header types |
| // directly from the last time we have previously processed this loop. This |
| // both avoids repeated work from the bytecode traverse below, and will |
| // also pick up types discovered while previously building the loop body. |
| for (size_t i = 0; i < loopHeaders_.length(); i++) { |
| if (loopHeaders_[i].pc == start) { |
| MBasicBlock* oldEntry = loopHeaders_[i].header; |
| |
| // If this block has been discarded, its resume points will have |
| // already discarded their operands. |
| if (!oldEntry->isDead()) { |
| MResumePoint* oldEntryRp = oldEntry->entryResumePoint(); |
| size_t stackDepth = oldEntryRp->stackDepth(); |
| for (size_t slot = 0; slot < stackDepth; slot++) { |
| MDefinition* oldDef = oldEntryRp->getOperand(slot); |
| if (!oldDef->isPhi()) { |
| MOZ_ASSERT(oldDef->block()->id() < oldEntry->id()); |
| MOZ_ASSERT(oldDef == entry->getSlot(slot)); |
| continue; |
| } |
| MPhi* oldPhi = oldDef->toPhi(); |
| MPhi* newPhi = entry->getSlot(slot)->toPhi(); |
| if (!newPhi->addBackedgeType(oldPhi->type(), oldPhi->resultTypeSet())) |
| return false; |
| } |
| } |
| |
| // Update the most recent header for this loop encountered, in case |
| // new types flow to the phis and the loop is processed at least |
| // three times. |
| loopHeaders_[i].header = entry; |
| return true; |
| } |
| } |
| if (!loopHeaders_.append(LoopHeader(start, entry))) |
| return false; |
| |
| jsbytecode* last = nullptr; |
| jsbytecode* earlier = nullptr; |
| for (jsbytecode* pc = start; pc != end; earlier = last, last = pc, pc += GetBytecodeLength(pc)) { |
| uint32_t slot; |
| if (*pc == JSOP_SETLOCAL) |
| slot = info().localSlot(GET_LOCALNO(pc)); |
| else if (*pc == JSOP_SETARG) |
| slot = info().argSlotUnchecked(GET_ARGNO(pc)); |
| else |
| continue; |
| if (slot >= info().firstStackSlot()) |
| continue; |
| if (!analysis().maybeInfo(pc)) |
| continue; |
| if (!last) |
| continue; |
| |
| MPhi* phi = entry->getSlot(slot)->toPhi(); |
| |
| if (*last == JSOP_POS) |
| last = earlier; |
| |
| if (CodeSpec[*last].format & JOF_TYPESET) { |
| TemporaryTypeSet* typeSet = bytecodeTypes(last); |
| if (!typeSet->empty()) { |
| MIRType type = typeSet->getKnownMIRType(); |
| if (!phi->addBackedgeType(type, typeSet)) |
| return false; |
| } |
| } else if (*last == JSOP_GETLOCAL || *last == JSOP_GETARG) { |
| uint32_t slot = (*last == JSOP_GETLOCAL) |
| ? info().localSlot(GET_LOCALNO(last)) |
| : info().argSlotUnchecked(GET_ARGNO(last)); |
| if (slot < info().firstStackSlot()) { |
| MPhi* otherPhi = entry->getSlot(slot)->toPhi(); |
| if (otherPhi->hasBackedgeType()) { |
| if (!phi->addBackedgeType(otherPhi->type(), otherPhi->resultTypeSet())) |
| return false; |
| } |
| } |
| } else { |
| MIRType type = MIRType_None; |
| switch (*last) { |
| case JSOP_VOID: |
| case JSOP_UNDEFINED: |
| type = MIRType_Undefined; |
| break; |
| case JSOP_GIMPLICITTHIS: |
| if (!script()->hasNonSyntacticScope()) |
| type = MIRType_Undefined; |
| break; |
| case JSOP_NULL: |
| type = MIRType_Null; |
| break; |
| case JSOP_ZERO: |
| case JSOP_ONE: |
| case JSOP_INT8: |
| case JSOP_INT32: |
| case JSOP_UINT16: |
| case JSOP_UINT24: |
| case JSOP_BITAND: |
| case JSOP_BITOR: |
| case JSOP_BITXOR: |
| case JSOP_BITNOT: |
| case JSOP_RSH: |
| case JSOP_LSH: |
| case JSOP_URSH: |
| type = MIRType_Int32; |
| break; |
| case JSOP_FALSE: |
| case JSOP_TRUE: |
| case JSOP_EQ: |
| case JSOP_NE: |
| case JSOP_LT: |
| case JSOP_LE: |
| case JSOP_GT: |
| case JSOP_GE: |
| case JSOP_NOT: |
| case JSOP_STRICTEQ: |
| case JSOP_STRICTNE: |
| case JSOP_IN: |
| case JSOP_INSTANCEOF: |
| type = MIRType_Boolean; |
| break; |
| case JSOP_DOUBLE: |
| type = MIRType_Double; |
| break; |
| case JSOP_STRING: |
| case JSOP_TOSTRING: |
| case JSOP_TYPEOF: |
| case JSOP_TYPEOFEXPR: |
| type = MIRType_String; |
| break; |
| case JSOP_SYMBOL: |
| type = MIRType_Symbol; |
| break; |
| case JSOP_ADD: |
| case JSOP_SUB: |
| case JSOP_MUL: |
| case JSOP_DIV: |
| case JSOP_MOD: |
| case JSOP_NEG: |
| type = inspector->expectedResultType(last); |
| default: |
| break; |
| } |
| if (type != MIRType_None) { |
| if (!phi->addBackedgeType(type, nullptr)) |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| bool |
| IonBuilder::pushLoop(CFGState::State initial, jsbytecode* stopAt, MBasicBlock* entry, bool osr, |
| jsbytecode* loopHead, jsbytecode* initialPc, |
| jsbytecode* bodyStart, jsbytecode* bodyEnd, |
| jsbytecode* exitpc, jsbytecode* continuepc) |
| { |
| ControlFlowInfo loop(cfgStack_.length(), continuepc); |
| if (!loops_.append(loop)) |
| return false; |
| |
| CFGState state; |
| state.state = initial; |
| state.stopAt = stopAt; |
| state.loop.bodyStart = bodyStart; |
| state.loop.bodyEnd = bodyEnd; |
| state.loop.exitpc = exitpc; |
| state.loop.continuepc = continuepc; |
| state.loop.entry = entry; |
| state.loop.osr = osr; |
| state.loop.successor = nullptr; |
| state.loop.breaks = nullptr; |
| state.loop.continues = nullptr; |
| state.loop.initialState = initial; |
| state.loop.initialPc = initialPc; |
| state.loop.initialStopAt = stopAt; |
| state.loop.loopHead = loopHead; |
| return cfgStack_.append(state); |
| } |
| |
| bool |
| IonBuilder::init() |
| { |
| if (!TypeScript::FreezeTypeSets(constraints(), script(), &thisTypes, &argTypes, &typeArray)) |
| return false; |
| |
| if (inlineCallInfo_) { |
| // If we're inlining, the actual this/argument types are not necessarily |
| // a subset of the script's observed types. |argTypes| is never accessed |
| // for inlined scripts, so we just null it. |
| thisTypes = inlineCallInfo_->thisArg()->resultTypeSet(); |
| argTypes = nullptr; |
| } |
| |
| if (!analysis().init(alloc(), gsn)) |
| return false; |
| |
| // The baseline script normally has the bytecode type map, but compute |
| // it ourselves if we do not have a baseline script. |
| if (script()->hasBaselineScript()) { |
| bytecodeTypeMap = script()->baselineScript()->bytecodeTypeMap(); |
| } else { |
| bytecodeTypeMap = alloc_->lifoAlloc()->newArrayUninitialized<uint32_t>(script()->nTypeSets()); |
| if (!bytecodeTypeMap) |
| return false; |
| FillBytecodeTypeMap(script(), bytecodeTypeMap); |
| } |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::build() |
| { |
| if (!init()) |
| return false; |
| |
| if (script()->hasBaselineScript()) |
| script()->baselineScript()->resetMaxInliningDepth(); |
| |
| if (!setCurrentAndSpecializePhis(newBlock(pc))) |
| return false; |
| if (!current) |
| return false; |
| |
| #ifdef JS_JITSPEW |
| if (info().isAnalysis()) { |
| JitSpew(JitSpew_IonScripts, "Analyzing script %s:%" PRIuSIZE " (%p) %s", |
| script()->filename(), script()->lineno(), (void*)script(), |
| AnalysisModeString(info().analysisMode())); |
| } else { |
| JitSpew(JitSpew_IonScripts, "%sompiling script %s:%" PRIuSIZE " (%p) (warmup-counter=%" PRIu32 ", level=%s)", |
| (script()->hasIonScript() ? "Rec" : "C"), |
| script()->filename(), script()->lineno(), (void*)script(), |
| script()->getWarmUpCount(), OptimizationLevelString(optimizationInfo().level())); |
| } |
| #endif |
| |
| initParameters(); |
| initLocals(); |
| |
| // Initialize something for the scope chain. We can bail out before the |
| // start instruction, but the snapshot is encoded *at* the start |
| // instruction, which means generating any code that could load into |
| // registers is illegal. |
| MInstruction* scope = MConstant::New(alloc(), UndefinedValue()); |
| current->add(scope); |
| current->initSlot(info().scopeChainSlot(), scope); |
| |
| // Initialize the return value. |
| MInstruction* returnValue = MConstant::New(alloc(), UndefinedValue()); |
| current->add(returnValue); |
| current->initSlot(info().returnValueSlot(), returnValue); |
| |
| // Initialize the arguments object slot to undefined if necessary. |
| if (info().hasArguments()) { |
| MInstruction* argsObj = MConstant::New(alloc(), UndefinedValue()); |
| current->add(argsObj); |
| current->initSlot(info().argsObjSlot(), argsObj); |
| } |
| |
| // Emit the start instruction, so we can begin real instructions. |
| current->add(MStart::New(alloc(), MStart::StartType_Default)); |
| |
| // Guard against over-recursion. Do this before we start unboxing, since |
| // this will create an OSI point that will read the incoming argument |
| // values, which is nice to do before their last real use, to minimize |
| // register/stack pressure. |
| MCheckOverRecursed* check = MCheckOverRecursed::New(alloc()); |
| current->add(check); |
| MResumePoint* entryRpCopy = MResumePoint::Copy(alloc(), current->entryResumePoint()); |
| if (!entryRpCopy) |
| return false; |
| check->setResumePoint(entryRpCopy); |
| |
| // Parameters have been checked to correspond to the typeset, now we unbox |
| // what we can in an infallible manner. |
| rewriteParameters(); |
| |
| // Check for redeclaration errors for global scripts. |
| if (!info().funMaybeLazy() && !info().module() && |
| script()->bindings.numBodyLevelLocals() > 0) |
| { |
| MGlobalNameConflictsCheck* redeclCheck = MGlobalNameConflictsCheck::New(alloc()); |
| current->add(redeclCheck); |
| MResumePoint* entryRpCopy = MResumePoint::Copy(alloc(), current->entryResumePoint()); |
| if (!entryRpCopy) |
| return false; |
| redeclCheck->setResumePoint(entryRpCopy); |
| } |
| |
| // It's safe to start emitting actual IR, so now build the scope chain. |
| if (!initScopeChain()) |
| return false; |
| |
| if (info().needsArgsObj() && !initArgumentsObject()) |
| return false; |
| |
| // The type analysis phase attempts to insert unbox operations near |
| // definitions of values. It also attempts to replace uses in resume points |
| // with the narrower, unboxed variants. However, we must prevent this |
| // replacement from happening on values in the entry snapshot. Otherwise we |
| // could get this: |
| // |
| // v0 = MParameter(0) |
| // v1 = MParameter(1) |
| // -- ResumePoint(v2, v3) |
| // v2 = Unbox(v0, INT32) |
| // v3 = Unbox(v1, INT32) |
| // |
| // So we attach the initial resume point to each parameter, which the type |
| // analysis explicitly checks (this is the same mechanism used for |
| // effectful operations). |
| for (uint32_t i = 0; i < info().endArgSlot(); i++) { |
| MInstruction* ins = current->getEntrySlot(i)->toInstruction(); |
| if (ins->type() != MIRType_Value) |
| continue; |
| |
| MResumePoint* entryRpCopy = MResumePoint::Copy(alloc(), current->entryResumePoint()); |
| if (!entryRpCopy) |
| return false; |
| ins->setResumePoint(entryRpCopy); |
| } |
| |
| // lazyArguments should never be accessed in |argsObjAliasesFormals| scripts. |
| if (info().hasArguments() && !info().argsObjAliasesFormals()) { |
| lazyArguments_ = MConstant::New(alloc(), MagicValue(JS_OPTIMIZED_ARGUMENTS)); |
| current->add(lazyArguments_); |
| } |
| |
| insertRecompileCheck(); |
| |
| if (!traverseBytecode()) |
| return false; |
| |
| // Discard unreferenced & pre-allocated resume points. |
| replaceMaybeFallbackFunctionGetter(nullptr); |
| |
| if (script_->hasBaselineScript() && |
| inlinedBytecodeLength_ > script_->baselineScript()->inlinedBytecodeLength()) |
| { |
| script_->baselineScript()->setInlinedBytecodeLength(inlinedBytecodeLength_); |
| } |
| |
| if (!maybeAddOsrTypeBarriers()) |
| return false; |
| |
| if (!processIterators()) |
| return false; |
| |
| if (!info().isAnalysis() && !abortedPreliminaryGroups().empty()) { |
| abortReason_ = AbortReason_PreliminaryObjects; |
| return false; |
| } |
| |
| if (shouldForceAbort()) { |
| abortReason_ = AbortReason_Disable; |
| return false; |
| } |
| |
| MOZ_ASSERT(loopDepth_ == 0); |
| abortReason_ = AbortReason_NoAbort; |
| return true; |
| } |
| |
| bool |
| IonBuilder::processIterators() |
| { |
| // Find phis that must directly hold an iterator live. |
| Vector<MPhi*, 0, SystemAllocPolicy> worklist; |
| for (size_t i = 0; i < iterators_.length(); i++) { |
| MInstruction* ins = iterators_[i]; |
| for (MUseDefIterator iter(ins); iter; iter++) { |
| if (iter.def()->isPhi()) { |
| if (!worklist.append(iter.def()->toPhi())) |
| return false; |
| } |
| } |
| } |
| |
| // Propagate the iterator and live status of phis to all other connected |
| // phis. |
| while (!worklist.empty()) { |
| MPhi* phi = worklist.popCopy(); |
| phi->setIterator(); |
| phi->setImplicitlyUsedUnchecked(); |
| |
| for (MUseDefIterator iter(phi); iter; iter++) { |
| if (iter.def()->isPhi()) { |
| MPhi* other = iter.def()->toPhi(); |
| if (!other->isIterator() && !worklist.append(other)) |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::buildInline(IonBuilder* callerBuilder, MResumePoint* callerResumePoint, |
| CallInfo& callInfo) |
| { |
| inlineCallInfo_ = &callInfo; |
| |
| if (!init()) |
| return false; |
| |
| JitSpew(JitSpew_IonScripts, "Inlining script %s:%" PRIuSIZE " (%p)", |
| script()->filename(), script()->lineno(), (void*)script()); |
| |
| callerBuilder_ = callerBuilder; |
| callerResumePoint_ = callerResumePoint; |
| |
| if (callerBuilder->failedBoundsCheck_) |
| failedBoundsCheck_ = true; |
| |
| if (callerBuilder->failedShapeGuard_) |
| failedShapeGuard_ = true; |
| |
| if (callerBuilder->failedLexicalCheck_) |
| failedLexicalCheck_ = true; |
| |
| safeForMinorGC_ = callerBuilder->safeForMinorGC_; |
| |
| // Generate single entrance block. |
| if (!setCurrentAndSpecializePhis(newBlock(pc))) |
| return false; |
| if (!current) |
| return false; |
| |
| current->setCallerResumePoint(callerResumePoint); |
| |
| // Connect the entrance block to the last block in the caller's graph. |
| MBasicBlock* predecessor = callerBuilder->current; |
| MOZ_ASSERT(predecessor == callerResumePoint->block()); |
| |
| predecessor->end(MGoto::New(alloc(), current)); |
| if (!current->addPredecessorWithoutPhis(predecessor)) |
| return false; |
| |
| // Initialize scope chain slot to Undefined. It's set later by |initScopeChain|. |
| MInstruction* scope = MConstant::New(alloc(), UndefinedValue()); |
| current->add(scope); |
| current->initSlot(info().scopeChainSlot(), scope); |
| |
| // Initialize |return value| slot. |
| MInstruction* returnValue = MConstant::New(alloc(), UndefinedValue()); |
| current->add(returnValue); |
| current->initSlot(info().returnValueSlot(), returnValue); |
| |
| // Initialize |arguments| slot. |
| if (info().hasArguments()) { |
| MInstruction* argsObj = MConstant::New(alloc(), UndefinedValue()); |
| current->add(argsObj); |
| current->initSlot(info().argsObjSlot(), argsObj); |
| } |
| |
| // Initialize |this| slot. |
| current->initSlot(info().thisSlot(), callInfo.thisArg()); |
| |
| JitSpew(JitSpew_Inlining, "Initializing %u arg slots", info().nargs()); |
| |
| // NB: Ion does not inline functions which |needsArgsObj|. So using argSlot() |
| // instead of argSlotUnchecked() below is OK |
| MOZ_ASSERT(!info().needsArgsObj()); |
| |
| // Initialize actually set arguments. |
| uint32_t existing_args = Min<uint32_t>(callInfo.argc(), info().nargs()); |
| for (size_t i = 0; i < existing_args; ++i) { |
| MDefinition* arg = callInfo.getArg(i); |
| current->initSlot(info().argSlot(i), arg); |
| } |
| |
| // Pass Undefined for missing arguments |
| for (size_t i = callInfo.argc(); i < info().nargs(); ++i) { |
| MConstant* arg = MConstant::New(alloc(), UndefinedValue()); |
| current->add(arg); |
| current->initSlot(info().argSlot(i), arg); |
| } |
| |
| // Initialize the scope chain now that args are initialized. |
| if (!initScopeChain(callInfo.fun())) |
| return false; |
| |
| JitSpew(JitSpew_Inlining, "Initializing %u local slots; fixed lexicals begin at %u", |
| info().nlocals(), info().fixedLexicalBegin()); |
| |
| initLocals(); |
| |
| JitSpew(JitSpew_Inlining, "Inline entry block MResumePoint %p, %u stack slots", |
| (void*) current->entryResumePoint(), current->entryResumePoint()->stackDepth()); |
| |
| // +2 for the scope chain and |this|, maybe another +1 for arguments object slot. |
| MOZ_ASSERT(current->entryResumePoint()->stackDepth() == info().totalSlots()); |
| |
| if (script_->argumentsHasVarBinding()) { |
| lazyArguments_ = MConstant::New(alloc(), MagicValue(JS_OPTIMIZED_ARGUMENTS)); |
| current->add(lazyArguments_); |
| } |
| |
| insertRecompileCheck(); |
| |
| if (!traverseBytecode()) |
| return false; |
| |
| // Discard unreferenced & pre-allocated resume points. |
| replaceMaybeFallbackFunctionGetter(nullptr); |
| |
| if (!info().isAnalysis() && !abortedPreliminaryGroups().empty()) { |
| abortReason_ = AbortReason_PreliminaryObjects; |
| return false; |
| } |
| |
| if (shouldForceAbort()) { |
| abortReason_ = AbortReason_Disable; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void |
| IonBuilder::rewriteParameter(uint32_t slotIdx, MDefinition* param, int32_t argIndex) |
| { |
| MOZ_ASSERT(param->isParameter() || param->isGetArgumentsObjectArg()); |
| |
| TemporaryTypeSet* types = param->resultTypeSet(); |
| MDefinition* actual = ensureDefiniteType(param, types->getKnownMIRType()); |
| if (actual == param) |
| return; |
| |
| // Careful! We leave the original MParameter in the entry resume point. The |
| // arguments still need to be checked unless proven otherwise at the call |
| // site, and these checks can bailout. We can end up: |
| // v0 = Parameter(0) |
| // v1 = Unbox(v0, INT32) |
| // -- ResumePoint(v0) |
| // |
| // As usual, it would be invalid for v1 to be captured in the initial |
| // resume point, rather than v0. |
| current->rewriteSlot(slotIdx, actual); |
| } |
| |
| // Apply Type Inference information to parameters early on, unboxing them if |
| // they have a definitive type. The actual guards will be emitted by the code |
| // generator, explicitly, as part of the function prologue. |
| void |
| IonBuilder::rewriteParameters() |
| { |
| MOZ_ASSERT(info().scopeChainSlot() == 0); |
| |
| if (!info().funMaybeLazy()) |
| return; |
| |
| for (uint32_t i = info().startArgSlot(); i < info().endArgSlot(); i++) { |
| MDefinition* param = current->getSlot(i); |
| rewriteParameter(i, param, param->toParameter()->index()); |
| } |
| } |
| |
| void |
| IonBuilder::initParameters() |
| { |
| if (!info().funMaybeLazy()) |
| return; |
| |
| // If we are doing OSR on a frame which initially executed in the |
| // interpreter and didn't accumulate type information, try to use that OSR |
| // frame to determine possible initial types for 'this' and parameters. |
| |
| if (thisTypes->empty() && baselineFrame_) { |
| TypeSet::Type type = baselineFrame_->thisType; |
| if (type.isSingletonUnchecked()) |
| checkNurseryObject(type.singleton()); |
| thisTypes->addType(type, alloc_->lifoAlloc()); |
| } |
| |
| MParameter* param = MParameter::New(alloc(), MParameter::THIS_SLOT, thisTypes); |
| current->add(param); |
| current->initSlot(info().thisSlot(), param); |
| |
| for (uint32_t i = 0; i < info().nargs(); i++) { |
| TemporaryTypeSet* types = &argTypes[i]; |
| if (types->empty() && baselineFrame_ && |
| !script_->baselineScript()->modifiesArguments()) |
| { |
| TypeSet::Type type = baselineFrame_->argTypes[i]; |
| if (type.isSingletonUnchecked()) |
| checkNurseryObject(type.singleton()); |
| types->addType(type, alloc_->lifoAlloc()); |
| } |
| |
| param = MParameter::New(alloc(), i, types); |
| current->add(param); |
| current->initSlot(info().argSlotUnchecked(i), param); |
| } |
| } |
| |
| void |
| IonBuilder::initLocals() |
| { |
| if (info().nlocals() == 0) |
| return; |
| |
| MConstant* undef = nullptr; |
| if (info().fixedLexicalBegin() > 0) { |
| undef = MConstant::New(alloc(), UndefinedValue()); |
| current->add(undef); |
| } |
| |
| MConstant* uninitLexical = nullptr; |
| if (info().fixedLexicalBegin() < info().nlocals()) { |
| uninitLexical = MConstant::New(alloc(), MagicValue(JS_UNINITIALIZED_LEXICAL)); |
| current->add(uninitLexical); |
| } |
| |
| for (uint32_t i = 0; i < info().nlocals(); i++) { |
| current->initSlot(info().localSlot(i), (i < info().fixedLexicalBegin() |
| ? undef |
| : uninitLexical)); |
| } |
| } |
| |
| bool |
| IonBuilder::initScopeChain(MDefinition* callee) |
| { |
| MInstruction* scope = nullptr; |
| |
| // If the script doesn't use the scopechain, then it's already initialized |
| // from earlier. However, always make a scope chain when |needsArgsObj| is true |
| // for the script, since arguments object construction requires the scope chain |
| // to be passed in. |
| if (!info().needsArgsObj() && !analysis().usesScopeChain()) |
| return true; |
| |
| // The scope chain is only tracked in scripts that have NAME opcodes which |
| // will try to access the scope. For other scripts, the scope instructions |
| // will be held live by resume points and code will still be generated for |
| // them, so just use a constant undefined value. |
| |
| if (JSFunction* fun = info().funMaybeLazy()) { |
| if (!callee) { |
| MCallee* calleeIns = MCallee::New(alloc()); |
| current->add(calleeIns); |
| callee = calleeIns; |
| } |
| scope = MFunctionEnvironment::New(alloc(), callee); |
| current->add(scope); |
| |
| // This reproduce what is done in CallObject::createForFunction. Skip |
| // this for analyses, as the script might not have a baseline script |
| // with template objects yet. |
| if (fun->needsCallObject() && !info().isAnalysis()) { |
| if (fun->isNamedLambda()) { |
| scope = createDeclEnvObject(callee, scope); |
| if (!scope) |
| return false; |
| } |
| |
| scope = createCallObject(callee, scope); |
| if (!scope) |
| return false; |
| } |
| } else if (ModuleObject* module = info().module()) { |
| // Modules use a pre-created scope object. |
| scope = constant(ObjectValue(module->initialEnvironment())); |
| } else { |
| // For global scripts without a non-syntactic global scope, the scope |
| // chain is the global lexical scope. |
| MOZ_ASSERT(!script()->isForEval()); |
| MOZ_ASSERT(!script()->hasNonSyntacticScope()); |
| scope = constant(ObjectValue(script()->global().lexicalScope())); |
| } |
| |
| current->setScopeChain(scope); |
| return true; |
| } |
| |
| bool |
| IonBuilder::initArgumentsObject() |
| { |
| JitSpew(JitSpew_IonMIR, "%s:%" PRIuSIZE " - Emitting code to initialize arguments object! block=%p", |
| script()->filename(), script()->lineno(), current); |
| MOZ_ASSERT(info().needsArgsObj()); |
| MCreateArgumentsObject* argsObj = MCreateArgumentsObject::New(alloc(), current->scopeChain()); |
| current->add(argsObj); |
| current->setArgumentsObject(argsObj); |
| return true; |
| } |
| |
| bool |
| IonBuilder::addOsrValueTypeBarrier(uint32_t slot, MInstruction** def_, |
| MIRType type, TemporaryTypeSet* typeSet) |
| { |
| MInstruction*& def = *def_; |
| MBasicBlock* osrBlock = def->block(); |
| |
| // Clear bogus type information added in newOsrPreheader(). |
| def->setResultType(MIRType_Value); |
| def->setResultTypeSet(nullptr); |
| |
| if (typeSet && !typeSet->unknown()) { |
| MInstruction* barrier = MTypeBarrier::New(alloc(), def, typeSet); |
| osrBlock->insertBefore(osrBlock->lastIns(), barrier); |
| osrBlock->rewriteSlot(slot, barrier); |
| def = barrier; |
| } else if (type == MIRType_Null || |
| type == MIRType_Undefined || |
| type == MIRType_MagicOptimizedArguments) |
| { |
| // No unbox instruction will be added below, so check the type by |
| // adding a type barrier for a singleton type set. |
| TypeSet::Type ntype = TypeSet::PrimitiveType(ValueTypeFromMIRType(type)); |
| LifoAlloc* lifoAlloc = alloc().lifoAlloc(); |
| typeSet = lifoAlloc->new_<TemporaryTypeSet>(lifoAlloc, ntype); |
| if (!typeSet) |
| return false; |
| MInstruction* barrier = MTypeBarrier::New(alloc(), def, typeSet); |
| osrBlock->insertBefore(osrBlock->lastIns(), barrier); |
| osrBlock->rewriteSlot(slot, barrier); |
| def = barrier; |
| } |
| |
| switch (type) { |
| case MIRType_Boolean: |
| case MIRType_Int32: |
| case MIRType_Double: |
| case MIRType_String: |
| case MIRType_Symbol: |
| case MIRType_Object: |
| if (type != def->type()) { |
| MUnbox* unbox = MUnbox::New(alloc(), def, type, MUnbox::Fallible); |
| osrBlock->insertBefore(osrBlock->lastIns(), unbox); |
| osrBlock->rewriteSlot(slot, unbox); |
| def = unbox; |
| } |
| break; |
| |
| case MIRType_Null: |
| { |
| MConstant* c = MConstant::New(alloc(), NullValue()); |
| osrBlock->insertBefore(osrBlock->lastIns(), c); |
| osrBlock->rewriteSlot(slot, c); |
| def = c; |
| break; |
| } |
| |
| case MIRType_Undefined: |
| { |
| MConstant* c = MConstant::New(alloc(), UndefinedValue()); |
| osrBlock->insertBefore(osrBlock->lastIns(), c); |
| osrBlock->rewriteSlot(slot, c); |
| def = c; |
| break; |
| } |
| |
| case MIRType_MagicOptimizedArguments: |
| MOZ_ASSERT(lazyArguments_); |
| osrBlock->rewriteSlot(slot, lazyArguments_); |
| def = lazyArguments_; |
| break; |
| |
| default: |
| break; |
| } |
| |
| MOZ_ASSERT(def == osrBlock->getSlot(slot)); |
| return true; |
| } |
| |
| bool |
| IonBuilder::maybeAddOsrTypeBarriers() |
| { |
| if (!info().osrPc()) |
| return true; |
| |
| // The loop has successfully been processed, and the loop header phis |
| // have their final type. Add unboxes and type barriers in the OSR |
| // block to check that the values have the appropriate type, and update |
| // the types in the preheader. |
| |
| MBasicBlock* osrBlock = graph().osrBlock(); |
| if (!osrBlock) { |
| // Because IonBuilder does not compile catch blocks, it's possible to |
| // end up without an OSR block if the OSR pc is only reachable via a |
| // break-statement inside the catch block. For instance: |
| // |
| // for (;;) { |
| // try { |
| // throw 3; |
| // } catch(e) { |
| // break; |
| // } |
| // } |
| // while (..) { } // <= OSR here, only reachable via catch block. |
| // |
| // For now we just abort in this case. |
| MOZ_ASSERT(graph().hasTryBlock()); |
| return abort("OSR block only reachable through catch block"); |
| } |
| |
| MBasicBlock* preheader = osrBlock->getSuccessor(0); |
| MBasicBlock* header = preheader->getSuccessor(0); |
| static const size_t OSR_PHI_POSITION = 1; |
| MOZ_ASSERT(preheader->getPredecessor(OSR_PHI_POSITION) == osrBlock); |
| |
| MResumePoint* headerRp = header->entryResumePoint(); |
| size_t stackDepth = headerRp->stackDepth(); |
| MOZ_ASSERT(stackDepth == osrBlock->stackDepth()); |
| for (uint32_t slot = info().startArgSlot(); slot < stackDepth; slot++) { |
| // Aliased slots are never accessed, since they need to go through |
| // the callobject. The typebarriers are added there and can be |
| // discarded here. |
| if (info().isSlotAliasedAtOsr(slot)) |
| continue; |
| |
| MInstruction* def = osrBlock->getSlot(slot)->toInstruction(); |
| MPhi* preheaderPhi = preheader->getSlot(slot)->toPhi(); |
| MPhi* headerPhi = headerRp->getOperand(slot)->toPhi(); |
| |
| MIRType type = headerPhi->type(); |
| TemporaryTypeSet* typeSet = headerPhi->resultTypeSet(); |
| |
| if (!addOsrValueTypeBarrier(slot, &def, type, typeSet)) |
| return false; |
| |
| preheaderPhi->replaceOperand(OSR_PHI_POSITION, def); |
| preheaderPhi->setResultType(type); |
| preheaderPhi->setResultTypeSet(typeSet); |
| } |
| |
| return true; |
| } |
| |
| // We try to build a control-flow graph in the order that it would be built as |
| // if traversing the AST. This leads to a nice ordering and lets us build SSA |
| // in one pass, since the bytecode is structured. |
| // |
| // We traverse the bytecode iteratively, maintaining a current basic block. |
| // Each basic block has a mapping of local slots to instructions, as well as a |
| // stack depth. As we encounter instructions we mutate this mapping in the |
| // current block. |
| // |
| // Things get interesting when we encounter a control structure. This can be |
| // either an IFEQ, downward GOTO, or a decompiler hint stashed away in source |
| // notes. Once we encounter such an opcode, we recover the structure of the |
| // control flow (its branches and bounds), and push it on a stack. |
| // |
| // As we continue traversing the bytecode, we look for points that would |
| // terminate the topmost control flow path pushed on the stack. These are: |
| // (1) The bounds of the current structure (end of a loop or join/edge of a |
| // branch). |
| // (2) A "return", "break", or "continue" statement. |
| // |
| // For (1), we expect that there is a current block in the progress of being |
| // built, and we complete the necessary edges in the CFG. For (2), we expect |
| // that there is no active block. |
| // |
| // For normal diamond join points, we construct Phi nodes as we add |
| // predecessors. For loops, care must be taken to propagate Phi nodes back |
| // through uses in the loop body. |
| bool |
| IonBuilder::traverseBytecode() |
| { |
| for (;;) { |
| MOZ_ASSERT(pc < info().limitPC()); |
| |
| for (;;) { |
| if (!alloc().ensureBallast()) |
| return false; |
| |
| // Check if we've hit an expected join point or edge in the bytecode. |
| // Leaving one control structure could place us at the edge of another, |
| // thus |while| instead of |if| so we don't skip any opcodes. |
| if (!cfgStack_.empty() && cfgStack_.back().stopAt == pc) { |
| ControlStatus status = processCfgStack(); |
| if (status == ControlStatus_Error) |
| return false; |
| if (status == ControlStatus_Abort) |
| return abort("Aborted while processing control flow"); |
| if (!current) |
| return true; |
| continue; |
| } |
| |
| // Some opcodes need to be handled early because they affect control |
| // flow, terminating the current basic block and/or instructing the |
| // traversal algorithm to continue from a new pc. |
| // |
| // (1) If the opcode does not affect control flow, then the opcode |
| // is inspected and transformed to IR. This is the process_opcode |
| // label. |
| // (2) A loop could be detected via a forward GOTO. In this case, |
| // we don't want to process the GOTO, but the following |
| // instruction. |
| // (3) A RETURN, STOP, BREAK, or CONTINUE may require processing the |
| // CFG stack to terminate open branches. |
| // |
| // Similar to above, snooping control flow could land us at another |
| // control flow point, so we iterate until it's time to inspect a real |
| // opcode. |
| ControlStatus status; |
| if ((status = snoopControlFlow(JSOp(*pc))) == ControlStatus_None) |
| break; |
| if (status == ControlStatus_Error) |
| return false; |
| if (status == ControlStatus_Abort) |
| return abort("Aborted while processing control flow"); |
| if (!current) |
| return true; |
| } |
| |
| #ifdef DEBUG |
| // In debug builds, after compiling this op, check that all values |
| // popped by this opcode either: |
| // |
| // (1) Have the ImplicitlyUsed flag set on them. |
| // (2) Have more uses than before compiling this op (the value is |
| // used as operand of a new MIR instruction). |
| // |
| // This is used to catch problems where IonBuilder pops a value without |
| // adding any SSA uses and doesn't call setImplicitlyUsedUnchecked on it. |
| Vector<MDefinition*, 4, JitAllocPolicy> popped(alloc()); |
| Vector<size_t, 4, JitAllocPolicy> poppedUses(alloc()); |
| unsigned nuses = GetUseCount(script_, script_->pcToOffset(pc)); |
| |
| for (unsigned i = 0; i < nuses; i++) { |
| MDefinition* def = current->peek(-int32_t(i + 1)); |
| if (!popped.append(def) || !poppedUses.append(def->defUseCount())) |
| return false; |
| } |
| #endif |
| |
| // Nothing in inspectOpcode() is allowed to advance the pc. |
| JSOp op = JSOp(*pc); |
| if (!inspectOpcode(op)) |
| return false; |
| |
| #ifdef DEBUG |
| for (size_t i = 0; i < popped.length(); i++) { |
| switch (op) { |
| case JSOP_POP: |
| case JSOP_POPN: |
| case JSOP_DUPAT: |
| case JSOP_DUP: |
| case JSOP_DUP2: |
| case JSOP_PICK: |
| case JSOP_SWAP: |
| case JSOP_SETARG: |
| case JSOP_SETLOCAL: |
| case JSOP_INITLEXICAL: |
| case JSOP_SETRVAL: |
| case JSOP_VOID: |
| // Don't require SSA uses for values popped by these ops. |
| break; |
| |
| case JSOP_POS: |
| case JSOP_TOID: |
| case JSOP_TOSTRING: |
| // These ops may leave their input on the stack without setting |
| // the ImplicitlyUsed flag. If this value will be popped immediately, |
| // we may replace it with |undefined|, but the difference is |
| // not observable. |
| MOZ_ASSERT(i == 0); |
| if (current->peek(-1) == popped[0]) |
| break; |
| // FALL THROUGH |
| |
| default: |
| MOZ_ASSERT(popped[i]->isImplicitlyUsed() || |
| |
| // MNewDerivedTypedObject instances are |
| // often dead unless they escape from the |
| // fn. See IonBuilder::loadTypedObjectData() |
| // for more details. |
| popped[i]->isNewDerivedTypedObject() || |
| |
| popped[i]->defUseCount() > poppedUses[i]); |
| break; |
| } |
| } |
| #endif |
| |
| pc += CodeSpec[op].length; |
| current->updateTrackedSite(bytecodeSite(pc)); |
| } |
| |
| return true; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::snoopControlFlow(JSOp op) |
| { |
| switch (op) { |
| case JSOP_NOP: |
| return maybeLoop(op, info().getNote(gsn, pc)); |
| |
| case JSOP_POP: |
| return maybeLoop(op, info().getNote(gsn, pc)); |
| |
| case JSOP_RETURN: |
| case JSOP_RETRVAL: |
| return processReturn(op); |
| |
| case JSOP_THROW: |
| return processThrow(); |
| |
| case JSOP_GOTO: |
| { |
| jssrcnote* sn = info().getNote(gsn, pc); |
| switch (sn ? SN_TYPE(sn) : SRC_NULL) { |
| case SRC_BREAK: |
| case SRC_BREAK2LABEL: |
| return processBreak(op, sn); |
| |
| case SRC_CONTINUE: |
| return processContinue(op); |
| |
| case SRC_SWITCHBREAK: |
| return processSwitchBreak(op); |
| |
| case SRC_WHILE: |
| case SRC_FOR_IN: |
| case SRC_FOR_OF: |
| // while (cond) { } |
| return whileOrForInLoop(sn); |
| |
| default: |
| // Hard assert for now - make an error later. |
| MOZ_CRASH("unknown goto case"); |
| } |
| break; |
| } |
| |
| case JSOP_TABLESWITCH: |
| return tableSwitch(op, info().getNote(gsn, pc)); |
| |
| case JSOP_IFNE: |
| // We should never reach an IFNE, it's a stopAt point, which will |
| // trigger closing the loop. |
| MOZ_CRASH("we should never reach an ifne!"); |
| |
| default: |
| break; |
| } |
| return ControlStatus_None; |
| } |
| |
| bool |
| IonBuilder::inspectOpcode(JSOp op) |
| { |
| MOZ_ASSERT(analysis_.maybeInfo(pc), "Compiling unreachable op"); |
| |
| switch (op) { |
| case JSOP_NOP: |
| case JSOP_LINENO: |
| case JSOP_LOOPENTRY: |
| return true; |
| |
| case JSOP_LABEL: |
| return jsop_label(); |
| |
| case JSOP_UNDEFINED: |
| // If this ever changes, change what JSOP_GIMPLICITTHIS does too. |
| return pushConstant(UndefinedValue()); |
| |
| case JSOP_IFEQ: |
| return jsop_ifeq(JSOP_IFEQ); |
| |
| case JSOP_TRY: |
| return jsop_try(); |
| |
| case JSOP_CONDSWITCH: |
| return jsop_condswitch(); |
| |
| case JSOP_BITNOT: |
| return jsop_bitnot(); |
| |
| case JSOP_BITAND: |
| case JSOP_BITOR: |
| case JSOP_BITXOR: |
| case JSOP_LSH: |
| case JSOP_RSH: |
| case JSOP_URSH: |
| return jsop_bitop(op); |
| |
| case JSOP_ADD: |
| case JSOP_SUB: |
| case JSOP_MUL: |
| case JSOP_DIV: |
| case JSOP_MOD: |
| return jsop_binary_arith(op); |
| |
| case JSOP_POW: |
| return jsop_pow(); |
| |
| case JSOP_POS: |
| return jsop_pos(); |
| |
| case JSOP_NEG: |
| return jsop_neg(); |
| |
| case JSOP_TOSTRING: |
| return jsop_tostring(); |
| |
| case JSOP_AND: |
| case JSOP_OR: |
| return jsop_andor(op); |
| |
| case JSOP_DEFVAR: |
| return jsop_defvar(GET_UINT32_INDEX(pc)); |
| |
| case JSOP_DEFLET: |
| case JSOP_DEFCONST: |
| return jsop_deflexical(GET_UINT32_INDEX(pc)); |
| |
| case JSOP_DEFFUN: |
| return jsop_deffun(GET_UINT32_INDEX(pc)); |
| |
| case JSOP_EQ: |
| case JSOP_NE: |
| case JSOP_STRICTEQ: |
| case JSOP_STRICTNE: |
| case JSOP_LT: |
| case JSOP_LE: |
| case JSOP_GT: |
| case JSOP_GE: |
| return jsop_compare(op); |
| |
| case JSOP_DOUBLE: |
| return pushConstant(info().getConst(pc)); |
| |
| case JSOP_STRING: |
| return pushConstant(StringValue(info().getAtom(pc))); |
| |
| case JSOP_SYMBOL: { |
| unsigned which = GET_UINT8(pc); |
| JS::Symbol* sym = compartment->runtime()->wellKnownSymbols().get(which); |
| return pushConstant(SymbolValue(sym)); |
| } |
| |
| case JSOP_ZERO: |
| return pushConstant(Int32Value(0)); |
| |
| case JSOP_ONE: |
| return pushConstant(Int32Value(1)); |
| |
| case JSOP_NULL: |
| return pushConstant(NullValue()); |
| |
| case JSOP_VOID: |
| current->pop(); |
| return pushConstant(UndefinedValue()); |
| |
| case JSOP_HOLE: |
| return pushConstant(MagicValue(JS_ELEMENTS_HOLE)); |
| |
| case JSOP_FALSE: |
| return pushConstant(BooleanValue(false)); |
| |
| case JSOP_TRUE: |
| return pushConstant(BooleanValue(true)); |
| |
| case JSOP_ARGUMENTS: |
| return jsop_arguments(); |
| |
| case JSOP_RUNONCE: |
| return jsop_runonce(); |
| |
| case JSOP_REST: |
| return jsop_rest(); |
| |
| case JSOP_GETARG: |
| if (info().argsObjAliasesFormals()) { |
| MGetArgumentsObjectArg* getArg = MGetArgumentsObjectArg::New(alloc(), |
| current->argumentsObject(), |
| GET_ARGNO(pc)); |
| current->add(getArg); |
| current->push(getArg); |
| } else { |
| current->pushArg(GET_ARGNO(pc)); |
| } |
| return true; |
| |
| case JSOP_SETARG: |
| return jsop_setarg(GET_ARGNO(pc)); |
| |
| case JSOP_GETLOCAL: |
| current->pushLocal(GET_LOCALNO(pc)); |
| return true; |
| |
| case JSOP_SETLOCAL: |
| current->setLocal(GET_LOCALNO(pc)); |
| return true; |
| |
| case JSOP_THROWSETCONST: |
| case JSOP_THROWSETALIASEDCONST: |
| return jsop_throwsetconst(); |
| |
| case JSOP_CHECKLEXICAL: |
| return jsop_checklexical(); |
| |
| case JSOP_INITLEXICAL: |
| current->setLocal(GET_LOCALNO(pc)); |
| return true; |
| |
| case JSOP_INITGLEXICAL: { |
| MOZ_ASSERT(!script()->hasNonSyntacticScope()); |
| MDefinition* value = current->pop(); |
| current->push(constant(ObjectValue(script()->global().lexicalScope()))); |
| current->push(value); |
| return jsop_setprop(info().getAtom(pc)->asPropertyName()); |
| } |
| |
| case JSOP_CHECKALIASEDLEXICAL: |
| return jsop_checkaliasedlet(ScopeCoordinate(pc)); |
| |
| case JSOP_INITALIASEDLEXICAL: |
| return jsop_setaliasedvar(ScopeCoordinate(pc)); |
| |
| case JSOP_UNINITIALIZED: |
| return pushConstant(MagicValue(JS_UNINITIALIZED_LEXICAL)); |
| |
| case JSOP_POP: |
| current->pop(); |
| |
| // POP opcodes frequently appear where values are killed, e.g. after |
| // SET* opcodes. Place a resume point afterwards to avoid capturing |
| // the dead value in later snapshots, except in places where that |
| // resume point is obviously unnecessary. |
| if (pc[JSOP_POP_LENGTH] == JSOP_POP) |
| return true; |
| return maybeInsertResume(); |
| |
| case JSOP_POPN: |
| for (uint32_t i = 0, n = GET_UINT16(pc); i < n; i++) |
| current->pop(); |
| return true; |
| |
| case JSOP_DUPAT: |
| current->pushSlot(current->stackDepth() - 1 - GET_UINT24(pc)); |
| return true; |
| |
| case JSOP_NEWINIT: |
| if (GET_UINT8(pc) == JSProto_Array) |
| return jsop_newarray(0); |
| return jsop_newobject(); |
| |
| case JSOP_NEWARRAY: |
| return jsop_newarray(GET_UINT32(pc)); |
| |
| case JSOP_NEWARRAY_COPYONWRITE: |
| return jsop_newarray_copyonwrite(); |
| |
| case JSOP_NEWOBJECT: |
| return jsop_newobject(); |
| |
| case JSOP_INITELEM: |
| case JSOP_INITHIDDENELEM: |
| return jsop_initelem(); |
| |
| case JSOP_INITELEM_ARRAY: |
| return jsop_initelem_array(); |
| |
| case JSOP_INITPROP: |
| case JSOP_INITLOCKEDPROP: |
| case JSOP_INITHIDDENPROP: |
| { |
| PropertyName* name = info().getAtom(pc)->asPropertyName(); |
| return jsop_initprop(name); |
| } |
| |
| case JSOP_MUTATEPROTO: |
| { |
| return jsop_mutateproto(); |
| } |
| |
| case JSOP_INITPROP_GETTER: |
| case JSOP_INITHIDDENPROP_GETTER: |
| case JSOP_INITPROP_SETTER: |
| case JSOP_INITHIDDENPROP_SETTER: { |
| PropertyName* name = info().getAtom(pc)->asPropertyName(); |
| return jsop_initprop_getter_setter(name); |
| } |
| |
| case JSOP_INITELEM_GETTER: |
| case JSOP_INITHIDDENELEM_GETTER: |
| case JSOP_INITELEM_SETTER: |
| case JSOP_INITHIDDENELEM_SETTER: |
| return jsop_initelem_getter_setter(); |
| |
| case JSOP_FUNCALL: |
| return jsop_funcall(GET_ARGC(pc)); |
| |
| case JSOP_FUNAPPLY: |
| return jsop_funapply(GET_ARGC(pc)); |
| |
| case JSOP_CALL: |
| case JSOP_CALLITER: |
| case JSOP_NEW: |
| case JSOP_SUPERCALL: |
| return jsop_call(GET_ARGC(pc), (JSOp)*pc == JSOP_NEW || (JSOp)*pc == JSOP_SUPERCALL); |
| |
| case JSOP_EVAL: |
| case JSOP_STRICTEVAL: |
| return jsop_eval(GET_ARGC(pc)); |
| |
| case JSOP_INT8: |
| return pushConstant(Int32Value(GET_INT8(pc))); |
| |
| case JSOP_UINT16: |
| return pushConstant(Int32Value(GET_UINT16(pc))); |
| |
| case JSOP_GETGNAME: |
| { |
| PropertyName* name = info().getAtom(pc)->asPropertyName(); |
| if (!script()->hasNonSyntacticScope()) |
| return jsop_getgname(name); |
| return jsop_getname(name); |
| } |
| |
| case JSOP_SETGNAME: |
| case JSOP_STRICTSETGNAME: |
| { |
| PropertyName* name = info().getAtom(pc)->asPropertyName(); |
| JSObject* obj = nullptr; |
| if (!script()->hasNonSyntacticScope()) |
| obj = testGlobalLexicalBinding(name); |
| if (obj) |
| return setStaticName(obj, name); |
| return jsop_setprop(name); |
| } |
| |
| case JSOP_GETNAME: |
| { |
| PropertyName* name = info().getAtom(pc)->asPropertyName(); |
| return jsop_getname(name); |
| } |
| |
| case JSOP_GETINTRINSIC: |
| { |
| PropertyName* name = info().getAtom(pc)->asPropertyName(); |
| return jsop_intrinsic(name); |
| } |
| |
| case JSOP_GETIMPORT: |
| { |
| PropertyName* name = info().getAtom(pc)->asPropertyName(); |
| return jsop_getimport(name); |
| } |
| |
| case JSOP_BINDGNAME: |
| if (!script()->hasNonSyntacticScope()) { |
| if (JSObject* scope = testGlobalLexicalBinding(info().getName(pc))) |
| return pushConstant(ObjectValue(*scope)); |
| } |
| // Fall through to JSOP_BINDNAME |
| case JSOP_BINDNAME: |
| return jsop_bindname(info().getName(pc)); |
| |
| case JSOP_DUP: |
| current->pushSlot(current->stackDepth() - 1); |
| return true; |
| |
| case JSOP_DUP2: |
| return jsop_dup2(); |
| |
| case JSOP_SWAP: |
| current->swapAt(-1); |
| return true; |
| |
| case JSOP_PICK: |
| current->pick(-GET_INT8(pc)); |
| return true; |
| |
| case JSOP_GETALIASEDVAR: |
| return jsop_getaliasedvar(ScopeCoordinate(pc)); |
| |
| case JSOP_SETALIASEDVAR: |
| return jsop_setaliasedvar(ScopeCoordinate(pc)); |
| |
| case JSOP_UINT24: |
| return pushConstant(Int32Value(GET_UINT24(pc))); |
| |
| case JSOP_INT32: |
| return pushConstant(Int32Value(GET_INT32(pc))); |
| |
| case JSOP_LOOPHEAD: |
| // JSOP_LOOPHEAD is handled when processing the loop header. |
| MOZ_CRASH("JSOP_LOOPHEAD outside loop"); |
| |
| case JSOP_GETELEM: |
| case JSOP_CALLELEM: |
| if (!jsop_getelem()) |
| return false; |
| if (op == JSOP_CALLELEM && !improveThisTypesForCall()) |
| return false; |
| return true; |
| |
| case JSOP_SETELEM: |
| case JSOP_STRICTSETELEM: |
| return jsop_setelem(); |
| |
| case JSOP_LENGTH: |
| return jsop_length(); |
| |
| case JSOP_NOT: |
| return jsop_not(); |
| |
| case JSOP_FUNCTIONTHIS: |
| return jsop_functionthis(); |
| |
| case JSOP_GLOBALTHIS: |
| return jsop_globalthis(); |
| |
| case JSOP_CALLEE: { |
| MDefinition* callee = getCallee(); |
| current->push(callee); |
| return true; |
| } |
| |
| case JSOP_GETPROP: |
| case JSOP_CALLPROP: |
| { |
| PropertyName* name = info().getAtom(pc)->asPropertyName(); |
| if (!jsop_getprop(name)) |
| return false; |
| if (op == JSOP_CALLPROP && !improveThisTypesForCall()) |
| return false; |
| return true; |
| } |
| |
| case JSOP_SETPROP: |
| case JSOP_STRICTSETPROP: |
| case JSOP_SETNAME: |
| case JSOP_STRICTSETNAME: |
| { |
| PropertyName* name = info().getAtom(pc)->asPropertyName(); |
| return jsop_setprop(name); |
| } |
| |
| case JSOP_DELPROP: |
| case JSOP_STRICTDELPROP: |
| { |
| PropertyName* name = info().getAtom(pc)->asPropertyName(); |
| return jsop_delprop(name); |
| } |
| |
| case JSOP_DELELEM: |
| case JSOP_STRICTDELELEM: |
| return jsop_delelem(); |
| |
| case JSOP_REGEXP: |
| return jsop_regexp(info().getRegExp(pc)); |
| |
| case JSOP_CALLSITEOBJ: |
| return pushConstant(ObjectValue(*(info().getObject(pc)))); |
| |
| case JSOP_OBJECT: |
| return jsop_object(info().getObject(pc)); |
| |
| case JSOP_TYPEOF: |
| case JSOP_TYPEOFEXPR: |
| return jsop_typeof(); |
| |
| case JSOP_TOID: |
| return jsop_toid(); |
| |
| case JSOP_LAMBDA: |
| return jsop_lambda(info().getFunction(pc)); |
| |
| case JSOP_LAMBDA_ARROW: |
| return jsop_lambda_arrow(info().getFunction(pc)); |
| |
| case JSOP_ITER: |
| return jsop_iter(GET_INT8(pc)); |
| |
| case JSOP_MOREITER: |
| return jsop_itermore(); |
| |
| case JSOP_ISNOITER: |
| return jsop_isnoiter(); |
| |
| case JSOP_ENDITER: |
| return jsop_iterend(); |
| |
| case JSOP_IN: |
| return jsop_in(); |
| |
| case JSOP_SETRVAL: |
| MOZ_ASSERT(!script()->noScriptRval()); |
| current->setSlot(info().returnValueSlot(), current->pop()); |
| return true; |
| |
| case JSOP_INSTANCEOF: |
| return jsop_instanceof(); |
| |
| case JSOP_DEBUGLEAVEBLOCK: |
| return true; |
| |
| case JSOP_DEBUGGER: |
| return jsop_debugger(); |
| |
| case JSOP_GIMPLICITTHIS: |
| if (!script()->hasNonSyntacticScope()) |
| return pushConstant(UndefinedValue()); |
| |
| // Just fall through to the unsupported bytecode case. |
| break; |
| |
| case JSOP_NEWTARGET: |
| return jsop_newtarget(); |
| |
| case JSOP_CHECKOBJCOERCIBLE: |
| return jsop_checkobjcoercible(); |
| |
| #ifdef DEBUG |
| case JSOP_PUSHBLOCKSCOPE: |
| case JSOP_FRESHENBLOCKSCOPE: |
| case JSOP_POPBLOCKSCOPE: |
| // These opcodes are currently unhandled by Ion, but in principle |
| // there's no reason they couldn't be. Whenever this happens, OSR will |
| // have to consider that JSOP_FRESHENBLOCK mutates the scope chain -- |
| // right now it caches the scope chain in MBasicBlock::scopeChain(). |
| // That stale value will have to be updated when JSOP_FRESHENBLOCK is |
| // encountered. |
| #endif |
| default: |
| break; |
| } |
| |
| // Track a simpler message, since the actionable abort message is a |
| // static string, and the internal opcode name isn't an actionable |
| // thing anyways. |
| trackActionableAbort("Unsupported bytecode"); |
| #ifdef DEBUG |
| return abort("Unsupported opcode: %s", CodeName[op]); |
| #else |
| return abort("Unsupported opcode: %d", op); |
| #endif |
| } |
| |
| // Given that the current control flow structure has ended forcefully, |
| // via a return, break, or continue (rather than joining), propagate the |
| // termination up. For example, a return nested 5 loops deep may terminate |
| // every outer loop at once, if there are no intervening conditionals: |
| // |
| // for (...) { |
| // for (...) { |
| // return x; |
| // } |
| // } |
| // |
| // If |current| is nullptr when this function returns, then there is no more |
| // control flow to be processed. |
| IonBuilder::ControlStatus |
| IonBuilder::processControlEnd() |
| { |
| MOZ_ASSERT(!current); |
| |
| if (cfgStack_.empty()) { |
| // If there is no more control flow to process, then this is the |
| // last return in the function. |
| return ControlStatus_Ended; |
| } |
| |
| return processCfgStack(); |
| } |
| |
| // Processes the top of the CFG stack. This is used from two places: |
| // (1) processControlEnd(), whereby a break, continue, or return may interrupt |
| // an in-progress CFG structure before reaching its actual termination |
| // point in the bytecode. |
| // (2) traverseBytecode(), whereby we reach the last instruction in a CFG |
| // structure. |
| IonBuilder::ControlStatus |
| IonBuilder::processCfgStack() |
| { |
| ControlStatus status = processCfgEntry(cfgStack_.back()); |
| |
| // If this terminated a CFG structure, act like processControlEnd() and |
| // keep propagating upward. |
| while (status == ControlStatus_Ended) { |
| popCfgStack(); |
| if (cfgStack_.empty()) |
| return status; |
| status = processCfgEntry(cfgStack_.back()); |
| } |
| |
| // If some join took place, the current structure is finished. |
| if (status == ControlStatus_Joined) |
| popCfgStack(); |
| |
| return status; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processCfgEntry(CFGState& state) |
| { |
| switch (state.state) { |
| case CFGState::IF_TRUE: |
| case CFGState::IF_TRUE_EMPTY_ELSE: |
| return processIfEnd(state); |
| |
| case CFGState::IF_ELSE_TRUE: |
| return processIfElseTrueEnd(state); |
| |
| case CFGState::IF_ELSE_FALSE: |
| return processIfElseFalseEnd(state); |
| |
| case CFGState::DO_WHILE_LOOP_BODY: |
| return processDoWhileBodyEnd(state); |
| |
| case CFGState::DO_WHILE_LOOP_COND: |
| return processDoWhileCondEnd(state); |
| |
| case CFGState::WHILE_LOOP_COND: |
| return processWhileCondEnd(state); |
| |
| case CFGState::WHILE_LOOP_BODY: |
| return processWhileBodyEnd(state); |
| |
| case CFGState::FOR_LOOP_COND: |
| return processForCondEnd(state); |
| |
| case CFGState::FOR_LOOP_BODY: |
| return processForBodyEnd(state); |
| |
| case CFGState::FOR_LOOP_UPDATE: |
| return processForUpdateEnd(state); |
| |
| case CFGState::TABLE_SWITCH: |
| return processNextTableSwitchCase(state); |
| |
| case CFGState::COND_SWITCH_CASE: |
| return processCondSwitchCase(state); |
| |
| case CFGState::COND_SWITCH_BODY: |
| return processCondSwitchBody(state); |
| |
| case CFGState::AND_OR: |
| return processAndOrEnd(state); |
| |
| case CFGState::LABEL: |
| return processLabelEnd(state); |
| |
| case CFGState::TRY: |
| return processTryEnd(state); |
| |
| default: |
| MOZ_CRASH("unknown cfgstate"); |
| } |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processIfEnd(CFGState& state) |
| { |
| bool thenBranchTerminated = !current; |
| if (!thenBranchTerminated) { |
| // Here, the false block is the join point. Create an edge from the |
| // current block to the false block. Note that a RETURN opcode |
| // could have already ended the block. |
| current->end(MGoto::New(alloc(), state.branch.ifFalse)); |
| |
| if (!state.branch.ifFalse->addPredecessor(alloc(), current)) |
| return ControlStatus_Error; |
| } |
| |
| if (!setCurrentAndSpecializePhis(state.branch.ifFalse)) |
| return ControlStatus_Error; |
| graph().moveBlockToEnd(current); |
| pc = current->pc(); |
| |
| if (thenBranchTerminated) { |
| // If we can't reach here via the then-branch, we can filter the types |
| // after the if-statement based on the if-condition. |
| MTest* test = state.branch.test; |
| if (!improveTypesAtTest(test->getOperand(0), test->ifTrue() == current, test)) |
| return ControlStatus_Error; |
| } |
| |
| return ControlStatus_Joined; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processIfElseTrueEnd(CFGState& state) |
| { |
| // We've reached the end of the true branch of an if-else. Don't |
| // create an edge yet, just transition to parsing the false branch. |
| state.state = CFGState::IF_ELSE_FALSE; |
| state.branch.ifTrue = current; |
| state.stopAt = state.branch.falseEnd; |
| pc = state.branch.ifFalse->pc(); |
| if (!setCurrentAndSpecializePhis(state.branch.ifFalse)) |
| return ControlStatus_Error; |
| graph().moveBlockToEnd(current); |
| |
| MTest* test = state.branch.test; |
| if (!improveTypesAtTest(test->getOperand(0), test->ifTrue() == current, test)) |
| return ControlStatus_Error; |
| |
| return ControlStatus_Jumped; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processIfElseFalseEnd(CFGState& state) |
| { |
| // Update the state to have the latest block from the false path. |
| state.branch.ifFalse = current; |
| |
| // To create the join node, we need an incoming edge that has not been |
| // terminated yet. |
| MBasicBlock* pred = state.branch.ifTrue |
| ? state.branch.ifTrue |
| : state.branch.ifFalse; |
| MBasicBlock* other = (pred == state.branch.ifTrue) ? state.branch.ifFalse : state.branch.ifTrue; |
| |
| if (!pred) |
| return ControlStatus_Ended; |
| |
| // Create a new block to represent the join. |
| MBasicBlock* join = newBlock(pred, state.branch.falseEnd); |
| if (!join) |
| return ControlStatus_Error; |
| |
| // Create edges from the true and false blocks as needed. |
| pred->end(MGoto::New(alloc(), join)); |
| |
| if (other) { |
| other->end(MGoto::New(alloc(), join)); |
| if (!join->addPredecessor(alloc(), other)) |
| return ControlStatus_Error; |
| } |
| |
| // Ignore unreachable remainder of false block if existent. |
| if (!setCurrentAndSpecializePhis(join)) |
| return ControlStatus_Error; |
| pc = current->pc(); |
| return ControlStatus_Joined; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processBrokenLoop(CFGState& state) |
| { |
| MOZ_ASSERT(!current); |
| |
| MOZ_ASSERT(loopDepth_); |
| loopDepth_--; |
| |
| // A broken loop is not a real loop (it has no header or backedge), so |
| // reset the loop depth. |
| for (MBasicBlockIterator i(graph().begin(state.loop.entry)); i != graph().end(); i++) { |
| if (i->loopDepth() > loopDepth_) |
| i->setLoopDepth(i->loopDepth() - 1); |
| } |
| |
| // If the loop started with a condition (while/for) then even if the |
| // structure never actually loops, the condition itself can still fail and |
| // thus we must resume at the successor, if one exists. |
| if (!setCurrentAndSpecializePhis(state.loop.successor)) |
| return ControlStatus_Error; |
| if (current) { |
| MOZ_ASSERT(current->loopDepth() == loopDepth_); |
| graph().moveBlockToEnd(current); |
| } |
| |
| // Join the breaks together and continue parsing. |
| if (state.loop.breaks) { |
| MBasicBlock* block = createBreakCatchBlock(state.loop.breaks, state.loop.exitpc); |
| if (!block) |
| return ControlStatus_Error; |
| |
| if (current) { |
| current->end(MGoto::New(alloc(), block)); |
| if (!block->addPredecessor(alloc(), current)) |
| return ControlStatus_Error; |
| } |
| |
| if (!setCurrentAndSpecializePhis(block)) |
| return ControlStatus_Error; |
| } |
| |
| // If the loop is not gated on a condition, and has only returns, we'll |
| // reach this case. For example: |
| // do { ... return; } while (); |
| if (!current) |
| return ControlStatus_Ended; |
| |
| // Otherwise, the loop is gated on a condition and/or has breaks so keep |
| // parsing at the successor. |
| pc = current->pc(); |
| return ControlStatus_Joined; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::finishLoop(CFGState& state, MBasicBlock* successor) |
| { |
| MOZ_ASSERT(current); |
| |
| MOZ_ASSERT(loopDepth_); |
| loopDepth_--; |
| MOZ_ASSERT_IF(successor, successor->loopDepth() == loopDepth_); |
| |
| // Compute phis in the loop header and propagate them throughout the loop, |
| // including the successor. |
| AbortReason r = state.loop.entry->setBackedge(current); |
| if (r == AbortReason_Alloc) |
| return ControlStatus_Error; |
| if (r == AbortReason_Disable) { |
| // If there are types for variables on the backedge that were not |
| // present at the original loop header, then uses of the variables' |
| // phis may have generated incorrect nodes. The new types have been |
| // incorporated into the header phis, so remove all blocks for the |
| // loop body and restart with the new types. |
| return restartLoop(state); |
| } |
| |
| if (successor) { |
| graph().moveBlockToEnd(successor); |
| successor->inheritPhis(state.loop.entry); |
| } |
| |
| if (state.loop.breaks) { |
| // Propagate phis placed in the header to individual break exit points. |
| DeferredEdge* edge = state.loop.breaks; |
| while (edge) { |
| edge->block->inheritPhis(state.loop.entry); |
| edge = edge->next; |
| } |
| |
| // Create a catch block to join all break exits. |
| MBasicBlock* block = createBreakCatchBlock(state.loop.breaks, state.loop.exitpc); |
| if (!block) |
| return ControlStatus_Error; |
| |
| if (successor) { |
| // Finally, create an unconditional edge from the successor to the |
| // catch block. |
| successor->end(MGoto::New(alloc(), block)); |
| if (!block->addPredecessor(alloc(), successor)) |
| return ControlStatus_Error; |
| } |
| successor = block; |
| } |
| |
| if (!setCurrentAndSpecializePhis(successor)) |
| return ControlStatus_Error; |
| |
| // An infinite loop (for (;;) { }) will not have a successor. |
| if (!current) |
| return ControlStatus_Ended; |
| |
| pc = current->pc(); |
| return ControlStatus_Joined; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::restartLoop(CFGState state) |
| { |
| spew("New types at loop header, restarting loop body"); |
| |
| if (JitOptions.limitScriptSize) { |
| if (++numLoopRestarts_ >= MAX_LOOP_RESTARTS) |
| return ControlStatus_Abort; |
| } |
| |
| MBasicBlock* header = state.loop.entry; |
| |
| // Discard unreferenced & pre-allocated resume points. |
| replaceMaybeFallbackFunctionGetter(nullptr); |
| |
| // Remove all blocks in the loop body other than the header, which has phis |
| // of the appropriate type and incoming edges to preserve. |
| graph().removeBlocksAfter(header); |
| |
| // Remove all instructions from the header itself, and all resume points |
| // except the entry resume point. |
| header->discardAllInstructions(); |
| header->discardAllResumePoints(/* discardEntry = */ false); |
| header->setStackDepth(header->getPredecessor(0)->stackDepth()); |
| |
| popCfgStack(); |
| |
| loopDepth_++; |
| |
| if (!pushLoop(state.loop.initialState, state.loop.initialStopAt, header, state.loop.osr, |
| state.loop.loopHead, state.loop.initialPc, |
| state.loop.bodyStart, state.loop.bodyEnd, |
| state.loop.exitpc, state.loop.continuepc)) |
| { |
| return ControlStatus_Error; |
| } |
| |
| CFGState& nstate = cfgStack_.back(); |
| |
| nstate.loop.condpc = state.loop.condpc; |
| nstate.loop.updatepc = state.loop.updatepc; |
| nstate.loop.updateEnd = state.loop.updateEnd; |
| |
| // Don't specializePhis(), as the header has been visited before and the |
| // phis have already had their type set. |
| setCurrent(header); |
| |
| if (!jsop_loophead(nstate.loop.loopHead)) |
| return ControlStatus_Error; |
| |
| pc = nstate.loop.initialPc; |
| return ControlStatus_Jumped; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processDoWhileBodyEnd(CFGState& state) |
| { |
| if (!processDeferredContinues(state)) |
| return ControlStatus_Error; |
| |
| // No current means control flow cannot reach the condition, so this will |
| // never loop. |
| if (!current) |
| return processBrokenLoop(state); |
| |
| MBasicBlock* header = newBlock(current, state.loop.updatepc); |
| if (!header) |
| return ControlStatus_Error; |
| current->end(MGoto::New(alloc(), header)); |
| |
| state.state = CFGState::DO_WHILE_LOOP_COND; |
| state.stopAt = state.loop.updateEnd; |
| pc = state.loop.updatepc; |
| if (!setCurrentAndSpecializePhis(header)) |
| return ControlStatus_Error; |
| return ControlStatus_Jumped; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processDoWhileCondEnd(CFGState& state) |
| { |
| MOZ_ASSERT(JSOp(*pc) == JSOP_IFNE); |
| |
| // We're guaranteed a |current|, it's impossible to break or return from |
| // inside the conditional expression. |
| MOZ_ASSERT(current); |
| |
| // Pop the last value, and create the successor block. |
| MDefinition* vins = current->pop(); |
| MBasicBlock* successor = newBlock(current, GetNextPc(pc), loopDepth_ - 1); |
| if (!successor) |
| return ControlStatus_Error; |
| |
| // Test for do {} while(false) and don't create a loop in that case. |
| if (vins->isConstantValue() && !vins->constantValue().isMagic()) { |
| if (!vins->constantToBoolean()) { |
| current->end(MGoto::New(alloc(), successor)); |
| current = nullptr; |
| |
| state.loop.successor = successor; |
| return processBrokenLoop(state); |
| } |
| } |
| |
| // Create the test instruction and end the current block. |
| MTest* test = newTest(vins, state.loop.entry, successor); |
| current->end(test); |
| return finishLoop(state, successor); |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processWhileCondEnd(CFGState& state) |
| { |
| MOZ_ASSERT(JSOp(*pc) == JSOP_IFNE || JSOp(*pc) == JSOP_IFEQ); |
| |
| // Balance the stack past the IFNE. |
| MDefinition* ins = current->pop(); |
| |
| // Create the body and successor blocks. |
| MBasicBlock* body = newBlock(current, state.loop.bodyStart); |
| state.loop.successor = newBlock(current, state.loop.exitpc, loopDepth_ - 1); |
| if (!body || !state.loop.successor) |
| return ControlStatus_Error; |
| |
| MTest* test; |
| if (JSOp(*pc) == JSOP_IFNE) |
| test = newTest(ins, body, state.loop.successor); |
| else |
| test = newTest(ins, state.loop.successor, body); |
| current->end(test); |
| |
| state.state = CFGState::WHILE_LOOP_BODY; |
| state.stopAt = state.loop.bodyEnd; |
| pc = state.loop.bodyStart; |
| if (!setCurrentAndSpecializePhis(body)) |
| return ControlStatus_Error; |
| |
| // Filter the types in the loop body. |
| if (!improveTypesAtTest(test->getOperand(0), test->ifTrue() == current, test)) |
| return ControlStatus_Error; |
| |
| // If this is a for-in loop, unbox the current value as string if possible. |
| if (ins->isIsNoIter()) { |
| MIteratorMore* iterMore = ins->toIsNoIter()->input()->toIteratorMore(); |
| jsbytecode* iterMorePc = iterMore->resumePoint()->pc(); |
| MOZ_ASSERT(*iterMorePc == JSOP_MOREITER); |
| |
| if (!nonStringIteration_ && !inspector->hasSeenNonStringIterMore(iterMorePc)) { |
| MDefinition* val = current->peek(-1); |
| MOZ_ASSERT(val == iterMore); |
| MInstruction* ins = MUnbox::New(alloc(), val, MIRType_String, MUnbox::Fallible, |
| Bailout_NonStringInputInvalidate); |
| current->add(ins); |
| current->rewriteAtDepth(-1, ins); |
| } |
| } |
| |
| return ControlStatus_Jumped; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processWhileBodyEnd(CFGState& state) |
| { |
| if (!processDeferredContinues(state)) |
| return ControlStatus_Error; |
| |
| if (!current) |
| return processBrokenLoop(state); |
| |
| current->end(MGoto::New(alloc(), state.loop.entry)); |
| return finishLoop(state, state.loop.successor); |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processForCondEnd(CFGState& state) |
| { |
| MOZ_ASSERT(JSOp(*pc) == JSOP_IFNE); |
| |
| // Balance the stack past the IFNE. |
| MDefinition* ins = current->pop(); |
| |
| // Create the body and successor blocks. |
| MBasicBlock* body = newBlock(current, state.loop.bodyStart); |
| state.loop.successor = newBlock(current, state.loop.exitpc, loopDepth_ - 1); |
| if (!body || !state.loop.successor) |
| return ControlStatus_Error; |
| |
| MTest* test = newTest(ins, body, state.loop.successor); |
| current->end(test); |
| |
| state.state = CFGState::FOR_LOOP_BODY; |
| state.stopAt = state.loop.bodyEnd; |
| pc = state.loop.bodyStart; |
| if (!setCurrentAndSpecializePhis(body)) |
| return ControlStatus_Error; |
| return ControlStatus_Jumped; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processForBodyEnd(CFGState& state) |
| { |
| if (!processDeferredContinues(state)) |
| return ControlStatus_Error; |
| |
| // If there is no updatepc, just go right to processing what would be the |
| // end of the update clause. Otherwise, |current| might be nullptr; if this is |
| // the case, the udpate is unreachable anyway. |
| if (!state.loop.updatepc || !current) |
| return processForUpdateEnd(state); |
| |
| pc = state.loop.updatepc; |
| |
| state.state = CFGState::FOR_LOOP_UPDATE; |
| state.stopAt = state.loop.updateEnd; |
| return ControlStatus_Jumped; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processForUpdateEnd(CFGState& state) |
| { |
| // If there is no current, we couldn't reach the loop edge and there was no |
| // update clause. |
| if (!current) |
| return processBrokenLoop(state); |
| |
| current->end(MGoto::New(alloc(), state.loop.entry)); |
| return finishLoop(state, state.loop.successor); |
| } |
| |
| IonBuilder::DeferredEdge* |
| IonBuilder::filterDeadDeferredEdges(DeferredEdge* edge) |
| { |
| DeferredEdge* head = edge; |
| DeferredEdge* prev = nullptr; |
| |
| while (edge) { |
| if (edge->block->isDead()) { |
| if (prev) |
| prev->next = edge->next; |
| else |
| head = edge->next; |
| } else { |
| prev = edge; |
| } |
| edge = edge->next; |
| } |
| |
| // There must be at least one deferred edge from a block that was not |
| // deleted; blocks are deleted when restarting processing of a loop, and |
| // the final version of the loop body will have edges from live blocks. |
| MOZ_ASSERT(head); |
| |
| return head; |
| } |
| |
| bool |
| IonBuilder::processDeferredContinues(CFGState& state) |
| { |
| // If there are any continues for this loop, and there is an update block, |
| // then we need to create a new basic block to house the update. |
| if (state.loop.continues) { |
| DeferredEdge* edge = filterDeadDeferredEdges(state.loop.continues); |
| |
| MBasicBlock* update = newBlock(edge->block, loops_.back().continuepc); |
| if (!update) |
| return false; |
| |
| if (current) { |
| current->end(MGoto::New(alloc(), update)); |
| if (!update->addPredecessor(alloc(), current)) |
| return false; |
| } |
| |
| // No need to use addPredecessor for first edge, |
| // because it is already predecessor. |
| edge->block->end(MGoto::New(alloc(), update)); |
| edge = edge->next; |
| |
| // Remaining edges |
| while (edge) { |
| edge->block->end(MGoto::New(alloc(), update)); |
| if (!update->addPredecessor(alloc(), edge->block)) |
| return false; |
| edge = edge->next; |
| } |
| state.loop.continues = nullptr; |
| |
| if (!setCurrentAndSpecializePhis(update)) |
| return ControlStatus_Error; |
| } |
| |
| return true; |
| } |
| |
| MBasicBlock* |
| IonBuilder::createBreakCatchBlock(DeferredEdge* edge, jsbytecode* pc) |
| { |
| edge = filterDeadDeferredEdges(edge); |
| |
| // Create block, using the first break statement as predecessor |
| MBasicBlock* successor = newBlock(edge->block, pc); |
| if (!successor) |
| return nullptr; |
| |
| // No need to use addPredecessor for first edge, |
| // because it is already predecessor. |
| edge->block->end(MGoto::New(alloc(), successor)); |
| edge = edge->next; |
| |
| // Finish up remaining breaks. |
| while (edge) { |
| edge->block->end(MGoto::New(alloc(), successor)); |
| if (!successor->addPredecessor(alloc(), edge->block)) |
| return nullptr; |
| edge = edge->next; |
| } |
| |
| return successor; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processNextTableSwitchCase(CFGState& state) |
| { |
| MOZ_ASSERT(state.state == CFGState::TABLE_SWITCH); |
| |
| state.tableswitch.currentBlock++; |
| |
| // Test if there are still unprocessed successors (cases/default) |
| if (state.tableswitch.currentBlock >= state.tableswitch.ins->numBlocks()) |
| return processSwitchEnd(state.tableswitch.breaks, state.tableswitch.exitpc); |
| |
| // Get the next successor |
| MBasicBlock* successor = state.tableswitch.ins->getBlock(state.tableswitch.currentBlock); |
| |
| // Add current block as predecessor if available. |
| // This means the previous case didn't have a break statement. |
| // So flow will continue in this block. |
| if (current) { |
| current->end(MGoto::New(alloc(), successor)); |
| if (!successor->addPredecessor(alloc(), current)) |
| return ControlStatus_Error; |
| } else { |
| // If this is an actual case statement, optimize by replacing the |
| // input to the switch case with the actual number of the case. |
| // This constant has been emitted when creating the case blocks. |
| if (state.tableswitch.ins->getDefault() != successor) { |
| MConstant* constant = successor->begin()->toConstant(); |
| for (uint32_t j = 0; j < successor->stackDepth(); j++) { |
| MDefinition* ins = successor->getSlot(j); |
| if (ins != state.tableswitch.ins->getOperand(0)) |
| continue; |
| |
| constant->setDependency(state.tableswitch.ins); |
| successor->setSlot(j, constant); |
| } |
| } |
| } |
| |
| // Insert successor after the current block, to maintain RPO. |
| graph().moveBlockToEnd(successor); |
| |
| // If this is the last successor the block should stop at the end of the tableswitch |
| // Else it should stop at the start of the next successor |
| if (state.tableswitch.currentBlock+1 < state.tableswitch.ins->numBlocks()) |
| state.stopAt = state.tableswitch.ins->getBlock(state.tableswitch.currentBlock+1)->pc(); |
| else |
| state.stopAt = state.tableswitch.exitpc; |
| |
| if (!setCurrentAndSpecializePhis(successor)) |
| return ControlStatus_Error; |
| pc = current->pc(); |
| return ControlStatus_Jumped; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processAndOrEnd(CFGState& state) |
| { |
| MOZ_ASSERT(current); |
| MBasicBlock* lhs = state.branch.ifFalse; |
| |
| // Create a new block to represent the join. |
| MBasicBlock* join = newBlock(current, state.stopAt); |
| if (!join) |
| return ControlStatus_Error; |
| |
| // End the rhs. |
| current->end(MGoto::New(alloc(), join)); |
| |
| // End the lhs. |
| lhs->end(MGoto::New(alloc(), join)); |
| if (!join->addPredecessor(alloc(), state.branch.ifFalse)) |
| return ControlStatus_Error; |
| |
| // Set the join path as current path. |
| if (!setCurrentAndSpecializePhis(join)) |
| return ControlStatus_Error; |
| pc = current->pc(); |
| return ControlStatus_Joined; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processLabelEnd(CFGState& state) |
| { |
| MOZ_ASSERT(state.state == CFGState::LABEL); |
| |
| // If there are no breaks and no current, controlflow is terminated. |
| if (!state.label.breaks && !current) |
| return ControlStatus_Ended; |
| |
| // If there are no breaks to this label, there's nothing to do. |
| if (!state.label.breaks) |
| return ControlStatus_Joined; |
| |
| MBasicBlock* successor = createBreakCatchBlock(state.label.breaks, state.stopAt); |
| if (!successor) |
| return ControlStatus_Error; |
| |
| if (current) { |
| current->end(MGoto::New(alloc(), successor)); |
| if (!successor->addPredecessor(alloc(), current)) |
| return ControlStatus_Error; |
| } |
| |
| pc = state.stopAt; |
| if (!setCurrentAndSpecializePhis(successor)) |
| return ControlStatus_Error; |
| return ControlStatus_Joined; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processTryEnd(CFGState& state) |
| { |
| MOZ_ASSERT(state.state == CFGState::TRY); |
| |
| if (!state.try_.successor) { |
| MOZ_ASSERT(!current); |
| return ControlStatus_Ended; |
| } |
| |
| if (current) { |
| current->end(MGoto::New(alloc(), state.try_.successor)); |
| |
| if (!state.try_.successor->addPredecessor(alloc(), current)) |
| return ControlStatus_Error; |
| } |
| |
| // Start parsing the code after this try-catch statement. |
| if (!setCurrentAndSpecializePhis(state.try_.successor)) |
| return ControlStatus_Error; |
| graph().moveBlockToEnd(current); |
| pc = current->pc(); |
| return ControlStatus_Joined; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processBreak(JSOp op, jssrcnote* sn) |
| { |
| MOZ_ASSERT(op == JSOP_GOTO); |
| |
| MOZ_ASSERT(SN_TYPE(sn) == SRC_BREAK || |
| SN_TYPE(sn) == SRC_BREAK2LABEL); |
| |
| // Find the break target. |
| jsbytecode* target = pc + GetJumpOffset(pc); |
| DebugOnly<bool> found = false; |
| |
| if (SN_TYPE(sn) == SRC_BREAK2LABEL) { |
| for (size_t i = labels_.length() - 1; i < labels_.length(); i--) { |
| CFGState& cfg = cfgStack_[labels_[i].cfgEntry]; |
| MOZ_ASSERT(cfg.state == CFGState::LABEL); |
| if (cfg.stopAt == target) { |
| cfg.label.breaks = new(alloc()) DeferredEdge(current, cfg.label.breaks); |
| found = true; |
| break; |
| } |
| } |
| } else { |
| for (size_t i = loops_.length() - 1; i < loops_.length(); i--) { |
| CFGState& cfg = cfgStack_[loops_[i].cfgEntry]; |
| MOZ_ASSERT(cfg.isLoop()); |
| if (cfg.loop.exitpc == target) { |
| cfg.loop.breaks = new(alloc()) DeferredEdge(current, cfg.loop.breaks); |
| found = true; |
| break; |
| } |
| } |
| } |
| |
| MOZ_ASSERT(found); |
| |
| setCurrent(nullptr); |
| pc += CodeSpec[op].length; |
| return processControlEnd(); |
| } |
| |
| static inline jsbytecode* |
| EffectiveContinue(jsbytecode* pc) |
| { |
| if (JSOp(*pc) == JSOP_GOTO) |
| return pc + GetJumpOffset(pc); |
| return pc; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processContinue(JSOp op) |
| { |
| MOZ_ASSERT(op == JSOP_GOTO); |
| |
| // Find the target loop. |
| CFGState* found = nullptr; |
| jsbytecode* target = pc + GetJumpOffset(pc); |
| for (size_t i = loops_.length() - 1; i < loops_.length(); i--) { |
| if (loops_[i].continuepc == target || |
| EffectiveContinue(loops_[i].continuepc) == target) |
| { |
| found = &cfgStack_[loops_[i].cfgEntry]; |
| break; |
| } |
| } |
| |
| // There must always be a valid target loop structure. If not, there's |
| // probably an off-by-something error in which pc we track. |
| MOZ_ASSERT(found); |
| CFGState& state = *found; |
| |
| state.loop.continues = new(alloc()) DeferredEdge(current, state.loop.continues); |
| |
| setCurrent(nullptr); |
| pc += CodeSpec[op].length; |
| return processControlEnd(); |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processSwitchBreak(JSOp op) |
| { |
| MOZ_ASSERT(op == JSOP_GOTO); |
| |
| // Find the target switch. |
| CFGState* found = nullptr; |
| jsbytecode* target = pc + GetJumpOffset(pc); |
| for (size_t i = switches_.length() - 1; i < switches_.length(); i--) { |
| if (switches_[i].continuepc == target) { |
| found = &cfgStack_[switches_[i].cfgEntry]; |
| break; |
| } |
| } |
| |
| // There must always be a valid target loop structure. If not, there's |
| // probably an off-by-something error in which pc we track. |
| MOZ_ASSERT(found); |
| CFGState& state = *found; |
| |
| DeferredEdge** breaks = nullptr; |
| switch (state.state) { |
| case CFGState::TABLE_SWITCH: |
| breaks = &state.tableswitch.breaks; |
| break; |
| case CFGState::COND_SWITCH_BODY: |
| breaks = &state.condswitch.breaks; |
| break; |
| default: |
| MOZ_CRASH("Unexpected switch state."); |
| } |
| |
| *breaks = new(alloc()) DeferredEdge(current, *breaks); |
| |
| setCurrent(nullptr); |
| pc += CodeSpec[op].length; |
| return processControlEnd(); |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processSwitchEnd(DeferredEdge* breaks, jsbytecode* exitpc) |
| { |
| // No break statements, no current. |
| // This means that control flow is cut-off from this point |
| // (e.g. all cases have return statements). |
| if (!breaks && !current) |
| return ControlStatus_Ended; |
| |
| // Create successor block. |
| // If there are breaks, create block with breaks as predecessor |
| // Else create a block with current as predecessor |
| MBasicBlock* successor = nullptr; |
| if (breaks) |
| successor = createBreakCatchBlock(breaks, exitpc); |
| else |
| successor = newBlock(current, exitpc); |
| |
| if (!successor) |
| return ControlStatus_Ended; |
| |
| // If there is current, the current block flows into this one. |
| // So current is also a predecessor to this block |
| if (current) { |
| current->end(MGoto::New(alloc(), successor)); |
| if (breaks) { |
| if (!successor->addPredecessor(alloc(), current)) |
| return ControlStatus_Error; |
| } |
| } |
| |
| pc = exitpc; |
| if (!setCurrentAndSpecializePhis(successor)) |
| return ControlStatus_Error; |
| return ControlStatus_Joined; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::maybeLoop(JSOp op, jssrcnote* sn) |
| { |
| // This function looks at the opcode and source note and tries to |
| // determine the structure of the loop. For some opcodes, like |
| // POP/NOP which are not explicitly control flow, this source note is |
| // optional. For opcodes with control flow, like GOTO, an unrecognized |
| // or not-present source note is a compilation failure. |
| switch (op) { |
| case JSOP_POP: |
| // for (init; ; update?) ... |
| if (sn && SN_TYPE(sn) == SRC_FOR) { |
| current->pop(); |
| return forLoop(op, sn); |
| } |
| break; |
| |
| case JSOP_NOP: |
| if (sn) { |
| // do { } while (cond) |
| if (SN_TYPE(sn) == SRC_WHILE) |
| return doWhileLoop(op, sn); |
| // Build a mapping such that given a basic block, whose successor |
| // has a phi |
| |
| // for (; ; update?) |
| if (SN_TYPE(sn) == SRC_FOR) |
| return forLoop(op, sn); |
| } |
| break; |
| |
| default: |
| MOZ_CRASH("unexpected opcode"); |
| } |
| |
| return ControlStatus_None; |
| } |
| |
| void |
| IonBuilder::assertValidLoopHeadOp(jsbytecode* pc) |
| { |
| #ifdef DEBUG |
| MOZ_ASSERT(JSOp(*pc) == JSOP_LOOPHEAD); |
| |
| // Make sure this is the next opcode after the loop header, |
| // unless the for loop is unconditional. |
| CFGState& state = cfgStack_.back(); |
| MOZ_ASSERT_IF((JSOp)*(state.loop.entry->pc()) == JSOP_GOTO, |
| GetNextPc(state.loop.entry->pc()) == pc); |
| |
| // do-while loops have a source note. |
| jssrcnote* sn = info().getNote(gsn, pc); |
| if (sn) { |
| jsbytecode* ifne = pc + GetSrcNoteOffset(sn, 0); |
| |
| jsbytecode* expected_ifne; |
| switch (state.state) { |
| case CFGState::DO_WHILE_LOOP_BODY: |
| expected_ifne = state.loop.updateEnd; |
| break; |
| |
| default: |
| MOZ_CRASH("JSOP_LOOPHEAD unexpected source note"); |
| } |
| |
| // Make sure this loop goes to the same ifne as the loop header's |
| // source notes or GOTO. |
| MOZ_ASSERT(ifne == expected_ifne); |
| } else { |
| MOZ_ASSERT(state.state != CFGState::DO_WHILE_LOOP_BODY); |
| } |
| #endif |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::doWhileLoop(JSOp op, jssrcnote* sn) |
| { |
| // do { } while() loops have the following structure: |
| // NOP ; SRC_WHILE (offset to COND) |
| // LOOPHEAD ; SRC_WHILE (offset to IFNE) |
| // LOOPENTRY |
| // ... ; body |
| // ... |
| // COND ; start of condition |
| // ... |
| // IFNE -> ; goes to LOOPHEAD |
| int condition_offset = GetSrcNoteOffset(sn, 0); |
| jsbytecode* conditionpc = pc + condition_offset; |
| |
| jssrcnote* sn2 = info().getNote(gsn, pc+1); |
| int offset = GetSrcNoteOffset(sn2, 0); |
| jsbytecode* ifne = pc + offset + 1; |
| MOZ_ASSERT(ifne > pc); |
| |
| // Verify that the IFNE goes back to a loophead op. |
| jsbytecode* loopHead = GetNextPc(pc); |
| MOZ_ASSERT(JSOp(*loopHead) == JSOP_LOOPHEAD); |
| MOZ_ASSERT(loopHead == ifne + GetJumpOffset(ifne)); |
| |
| jsbytecode* loopEntry = GetNextPc(loopHead); |
| bool canOsr = LoopEntryCanIonOsr(loopEntry); |
| bool osr = info().hasOsrAt(loopEntry); |
| |
| if (osr) { |
| MBasicBlock* preheader = newOsrPreheader(current, loopEntry, pc); |
| if (!preheader) |
| return ControlStatus_Error; |
| current->end(MGoto::New(alloc(), preheader)); |
| if (!setCurrentAndSpecializePhis(preheader)) |
| return ControlStatus_Error; |
| } |
| |
| unsigned stackPhiCount = 0; |
| MBasicBlock* header = newPendingLoopHeader(current, loopEntry, osr, canOsr, stackPhiCount); |
| if (!header) |
| return ControlStatus_Error; |
| current->end(MGoto::New(alloc(), header)); |
| |
| jsbytecode* loophead = GetNextPc(pc); |
| jsbytecode* bodyStart = GetNextPc(loophead); |
| jsbytecode* bodyEnd = conditionpc; |
| jsbytecode* exitpc = GetNextPc(ifne); |
| if (!analyzeNewLoopTypes(header, bodyStart, exitpc)) |
| return ControlStatus_Error; |
| if (!pushLoop(CFGState::DO_WHILE_LOOP_BODY, conditionpc, header, osr, |
| loopHead, bodyStart, bodyStart, bodyEnd, exitpc, conditionpc)) |
| { |
| return ControlStatus_Error; |
| } |
| |
| CFGState& state = cfgStack_.back(); |
| state.loop.updatepc = conditionpc; |
| state.loop.updateEnd = ifne; |
| |
| if (!setCurrentAndSpecializePhis(header)) |
| return ControlStatus_Error; |
| if (!jsop_loophead(loophead)) |
| return ControlStatus_Error; |
| |
| pc = bodyStart; |
| return ControlStatus_Jumped; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::whileOrForInLoop(jssrcnote* sn) |
| { |
| // while (cond) { } loops have the following structure: |
| // GOTO cond ; SRC_WHILE (offset to IFNE) |
| // LOOPHEAD |
| // ... |
| // cond: |
| // LOOPENTRY |
| // ... |
| // IFNE ; goes to LOOPHEAD |
| // for (x in y) { } loops are similar; the cond will be a MOREITER. |
| MOZ_ASSERT(SN_TYPE(sn) == SRC_FOR_OF || SN_TYPE(sn) == SRC_FOR_IN || SN_TYPE(sn) == SRC_WHILE); |
| int ifneOffset = GetSrcNoteOffset(sn, 0); |
| jsbytecode* ifne = pc + ifneOffset; |
| MOZ_ASSERT(ifne > pc); |
| |
| // Verify that the IFNE goes back to a loophead op. |
| MOZ_ASSERT(JSOp(*GetNextPc(pc)) == JSOP_LOOPHEAD); |
| MOZ_ASSERT(GetNextPc(pc) == ifne + GetJumpOffset(ifne)); |
| |
| jsbytecode* loopEntry = pc + GetJumpOffset(pc); |
| bool canOsr = LoopEntryCanIonOsr(loopEntry); |
| bool osr = info().hasOsrAt(loopEntry); |
| |
| if (osr) { |
| MBasicBlock* preheader = newOsrPreheader(current, loopEntry, pc); |
| if (!preheader) |
| return ControlStatus_Error; |
| current->end(MGoto::New(alloc(), preheader)); |
| if (!setCurrentAndSpecializePhis(preheader)) |
| return ControlStatus_Error; |
| } |
| |
| unsigned stackPhiCount; |
| if (SN_TYPE(sn) == SRC_FOR_OF) |
| stackPhiCount = 2; |
| else if (SN_TYPE(sn) == SRC_FOR_IN) |
| stackPhiCount = 1; |
| else |
| stackPhiCount = 0; |
| |
| MBasicBlock* header = newPendingLoopHeader(current, loopEntry, osr, canOsr, stackPhiCount); |
| if (!header) |
| return ControlStatus_Error; |
| current->end(MGoto::New(alloc(), header)); |
| |
| // Skip past the JSOP_LOOPHEAD for the body start. |
| jsbytecode* loopHead = GetNextPc(pc); |
| jsbytecode* bodyStart = GetNextPc(loopHead); |
| jsbytecode* bodyEnd = pc + GetJumpOffset(pc); |
| jsbytecode* exitpc = GetNextPc(ifne); |
| jsbytecode* continuepc = pc; |
| if (!analyzeNewLoopTypes(header, bodyStart, exitpc)) |
| return ControlStatus_Error; |
| if (!pushLoop(CFGState::WHILE_LOOP_COND, ifne, header, osr, |
| loopHead, bodyEnd, bodyStart, bodyEnd, exitpc, continuepc)) |
| { |
| return ControlStatus_Error; |
| } |
| |
| // Parse the condition first. |
| if (!setCurrentAndSpecializePhis(header)) |
| return ControlStatus_Error; |
| if (!jsop_loophead(loopHead)) |
| return ControlStatus_Error; |
| |
| pc = bodyEnd; |
| return ControlStatus_Jumped; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::forLoop(JSOp op, jssrcnote* sn) |
| { |
| // Skip the NOP. |
| MOZ_ASSERT(op == JSOP_NOP); |
| pc = GetNextPc(pc); |
| |
| jsbytecode* condpc = pc + GetSrcNoteOffset(sn, 0); |
| jsbytecode* updatepc = pc + GetSrcNoteOffset(sn, 1); |
| jsbytecode* ifne = pc + GetSrcNoteOffset(sn, 2); |
| jsbytecode* exitpc = GetNextPc(ifne); |
| |
| // for loops have the following structures: |
| // |
| // NOP or POP |
| // [GOTO cond | NOP] |
| // LOOPHEAD |
| // body: |
| // ; [body] |
| // [increment:] |
| // [FRESHENBLOCKSCOPE, if needed by a cloned block] |
| // ; [increment] |
| // [cond:] |
| // LOOPENTRY |
| // GOTO body |
| // |
| // If there is a condition (condpc != ifne), this acts similar to a while |
| // loop otherwise, it acts like a do-while loop. |
| // |
| // Note that currently Ion does not compile pushblockscope/popblockscope as |
| // necessary prerequisites to freshenblockscope. So the code below doesn't |
| // and needn't consider the implications of freshenblockscope. |
| jsbytecode* bodyStart = pc; |
| jsbytecode* bodyEnd = updatepc; |
| jsbytecode* loopEntry = condpc; |
| if (condpc != ifne) { |
| MOZ_ASSERT(JSOp(*bodyStart) == JSOP_GOTO); |
| MOZ_ASSERT(bodyStart + GetJumpOffset(bodyStart) == condpc); |
| bodyStart = GetNextPc(bodyStart); |
| } else { |
| // No loop condition, such as for(j = 0; ; j++) |
| if (op != JSOP_NOP) { |
| // If the loop starts with POP, we have to skip a NOP. |
| MOZ_ASSERT(JSOp(*bodyStart) == JSOP_NOP); |
| bodyStart = GetNextPc(bodyStart); |
| } |
| loopEntry = GetNextPc(bodyStart); |
| } |
| jsbytecode* loopHead = bodyStart; |
| MOZ_ASSERT(JSOp(*bodyStart) == JSOP_LOOPHEAD); |
| MOZ_ASSERT(ifne + GetJumpOffset(ifne) == bodyStart); |
| bodyStart = GetNextPc(bodyStart); |
| |
| bool osr = info().hasOsrAt(loopEntry); |
| bool canOsr = LoopEntryCanIonOsr(loopEntry); |
| |
| if (osr) { |
| MBasicBlock* preheader = newOsrPreheader(current, loopEntry, pc); |
| if (!preheader) |
| return ControlStatus_Error; |
| current->end(MGoto::New(alloc(), preheader)); |
| if (!setCurrentAndSpecializePhis(preheader)) |
| return ControlStatus_Error; |
| } |
| |
| unsigned stackPhiCount = 0; |
| MBasicBlock* header = newPendingLoopHeader(current, loopEntry, osr, canOsr, stackPhiCount); |
| if (!header) |
| return ControlStatus_Error; |
| current->end(MGoto::New(alloc(), header)); |
| |
| // If there is no condition, we immediately parse the body. Otherwise, we |
| // parse the condition. |
| jsbytecode* stopAt; |
| CFGState::State initial; |
| if (condpc != ifne) { |
| pc = condpc; |
| stopAt = ifne; |
| initial = CFGState::FOR_LOOP_COND; |
| } else { |
| pc = bodyStart; |
| stopAt = bodyEnd; |
| initial = CFGState::FOR_LOOP_BODY; |
| } |
| |
| if (!analyzeNewLoopTypes(header, bodyStart, exitpc)) |
| return ControlStatus_Error; |
| if (!pushLoop(initial, stopAt, header, osr, |
| loopHead, pc, bodyStart, bodyEnd, exitpc, updatepc)) |
| { |
| return ControlStatus_Error; |
| } |
| |
| CFGState& state = cfgStack_.back(); |
| state.loop.condpc = (condpc != ifne) ? condpc : nullptr; |
| state.loop.updatepc = (updatepc != condpc) ? updatepc : nullptr; |
| if (state.loop.updatepc) |
| state.loop.updateEnd = condpc; |
| |
| if (!setCurrentAndSpecializePhis(header)) |
| return ControlStatus_Error; |
| if (!jsop_loophead(loopHead)) |
| return ControlStatus_Error; |
| |
| return ControlStatus_Jumped; |
| } |
| |
| int |
| IonBuilder::CmpSuccessors(const void* a, const void* b) |
| { |
| const MBasicBlock* a0 = * (MBasicBlock * const*)a; |
| const MBasicBlock* b0 = * (MBasicBlock * const*)b; |
| if (a0->pc() == b0->pc()) |
| return 0; |
| |
| return (a0->pc() > b0->pc()) ? 1 : -1; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::tableSwitch(JSOp op, jssrcnote* sn) |
| { |
| // TableSwitch op contains the following data |
| // (length between data is JUMP_OFFSET_LEN) |
| // |
| // 0: Offset of default case |
| // 1: Lowest number in tableswitch |
| // 2: Highest number in tableswitch |
| // 3: Offset of case low |
| // 4: Offset of case low+1 |
| // .: ... |
| // .: Offset of case high |
| |
| MOZ_ASSERT(op == JSOP_TABLESWITCH); |
| MOZ_ASSERT(SN_TYPE(sn) == SRC_TABLESWITCH); |
| |
| // Pop input. |
| MDefinition* ins = current->pop(); |
| |
| // Get the default and exit pc |
| jsbytecode* exitpc = pc + GetSrcNoteOffset(sn, 0); |
| jsbytecode* defaultpc = pc + GET_JUMP_OFFSET(pc); |
| |
| MOZ_ASSERT(defaultpc > pc && defaultpc <= exitpc); |
| |
| // Get the low and high from the tableswitch |
| jsbytecode* pc2 = pc; |
| pc2 += JUMP_OFFSET_LEN; |
| int low = GET_JUMP_OFFSET(pc2); |
| pc2 += JUMP_OFFSET_LEN; |
| int high = GET_JUMP_OFFSET(pc2); |
| pc2 += JUMP_OFFSET_LEN; |
| |
| // Create MIR instruction |
| MTableSwitch* tableswitch = MTableSwitch::New(alloc(), ins, low, high); |
| |
| // Create default case |
| MBasicBlock* defaultcase = newBlock(current, defaultpc); |
| if (!defaultcase) |
| return ControlStatus_Error; |
| |
| if (!tableswitch->addDefault(defaultcase)) |
| return ControlStatus_Error; |
| |
| if (!tableswitch->addBlock(defaultcase)) |
| return ControlStatus_Error; |
| |
| // Create cases |
| jsbytecode* casepc = nullptr; |
| for (int i = 0; i < high-low+1; i++) { |
| casepc = pc + GET_JUMP_OFFSET(pc2); |
| |
| MOZ_ASSERT(casepc >= pc && casepc <= exitpc); |
| MBasicBlock* caseblock; |
| |
| if (casepc == pc) { |
| // If the casepc equals the current pc, it is not a written case, |
| // but a filled gap. That way we can use a tableswitch instead of |
| // condswitch, even if not all numbers are consecutive. |
| // In that case this block goes to the default case |
| caseblock = newBlock(current, defaultpc); |
| if (!caseblock) |
| return ControlStatus_Error; |
| caseblock->end(MGoto::New(alloc(), defaultcase)); |
| if (!defaultcase->addPredecessor(alloc(), caseblock)) |
| return ControlStatus_Error; |
| } else { |
| // If this is an actual case (not filled gap), |
| // add this block to the list that still needs to get processed. |
| caseblock = newBlock(current, casepc); |
| if (!caseblock) |
| return ControlStatus_Error; |
| |
| if (!tableswitch->addBlock(caseblock)) |
| return ControlStatus_Error; |
| |
| // Add constant to indicate which case this is for use by |
| // processNextTableSwitchCase. |
| MConstant* constant = MConstant::New(alloc(), Int32Value(i + low)); |
| caseblock->add(constant); |
| } |
| |
| size_t caseIndex; |
| if (!tableswitch->addSuccessor(caseblock, &caseIndex)) |
| return ControlStatus_Error; |
| |
| if (!tableswitch->addCase(caseIndex)) |
| return ControlStatus_Error; |
| |
| pc2 += JUMP_OFFSET_LEN; |
| } |
| |
| // Move defaultcase to the end, to maintain RPO. |
| graph().moveBlockToEnd(defaultcase); |
| |
| MOZ_ASSERT(tableswitch->numCases() == (uint32_t)(high - low + 1)); |
| MOZ_ASSERT(tableswitch->numSuccessors() > 0); |
| |
| // Sort the list of blocks that still needs to get processed by pc |
| qsort(tableswitch->blocks(), tableswitch->numBlocks(), |
| sizeof(MBasicBlock*), CmpSuccessors); |
| |
| // Create info |
| ControlFlowInfo switchinfo(cfgStack_.length(), exitpc); |
| if (!switches_.append(switchinfo)) |
| return ControlStatus_Error; |
| |
| // Use a state to retrieve some information |
| CFGState state = CFGState::TableSwitch(exitpc, tableswitch); |
| |
| // Save the MIR instruction as last instruction of this block. |
| current->end(tableswitch); |
| |
| // If there is only one successor the block should stop at the end of the switch |
| // Else it should stop at the start of the next successor |
| if (tableswitch->numBlocks() > 1) |
| state.stopAt = tableswitch->getBlock(1)->pc(); |
| if (!setCurrentAndSpecializePhis(tableswitch->getBlock(0))) |
| return ControlStatus_Error; |
| |
| if (!cfgStack_.append(state)) |
| return ControlStatus_Error; |
| |
| pc = current->pc(); |
| return ControlStatus_Jumped; |
| } |
| |
| bool |
| IonBuilder::replaceTypeSet(MDefinition* subject, TemporaryTypeSet* type, MTest* test) |
| { |
| if (type->unknown()) |
| return true; |
| |
| if (subject->resultTypeSet() && type->equals(subject->resultTypeSet())) |
| return true; |
| |
| MInstruction* replace = nullptr; |
| MDefinition* ins; |
| |
| for (uint32_t i = 0; i < current->stackDepth(); i++) { |
| ins = current->getSlot(i); |
| |
| // Instead of creating a new MFilterTypeSet, try to update the old one. |
| if (ins->isFilterTypeSet() && ins->getOperand(0) == subject && |
| ins->dependency() == test) |
| { |
| TemporaryTypeSet* intersect = |
| TypeSet::intersectSets(ins->resultTypeSet(), type, alloc_->lifoAlloc()); |
| if (!intersect) |
| return false; |
| |
| ins->toFilterTypeSet()->setResultType(intersect->getKnownMIRType()); |
| ins->toFilterTypeSet()->setResultTypeSet(intersect); |
| |
| if (ins->type() == MIRType_Undefined) |
| current->setSlot(i, constant(UndefinedValue())); |
| if (ins->type() == MIRType_Null) |
| current->setSlot(i, constant(NullValue())); |
| continue; |
| } |
| |
| if (ins == subject) { |
| if (!replace) { |
| replace = MFilterTypeSet::New(alloc(), subject, type); |
| if (!replace) |
| return false; |
| |
| current->add(replace); |
| |
| // Make sure we don't hoist it above the MTest, we can use the |
| // 'dependency' of an MInstruction. This is normally used by |
| // Alias Analysis, but won't get overwritten, since this |
| // instruction doesn't have an AliasSet. |
| replace->setDependency(test); |
| |
| if (replace->type() == MIRType_Undefined) |
| replace = constant(UndefinedValue()); |
| if (replace->type() == MIRType_Null) |
| replace = constant(NullValue()); |
| } |
| current->setSlot(i, replace); |
| } |
| } |
| return true; |
| } |
| |
| bool |
| IonBuilder::detectAndOrStructure(MPhi* ins, bool* branchIsAnd) |
| { |
| // Look for a triangle pattern: |
| // |
| // initialBlock |
| // / | |
| // branchBlock | |
| // \ | |
| // testBlock |
| // |
| // Where ins is a phi from testBlock which combines two values |
| // pushed onto the stack by initialBlock and branchBlock. |
| |
| if (ins->numOperands() != 2) |
| return false; |
| |
| MBasicBlock* testBlock = ins->block(); |
| MOZ_ASSERT(testBlock->numPredecessors() == 2); |
| |
| MBasicBlock* initialBlock; |
| MBasicBlock* branchBlock; |
| if (testBlock->getPredecessor(0)->lastIns()->isTest()) { |
| initialBlock = testBlock->getPredecessor(0); |
| branchBlock = testBlock->getPredecessor(1); |
| } else if (testBlock->getPredecessor(1)->lastIns()->isTest()) { |
| initialBlock = testBlock->getPredecessor(1); |
| branchBlock = testBlock->getPredecessor(0); |
| } else { |
| return false; |
| } |
| |
| if (branchBlock->numSuccessors() != 1) |
| return false; |
| |
| if (branchBlock->numPredecessors() != 1 || branchBlock->getPredecessor(0) != initialBlock) |
| return false; |
| |
| if (initialBlock->numSuccessors() != 2) |
| return false; |
| |
| MDefinition* branchResult = ins->getOperand(testBlock->indexForPredecessor(branchBlock)); |
| MDefinition* initialResult = ins->getOperand(testBlock->indexForPredecessor(initialBlock)); |
| |
| if (branchBlock->stackDepth() != initialBlock->stackDepth()) |
| return false; |
| if (branchBlock->stackDepth() != testBlock->stackDepth() + 1) |
| return false; |
| if (branchResult != branchBlock->peek(-1) || initialResult != initialBlock->peek(-1)) |
| return false; |
| |
| MTest* initialTest = initialBlock->lastIns()->toTest(); |
| bool branchIsTrue = branchBlock == initialTest->ifTrue(); |
| if (initialTest->input() == ins->getOperand(0)) |
| *branchIsAnd = branchIsTrue != (testBlock->getPredecessor(0) == branchBlock); |
| else if (initialTest->input() == ins->getOperand(1)) |
| *branchIsAnd = branchIsTrue != (testBlock->getPredecessor(1) == branchBlock); |
| else |
| return false; |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::improveTypesAtCompare(MCompare* ins, bool trueBranch, MTest* test) |
| { |
| if (ins->compareType() == MCompare::Compare_Undefined || |
| ins->compareType() == MCompare::Compare_Null) |
| { |
| return improveTypesAtNullOrUndefinedCompare(ins, trueBranch, test); |
| } |
| |
| if ((ins->lhs()->isTypeOf() || ins->rhs()->isTypeOf()) && |
| (ins->lhs()->isConstantValue() || ins->rhs()->isConstantValue())) |
| { |
| return improveTypesAtTypeOfCompare(ins, trueBranch, test); |
| } |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::improveTypesAtTypeOfCompare(MCompare* ins, bool trueBranch, MTest* test) |
| { |
| MTypeOf* typeOf = ins->lhs()->isTypeOf() ? ins->lhs()->toTypeOf() : ins->rhs()->toTypeOf(); |
| const Value* constant = |
| ins->lhs()->isConstant() ? ins->lhs()->constantVp() : ins->rhs()->constantVp(); |
| |
| if (!constant->isString()) |
| return true; |
| |
| bool equal = ins->jsop() == JSOP_EQ || ins->jsop() == JSOP_STRICTEQ; |
| bool notEqual = ins->jsop() == JSOP_NE || ins->jsop() == JSOP_STRICTNE; |
| |
| if (notEqual) |
| trueBranch = !trueBranch; |
| |
| // Relational compares not supported. |
| if (!equal && !notEqual) |
| return true; |
| |
| MDefinition* subject = typeOf->input(); |
| TemporaryTypeSet* inputTypes = subject->resultTypeSet(); |
| |
| // Create temporary typeset equal to the type if there is no resultTypeSet. |
| TemporaryTypeSet tmp; |
| if (!inputTypes) { |
| if (subject->type() == MIRType_Value) |
| return true; |
| inputTypes = &tmp; |
| tmp.addType(TypeSet::PrimitiveType(ValueTypeFromMIRType(subject->type())), alloc_->lifoAlloc()); |
| } |
| |
| if (inputTypes->unknown()) |
| return true; |
| |
| // Note: we cannot remove the AnyObject type in the false branch, |
| // since there are multiple ways to get an object. That is the reason |
| // for the 'trueBranch' test. |
| TemporaryTypeSet filter; |
| const JSAtomState& names = GetJitContext()->runtime->names(); |
| if (constant->toString() == TypeName(JSTYPE_VOID, names)) { |
| filter.addType(TypeSet::UndefinedType(), alloc_->lifoAlloc()); |
| if (typeOf->inputMaybeCallableOrEmulatesUndefined() && trueBranch) |
| filter.addType(TypeSet::AnyObjectType(), alloc_->lifoAlloc()); |
| } else if (constant->toString() == TypeName(JSTYPE_BOOLEAN, names)) { |
| filter.addType(TypeSet::BooleanType(), alloc_->lifoAlloc()); |
| } else if (constant->toString() == TypeName(JSTYPE_NUMBER, names)) { |
| filter.addType(TypeSet::Int32Type(), alloc_->lifoAlloc()); |
| filter.addType(TypeSet::DoubleType(), alloc_->lifoAlloc()); |
| } else if (constant->toString() == TypeName(JSTYPE_STRING, names)) { |
| filter.addType(TypeSet::StringType(), alloc_->lifoAlloc()); |
| } else if (constant->toString() == TypeName(JSTYPE_SYMBOL, names)) { |
| filter.addType(TypeSet::SymbolType(), alloc_->lifoAlloc()); |
| } else if (constant->toString() == TypeName(JSTYPE_OBJECT, names)) { |
| filter.addType(TypeSet::NullType(), alloc_->lifoAlloc()); |
| if (trueBranch) |
| filter.addType(TypeSet::AnyObjectType(), alloc_->lifoAlloc()); |
| } else if (constant->toString() == TypeName(JSTYPE_FUNCTION, names)) { |
| if (typeOf->inputMaybeCallableOrEmulatesUndefined() && trueBranch) |
| filter.addType(TypeSet::AnyObjectType(), alloc_->lifoAlloc()); |
| } else { |
| return true; |
| } |
| |
| TemporaryTypeSet* type; |
| if (trueBranch) |
| type = TypeSet::intersectSets(&filter, inputTypes, alloc_->lifoAlloc()); |
| else |
| type = TypeSet::removeSet(inputTypes, &filter, alloc_->lifoAlloc()); |
| |
| if (!type) |
| return false; |
| |
| return replaceTypeSet(subject, type, test); |
| } |
| |
| bool |
| IonBuilder::improveTypesAtNullOrUndefinedCompare(MCompare* ins, bool trueBranch, MTest* test) |
| { |
| MOZ_ASSERT(ins->compareType() == MCompare::Compare_Undefined || |
| ins->compareType() == MCompare::Compare_Null); |
| |
| // altersUndefined/Null represents if we can filter/set Undefined/Null. |
| bool altersUndefined, altersNull; |
| JSOp op = ins->jsop(); |
| |
| switch(op) { |
| case JSOP_STRICTNE: |
| case JSOP_STRICTEQ: |
| altersUndefined = ins->compareType() == MCompare::Compare_Undefined; |
| altersNull = ins->compareType() == MCompare::Compare_Null; |
| break; |
| case JSOP_NE: |
| case JSOP_EQ: |
| altersUndefined = altersNull = true; |
| break; |
| default: |
| MOZ_CRASH("Relational compares not supported"); |
| } |
| |
| MDefinition* subject = ins->lhs(); |
| TemporaryTypeSet* inputTypes = subject->resultTypeSet(); |
| |
| MOZ_ASSERT(IsNullOrUndefined(ins->rhs()->type())); |
| |
| // Create temporary typeset equal to the type if there is no resultTypeSet. |
| TemporaryTypeSet tmp; |
| if (!inputTypes) { |
| if (subject->type() == MIRType_Value) |
| return true; |
| inputTypes = &tmp; |
| tmp.addType(TypeSet::PrimitiveType(ValueTypeFromMIRType(subject->type())), alloc_->lifoAlloc()); |
| } |
| |
| if (inputTypes->unknown()) |
| return true; |
| |
| TemporaryTypeSet* type; |
| |
| // Decide if we need to filter the type or set it. |
| if ((op == JSOP_STRICTEQ || op == JSOP_EQ) ^ trueBranch) { |
| // Remove undefined/null |
| TemporaryTypeSet remove; |
| if (altersUndefined) |
| remove.addType(TypeSet::UndefinedType(), alloc_->lifoAlloc()); |
| if (altersNull) |
| remove.addType(TypeSet::NullType(), alloc_->lifoAlloc()); |
| |
| type = TypeSet::removeSet(inputTypes, &remove, alloc_->lifoAlloc()); |
| } else { |
| // Set undefined/null. |
| TemporaryTypeSet base; |
| if (altersUndefined) { |
| base.addType(TypeSet::UndefinedType(), alloc_->lifoAlloc()); |
| // If TypeSet emulates undefined, then we cannot filter the objects. |
| if (inputTypes->maybeEmulatesUndefined(constraints())) |
| base.addType(TypeSet::AnyObjectType(), alloc_->lifoAlloc()); |
| } |
| |
| if (altersNull) |
| base.addType(TypeSet::NullType(), alloc_->lifoAlloc()); |
| |
| type = TypeSet::intersectSets(&base, inputTypes, alloc_->lifoAlloc()); |
| } |
| |
| if (!type) |
| return false; |
| |
| return replaceTypeSet(subject, type, test); |
| } |
| |
| bool |
| IonBuilder::improveTypesAtTest(MDefinition* ins, bool trueBranch, MTest* test) |
| { |
| // We explore the test condition to try and deduce as much type information |
| // as possible. |
| |
| // All branches of this switch that don't want to fall through to the |
| // default behavior must return. The default behavior assumes that a true |
| // test means the incoming ins is not null or undefined and that a false |
| // tests means it's one of null, undefined, false, 0, "", and objects |
| // emulating undefined |
| switch (ins->op()) { |
| case MDefinition::Op_Not: |
| return improveTypesAtTest(ins->toNot()->getOperand(0), !trueBranch, test); |
| case MDefinition::Op_IsObject: { |
| MDefinition* subject = ins->getOperand(0); |
| TemporaryTypeSet* oldType = subject->resultTypeSet(); |
| |
| // Create temporary typeset equal to the type if there is no resultTypeSet. |
| TemporaryTypeSet tmp; |
| if (!oldType) { |
| if (subject->type() == MIRType_Value) |
| return true; |
| oldType = &tmp; |
| tmp.addType(TypeSet::PrimitiveType(ValueTypeFromMIRType(subject->type())), alloc_->lifoAlloc()); |
| } |
| |
| if (oldType->unknown()) |
| return true; |
| |
| TemporaryTypeSet* type = nullptr; |
| if (trueBranch) |
| type = oldType->cloneObjectsOnly(alloc_->lifoAlloc()); |
| else |
| type = oldType->cloneWithoutObjects(alloc_->lifoAlloc()); |
| |
| if (!type) |
| return false; |
| |
| return replaceTypeSet(subject, type, test); |
| } |
| case MDefinition::Op_Phi: { |
| bool branchIsAnd = true; |
| if (!detectAndOrStructure(ins->toPhi(), &branchIsAnd)) { |
| // Just fall through to the default behavior. |
| break; |
| } |
| |
| // Now we have detected the triangular structure and determined if it |
| // was an AND or an OR. |
| if (branchIsAnd) { |
| if (trueBranch) { |
| if (!improveTypesAtTest(ins->toPhi()->getOperand(0), true, test)) |
| return false; |
| if (!improveTypesAtTest(ins->toPhi()->getOperand(1), true, test)) |
| return false; |
| } |
| } else { |
| /* |
| * if (a || b) { |
| * ... |
| * } else { |
| * ... |
| * } |
| * |
| * If we have a statements like the one described above, |
| * And we are in the else branch of it. It amounts to: |
| * if (!(a || b)) and being in the true branch. |
| * |
| * Simplifying, we have (!a && !b) |
| * In this case we can use the same logic we use for branchIsAnd |
| * |
| */ |
| if (!trueBranch) { |
| if (!improveTypesAtTest(ins->toPhi()->getOperand(0), false, test)) |
| return false; |
| if (!improveTypesAtTest(ins->toPhi()->getOperand(1), false, test)) |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| case MDefinition::Op_Compare: |
| return improveTypesAtCompare(ins->toCompare(), trueBranch, test); |
| |
| default: |
| break; |
| } |
| |
| // By default MTest tests ToBoolean(input). As a result in the true branch we can filter |
| // undefined and null. In false branch we can only encounter undefined, null, false, 0, "" |
| // and objects that emulate undefined. |
| |
| TemporaryTypeSet* oldType = ins->resultTypeSet(); |
| TemporaryTypeSet* type; |
| |
| // Create temporary typeset equal to the type if there is no resultTypeSet. |
| TemporaryTypeSet tmp; |
| if (!oldType) { |
| if (ins->type() == MIRType_Value) |
| return true; |
| oldType = &tmp; |
| tmp.addType(TypeSet::PrimitiveType(ValueTypeFromMIRType(ins->type())), alloc_->lifoAlloc()); |
| } |
| |
| // If ins does not have a typeset we return as we cannot optimize. |
| if (oldType->unknown()) |
| return true; |
| |
| // Decide either to set or remove. |
| if (trueBranch) { |
| TemporaryTypeSet remove; |
| remove.addType(TypeSet::UndefinedType(), alloc_->lifoAlloc()); |
| remove.addType(TypeSet::NullType(), alloc_->lifoAlloc()); |
| type = TypeSet::removeSet(oldType, &remove, alloc_->lifoAlloc()); |
| } else { |
| TemporaryTypeSet base; |
| base.addType(TypeSet::UndefinedType(), alloc_->lifoAlloc()); // ToBoolean(undefined) == false |
| base.addType(TypeSet::NullType(), alloc_->lifoAlloc()); // ToBoolean(null) == false |
| base.addType(TypeSet::BooleanType(), alloc_->lifoAlloc()); // ToBoolean(false) == false |
| base.addType(TypeSet::Int32Type(), alloc_->lifoAlloc()); // ToBoolean(0) == false |
| base.addType(TypeSet::DoubleType(), alloc_->lifoAlloc()); // ToBoolean(0.0) == false |
| base.addType(TypeSet::StringType(), alloc_->lifoAlloc()); // ToBoolean("") == false |
| |
| // If the typeset does emulate undefined, then we cannot filter out |
| // objects. |
| if (oldType->maybeEmulatesUndefined(constraints())) |
| base.addType(TypeSet::AnyObjectType(), alloc_->lifoAlloc()); |
| |
| type = TypeSet::intersectSets(&base, oldType, alloc_->lifoAlloc()); |
| } |
| |
| return type && replaceTypeSet(ins, type, test); |
| } |
| |
| bool |
| IonBuilder::jsop_label() |
| { |
| MOZ_ASSERT(JSOp(*pc) == JSOP_LABEL); |
| |
| jsbytecode* endpc = pc + GET_JUMP_OFFSET(pc); |
| MOZ_ASSERT(endpc > pc); |
| |
| ControlFlowInfo label(cfgStack_.length(), endpc); |
| if (!labels_.append(label)) |
| return false; |
| |
| return cfgStack_.append(CFGState::Label(endpc)); |
| } |
| |
| bool |
| IonBuilder::jsop_condswitch() |
| { |
| // CondSwitch op looks as follows: |
| // condswitch [length +exit_pc; first case offset +next-case ] |
| // { |
| // { |
| // ... any code ... |
| // case (+jump) [pcdelta offset +next-case] |
| // }+ |
| // default (+jump) |
| // ... jump targets ... |
| // } |
| // |
| // The default case is always emitted even if there is no default case in |
| // the source. The last case statement pcdelta source note might have a 0 |
| // offset on the last case (not all the time). |
| // |
| // A conditional evaluate the condition of each case and compare it to the |
| // switch value with a strict equality. Cases conditions are iterated |
| // linearly until one is matching. If one case succeeds, the flow jumps into |
| // the corresponding body block. The body block might alias others and |
| // might continue in the next body block if the body is not terminated with |
| // a break. |
| // |
| // Algorithm: |
| // 1/ Loop over the case chain to reach the default target |
| // & Estimate the number of uniq bodies. |
| // 2/ Generate code for all cases (see processCondSwitchCase). |
| // 3/ Generate code for all bodies (see processCondSwitchBody). |
| |
| MOZ_ASSERT(JSOp(*pc) == JSOP_CONDSWITCH); |
| jssrcnote* sn = info().getNote(gsn, pc); |
| MOZ_ASSERT(SN_TYPE(sn) == SRC_CONDSWITCH); |
| |
| // Get the exit pc |
| jsbytecode* exitpc = pc + GetSrcNoteOffset(sn, 0); |
| jsbytecode* firstCase = pc + GetSrcNoteOffset(sn, 1); |
| |
| // Iterate all cases in the conditional switch. |
| // - Stop at the default case. (always emitted after the last case) |
| // - Estimate the number of uniq bodies. This estimation might be off by 1 |
| // if the default body alias a case body. |
| jsbytecode* curCase = firstCase; |
| jsbytecode* lastTarget = GetJumpOffset(curCase) + curCase; |
| size_t nbBodies = 2; // default target and the first body. |
| |
| MOZ_ASSERT(pc < curCase && curCase <= exitpc); |
| while (JSOp(*curCase) == JSOP_CASE) { |
| // Fetch the next case. |
| jssrcnote* caseSn = info().getNote(gsn, curCase); |
| MOZ_ASSERT(caseSn && SN_TYPE(caseSn) == SRC_NEXTCASE); |
| ptrdiff_t off = GetSrcNoteOffset(caseSn, 0); |
| curCase = off ? curCase + off : GetNextPc(curCase); |
| MOZ_ASSERT(pc < curCase && curCase <= exitpc); |
| |
| // Count non-aliased cases. |
| jsbytecode* curTarget = GetJumpOffset(curCase) + curCase; |
| if (lastTarget < curTarget) |
| nbBodies++; |
| lastTarget = curTarget; |
| } |
| |
| // The current case now be the default case which jump to the body of the |
| // default case, which might be behind the last target. |
| MOZ_ASSERT(JSOp(*curCase) == JSOP_DEFAULT); |
| jsbytecode* defaultTarget = GetJumpOffset(curCase) + curCase; |
| MOZ_ASSERT(curCase < defaultTarget && defaultTarget <= exitpc); |
| |
| // Allocate the current graph state. |
| CFGState state = CFGState::CondSwitch(this, exitpc, defaultTarget); |
| if (!state.condswitch.bodies || !state.condswitch.bodies->init(alloc(), nbBodies)) |
| return ControlStatus_Error; |
| |
| // We loop on case conditions with processCondSwitchCase. |
| MOZ_ASSERT(JSOp(*firstCase) == JSOP_CASE); |
| state.stopAt = firstCase; |
| state.state = CFGState::COND_SWITCH_CASE; |
| |
| return cfgStack_.append(state); |
| } |
| |
| IonBuilder::CFGState |
| IonBuilder::CFGState::CondSwitch(IonBuilder* builder, jsbytecode* exitpc, jsbytecode* defaultTarget) |
| { |
| CFGState state; |
| state.state = COND_SWITCH_CASE; |
| state.stopAt = nullptr; |
| state.condswitch.bodies = (FixedList<MBasicBlock*>*)builder->alloc_->allocate( |
| sizeof(FixedList<MBasicBlock*>)); |
| state.condswitch.currentIdx = 0; |
| state.condswitch.defaultTarget = defaultTarget; |
| state.condswitch.defaultIdx = uint32_t(-1); |
| state.condswitch.exitpc = exitpc; |
| state.condswitch.breaks = nullptr; |
| return state; |
| } |
| |
| IonBuilder::CFGState |
| IonBuilder::CFGState::Label(jsbytecode* exitpc) |
| { |
| CFGState state; |
| state.state = LABEL; |
| state.stopAt = exitpc; |
| state.label.breaks = nullptr; |
| return state; |
| } |
| |
| IonBuilder::CFGState |
| IonBuilder::CFGState::Try(jsbytecode* exitpc, MBasicBlock* successor) |
| { |
| CFGState state; |
| state.state = TRY; |
| state.stopAt = exitpc; |
| state.try_.successor = successor; |
| return state; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processCondSwitchCase(CFGState& state) |
| { |
| MOZ_ASSERT(state.state == CFGState::COND_SWITCH_CASE); |
| MOZ_ASSERT(!state.condswitch.breaks); |
| MOZ_ASSERT(current); |
| MOZ_ASSERT(JSOp(*pc) == JSOP_CASE); |
| FixedList<MBasicBlock*>& bodies = *state.condswitch.bodies; |
| jsbytecode* defaultTarget = state.condswitch.defaultTarget; |
| uint32_t& currentIdx = state.condswitch.currentIdx; |
| jsbytecode* lastTarget = currentIdx ? bodies[currentIdx - 1]->pc() : nullptr; |
| |
| // Fetch the following case in which we will continue. |
| jssrcnote* sn = info().getNote(gsn, pc); |
| ptrdiff_t off = GetSrcNoteOffset(sn, 0); |
| jsbytecode* casePc = off ? pc + off : GetNextPc(pc); |
| bool caseIsDefault = JSOp(*casePc) == JSOP_DEFAULT; |
| MOZ_ASSERT(JSOp(*casePc) == JSOP_CASE || caseIsDefault); |
| |
| // Allocate the block of the matching case. |
| bool bodyIsNew = false; |
| MBasicBlock* bodyBlock = nullptr; |
| jsbytecode* bodyTarget = pc + GetJumpOffset(pc); |
| if (lastTarget < bodyTarget) { |
| // If the default body is in the middle or aliasing the current target. |
| if (lastTarget < defaultTarget && defaultTarget <= bodyTarget) { |
| MOZ_ASSERT(state.condswitch.defaultIdx == uint32_t(-1)); |
| state.condswitch.defaultIdx = currentIdx; |
| bodies[currentIdx] = nullptr; |
| // If the default body does not alias any and it would be allocated |
| // later and stored in the defaultIdx location. |
| if (defaultTarget < bodyTarget) |
| currentIdx++; |
| } |
| |
| bodyIsNew = true; |
| // Pop switch and case operands. |
| bodyBlock = newBlockPopN(current, bodyTarget, 2); |
| bodies[currentIdx++] = bodyBlock; |
| } else { |
| // This body alias the previous one. |
| MOZ_ASSERT(lastTarget == bodyTarget); |
| MOZ_ASSERT(currentIdx > 0); |
| bodyBlock = bodies[currentIdx - 1]; |
| } |
| |
| if (!bodyBlock) |
| return ControlStatus_Error; |
| |
| lastTarget = bodyTarget; |
| |
| // Allocate the block of the non-matching case. This can either be a normal |
| // case or the default case. |
| bool caseIsNew = false; |
| MBasicBlock* caseBlock = nullptr; |
| if (!caseIsDefault) { |
| caseIsNew = true; |
| // Pop the case operand. |
| caseBlock = newBlockPopN(current, GetNextPc(pc), 1); |
| } else { |
| // The non-matching case is the default case, which jump directly to its |
| // body. Skip the creation of a default case block and directly create |
| // the default body if it does not alias any previous body. |
| |
| if (state.condswitch.defaultIdx == uint32_t(-1)) { |
| // The default target is the last target. |
| MOZ_ASSERT(lastTarget < defaultTarget); |
| state.condswitch.defaultIdx = currentIdx++; |
| caseIsNew = true; |
| } else if (bodies[state.condswitch.defaultIdx] == nullptr) { |
| // The default target is in the middle and it does not alias any |
| // case target. |
| MOZ_ASSERT(defaultTarget < lastTarget); |
| caseIsNew = true; |
| } else { |
| // The default target is in the middle and it alias a case target. |
| MOZ_ASSERT(defaultTarget <= lastTarget); |
| caseBlock = bodies[state.condswitch.defaultIdx]; |
| } |
| |
| // Allocate and register the default body. |
| if (caseIsNew) { |
| // Pop the case & switch operands. |
| caseBlock = newBlockPopN(current, defaultTarget, 2); |
| bodies[state.condswitch.defaultIdx] = caseBlock; |
| } |
| } |
| |
| if (!caseBlock) |
| return ControlStatus_Error; |
| |
| // Terminate the last case condition block by emitting the code |
| // corresponding to JSOP_CASE bytecode. |
| if (bodyBlock != caseBlock) { |
| MDefinition* caseOperand = current->pop(); |
| MDefinition* switchOperand = current->peek(-1); |
| |
| if (!jsop_compare(JSOP_STRICTEQ, switchOperand, caseOperand)) |
| return ControlStatus_Error; |
| MInstruction* cmpResult = current->pop()->toInstruction(); |
| MOZ_ASSERT(!cmpResult->isEffectful()); |
| current->end(newTest(cmpResult, bodyBlock, caseBlock)); |
| |
| // Add last case as predecessor of the body if the body is aliasing |
| // the previous case body. |
| if (!bodyIsNew && !bodyBlock->addPredecessorPopN(alloc(), current, 1)) |
| return ControlStatus_Error; |
| |
| // Add last case as predecessor of the non-matching case if the |
| // non-matching case is an aliased default case. We need to pop the |
| // switch operand as we skip the default case block and use the default |
| // body block directly. |
| MOZ_ASSERT_IF(!caseIsNew, caseIsDefault); |
| if (!caseIsNew && !caseBlock->addPredecessorPopN(alloc(), current, 1)) |
| return ControlStatus_Error; |
| } else { |
| // The default case alias the last case body. |
| MOZ_ASSERT(caseIsDefault); |
| current->pop(); // Case operand |
| current->pop(); // Switch operand |
| current->end(MGoto::New(alloc(), bodyBlock)); |
| if (!bodyIsNew && !bodyBlock->addPredecessor(alloc(), current)) |
| return ControlStatus_Error; |
| } |
| |
| if (caseIsDefault) { |
| // The last case condition is finished. Loop in processCondSwitchBody, |
| // with potential stops in processSwitchBreak. Check that the bodies |
| // fixed list is over-estimate by at most 1, and shrink the size such as |
| // length can be used as an upper bound while iterating bodies. |
| MOZ_ASSERT(currentIdx == bodies.length() || currentIdx + 1 == bodies.length()); |
| bodies.shrink(bodies.length() - currentIdx); |
| |
| // Handle break statements in processSwitchBreak while processing |
| // bodies. |
| ControlFlowInfo breakInfo(cfgStack_.length() - 1, state.condswitch.exitpc); |
| if (!switches_.append(breakInfo)) |
| return ControlStatus_Error; |
| |
| // Jump into the first body. |
| currentIdx = 0; |
| setCurrent(nullptr); |
| state.state = CFGState::COND_SWITCH_BODY; |
| return processCondSwitchBody(state); |
| } |
| |
| // Continue until the case condition. |
| if (!setCurrentAndSpecializePhis(caseBlock)) |
| return ControlStatus_Error; |
| pc = current->pc(); |
| state.stopAt = casePc; |
| return ControlStatus_Jumped; |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processCondSwitchBody(CFGState& state) |
| { |
| MOZ_ASSERT(state.state == CFGState::COND_SWITCH_BODY); |
| MOZ_ASSERT(pc <= state.condswitch.exitpc); |
| FixedList<MBasicBlock*>& bodies = *state.condswitch.bodies; |
| uint32_t& currentIdx = state.condswitch.currentIdx; |
| |
| MOZ_ASSERT(currentIdx <= bodies.length()); |
| if (currentIdx == bodies.length()) { |
| MOZ_ASSERT_IF(current, pc == state.condswitch.exitpc); |
| return processSwitchEnd(state.condswitch.breaks, state.condswitch.exitpc); |
| } |
| |
| // Get the next body |
| MBasicBlock* nextBody = bodies[currentIdx++]; |
| MOZ_ASSERT_IF(current, pc == nextBody->pc()); |
| |
| // Fix the reverse post-order iteration. |
| graph().moveBlockToEnd(nextBody); |
| |
| // The last body continue into the new one. |
| if (current) { |
| current->end(MGoto::New(alloc(), nextBody)); |
| if (!nextBody->addPredecessor(alloc(), current)) |
| return ControlStatus_Error; |
| } |
| |
| // Continue in the next body. |
| if (!setCurrentAndSpecializePhis(nextBody)) |
| return ControlStatus_Error; |
| pc = current->pc(); |
| |
| if (currentIdx < bodies.length()) |
| state.stopAt = bodies[currentIdx]->pc(); |
| else |
| state.stopAt = state.condswitch.exitpc; |
| return ControlStatus_Jumped; |
| } |
| |
| bool |
| IonBuilder::jsop_andor(JSOp op) |
| { |
| MOZ_ASSERT(op == JSOP_AND || op == JSOP_OR); |
| |
| jsbytecode* rhsStart = pc + CodeSpec[op].length; |
| jsbytecode* joinStart = pc + GetJumpOffset(pc); |
| MOZ_ASSERT(joinStart > pc); |
| |
| // We have to leave the LHS on the stack. |
| MDefinition* lhs = current->peek(-1); |
| |
| MBasicBlock* evalLhs = newBlock(current, joinStart); |
| MBasicBlock* evalRhs = newBlock(current, rhsStart); |
| if (!evalLhs || !evalRhs) |
| return false; |
| |
| MTest* test = (op == JSOP_AND) |
| ? newTest(lhs, evalRhs, evalLhs) |
| : newTest(lhs, evalLhs, evalRhs); |
| current->end(test); |
| |
| // Create the lhs block and specialize. |
| if (!setCurrentAndSpecializePhis(evalLhs)) |
| return false; |
| |
| if (!improveTypesAtTest(test->getOperand(0), test->ifTrue() == current, test)) |
| return false; |
| |
| // Create the rhs block. |
| if (!cfgStack_.append(CFGState::AndOr(joinStart, evalLhs))) |
| return false; |
| |
| if (!setCurrentAndSpecializePhis(evalRhs)) |
| return false; |
| |
| if (!improveTypesAtTest(test->getOperand(0), test->ifTrue() == current, test)) |
| return false; |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_dup2() |
| { |
| uint32_t lhsSlot = current->stackDepth() - 2; |
| uint32_t rhsSlot = current->stackDepth() - 1; |
| current->pushSlot(lhsSlot); |
| current->pushSlot(rhsSlot); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_loophead(jsbytecode* pc) |
| { |
| assertValidLoopHeadOp(pc); |
| |
| current->add(MInterruptCheck::New(alloc())); |
| insertRecompileCheck(); |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_ifeq(JSOp op) |
| { |
| // IFEQ always has a forward offset. |
| jsbytecode* trueStart = pc + CodeSpec[op].length; |
| jsbytecode* falseStart = pc + GetJumpOffset(pc); |
| MOZ_ASSERT(falseStart > pc); |
| |
| // We only handle cases that emit source notes. |
| jssrcnote* sn = info().getNote(gsn, pc); |
| if (!sn) |
| return abort("expected sourcenote"); |
| |
| MDefinition* ins = current->pop(); |
| |
| // Create true and false branches. |
| MBasicBlock* ifTrue = newBlock(current, trueStart); |
| MBasicBlock* ifFalse = newBlock(current, falseStart); |
| if (!ifTrue || !ifFalse) |
| return false; |
| |
| MTest* test = newTest(ins, ifTrue, ifFalse); |
| current->end(test); |
| |
| // The bytecode for if/ternary gets emitted either like this: |
| // |
| // IFEQ X ; src note (IF_ELSE, COND) points to the GOTO |
| // ... |
| // GOTO Z |
| // X: ... ; else/else if |
| // ... |
| // Z: ; join |
| // |
| // Or like this: |
| // |
| // IFEQ X ; src note (IF) has no offset |
| // ... |
| // Z: ... ; join |
| // |
| // We want to parse the bytecode as if we were parsing the AST, so for the |
| // IF_ELSE/COND cases, we use the source note and follow the GOTO. For the |
| // IF case, the IFEQ offset is the join point. |
| switch (SN_TYPE(sn)) { |
| case SRC_IF: |
| if (!cfgStack_.append(CFGState::If(falseStart, test))) |
| return false; |
| break; |
| |
| case SRC_IF_ELSE: |
| case SRC_COND: |
| { |
| // Infer the join point from the JSOP_GOTO[X] sitting here, then |
| // assert as we much we can that this is the right GOTO. |
| jsbytecode* trueEnd = pc + GetSrcNoteOffset(sn, 0); |
| MOZ_ASSERT(trueEnd > pc); |
| MOZ_ASSERT(trueEnd < falseStart); |
| MOZ_ASSERT(JSOp(*trueEnd) == JSOP_GOTO); |
| MOZ_ASSERT(!info().getNote(gsn, trueEnd)); |
| |
| jsbytecode* falseEnd = trueEnd + GetJumpOffset(trueEnd); |
| MOZ_ASSERT(falseEnd > trueEnd); |
| MOZ_ASSERT(falseEnd >= falseStart); |
| |
| if (!cfgStack_.append(CFGState::IfElse(trueEnd, falseEnd, test))) |
| return false; |
| break; |
| } |
| |
| default: |
| MOZ_CRASH("unexpected source note type"); |
| } |
| |
| // Switch to parsing the true branch. Note that no PC update is needed, |
| // it's the next instruction. |
| if (!setCurrentAndSpecializePhis(ifTrue)) |
| return false; |
| |
| // Filter the types in the true branch. |
| if (!improveTypesAtTest(test->getOperand(0), test->ifTrue() == current, test)) |
| return false; |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_try() |
| { |
| MOZ_ASSERT(JSOp(*pc) == JSOP_TRY); |
| |
| // Try-finally is not yet supported. |
| if (analysis().hasTryFinally()) |
| return abort("Has try-finally"); |
| |
| // Try-catch within inline frames is not yet supported. |
| MOZ_ASSERT(!isInlineBuilder()); |
| |
| // Try-catch during the arguments usage analysis is not yet supported. Code |
| // accessing the arguments within the 'catch' block is not accounted for. |
| if (info().analysisMode() == Analysis_ArgumentsUsage) |
| return abort("Try-catch during arguments usage analysis"); |
| |
| graph().setHasTryBlock(); |
| |
| jssrcnote* sn = info().getNote(gsn, pc); |
| MOZ_ASSERT(SN_TYPE(sn) == SRC_TRY); |
| |
| // Get the pc of the last instruction in the try block. It's a JSOP_GOTO to |
| // jump over the catch block. |
| jsbytecode* endpc = pc + GetSrcNoteOffset(sn, 0); |
| MOZ_ASSERT(JSOp(*endpc) == JSOP_GOTO); |
| MOZ_ASSERT(GetJumpOffset(endpc) > 0); |
| |
| jsbytecode* afterTry = endpc + GetJumpOffset(endpc); |
| |
| // If controlflow in the try body is terminated (by a return or throw |
| // statement), the code after the try-statement may still be reachable |
| // via the catch block (which we don't compile) and OSR can enter it. |
| // For example: |
| // |
| // try { |
| // throw 3; |
| // } catch(e) { } |
| // |
| // for (var i=0; i<1000; i++) {} |
| // |
| // To handle this, we create two blocks: one for the try block and one |
| // for the code following the try-catch statement. Both blocks are |
| // connected to the graph with an MGotoWithFake instruction that always |
| // jumps to the try block. This ensures the successor block always has a |
| // predecessor. |
| // |
| // If the code after the try block is unreachable (control flow in both the |
| // try and catch blocks is terminated), only create the try block, to avoid |
| // parsing unreachable code. |
| |
| MBasicBlock* tryBlock = newBlock(current, GetNextPc(pc)); |
| if (!tryBlock) |
| return false; |
| |
| MBasicBlock* successor; |
| if (analysis().maybeInfo(afterTry)) { |
| successor = newBlock(current, afterTry); |
| if (!successor) |
| return false; |
| |
| current->end(MGotoWithFake::New(alloc(), tryBlock, successor)); |
| } else { |
| successor = nullptr; |
| current->end(MGoto::New(alloc(), tryBlock)); |
| } |
| |
| if (!cfgStack_.append(CFGState::Try(endpc, successor))) |
| return false; |
| |
| // The baseline compiler should not attempt to enter the catch block |
| // via OSR. |
| MOZ_ASSERT(info().osrPc() < endpc || info().osrPc() >= afterTry); |
| |
| // Start parsing the try block. |
| return setCurrentAndSpecializePhis(tryBlock); |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processReturn(JSOp op) |
| { |
| MDefinition* def; |
| switch (op) { |
| case JSOP_RETURN: |
| // Return the last instruction. |
| def = current->pop(); |
| break; |
| |
| case JSOP_RETRVAL: |
| // Return undefined eagerly if script doesn't use return value. |
| if (script()->noScriptRval()) { |
| MInstruction* ins = MConstant::New(alloc(), UndefinedValue()); |
| current->add(ins); |
| def = ins; |
| break; |
| } |
| |
| def = current->getSlot(info().returnValueSlot()); |
| break; |
| |
| default: |
| def = nullptr; |
| MOZ_CRASH("unknown return op"); |
| } |
| |
| MReturn* ret = MReturn::New(alloc(), def); |
| current->end(ret); |
| |
| if (!graph().addReturn(current)) |
| return ControlStatus_Error; |
| |
| // Make sure no one tries to use this block now. |
| setCurrent(nullptr); |
| return processControlEnd(); |
| } |
| |
| IonBuilder::ControlStatus |
| IonBuilder::processThrow() |
| { |
| MDefinition* def = current->pop(); |
| |
| // MThrow is not marked as effectful. This means when it throws and we |
| // are inside a try block, we could use an earlier resume point and this |
| // resume point may not be up-to-date, for example: |
| // |
| // (function() { |
| // try { |
| // var x = 1; |
| // foo(); // resume point |
| // x = 2; |
| // throw foo; |
| // } catch(e) { |
| // print(x); |
| // } |
| // ])(); |
| // |
| // If we use the resume point after the call, this will print 1 instead |
| // of 2. To fix this, we create a resume point right before the MThrow. |
| // |
| // Note that this is not a problem for instructions other than MThrow |
| // because they are either marked as effectful (have their own resume |
| // point) or cannot throw a catchable exception. |
| // |
| // We always install this resume point (instead of only when the function |
| // has a try block) in order to handle the Debugger onExceptionUnwind |
| // hook. When we need to handle the hook, we bail out to baseline right |
| // after the throw and propagate the exception when debug mode is on. This |
| // is opposed to the normal behavior of resuming directly in the |
| // associated catch block. |
| MNop* nop = MNop::New(alloc()); |
| current->add(nop); |
| |
| if (!resumeAfter(nop)) |
| return ControlStatus_Error; |
| |
| MThrow* ins = MThrow::New(alloc(), def); |
| current->end(ins); |
| |
| // Make sure no one tries to use this block now. |
| setCurrent(nullptr); |
| return processControlEnd(); |
| } |
| |
| bool |
| IonBuilder::pushConstant(const Value& v) |
| { |
| current->push(constant(v)); |
| return true; |
| } |
| |
| bool |
| IonBuilder::bitnotTrySpecialized(bool* emitted, MDefinition* input) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // Try to emit a specialized bitnot instruction based on the input type |
| // of the operand. |
| |
| if (input->mightBeType(MIRType_Object) || input->mightBeType(MIRType_Symbol)) |
| return true; |
| |
| MBitNot* ins = MBitNot::New(alloc(), input); |
| ins->setSpecialization(MIRType_Int32); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_bitnot() |
| { |
| bool emitted = false; |
| |
| MDefinition* input = current->pop(); |
| |
| if (!forceInlineCaches()) { |
| if (!bitnotTrySpecialized(&emitted, input) || emitted) |
| return emitted; |
| } |
| |
| if (!arithTrySharedStub(&emitted, JSOP_BITNOT, nullptr, input) || emitted) |
| return emitted; |
| |
| // Not possible to optimize. Do a slow vm call. |
| MBitNot* ins = MBitNot::New(alloc(), input); |
| |
| current->add(ins); |
| current->push(ins); |
| MOZ_ASSERT(ins->isEffectful()); |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_bitop(JSOp op) |
| { |
| // Pop inputs. |
| MDefinition* right = current->pop(); |
| MDefinition* left = current->pop(); |
| |
| MBinaryBitwiseInstruction* ins; |
| switch (op) { |
| case JSOP_BITAND: |
| ins = MBitAnd::New(alloc(), left, right); |
| break; |
| |
| case JSOP_BITOR: |
| ins = MBitOr::New(alloc(), left, right); |
| break; |
| |
| case JSOP_BITXOR: |
| ins = MBitXor::New(alloc(), left, right); |
| break; |
| |
| case JSOP_LSH: |
| ins = MLsh::New(alloc(), left, right); |
| break; |
| |
| case JSOP_RSH: |
| ins = MRsh::New(alloc(), left, right); |
| break; |
| |
| case JSOP_URSH: |
| ins = MUrsh::New(alloc(), left, right); |
| break; |
| |
| default: |
| MOZ_CRASH("unexpected bitop"); |
| } |
| |
| current->add(ins); |
| ins->infer(inspector, pc); |
| |
| current->push(ins); |
| if (ins->isEffectful() && !resumeAfter(ins)) |
| return false; |
| |
| return true; |
| } |
| |
| MDefinition::Opcode |
| JSOpToMDefinition(JSOp op) |
| { |
| switch (op) { |
| case JSOP_ADD: |
| return MDefinition::Op_Add; |
| case JSOP_SUB: |
| return MDefinition::Op_Sub; |
| case JSOP_MUL: |
| return MDefinition::Op_Mul; |
| case JSOP_DIV: |
| return MDefinition::Op_Div; |
| case JSOP_MOD: |
| return MDefinition::Op_Mod; |
| default: |
| MOZ_CRASH("unexpected binary opcode"); |
| } |
| } |
| |
| bool |
| IonBuilder::binaryArithTryConcat(bool* emitted, JSOp op, MDefinition* left, MDefinition* right) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // Try to convert an addition into a concat operation if the inputs |
| // indicate this might be a concatenation. |
| |
| // Only try to replace this with concat when we have an addition. |
| if (op != JSOP_ADD) |
| return true; |
| |
| // Make sure one of the inputs is a string. |
| if (left->type() != MIRType_String && right->type() != MIRType_String) |
| return true; |
| |
| // The none-string input (if present) should be atleast a numerical type. |
| // Which we can easily coerce to string. |
| if (right->type() != MIRType_String && !IsNumberType(right->type())) |
| return true; |
| if (left->type() != MIRType_String && !IsNumberType(left->type())) |
| return true; |
| |
| MConcat* ins = MConcat::New(alloc(), left, right); |
| current->add(ins); |
| current->push(ins); |
| |
| if (!maybeInsertResume()) |
| return false; |
| |
| *emitted = true; |
| return true; |
| } |
| |
| static inline bool |
| SimpleArithOperand(MDefinition* op) |
| { |
| return !op->mightBeType(MIRType_Object) |
| && !op->mightBeType(MIRType_String) |
| && !op->mightBeType(MIRType_Symbol) |
| && !op->mightBeType(MIRType_MagicOptimizedArguments) |
| && !op->mightBeType(MIRType_MagicHole) |
| && !op->mightBeType(MIRType_MagicIsConstructing); |
| } |
| |
| bool |
| IonBuilder::binaryArithTrySpecialized(bool* emitted, JSOp op, MDefinition* left, MDefinition* right) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // Try to emit a specialized binary instruction based on the input types |
| // of the operands. |
| |
| // Anything complex - strings, symbols, and objects - are not specialized |
| if (!SimpleArithOperand(left) || !SimpleArithOperand(right)) |
| return true; |
| |
| // One of the inputs need to be a number. |
| if (!IsNumberType(left->type()) && !IsNumberType(right->type())) |
| return true; |
| |
| MDefinition::Opcode defOp = JSOpToMDefinition(op); |
| MBinaryArithInstruction* ins = MBinaryArithInstruction::New(alloc(), defOp, left, right); |
| ins->setNumberSpecialization(alloc(), inspector, pc); |
| |
| if (op == JSOP_ADD || op == JSOP_MUL) |
| ins->setCommutative(); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| MOZ_ASSERT(!ins->isEffectful()); |
| if (!maybeInsertResume()) |
| return false; |
| |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::binaryArithTrySpecializedOnBaselineInspector(bool* emitted, JSOp op, |
| MDefinition* left, MDefinition* right) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // Try to emit a specialized binary instruction speculating the |
| // type using the baseline caches. |
| |
| MIRType specialization = inspector->expectedBinaryArithSpecialization(pc); |
| if (specialization == MIRType_None) |
| return true; |
| |
| MDefinition::Opcode def_op = JSOpToMDefinition(op); |
| MBinaryArithInstruction* ins = MBinaryArithInstruction::New(alloc(), def_op, left, right); |
| ins->setSpecialization(specialization); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| MOZ_ASSERT(!ins->isEffectful()); |
| if (!maybeInsertResume()) |
| return false; |
| |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::arithTrySharedStub(bool* emitted, JSOp op, |
| MDefinition* left, MDefinition* right) |
| { |
| MOZ_ASSERT(*emitted == false); |
| JSOp actualOp = JSOp(*pc); |
| |
| // Try to emit a shared stub cache. |
| |
| if (JitOptions.disableSharedStubs) |
| return true; |
| |
| // The actual jsop 'jsop_pos' is not supported yet. |
| if (actualOp == JSOP_POS) |
| return true; |
| |
| MInstruction* stub = nullptr; |
| switch (actualOp) { |
| case JSOP_NEG: |
| case JSOP_BITNOT: |
| MOZ_ASSERT_IF(op == JSOP_MUL, left->isConstantValue() && |
| left->constantValue().toInt32() == -1); |
| MOZ_ASSERT_IF(op != JSOP_MUL, !left); |
| |
| stub = MUnarySharedStub::New(alloc(), right); |
| break; |
| case JSOP_ADD: |
| case JSOP_SUB: |
| case JSOP_MUL: |
| case JSOP_DIV: |
| case JSOP_MOD: |
| stub = MBinarySharedStub::New(alloc(), left, right); |
| break; |
| default: |
| MOZ_CRASH("unsupported arith"); |
| } |
| |
| current->add(stub); |
| current->push(stub); |
| |
| // Decrease type from 'any type' to 'empty type' when one of the operands |
| // is 'empty typed'. |
| maybeMarkEmpty(stub); |
| |
| if (!resumeAfter(stub)) |
| return false; |
| |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_binary_arith(JSOp op, MDefinition* left, MDefinition* right) |
| { |
| bool emitted = false; |
| |
| if (!forceInlineCaches()) { |
| if (!binaryArithTryConcat(&emitted, op, left, right) || emitted) |
| return emitted; |
| |
| if (!binaryArithTrySpecialized(&emitted, op, left, right) || emitted) |
| return emitted; |
| |
| if (!binaryArithTrySpecializedOnBaselineInspector(&emitted, op, left, right) || emitted) |
| return emitted; |
| } |
| |
| if (!arithTrySharedStub(&emitted, op, left, right) || emitted) |
| return emitted; |
| |
| // Not possible to optimize. Do a slow vm call. |
| MDefinition::Opcode def_op = JSOpToMDefinition(op); |
| MBinaryArithInstruction* ins = MBinaryArithInstruction::New(alloc(), def_op, left, right); |
| |
| // Decrease type from 'any type' to 'empty type' when one of the operands |
| // is 'empty typed'. |
| maybeMarkEmpty(ins); |
| |
| current->add(ins); |
| current->push(ins); |
| MOZ_ASSERT(ins->isEffectful()); |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_binary_arith(JSOp op) |
| { |
| MDefinition* right = current->pop(); |
| MDefinition* left = current->pop(); |
| |
| return jsop_binary_arith(op, left, right); |
| } |
| |
| bool |
| IonBuilder::jsop_pow() |
| { |
| MDefinition* exponent = current->pop(); |
| MDefinition* base = current->pop(); |
| |
| if (inlineMathPowHelper(base, exponent, MIRType_Double) == InliningStatus_Inlined) { |
| base->setImplicitlyUsedUnchecked(); |
| exponent->setImplicitlyUsedUnchecked(); |
| return true; |
| } |
| |
| // For now, use MIRType_Double, as a safe cover-all. See bug 1188079. |
| MPow* pow = MPow::New(alloc(), base, exponent, MIRType_Double); |
| current->add(pow); |
| current->push(pow); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_pos() |
| { |
| if (IsNumberType(current->peek(-1)->type())) { |
| // Already int32 or double. Set the operand as implicitly used so it |
| // doesn't get optimized out if it has no other uses, as we could bail |
| // out. |
| current->peek(-1)->setImplicitlyUsedUnchecked(); |
| return true; |
| } |
| |
| // Compile +x as x * 1. |
| MDefinition* value = current->pop(); |
| MConstant* one = MConstant::New(alloc(), Int32Value(1)); |
| current->add(one); |
| |
| return jsop_binary_arith(JSOP_MUL, value, one); |
| } |
| |
| bool |
| IonBuilder::jsop_neg() |
| { |
| // Since JSOP_NEG does not use a slot, we cannot push the MConstant. |
| // The MConstant is therefore passed to JSOP_MUL without slot traffic. |
| MConstant* negator = MConstant::New(alloc(), Int32Value(-1)); |
| current->add(negator); |
| |
| MDefinition* right = current->pop(); |
| |
| return jsop_binary_arith(JSOP_MUL, negator, right); |
| } |
| |
| bool |
| IonBuilder::jsop_tostring() |
| { |
| if (current->peek(-1)->type() == MIRType_String) |
| return true; |
| |
| MDefinition* value = current->pop(); |
| MToString* ins = MToString::New(alloc(), value); |
| current->add(ins); |
| current->push(ins); |
| MOZ_ASSERT(!ins->isEffectful()); |
| return true; |
| } |
| |
| class AutoAccumulateReturns |
| { |
| MIRGraph& graph_; |
| MIRGraphReturns* prev_; |
| |
| public: |
| AutoAccumulateReturns(MIRGraph& graph, MIRGraphReturns& returns) |
| : graph_(graph) |
| { |
| prev_ = graph_.returnAccumulator(); |
| graph_.setReturnAccumulator(&returns); |
| } |
| ~AutoAccumulateReturns() { |
| graph_.setReturnAccumulator(prev_); |
| } |
| }; |
| |
| bool |
| IonBuilder::inlineScriptedCall(CallInfo& callInfo, JSFunction* target) |
| { |
| MOZ_ASSERT(target->hasScript()); |
| MOZ_ASSERT(IsIonInlinablePC(pc)); |
| |
| callInfo.setImplicitlyUsedUnchecked(); |
| |
| // Ensure sufficient space in the slots: needed for inlining from FUNAPPLY. |
| uint32_t depth = current->stackDepth() + callInfo.numFormals(); |
| if (depth > current->nslots()) { |
| if (!current->increaseSlots(depth - current->nslots())) |
| return false; |
| } |
| |
| // Create new |this| on the caller-side for inlined constructors. |
| if (callInfo.constructing()) { |
| MDefinition* thisDefn = createThis(target, callInfo.fun(), callInfo.getNewTarget()); |
| if (!thisDefn) |
| return false; |
| callInfo.setThis(thisDefn); |
| } |
| |
| // Capture formals in the outer resume point. |
| callInfo.pushFormals(current); |
| |
| MResumePoint* outerResumePoint = |
| MResumePoint::New(alloc(), current, pc, MResumePoint::Outer); |
| if (!outerResumePoint) |
| return false; |
| current->setOuterResumePoint(outerResumePoint); |
| |
| // Pop formals again, except leave |fun| on stack for duration of call. |
| callInfo.popFormals(current); |
| current->push(callInfo.fun()); |
| |
| JSScript* calleeScript = target->nonLazyScript(); |
| BaselineInspector inspector(calleeScript); |
| |
| // Improve type information of |this| when not set. |
| if (callInfo.constructing() && |
| !callInfo.thisArg()->resultTypeSet()) |
| { |
| StackTypeSet* types = TypeScript::ThisTypes(calleeScript); |
| if (types && !types->unknown()) { |
| TemporaryTypeSet* clonedTypes = types->clone(alloc_->lifoAlloc()); |
| if (!clonedTypes) |
| return oom(); |
| MTypeBarrier* barrier = MTypeBarrier::New(alloc(), callInfo.thisArg(), clonedTypes); |
| current->add(barrier); |
| if (barrier->type() == MIRType_Undefined) |
| callInfo.setThis(constant(UndefinedValue())); |
| else if (barrier->type() == MIRType_Null) |
| callInfo.setThis(constant(NullValue())); |
| else |
| callInfo.setThis(barrier); |
| } |
| } |
| |
| // Start inlining. |
| LifoAlloc* lifoAlloc = alloc_->lifoAlloc(); |
| InlineScriptTree* inlineScriptTree = |
| info().inlineScriptTree()->addCallee(alloc_, pc, calleeScript); |
| if (!inlineScriptTree) |
| return false; |
| CompileInfo* info = lifoAlloc->new_<CompileInfo>(calleeScript, target, |
| (jsbytecode*)nullptr, callInfo.constructing(), |
| this->info().analysisMode(), |
| /* needsArgsObj = */ false, |
| inlineScriptTree); |
| if (!info) |
| return false; |
| |
| MIRGraphReturns returns(alloc()); |
| AutoAccumulateReturns aar(graph(), returns); |
| |
| // Build the graph. |
| IonBuilder inlineBuilder(analysisContext, compartment, options, &alloc(), &graph(), constraints(), |
| &inspector, info, &optimizationInfo(), nullptr, inliningDepth_ + 1, |
| loopDepth_); |
| if (!inlineBuilder.buildInline(this, outerResumePoint, callInfo)) { |
| if (analysisContext && analysisContext->isExceptionPending()) { |
| JitSpew(JitSpew_IonAbort, "Inline builder raised exception."); |
| abortReason_ = AbortReason_Error; |
| return false; |
| } |
| |
| // Inlining the callee failed. Mark the callee as uninlineable only if |
| // the inlining was aborted for a non-exception reason. |
| if (inlineBuilder.abortReason_ == AbortReason_Disable) { |
| calleeScript->setUninlineable(); |
| abortReason_ = AbortReason_Inlining; |
| } else if (inlineBuilder.abortReason_ == AbortReason_Inlining) { |
| abortReason_ = AbortReason_Inlining; |
| } else if (inlineBuilder.abortReason_ == AbortReason_PreliminaryObjects) { |
| const ObjectGroupVector& groups = inlineBuilder.abortedPreliminaryGroups(); |
| MOZ_ASSERT(!groups.empty()); |
| for (size_t i = 0; i < groups.length(); i++) |
| addAbortedPreliminaryGroup(groups[i]); |
| abortReason_ = AbortReason_PreliminaryObjects; |
| } |
| |
| return false; |
| } |
| |
| // Create return block. |
| jsbytecode* postCall = GetNextPc(pc); |
| MBasicBlock* returnBlock = newBlock(nullptr, postCall); |
| if (!returnBlock) |
| return false; |
| returnBlock->setCallerResumePoint(callerResumePoint_); |
| |
| // Inherit the slots from current and pop |fun|. |
| returnBlock->inheritSlots(current); |
| returnBlock->pop(); |
| |
| // Accumulate return values. |
| if (returns.empty()) { |
| // Inlining of functions that have no exit is not supported. |
| calleeScript->setUninlineable(); |
| abortReason_ = AbortReason_Inlining; |
| return false; |
| } |
| MDefinition* retvalDefn = patchInlinedReturns(callInfo, returns, returnBlock); |
| if (!retvalDefn) |
| return false; |
| returnBlock->push(retvalDefn); |
| |
| // Initialize entry slots now that the stack has been fixed up. |
| if (!returnBlock->initEntrySlots(alloc())) |
| return false; |
| |
| return setCurrentAndSpecializePhis(returnBlock); |
| } |
| |
| MDefinition* |
| IonBuilder::patchInlinedReturn(CallInfo& callInfo, MBasicBlock* exit, MBasicBlock* bottom) |
| { |
| // Replaces the MReturn in the exit block with an MGoto. |
| MDefinition* rdef = exit->lastIns()->toReturn()->input(); |
| exit->discardLastIns(); |
| |
| // Constructors must be patched by the caller to always return an object. |
| if (callInfo.constructing()) { |
| if (rdef->type() == MIRType_Value) { |
| // Unknown return: dynamically detect objects. |
| MReturnFromCtor* filter = MReturnFromCtor::New(alloc(), rdef, callInfo.thisArg()); |
| exit->add(filter); |
| rdef = filter; |
| } else if (rdef->type() != MIRType_Object) { |
| // Known non-object return: force |this|. |
| rdef = callInfo.thisArg(); |
| } |
| } else if (callInfo.isSetter()) { |
| // Setters return their argument, not whatever value is returned. |
| rdef = callInfo.getArg(0); |
| } |
| |
| if (!callInfo.isSetter()) |
| rdef = specializeInlinedReturn(rdef, exit); |
| |
| MGoto* replacement = MGoto::New(alloc(), bottom); |
| exit->end(replacement); |
| if (!bottom->addPredecessorWithoutPhis(exit)) |
| return nullptr; |
| |
| return rdef; |
| } |
| |
| MDefinition* |
| IonBuilder::specializeInlinedReturn(MDefinition* rdef, MBasicBlock* exit) |
| { |
| // Remove types from the return definition that weren't observed. |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| |
| // The observed typeset doesn't contain extra information. |
| if (types->empty() || types->unknown()) |
| return rdef; |
| |
| // Decide if specializing is needed using the result typeset if available, |
| // else use the result type. |
| |
| if (rdef->resultTypeSet()) { |
| // Don't specialize if return typeset is a subset of the |
| // observed typeset. The return typeset is already more specific. |
| if (rdef->resultTypeSet()->isSubset(types)) |
| return rdef; |
| } else { |
| MIRType observedType = types->getKnownMIRType(); |
| |
| // Don't specialize if type is MIRType_Float32 and TI reports |
| // MIRType_Double. Float is more specific than double. |
| if (observedType == MIRType_Double && rdef->type() == MIRType_Float32) |
| return rdef; |
| |
| // Don't specialize if types are inaccordance, except for MIRType_Value |
| // and MIRType_Object (when not unknown object), since the typeset |
| // contains more specific information. |
| if (observedType == rdef->type() && |
| observedType != MIRType_Value && |
| (observedType != MIRType_Object || types->unknownObject())) |
| { |
| return rdef; |
| } |
| } |
| |
| setCurrent(exit); |
| |
| MTypeBarrier* barrier = nullptr; |
| rdef = addTypeBarrier(rdef, types, BarrierKind::TypeSet, &barrier); |
| if (barrier) |
| barrier->setNotMovable(); |
| |
| return rdef; |
| } |
| |
| MDefinition* |
| IonBuilder::patchInlinedReturns(CallInfo& callInfo, MIRGraphReturns& returns, MBasicBlock* bottom) |
| { |
| // Replaces MReturns with MGotos, returning the MDefinition |
| // representing the return value, or nullptr. |
| MOZ_ASSERT(returns.length() > 0); |
| |
| if (returns.length() == 1) |
| return patchInlinedReturn(callInfo, returns[0], bottom); |
| |
| // Accumulate multiple returns with a phi. |
| MPhi* phi = MPhi::New(alloc()); |
| if (!phi->reserveLength(returns.length())) |
| return nullptr; |
| |
| for (size_t i = 0; i < returns.length(); i++) { |
| MDefinition* rdef = patchInlinedReturn(callInfo, returns[i], bottom); |
| if (!rdef) |
| return nullptr; |
| phi->addInput(rdef); |
| } |
| |
| bottom->addPhi(phi); |
| return phi; |
| } |
| |
| IonBuilder::InliningDecision |
| IonBuilder::makeInliningDecision(JSObject* targetArg, CallInfo& callInfo) |
| { |
| // When there is no target, inlining is impossible. |
| if (targetArg == nullptr) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineNoTarget); |
| return InliningDecision_DontInline; |
| } |
| |
| // Inlining non-function targets is handled by inlineNonFunctionCall(). |
| if (!targetArg->is<JSFunction>()) |
| return InliningDecision_Inline; |
| |
| JSFunction* target = &targetArg->as<JSFunction>(); |
| |
| // Never inline during the arguments usage analysis. |
| if (info().analysisMode() == Analysis_ArgumentsUsage) |
| return InliningDecision_DontInline; |
| |
| // Native functions provide their own detection in inlineNativeCall(). |
| if (target->isNative()) |
| return InliningDecision_Inline; |
| |
| // Determine whether inlining is possible at callee site |
| InliningDecision decision = canInlineTarget(target, callInfo); |
| if (decision != InliningDecision_Inline) |
| return decision; |
| |
| // Heuristics! |
| JSScript* targetScript = target->nonLazyScript(); |
| |
| // Callee must not be excessively large. |
| // This heuristic also applies to the callsite as a whole. |
| bool offThread = options.offThreadCompilationAvailable(); |
| if (targetScript->length() > optimizationInfo().inlineMaxBytecodePerCallSite(offThread)) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineBigCallee); |
| return DontInline(targetScript, "Vetoed: callee excessively large"); |
| } |
| |
| // Callee must have been called a few times to have somewhat stable |
| // type information, except for definite properties analysis, |
| // as the caller has not run yet. |
| if (targetScript->getWarmUpCount() < optimizationInfo().inliningWarmUpThreshold() && |
| !targetScript->baselineScript()->ionCompiledOrInlined() && |
| info().analysisMode() != Analysis_DefiniteProperties) |
| { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineNotHot); |
| JitSpew(JitSpew_Inlining, "Cannot inline %s:%" PRIuSIZE ": callee is insufficiently hot.", |
| targetScript->filename(), targetScript->lineno()); |
| return InliningDecision_WarmUpCountTooLow; |
| } |
| |
| // Don't inline if the callee is known to inline a lot of code, to avoid |
| // huge MIR graphs. |
| uint32_t inlinedBytecodeLength = targetScript->baselineScript()->inlinedBytecodeLength(); |
| if (inlinedBytecodeLength > optimizationInfo().inlineMaxCalleeInlinedBytecodeLength()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineBigCalleeInlinedBytecodeLength); |
| return DontInline(targetScript, "Vetoed: callee inlinedBytecodeLength is too big"); |
| } |
| |
| IonBuilder* outerBuilder = outermostBuilder(); |
| |
| // Cap the total bytecode length we inline under a single script, to avoid |
| // excessive inlining in pathological cases. |
| size_t totalBytecodeLength = outerBuilder->inlinedBytecodeLength_ + targetScript->length(); |
| if (totalBytecodeLength > optimizationInfo().inlineMaxTotalBytecodeLength()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineExceededTotalBytecodeLength); |
| return DontInline(targetScript, "Vetoed: exceeding max total bytecode length"); |
| } |
| |
| // Cap the inlining depth. |
| |
| uint32_t maxInlineDepth; |
| if (JitOptions.isSmallFunction(targetScript)) { |
| maxInlineDepth = optimizationInfo().smallFunctionMaxInlineDepth(); |
| } else { |
| maxInlineDepth = optimizationInfo().maxInlineDepth(); |
| |
| // Caller must not be excessively large. |
| if (script()->length() >= optimizationInfo().inliningMaxCallerBytecodeLength()) { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineBigCaller); |
| return DontInline(targetScript, "Vetoed: caller excessively large"); |
| } |
| } |
| |
| BaselineScript* outerBaseline = outermostBuilder()->script()->baselineScript(); |
| if (inliningDepth_ >= maxInlineDepth) { |
| // We hit the depth limit and won't inline this function. Give the |
| // outermost script a max inlining depth of 0, so that it won't be |
| // inlined in other scripts. This heuristic is currently only used |
| // when we're inlining scripts with loops, see the comment below. |
| outerBaseline->setMaxInliningDepth(0); |
| |
| trackOptimizationOutcome(TrackedOutcome::CantInlineExceededDepth); |
| return DontInline(targetScript, "Vetoed: exceeding allowed inline depth"); |
| } |
| |
| // Inlining functions with loops can be complicated. For instance, if we're |
| // close to the inlining depth limit and we inline the function f below, we |
| // can no longer inline the call to g: |
| // |
| // function f() { |
| // while (cond) { |
| // g(); |
| // } |
| // } |
| // |
| // If the loop has many iterations, it's more efficient to call f and inline |
| // g in f. |
| // |
| // To avoid this problem, we record a separate max inlining depth for each |
| // script, indicating at which depth we won't be able to inline all functions |
| // we inlined this time. This solves the issue above, because we will only |
| // inline f if it means we can also inline g. |
| if (targetScript->hasLoops() && |
| inliningDepth_ >= targetScript->baselineScript()->maxInliningDepth()) |
| { |
| trackOptimizationOutcome(TrackedOutcome::CantInlineExceededDepth); |
| return DontInline(targetScript, "Vetoed: exceeding allowed script inline depth"); |
| } |
| |
| // Update the max depth at which we can inline the outer script. |
| MOZ_ASSERT(maxInlineDepth > inliningDepth_); |
| uint32_t scriptInlineDepth = maxInlineDepth - inliningDepth_ - 1; |
| if (scriptInlineDepth < outerBaseline->maxInliningDepth()) |
| outerBaseline->setMaxInliningDepth(scriptInlineDepth); |
| |
| // End of heuristics, we will inline this function. |
| |
| // TI calls ObjectStateChange to trigger invalidation of the caller. |
| TypeSet::ObjectKey* targetKey = TypeSet::ObjectKey::get(target); |
| targetKey->watchStateChangeForInlinedCall(constraints()); |
| |
| outerBuilder->inlinedBytecodeLength_ += targetScript->length(); |
| |
| return InliningDecision_Inline; |
| } |
| |
| bool |
| IonBuilder::selectInliningTargets(const ObjectVector& targets, CallInfo& callInfo, BoolVector& choiceSet, |
| uint32_t* numInlineable) |
| { |
| *numInlineable = 0; |
| uint32_t totalSize = 0; |
| |
| // For each target, ask whether it may be inlined. |
| if (!choiceSet.reserve(targets.length())) |
| return false; |
| |
| // Don't inline polymorphic sites during the definite properties analysis. |
| // AddClearDefiniteFunctionUsesInScript depends on this for correctness. |
| if (info().analysisMode() == Analysis_DefiniteProperties && targets.length() > 1) |
| return true; |
| |
| for (size_t i = 0; i < targets.length(); i++) { |
| JSObject* target = targets[i]; |
| |
| trackOptimizationAttempt(TrackedStrategy::Call_Inline); |
| trackTypeInfo(TrackedTypeSite::Call_Target, target); |
| |
| bool inlineable; |
| InliningDecision decision = makeInliningDecision(target, callInfo); |
| switch (decision) { |
| case InliningDecision_Error: |
| return false; |
| case InliningDecision_DontInline: |
| case InliningDecision_WarmUpCountTooLow: |
| inlineable = false; |
| break; |
| case InliningDecision_Inline: |
| inlineable = true; |
| break; |
| default: |
| MOZ_CRASH("Unhandled InliningDecision value!"); |
| } |
| |
| if (target->is<JSFunction>()) { |
| // Enforce a maximum inlined bytecode limit at the callsite. |
| if (inlineable && target->as<JSFunction>().isInterpreted()) { |
| totalSize += target->as<JSFunction>().nonLazyScript()->length(); |
| bool offThread = options.offThreadCompilationAvailable(); |
| if (totalSize > optimizationInfo().inlineMaxBytecodePerCallSite(offThread)) |
| inlineable = false; |
| } |
| } else { |
| // Non-function targets are not supported by polymorphic inlining. |
| inlineable = false; |
| } |
| |
| choiceSet.infallibleAppend(inlineable); |
| if (inlineable) |
| *numInlineable += 1; |
| } |
| |
| // If optimization tracking is turned on and one of the inlineable targets |
| // is a native, track the type info of the call. Most native inlinings |
| // depend on the types of the arguments and the return value. |
| if (isOptimizationTrackingEnabled()) { |
| for (size_t i = 0; i < targets.length(); i++) { |
| if (choiceSet[i] && targets[i]->as<JSFunction>().isNative()) { |
| trackTypeInfo(callInfo); |
| break; |
| } |
| } |
| } |
| |
| MOZ_ASSERT(choiceSet.length() == targets.length()); |
| return true; |
| } |
| |
| static bool |
| CanInlineGetPropertyCache(MGetPropertyCache* cache, MDefinition* thisDef) |
| { |
| MOZ_ASSERT(cache->object()->type() == MIRType_Object); |
| if (cache->object() != thisDef) |
| return false; |
| |
| InlinePropertyTable* table = cache->propTable(); |
| if (!table) |
| return false; |
| if (table->numEntries() == 0) |
| return false; |
| return true; |
| } |
| |
| class WrapMGetPropertyCache |
| { |
| MGetPropertyCache* cache_; |
| |
| private: |
| void discardPriorResumePoint() { |
| if (!cache_) |
| return; |
| |
| InlinePropertyTable* propTable = cache_->propTable(); |
| if (!propTable) |
| return; |
| MResumePoint* rp = propTable->takePriorResumePoint(); |
| if (!rp) |
| return; |
| cache_->block()->discardPreAllocatedResumePoint(rp); |
| } |
| |
| public: |
| explicit WrapMGetPropertyCache(MGetPropertyCache* cache) |
| : cache_(cache) |
| { } |
| |
| ~WrapMGetPropertyCache() { |
| discardPriorResumePoint(); |
| } |
| |
| MGetPropertyCache* get() { |
| return cache_; |
| } |
| MGetPropertyCache* operator->() { |
| return get(); |
| } |
| |
| // This function returns the cache given to the constructor if the |
| // GetPropertyCache can be moved into the ObjectGroup fallback path. |
| MGetPropertyCache* moveableCache(bool hasTypeBarrier, MDefinition* thisDef) { |
| // If we have unhandled uses of the MGetPropertyCache, then we cannot |
| // move it to the ObjectGroup fallback path. |
| if (!hasTypeBarrier) { |
| if (cache_->hasUses()) |
| return nullptr; |
| } else { |
| // There is the TypeBarrier consumer, so we check that this is the |
| // only consumer. |
| MOZ_ASSERT(cache_->hasUses()); |
| if (!cache_->hasOneUse()) |
| return nullptr; |
| } |
| |
| // If the this-object is not identical to the object of the |
| // MGetPropertyCache, then we cannot use the InlinePropertyTable, or if |
| // we do not yet have enough information from the ObjectGroup. |
| if (!CanInlineGetPropertyCache(cache_, thisDef)) |
| return nullptr; |
| |
| MGetPropertyCache* ret = cache_; |
| cache_ = nullptr; |
| return ret; |
| } |
| }; |
| |
| MGetPropertyCache* |
| IonBuilder::getInlineableGetPropertyCache(CallInfo& callInfo) |
| { |
| if (callInfo.constructing()) |
| return nullptr; |
| |
| MDefinition* thisDef = callInfo.thisArg(); |
| if (thisDef->type() != MIRType_Object) |
| return nullptr; |
| |
| MDefinition* funcDef = callInfo.fun(); |
| if (funcDef->type() != MIRType_Object) |
| return nullptr; |
| |
| // MGetPropertyCache with no uses may be optimized away. |
| if (funcDef->isGetPropertyCache()) { |
| WrapMGetPropertyCache cache(funcDef->toGetPropertyCache()); |
| return cache.moveableCache(/* hasTypeBarrier = */ false, thisDef); |
| } |
| |
| // Optimize away the following common pattern: |
| // MTypeBarrier[MIRType_Object] <- MGetPropertyCache |
| if (funcDef->isTypeBarrier()) { |
| MTypeBarrier* barrier = funcDef->toTypeBarrier(); |
| if (barrier->hasUses()) |
| return nullptr; |
| if (barrier->type() != MIRType_Object) |
| return nullptr; |
| if (!barrier->input()->isGetPropertyCache()) |
| return nullptr; |
| |
| WrapMGetPropertyCache cache(barrier->input()->toGetPropertyCache()); |
| return cache.moveableCache(/* hasTypeBarrier = */ true, thisDef); |
| } |
| |
| return nullptr; |
| } |
| |
| IonBuilder::InliningStatus |
| IonBuilder::inlineSingleCall(CallInfo& callInfo, JSObject* targetArg) |
| { |
| if (!targetArg->is<JSFunction>()) { |
| InliningStatus status = inlineNonFunctionCall(callInfo, targetArg); |
| trackInlineSuccess(status); |
| return status; |
| } |
| |
| JSFunction* target = &targetArg->as<JSFunction>(); |
| if (target->isNative()) { |
| InliningStatus status = inlineNativeCall(callInfo, target); |
| trackInlineSuccess(status); |
| return status; |
| } |
| |
| // Track success now, as inlining a scripted call makes a new return block |
| // which has a different pc than the current call pc. |
| trackInlineSuccess(); |
| if (!inlineScriptedCall(callInfo, target)) |
| return InliningStatus_Error; |
| return InliningStatus_Inlined; |
| } |
| |
| IonBuilder::InliningStatus |
| IonBuilder::inlineCallsite(const ObjectVector& targets, CallInfo& callInfo) |
| { |
| if (targets.empty()) { |
| trackOptimizationAttempt(TrackedStrategy::Call_Inline); |
| trackOptimizationOutcome(TrackedOutcome::CantInlineNoTarget); |
| return InliningStatus_NotInlined; |
| } |
| |
| // Is the function provided by an MGetPropertyCache? |
| // If so, the cache may be movable to a fallback path, with a dispatch |
| // instruction guarding on the incoming ObjectGroup. |
| WrapMGetPropertyCache propCache(getInlineableGetPropertyCache(callInfo)); |
| keepFallbackFunctionGetter(propCache.get()); |
| |
| // Inline single targets -- unless they derive from a cache, in which case |
| // avoiding the cache and guarding is still faster. |
| if (!propCache.get() && targets.length() == 1) { |
| JSObject* target = targets[0]; |
| |
| trackOptimizationAttempt(TrackedStrategy::Call_Inline); |
| trackTypeInfo(TrackedTypeSite::Call_Target, target); |
| |
| InliningDecision decision = makeInliningDecision(target, callInfo); |
| switch (decision) { |
| case InliningDecision_Error: |
| return InliningStatus_Error; |
| case InliningDecision_DontInline: |
| return InliningStatus_NotInlined; |
| case InliningDecision_WarmUpCountTooLow: |
| return InliningStatus_WarmUpCountTooLow; |
| case InliningDecision_Inline: |
| break; |
| } |
| |
| // Inlining will elminate uses of the original callee, but it needs to |
| // be preserved in phis if we bail out. Mark the old callee definition as |
| // implicitly used to ensure this happens. |
| callInfo.fun()->setImplicitlyUsedUnchecked(); |
| |
| // If the callee is not going to be a lambda (which may vary across |
| // different invocations), then the callee definition can be replaced by a |
| // constant. |
| if (target->isSingleton()) { |
| // Replace the function with an MConstant. |
| MConstant* constFun = constant(ObjectValue(*target)); |
| if (callInfo.constructing() && callInfo.getNewTarget() == callInfo.fun()) |
| callInfo.setNewTarget(constFun); |
| callInfo.setFun(constFun); |
| } |
| |
| return inlineSingleCall(callInfo, target); |
| } |
| |
| // Choose a subset of the targets for polymorphic inlining. |
| BoolVector choiceSet(alloc()); |
| uint32_t numInlined; |
| if (!selectInliningTargets(targets, callInfo, choiceSet, &numInlined)) |
| return InliningStatus_Error; |
| if (numInlined == 0) |
| return InliningStatus_NotInlined; |
| |
| // Perform a polymorphic dispatch. |
| if (!inlineCalls(callInfo, targets, choiceSet, propCache.get())) |
| return InliningStatus_Error; |
| |
| return InliningStatus_Inlined; |
| } |
| |
| bool |
| IonBuilder::inlineGenericFallback(JSFunction* target, CallInfo& callInfo, MBasicBlock* dispatchBlock) |
| { |
| // Generate a new block with all arguments on-stack. |
| MBasicBlock* fallbackBlock = newBlock(dispatchBlock, pc); |
| if (!fallbackBlock) |
| return false; |
| |
| // Create a new CallInfo to track modified state within this block. |
| CallInfo fallbackInfo(alloc(), callInfo.constructing()); |
| if (!fallbackInfo.init(callInfo)) |
| return false; |
| fallbackInfo.popFormals(fallbackBlock); |
| |
| // Generate an MCall, which uses stateful |current|. |
| if (!setCurrentAndSpecializePhis(fallbackBlock)) |
| return false; |
| if (!makeCall(target, fallbackInfo)) |
| return false; |
| |
| // Pass return block to caller as |current|. |
| return true; |
| } |
| |
| bool |
| IonBuilder::inlineObjectGroupFallback(CallInfo& callInfo, MBasicBlock* dispatchBlock, |
| MObjectGroupDispatch* dispatch, MGetPropertyCache* cache, |
| MBasicBlock** fallbackTarget) |
| { |
| // Getting here implies the following: |
| // 1. The call function is an MGetPropertyCache, or an MGetPropertyCache |
| // followed by an MTypeBarrier. |
| MOZ_ASSERT(callInfo.fun()->isGetPropertyCache() || callInfo.fun()->isTypeBarrier()); |
| |
| // 2. The MGetPropertyCache has inlineable cases by guarding on the ObjectGroup. |
| MOZ_ASSERT(dispatch->numCases() > 0); |
| |
| // 3. The MGetPropertyCache (and, if applicable, MTypeBarrier) only |
| // have at most a single use. |
| MOZ_ASSERT_IF(callInfo.fun()->isGetPropertyCache(), !cache->hasUses()); |
| MOZ_ASSERT_IF(callInfo.fun()->isTypeBarrier(), cache->hasOneUse()); |
| |
| // This means that no resume points yet capture the MGetPropertyCache, |
| // so everything from the MGetPropertyCache up until the call is movable. |
| // We now move the MGetPropertyCache and friends into a fallback path. |
| MOZ_ASSERT(cache->idempotent()); |
| |
| // Create a new CallInfo to track modified state within the fallback path. |
| CallInfo fallbackInfo(alloc(), callInfo.constructing()); |
| if (!fallbackInfo.init(callInfo)) |
| return false; |
| |
| // Capture stack prior to the call operation. This captures the function. |
| MResumePoint* preCallResumePoint = |
| MResumePoint::New(alloc(), dispatchBlock, pc, MResumePoint::ResumeAt); |
| if (!preCallResumePoint) |
| return false; |
| |
| DebugOnly<size_t> preCallFuncIndex = preCallResumePoint->stackDepth() - callInfo.numFormals(); |
| MOZ_ASSERT(preCallResumePoint->getOperand(preCallFuncIndex) == fallbackInfo.fun()); |
| |
| // In the dispatch block, replace the function's slot entry with Undefined. |
| MConstant* undefined = MConstant::New(alloc(), UndefinedValue()); |
| dispatchBlock->add(undefined); |
| dispatchBlock->rewriteAtDepth(-int(callInfo.numFormals()), undefined); |
| |
| // Construct a block that does nothing but remove formals from the stack. |
| // This is effectively changing the entry resume point of the later fallback block. |
| MBasicBlock* prepBlock = newBlock(dispatchBlock, pc); |
| if (!prepBlock) |
| return false; |
| fallbackInfo.popFormals(prepBlock); |
| |
| // Construct a block into which the MGetPropertyCache can be moved. |
| // This is subtle: the pc and resume point are those of the MGetPropertyCache! |
| InlinePropertyTable* propTable = cache->propTable(); |
| MResumePoint* priorResumePoint = propTable->takePriorResumePoint(); |
| MOZ_ASSERT(propTable->pc() != nullptr); |
| MOZ_ASSERT(priorResumePoint != nullptr); |
| MBasicBlock* getPropBlock = newBlock(prepBlock, propTable->pc(), priorResumePoint); |
| if (!getPropBlock) |
| return false; |
| |
| prepBlock->end(MGoto::New(alloc(), getPropBlock)); |
| |
| // Since the getPropBlock inherited the stack from right before the MGetPropertyCache, |
| // the target of the MGetPropertyCache is still on the stack. |
| DebugOnly<MDefinition*> checkObject = getPropBlock->pop(); |
| MOZ_ASSERT(checkObject == cache->object()); |
| |
| // Move the MGetPropertyCache and friends into the getPropBlock. |
| if (fallbackInfo.fun()->isGetPropertyCache()) { |
| MOZ_ASSERT(fallbackInfo.fun()->toGetPropertyCache() == cache); |
| getPropBlock->addFromElsewhere(cache); |
| getPropBlock->push(cache); |
| } else { |
| MTypeBarrier* barrier = callInfo.fun()->toTypeBarrier(); |
| MOZ_ASSERT(barrier->type() == MIRType_Object); |
| MOZ_ASSERT(barrier->input()->isGetPropertyCache()); |
| MOZ_ASSERT(barrier->input()->toGetPropertyCache() == cache); |
| |
| getPropBlock->addFromElsewhere(cache); |
| getPropBlock->addFromElsewhere(barrier); |
| getPropBlock->push(barrier); |
| } |
| |
| // Construct an end block with the correct resume point. |
| MBasicBlock* preCallBlock = newBlock(getPropBlock, pc, preCallResumePoint); |
| if (!preCallBlock) |
| return false; |
| getPropBlock->end(MGoto::New(alloc(), preCallBlock)); |
| |
| // Now inline the MCallGeneric, using preCallBlock as the dispatch point. |
| if (!inlineGenericFallback(nullptr, fallbackInfo, preCallBlock)) |
| return false; |
| |
| // inlineGenericFallback() set the return block as |current|. |
| preCallBlock->end(MGoto::New(alloc(), current)); |
| *fallbackTarget = prepBlock; |
| return true; |
| } |
| |
| bool |
| IonBuilder::inlineCalls(CallInfo& callInfo, const ObjectVector& targets, BoolVector& choiceSet, |
| MGetPropertyCache* maybeCache) |
| { |
| // Only handle polymorphic inlining. |
| MOZ_ASSERT(IsIonInlinablePC(pc)); |
| MOZ_ASSERT(choiceSet.length() == targets.length()); |
| MOZ_ASSERT_IF(!maybeCache, targets.length() >= 2); |
| MOZ_ASSERT_IF(maybeCache, targets.length() >= 1); |
| |
| MBasicBlock* dispatchBlock = current; |
| callInfo.setImplicitlyUsedUnchecked(); |
| callInfo.pushFormals(dispatchBlock); |
| |
| // Patch any InlinePropertyTable to only contain functions that are |
| // inlineable. The InlinePropertyTable will also be patched at the end to |
| // exclude native functions that vetoed inlining. |
| if (maybeCache) { |
| InlinePropertyTable* propTable = maybeCache->propTable(); |
| propTable->trimToTargets(targets); |
| if (propTable->numEntries() == 0) |
| maybeCache = nullptr; |
| } |
| |
| // Generate a dispatch based on guard kind. |
| MDispatchInstruction* dispatch; |
| if (maybeCache) { |
| dispatch = MObjectGroupDispatch::New(alloc(), maybeCache->object(), maybeCache->propTable()); |
| callInfo.fun()->setImplicitlyUsedUnchecked(); |
| } else { |
| dispatch = MFunctionDispatch::New(alloc(), callInfo.fun()); |
| } |
| |
| // Generate a return block to host the rval-collecting MPhi. |
| jsbytecode* postCall = GetNextPc(pc); |
| MBasicBlock* returnBlock = newBlock(nullptr, postCall); |
| if (!returnBlock) |
| return false; |
| returnBlock->setCallerResumePoint(callerResumePoint_); |
| |
| // Set up stack, used to manually create a post-call resume point. |
| returnBlock->inheritSlots(dispatchBlock); |
| callInfo.popFormals(returnBlock); |
| |
| MPhi* retPhi = MPhi::New(alloc()); |
| returnBlock->addPhi(retPhi); |
| returnBlock->push(retPhi); |
| |
| // Create a resume point from current stack state. |
| returnBlock->initEntrySlots(alloc()); |
| |
| // Reserve the capacity for the phi. |
| // Note: this is an upperbound. Unreachable targets and uninlineable natives are also counted. |
| uint32_t count = 1; // Possible fallback block. |
| for (uint32_t i = 0; i < targets.length(); i++) { |
| if (choiceSet[i]) |
| count++; |
| } |
| retPhi->reserveLength(count); |
| |
| // Inline each of the inlineable targets. |
| for (uint32_t i = 0; i < targets.length(); i++) { |
| // Target must be inlineable. |
| if (!choiceSet[i]) |
| continue; |
| |
| // Even though we made one round of inline decisions already, we may |
| // be amending them below. |
| amendOptimizationAttempt(i); |
| |
| // Target must be reachable by the MDispatchInstruction. |
| JSFunction* target = &targets[i]->as<JSFunction>(); |
| if (maybeCache && !maybeCache->propTable()->hasFunction(target)) { |
| choiceSet[i] = false; |
| trackOptimizationOutcome(TrackedOutcome::CantInlineNotInDispatch); |
| continue; |
| } |
| |
| MBasicBlock* inlineBlock = newBlock(dispatchBlock, pc); |
| if (!inlineBlock) |
| return false; |
| |
| // Create a function MConstant to use in the entry ResumePoint. If we |
| // can't use a constant, add a no-op MPolyInlineGuard, to prevent |
| // hoisting scope chain gets above the dispatch instruction. |
| MInstruction* funcDef; |
| if (target->isSingleton()) |
| funcDef = MConstant::New(alloc(), ObjectValue(*target), constraints()); |
| else |
| funcDef = MPolyInlineGuard::New(alloc(), callInfo.fun()); |
| |
| funcDef->setImplicitlyUsedUnchecked(); |
| dispatchBlock->add(funcDef); |
| |
| // Use the inlined callee in the inline resume point and on stack. |
| int funIndex = inlineBlock->entryResumePoint()->stackDepth() - callInfo.numFormals(); |
| inlineBlock->entryResumePoint()->replaceOperand(funIndex, funcDef); |
| inlineBlock->rewriteSlot(funIndex, funcDef); |
| |
| // Create a new CallInfo to track modified state within the inline block. |
| CallInfo inlineInfo(alloc(), callInfo.constructing()); |
| if (!inlineInfo.init(callInfo)) |
| return false; |
| inlineInfo.popFormals(inlineBlock); |
| inlineInfo.setFun(funcDef); |
| |
| if (maybeCache) { |
| // Assign the 'this' value a TypeSet specialized to the groups that |
| // can generate this inlining target. |
| MOZ_ASSERT(callInfo.thisArg() == maybeCache->object()); |
| TemporaryTypeSet* thisTypes = maybeCache->propTable()->buildTypeSetForFunction(target); |
| if (!thisTypes) |
| return false; |
| |
| MFilterTypeSet* filter = MFilterTypeSet::New(alloc(), inlineInfo.thisArg(), thisTypes); |
| inlineBlock->add(filter); |
| inlineInfo.setThis(filter); |
| } |
| |
| // Inline the call into the inlineBlock. |
| if (!setCurrentAndSpecializePhis(inlineBlock)) |
| return false; |
| InliningStatus status = inlineSingleCall(inlineInfo, target); |
| if (status == InliningStatus_Error) |
| return false; |
| |
| // Natives may veto inlining. |
| if (status == InliningStatus_NotInlined) { |
| MOZ_ASSERT(target->isNative()); |
| MOZ_ASSERT(current == inlineBlock); |
| graph().removeBlock(inlineBlock); |
| choiceSet[i] = false; |
| continue; |
| } |
| |
| // inlineSingleCall() changed |current| to the inline return block. |
| MBasicBlock* inlineReturnBlock = current; |
| setCurrent(dispatchBlock); |
| |
| // Connect the inline path to the returnBlock. |
| ObjectGroup* funcGroup = target->isSingleton() ? nullptr : target->group(); |
| if (!dispatch->addCase(target, funcGroup, inlineBlock)) |
| return false; |
| |
| MDefinition* retVal = inlineReturnBlock->peek(-1); |
| retPhi->addInput(retVal); |
| inlineReturnBlock->end(MGoto::New(alloc(), returnBlock)); |
| if (!returnBlock->addPredecessorWithoutPhis(inlineReturnBlock)) |
| return false; |
| } |
| |
| // Patch the InlinePropertyTable to not dispatch to vetoed paths. |
| bool useFallback; |
| if (maybeCache) { |
| InlinePropertyTable* propTable = maybeCache->propTable(); |
| propTable->trimTo(targets, choiceSet); |
| |
| if (propTable->numEntries() == 0) { |
| // If all paths were vetoed, output only a generic fallback path. |
| MOZ_ASSERT(dispatch->numCases() == 0); |
| maybeCache = nullptr; |
| useFallback = true; |
| } else { |
| // We need a fallback path if the ObjectGroup dispatch does not |
| // handle all incoming objects. |
| useFallback = false; |
| TemporaryTypeSet* objectTypes = maybeCache->object()->resultTypeSet(); |
| for (uint32_t i = 0; i < objectTypes->getObjectCount(); i++) { |
| TypeSet::ObjectKey* obj = objectTypes->getObject(i); |
| if (!obj) |
| continue; |
| |
| if (!obj->isGroup()) { |
| useFallback = true; |
| break; |
| } |
| |
| if (!propTable->hasObjectGroup(obj->group())) { |
| useFallback = true; |
| break; |
| } |
| } |
| |
| if (!useFallback) { |
| // The object group dispatch handles all possible incoming |
| // objects, so the cache and barrier will not be reached and |
| // can be eliminated. |
| if (callInfo.fun()->isGetPropertyCache()) { |
| MOZ_ASSERT(callInfo.fun() == maybeCache); |
| } else { |
| MTypeBarrier* barrier = callInfo.fun()->toTypeBarrier(); |
| MOZ_ASSERT(!barrier->hasUses()); |
| MOZ_ASSERT(barrier->type() == MIRType_Object); |
| MOZ_ASSERT(barrier->input()->isGetPropertyCache()); |
| MOZ_ASSERT(barrier->input()->toGetPropertyCache() == maybeCache); |
| barrier->block()->discard(barrier); |
| } |
| |
| MOZ_ASSERT(!maybeCache->hasUses()); |
| maybeCache->block()->discard(maybeCache); |
| } |
| } |
| } else { |
| useFallback = dispatch->numCases() < targets.length(); |
| } |
| |
| // If necessary, generate a fallback path. |
| if (useFallback) { |
| // Generate fallback blocks, and set |current| to the fallback return block. |
| if (maybeCache) { |
| MBasicBlock* fallbackTarget; |
| if (!inlineObjectGroupFallback(callInfo, dispatchBlock, |
| dispatch->toObjectGroupDispatch(), |
| maybeCache, &fallbackTarget)) |
| { |
| return false; |
| } |
| dispatch->addFallback(fallbackTarget); |
| } else { |
| JSFunction* remaining = nullptr; |
| |
| // If there is only 1 remaining case, we can annotate the fallback call |
| // with the target information. |
| if (dispatch->numCases() + 1 == targets.length()) { |
| for (uint32_t i = 0; i < targets.length(); i++) { |
| if (choiceSet[i]) |
| continue; |
| |
| MOZ_ASSERT(!remaining); |
| if (targets[i]->is<JSFunction>() && targets[i]->as<JSFunction>().isSingleton()) |
| remaining = &targets[i]->as<JSFunction>(); |
| break; |
| } |
| } |
| |
| if (!inlineGenericFallback(remaining, callInfo, dispatchBlock)) |
| return false; |
| dispatch->addFallback(current); |
| } |
| |
| MBasicBlock* fallbackReturnBlock = current; |
| |
| // Connect fallback case to return infrastructure. |
| MDefinition* retVal = fallbackReturnBlock->peek(-1); |
| retPhi->addInput(retVal); |
| fallbackReturnBlock->end(MGoto::New(alloc(), returnBlock)); |
| if (!returnBlock->addPredecessorWithoutPhis(fallbackReturnBlock)) |
| return false; |
| } |
| |
| // Finally add the dispatch instruction. |
| // This must be done at the end so that add() may be called above. |
| dispatchBlock->end(dispatch); |
| |
| // Check the depth change: +1 for retval |
| MOZ_ASSERT(returnBlock->stackDepth() == dispatchBlock->stackDepth() - callInfo.numFormals() + 1); |
| |
| graph().moveBlockToEnd(returnBlock); |
| return setCurrentAndSpecializePhis(returnBlock); |
| } |
| |
| MInstruction* |
| IonBuilder::createDeclEnvObject(MDefinition* callee, MDefinition* scope) |
| { |
| // Get a template CallObject that we'll use to generate inline object |
| // creation. |
| DeclEnvObject* templateObj = inspector->templateDeclEnvObject(); |
| |
| // One field is added to the function to handle its name. This cannot be a |
| // dynamic slot because there is still plenty of room on the DeclEnv object. |
| MOZ_ASSERT(!templateObj->hasDynamicSlots()); |
| |
| // Allocate the actual object. It is important that no intervening |
| // instructions could potentially bailout, thus leaking the dynamic slots |
| // pointer. |
| MInstruction* declEnvObj = MNewDeclEnvObject::New(alloc(), templateObj); |
| current->add(declEnvObj); |
| |
| // Initialize the object's reserved slots. No post barrier is needed here: |
| // the object will be allocated in the nursery if possible, and if the |
| // tenured heap is used instead, a minor collection will have been performed |
| // that moved scope/callee to the tenured heap. |
| current->add(MStoreFixedSlot::New(alloc(), declEnvObj, DeclEnvObject::enclosingScopeSlot(), scope)); |
| current->add(MStoreFixedSlot::New(alloc(), declEnvObj, DeclEnvObject::lambdaSlot(), callee)); |
| |
| return declEnvObj; |
| } |
| |
| MInstruction* |
| IonBuilder::createCallObject(MDefinition* callee, MDefinition* scope) |
| { |
| // Get a template CallObject that we'll use to generate inline object |
| // creation. |
| CallObject* templateObj = inspector->templateCallObject(); |
| |
| // Allocate the object. Run-once scripts need a singleton type, so always do |
| // a VM call in such cases. |
| MNullaryInstruction* callObj; |
| if (script()->treatAsRunOnce()) |
| callObj = MNewRunOnceCallObject::New(alloc(), templateObj); |
| else |
| callObj = MNewCallObject::New(alloc(), templateObj); |
| current->add(callObj); |
| |
| // Initialize the object's reserved slots. No post barrier is needed here, |
| // for the same reason as in createDeclEnvObject. |
| current->add(MStoreFixedSlot::New(alloc(), callObj, CallObject::enclosingScopeSlot(), scope)); |
| current->add(MStoreFixedSlot::New(alloc(), callObj, CallObject::calleeSlot(), callee)); |
| |
| // Initialize argument slots. |
| MSlots* slots = nullptr; |
| for (AliasedFormalIter i(script()); i; i++) { |
| unsigned slot = i.scopeSlot(); |
| unsigned formal = i.frameIndex(); |
| MDefinition* param = current->getSlot(info().argSlotUnchecked(formal)); |
| if (slot >= templateObj->numFixedSlots()) { |
| if (!slots) { |
| slots = MSlots::New(alloc(), callObj); |
| current->add(slots); |
| } |
| current->add(MStoreSlot::New(alloc(), slots, slot - templateObj->numFixedSlots(), param)); |
| } else { |
| current->add(MStoreFixedSlot::New(alloc(), callObj, slot, param)); |
| } |
| } |
| |
| return callObj; |
| } |
| |
| MDefinition* |
| IonBuilder::createThisScripted(MDefinition* callee, MDefinition* newTarget) |
| { |
| // Get callee.prototype. |
| // |
| // This instruction MUST be idempotent: since it does not correspond to an |
| // explicit operation in the bytecode, we cannot use resumeAfter(). |
| // Getters may not override |prototype| fetching, so this operation is indeed idempotent. |
| // - First try an idempotent property cache. |
| // - Upon failing idempotent property cache, we can't use a non-idempotent cache, |
| // therefore we fallback to CallGetProperty |
| // |
| // Note: both CallGetProperty and GetPropertyCache can trigger a GC, |
| // and thus invalidation. |
| MInstruction* getProto; |
| if (!invalidatedIdempotentCache()) { |
| MConstant* id = constant(StringValue(names().prototype)); |
| MGetPropertyCache* getPropCache = MGetPropertyCache::New(alloc(), newTarget, id, |
| /* monitored = */ false); |
| getPropCache->setIdempotent(); |
| getProto = getPropCache; |
| } else { |
| MCallGetProperty* callGetProp = MCallGetProperty::New(alloc(), newTarget, names().prototype); |
| callGetProp->setIdempotent(); |
| getProto = callGetProp; |
| } |
| current->add(getProto); |
| |
| // Create this from prototype |
| MCreateThisWithProto* createThis = MCreateThisWithProto::New(alloc(), callee, newTarget, getProto); |
| current->add(createThis); |
| |
| return createThis; |
| } |
| |
| JSObject* |
| IonBuilder::getSingletonPrototype(JSFunction* target) |
| { |
| TypeSet::ObjectKey* targetKey = TypeSet::ObjectKey::get(target); |
| if (targetKey->unknownProperties()) |
| return nullptr; |
| |
| jsid protoid = NameToId(names().prototype); |
| HeapTypeSetKey protoProperty = targetKey->property(protoid); |
| |
| return protoProperty.singleton(constraints()); |
| } |
| |
| MDefinition* |
| IonBuilder::createThisScriptedSingleton(JSFunction* target, MDefinition* callee) |
| { |
| // Get the singleton prototype (if exists) |
| JSObject* proto = getSingletonPrototype(target); |
| if (!proto) |
| return nullptr; |
| |
| JSObject* templateObject = inspector->getTemplateObject(pc); |
| if (!templateObject) |
| return nullptr; |
| if (!templateObject->is<PlainObject>() && !templateObject->is<UnboxedPlainObject>()) |
| return nullptr; |
| if (templateObject->getProto() != proto) |
| return nullptr; |
| |
| TypeSet::ObjectKey* templateObjectKey = TypeSet::ObjectKey::get(templateObject->group()); |
| if (templateObjectKey->hasFlags(constraints(), OBJECT_FLAG_NEW_SCRIPT_CLEARED)) |
| return nullptr; |
| |
| StackTypeSet* thisTypes = TypeScript::ThisTypes(target->nonLazyScript()); |
| if (!thisTypes || !thisTypes->hasType(TypeSet::ObjectType(templateObject))) |
| return nullptr; |
| |
| // Generate an inline path to create a new |this| object with |
| // the given singleton prototype. |
| MConstant* templateConst = MConstant::NewConstraintlessObject(alloc(), templateObject); |
| MCreateThisWithTemplate* createThis = |
| MCreateThisWithTemplate::New(alloc(), constraints(), templateConst, |
| templateObject->group()->initialHeap(constraints())); |
| current->add(templateConst); |
| current->add(createThis); |
| |
| return createThis; |
| } |
| |
| MDefinition* |
| IonBuilder::createThisScriptedBaseline(MDefinition* callee) |
| { |
| // Try to inline |this| creation based on Baseline feedback. |
| |
| JSFunction* target = inspector->getSingleCallee(pc); |
| if (!target || !target->hasScript()) |
| return nullptr; |
| |
| JSObject* templateObject = inspector->getTemplateObject(pc); |
| if (!templateObject) |
| return nullptr; |
| if (!templateObject->is<PlainObject>() && !templateObject->is<UnboxedPlainObject>()) |
| return nullptr; |
| |
| Shape* shape = target->lookupPure(compartment->runtime()->names().prototype); |
| if (!shape || !shape->hasDefaultGetter() || !shape->hasSlot()) |
| return nullptr; |
| |
| Value protov = target->getSlot(shape->slot()); |
| if (!protov.isObject()) |
| return nullptr; |
| |
| JSObject* proto = checkNurseryObject(&protov.toObject()); |
| if (proto != templateObject->getProto()) |
| return nullptr; |
| |
| TypeSet::ObjectKey* templateObjectKey = TypeSet::ObjectKey::get(templateObject->group()); |
| if (templateObjectKey->hasFlags(constraints(), OBJECT_FLAG_NEW_SCRIPT_CLEARED)) |
| return nullptr; |
| |
| StackTypeSet* thisTypes = TypeScript::ThisTypes(target->nonLazyScript()); |
| if (!thisTypes || !thisTypes->hasType(TypeSet::ObjectType(templateObject))) |
| return nullptr; |
| |
| // Shape guard. |
| callee = addShapeGuard(callee, target->lastProperty(), Bailout_ShapeGuard); |
| |
| // Guard callee.prototype == proto. |
| MOZ_ASSERT(shape->numFixedSlots() == 0, "Must be a dynamic slot"); |
| MSlots* slots = MSlots::New(alloc(), callee); |
| current->add(slots); |
| MLoadSlot* prototype = MLoadSlot::New(alloc(), slots, shape->slot()); |
| current->add(prototype); |
| MDefinition* protoConst = constant(ObjectValue(*proto)); |
| MGuardObjectIdentity* guard = MGuardObjectIdentity::New(alloc(), prototype, protoConst, |
| /* bailOnEquality = */ false); |
| current->add(guard); |
| |
| // Generate an inline path to create a new |this| object with |
| // the given prototype. |
| MConstant* templateConst = MConstant::NewConstraintlessObject(alloc(), templateObject); |
| MCreateThisWithTemplate* createThis = |
| MCreateThisWithTemplate::New(alloc(), constraints(), templateConst, |
| templateObject->group()->initialHeap(constraints())); |
| current->add(templateConst); |
| current->add(createThis); |
| |
| return createThis; |
| } |
| |
| MDefinition* |
| IonBuilder::createThis(JSFunction* target, MDefinition* callee, MDefinition* newTarget) |
| { |
| // Create |this| for unknown target. |
| if (!target) { |
| if (MDefinition* createThis = createThisScriptedBaseline(callee)) |
| return createThis; |
| |
| MCreateThis* createThis = MCreateThis::New(alloc(), callee, newTarget); |
| current->add(createThis); |
| return createThis; |
| } |
| |
| // Native constructors build the new Object themselves. |
| if (target->isNative()) { |
| if (!target->isConstructor()) |
| return nullptr; |
| |
| MConstant* magic = MConstant::New(alloc(), MagicValue(JS_IS_CONSTRUCTING)); |
| current->add(magic); |
| return magic; |
| } |
| |
| if (target->isDerivedClassConstructor()) { |
| MOZ_ASSERT(target->isClassConstructor()); |
| return constant(MagicValue(JS_UNINITIALIZED_LEXICAL)); |
| } |
| |
| // Try baking in the prototype. |
| if (MDefinition* createThis = createThisScriptedSingleton(target, callee)) |
| return createThis; |
| |
| if (MDefinition* createThis = createThisScriptedBaseline(callee)) |
| return createThis; |
| |
| return createThisScripted(callee, newTarget); |
| } |
| |
| bool |
| IonBuilder::jsop_funcall(uint32_t argc) |
| { |
| // Stack for JSOP_FUNCALL: |
| // 1: arg0 |
| // ... |
| // argc: argN |
| // argc+1: JSFunction*, the 'f' in |f.call()|, in |this| position. |
| // argc+2: The native 'call' function. |
| |
| int calleeDepth = -((int)argc + 2); |
| int funcDepth = -((int)argc + 1); |
| |
| // If |Function.prototype.call| may be overridden, don't optimize callsite. |
| TemporaryTypeSet* calleeTypes = current->peek(calleeDepth)->resultTypeSet(); |
| JSFunction* native = getSingleCallTarget(calleeTypes); |
| if (!native || !native->isNative() || native->native() != &fun_call) { |
| CallInfo callInfo(alloc(), false); |
| if (!callInfo.init(current, argc)) |
| return false; |
| return makeCall(native, callInfo); |
| } |
| current->peek(calleeDepth)->setImplicitlyUsedUnchecked(); |
| |
| // Extract call target. |
| TemporaryTypeSet* funTypes = current->peek(funcDepth)->resultTypeSet(); |
| JSFunction* target = getSingleCallTarget(funTypes); |
| |
| // Shimmy the slots down to remove the native 'call' function. |
| current->shimmySlots(funcDepth - 1); |
| |
| bool zeroArguments = (argc == 0); |
| |
| // If no |this| argument was provided, explicitly pass Undefined. |
| // Pushing is safe here, since one stack slot has been removed. |
| if (zeroArguments) { |
| pushConstant(UndefinedValue()); |
| } else { |
| // |this| becomes implicit in the call. |
| argc -= 1; |
| } |
| |
| CallInfo callInfo(alloc(), false); |
| if (!callInfo.init(current, argc)) |
| return false; |
| |
| // Try to inline the call. |
| if (!zeroArguments) { |
| InliningDecision decision = makeInliningDecision(target, callInfo); |
| switch (decision) { |
| case InliningDecision_Error: |
| return false; |
| case InliningDecision_DontInline: |
| case InliningDecision_WarmUpCountTooLow: |
| break; |
| case InliningDecision_Inline: |
| if (target->isInterpreted()) |
| return inlineScriptedCall(callInfo, target); |
| break; |
| } |
| } |
| |
| // Call without inlining. |
| return makeCall(target, callInfo); |
| } |
| |
| bool |
| IonBuilder::jsop_funapply(uint32_t argc) |
| { |
| int calleeDepth = -((int)argc + 2); |
| |
| TemporaryTypeSet* calleeTypes = current->peek(calleeDepth)->resultTypeSet(); |
| JSFunction* native = getSingleCallTarget(calleeTypes); |
| if (argc != 2 || info().analysisMode() == Analysis_ArgumentsUsage) { |
| CallInfo callInfo(alloc(), false); |
| if (!callInfo.init(current, argc)) |
| return false; |
| return makeCall(native, callInfo); |
| } |
| |
| // Disable compilation if the second argument to |apply| cannot be guaranteed |
| // to be either definitely |arguments| or definitely not |arguments|. |
| MDefinition* argument = current->peek(-1); |
| if (script()->argumentsHasVarBinding() && |
| argument->mightBeType(MIRType_MagicOptimizedArguments) && |
| argument->type() != MIRType_MagicOptimizedArguments) |
| { |
| return abort("fun.apply with MaybeArguments"); |
| } |
| |
| // Fallback to regular call if arg 2 is not definitely |arguments|. |
| if (argument->type() != MIRType_MagicOptimizedArguments) { |
| // Optimize fun.apply(self, array) if the length is sane and there are no holes. |
| TemporaryTypeSet* objTypes = argument->resultTypeSet(); |
| if (native && native->isNative() && native->native() == fun_apply && |
| objTypes && |
| objTypes->getKnownClass(constraints()) == &ArrayObject::class_ && |
| !objTypes->hasObjectFlags(constraints(), OBJECT_FLAG_LENGTH_OVERFLOW) && |
| ElementAccessIsPacked(constraints(), argument)) |
| { |
| return jsop_funapplyarray(argc); |
| } |
| |
| CallInfo callInfo(alloc(), false); |
| if (!callInfo.init(current, argc)) |
| return false; |
| return makeCall(native, callInfo); |
| } |
| |
| if ((!native || !native->isNative() || |
| native->native() != fun_apply) && |
| info().analysisMode() != Analysis_DefiniteProperties) |
| { |
| return abort("fun.apply speculation failed"); |
| } |
| |
| // Use funapply that definitely uses |arguments| |
| return jsop_funapplyarguments(argc); |
| } |
| |
| bool |
| IonBuilder::jsop_funapplyarray(uint32_t argc) |
| { |
| MOZ_ASSERT(argc == 2); |
| |
| int funcDepth = -((int)argc + 1); |
| |
| // Extract call target. |
| TemporaryTypeSet* funTypes = current->peek(funcDepth)->resultTypeSet(); |
| JSFunction* target = getSingleCallTarget(funTypes); |
| |
| // Pop the array agument |
| MDefinition* argObj = current->pop(); |
| |
| MElements* elements = MElements::New(alloc(), argObj); |
| current->add(elements); |
| |
| // Pop the |this| argument. |
| MDefinition* argThis = current->pop(); |
| |
| // Unwrap the (JSFunction *) parameter. |
| MDefinition* argFunc = current->pop(); |
| |
| // Pop apply function. |
| MDefinition* nativeFunc = current->pop(); |
| nativeFunc->setImplicitlyUsedUnchecked(); |
| |
| MApplyArray* apply = MApplyArray::New(alloc(), target, argFunc, elements, argThis); |
| current->add(apply); |
| current->push(apply); |
| if (!resumeAfter(apply)) |
| return false; |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| return pushTypeBarrier(apply, types, BarrierKind::TypeSet); |
| } |
| |
| bool |
| IonBuilder::jsop_funapplyarguments(uint32_t argc) |
| { |
| // Stack for JSOP_FUNAPPLY: |
| // 1: Vp |
| // 2: This |
| // argc+1: JSFunction*, the 'f' in |f.call()|, in |this| position. |
| // argc+2: The native 'apply' function. |
| |
| int funcDepth = -((int)argc + 1); |
| |
| // Extract call target. |
| TemporaryTypeSet* funTypes = current->peek(funcDepth)->resultTypeSet(); |
| JSFunction* target = getSingleCallTarget(funTypes); |
| |
| // When this script isn't inlined, use MApplyArgs, |
| // to copy the arguments from the stack and call the function |
| if (inliningDepth_ == 0 && info().analysisMode() != Analysis_DefiniteProperties) { |
| // The array argument corresponds to the arguments object. As the JIT |
| // is implicitly reading the arguments object in the next instruction, |
| // we need to prevent the deletion of the arguments object from resume |
| // points, so that Baseline will behave correctly after a bailout. |
| MDefinition* vp = current->pop(); |
| vp->setImplicitlyUsedUnchecked(); |
| |
| MDefinition* argThis = current->pop(); |
| |
| // Unwrap the (JSFunction*) parameter. |
| MDefinition* argFunc = current->pop(); |
| |
| // Pop apply function. |
| MDefinition* nativeFunc = current->pop(); |
| nativeFunc->setImplicitlyUsedUnchecked(); |
| |
| MArgumentsLength* numArgs = MArgumentsLength::New(alloc()); |
| current->add(numArgs); |
| |
| MApplyArgs* apply = MApplyArgs::New(alloc(), target, argFunc, numArgs, argThis); |
| current->add(apply); |
| current->push(apply); |
| if (!resumeAfter(apply)) |
| return false; |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| return pushTypeBarrier(apply, types, BarrierKind::TypeSet); |
| } |
| |
| // When inlining we have the arguments the function gets called with |
| // and can optimize even more, by just calling the functions with the args. |
| // We also try this path when doing the definite properties analysis, as we |
| // can inline the apply() target and don't care about the actual arguments |
| // that were passed in. |
| |
| CallInfo callInfo(alloc(), false); |
| |
| // Vp |
| MDefinition* vp = current->pop(); |
| vp->setImplicitlyUsedUnchecked(); |
| |
| // Arguments |
| if (inliningDepth_) { |
| if (!callInfo.setArgs(inlineCallInfo_->argv())) |
| return false; |
| } |
| |
| // This |
| MDefinition* argThis = current->pop(); |
| callInfo.setThis(argThis); |
| |
| // Pop function parameter. |
| MDefinition* argFunc = current->pop(); |
| callInfo.setFun(argFunc); |
| |
| // Pop apply function. |
| MDefinition* nativeFunc = current->pop(); |
| nativeFunc->setImplicitlyUsedUnchecked(); |
| |
| // Try to inline the call. |
| InliningDecision decision = makeInliningDecision(target, callInfo); |
| switch (decision) { |
| case InliningDecision_Error: |
| return false; |
| case InliningDecision_DontInline: |
| case InliningDecision_WarmUpCountTooLow: |
| break; |
| case InliningDecision_Inline: |
| if (target->isInterpreted()) |
| return inlineScriptedCall(callInfo, target); |
| } |
| |
| return makeCall(target, callInfo); |
| } |
| |
| bool |
| IonBuilder::jsop_call(uint32_t argc, bool constructing) |
| { |
| startTrackingOptimizations(); |
| |
| // If this call has never executed, try to seed the observed type set |
| // based on how the call result is used. |
| TemporaryTypeSet* observed = bytecodeTypes(pc); |
| if (observed->empty()) { |
| if (BytecodeFlowsToBitop(pc)) { |
| observed->addType(TypeSet::Int32Type(), alloc_->lifoAlloc()); |
| } else if (*GetNextPc(pc) == JSOP_POS) { |
| // Note: this is lame, overspecialized on the code patterns used |
| // by asm.js and should be replaced by a more general mechanism. |
| // See bug 870847. |
| observed->addType(TypeSet::DoubleType(), alloc_->lifoAlloc()); |
| } |
| } |
| |
| int calleeDepth = -((int)argc + 2 + constructing); |
| |
| // Acquire known call target if existent. |
| ObjectVector targets(alloc()); |
| TemporaryTypeSet* calleeTypes = current->peek(calleeDepth)->resultTypeSet(); |
| if (calleeTypes && !getPolyCallTargets(calleeTypes, constructing, targets, 4)) |
| return false; |
| |
| CallInfo callInfo(alloc(), constructing); |
| if (!callInfo.init(current, argc)) |
| return false; |
| |
| // Try inlining |
| InliningStatus status = inlineCallsite(targets, callInfo); |
| if (status == InliningStatus_Inlined) |
| return true; |
| if (status == InliningStatus_Error) |
| return false; |
| |
| // No inline, just make the call. |
| JSFunction* target = nullptr; |
| if (targets.length() == 1 && targets[0]->is<JSFunction>()) |
| target = &targets[0]->as<JSFunction>(); |
| |
| if (target && status == InliningStatus_WarmUpCountTooLow) { |
| MRecompileCheck* check = |
| MRecompileCheck::New(alloc(), target->nonLazyScript(), |
| optimizationInfo().inliningRecompileThreshold(), |
| MRecompileCheck::RecompileCheck_Inlining); |
| current->add(check); |
| } |
| |
| return makeCall(target, callInfo); |
| } |
| |
| bool |
| IonBuilder::testShouldDOMCall(TypeSet* inTypes, JSFunction* func, JSJitInfo::OpType opType) |
| { |
| if (!func->isNative() || !func->jitInfo()) |
| return false; |
| |
| // If all the DOM objects flowing through are legal with this |
| // property, we can bake in a call to the bottom half of the DOM |
| // accessor |
| DOMInstanceClassHasProtoAtDepth instanceChecker = |
| compartment->runtime()->DOMcallbacks()->instanceClassMatchesProto; |
| |
| const JSJitInfo* jinfo = func->jitInfo(); |
| if (jinfo->type() != opType) |
| return false; |
| |
| for (unsigned i = 0; i < inTypes->getObjectCount(); i++) { |
| TypeSet::ObjectKey* key = inTypes->getObject(i); |
| if (!key) |
| continue; |
| |
| if (!key->hasStableClassAndProto(constraints())) |
| return false; |
| |
| if (!instanceChecker(key->clasp(), jinfo->protoID, jinfo->depth)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool |
| ArgumentTypesMatch(MDefinition* def, StackTypeSet* calleeTypes) |
| { |
| if (!calleeTypes) |
| return false; |
| |
| if (def->resultTypeSet()) { |
| MOZ_ASSERT(def->type() == MIRType_Value || def->mightBeType(def->type())); |
| return def->resultTypeSet()->isSubset(calleeTypes); |
| } |
| |
| if (def->type() == MIRType_Value) |
| return false; |
| |
| if (def->type() == MIRType_Object) |
| return calleeTypes->unknownObject(); |
| |
| return calleeTypes->mightBeMIRType(def->type()); |
| } |
| |
| bool |
| IonBuilder::testNeedsArgumentCheck(JSFunction* target, CallInfo& callInfo) |
| { |
| // If we have a known target, check if the caller arg types are a subset of callee. |
| // Since typeset accumulates and can't decrease that means we don't need to check |
| // the arguments anymore. |
| if (!target->hasScript()) |
| return true; |
| |
| JSScript* targetScript = target->nonLazyScript(); |
| |
| if (!ArgumentTypesMatch(callInfo.thisArg(), TypeScript::ThisTypes(targetScript))) |
| return true; |
| uint32_t expected_args = Min<uint32_t>(callInfo.argc(), target->nargs()); |
| for (size_t i = 0; i < expected_args; i++) { |
| if (!ArgumentTypesMatch(callInfo.getArg(i), TypeScript::ArgTypes(targetScript, i))) |
| return true; |
| } |
| for (size_t i = callInfo.argc(); i < target->nargs(); i++) { |
| if (!TypeScript::ArgTypes(targetScript, i)->mightBeMIRType(MIRType_Undefined)) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| MCall* |
| IonBuilder::makeCallHelper(JSFunction* target, CallInfo& callInfo) |
| { |
| // This function may be called with mutated stack. |
| // Querying TI for popped types is invalid. |
| |
| uint32_t targetArgs = callInfo.argc(); |
| |
| // Collect number of missing arguments provided that the target is |
| // scripted. Native functions are passed an explicit 'argc' parameter. |
| if (target && !target->isNative()) |
| targetArgs = Max<uint32_t>(target->nargs(), callInfo.argc()); |
| |
| bool isDOMCall = false; |
| if (target && !callInfo.constructing()) { |
| // We know we have a single call target. Check whether the "this" types |
| // are DOM types and our function a DOM function, and if so flag the |
| // MCall accordingly. |
| TemporaryTypeSet* thisTypes = callInfo.thisArg()->resultTypeSet(); |
| if (thisTypes && |
| thisTypes->getKnownMIRType() == MIRType_Object && |
| thisTypes->isDOMClass(constraints()) && |
| testShouldDOMCall(thisTypes, target, JSJitInfo::Method)) |
| { |
| isDOMCall = true; |
| } |
| } |
| |
| MCall* call = MCall::New(alloc(), target, targetArgs + 1 + callInfo.constructing(), |
| callInfo.argc(), callInfo.constructing(), isDOMCall); |
| if (!call) |
| return nullptr; |
| |
| if (callInfo.constructing()) |
| call->addArg(targetArgs + 1, callInfo.getNewTarget()); |
| |
| // Explicitly pad any missing arguments with |undefined|. |
| // This permits skipping the argumentsRectifier. |
| for (int i = targetArgs; i > (int)callInfo.argc(); i--) { |
| MOZ_ASSERT_IF(target, !target->isNative()); |
| MConstant* undef = constant(UndefinedValue()); |
| call->addArg(i, undef); |
| } |
| |
| // Add explicit arguments. |
| // Skip addArg(0) because it is reserved for this |
| for (int32_t i = callInfo.argc() - 1; i >= 0; i--) |
| call->addArg(i + 1, callInfo.getArg(i)); |
| |
| // Now that we've told it about all the args, compute whether it's movable |
| call->computeMovable(); |
| |
| // Inline the constructor on the caller-side. |
| if (callInfo.constructing()) { |
| MDefinition* create = createThis(target, callInfo.fun(), callInfo.getNewTarget()); |
| if (!create) { |
| abort("Failure inlining constructor for call."); |
| return nullptr; |
| } |
| |
| callInfo.thisArg()->setImplicitlyUsedUnchecked(); |
| callInfo.setThis(create); |
| } |
| |
| // Pass |this| and function. |
| MDefinition* thisArg = callInfo.thisArg(); |
| call->addArg(0, thisArg); |
| |
| if (target && !testNeedsArgumentCheck(target, callInfo)) |
| call->disableArgCheck(); |
| |
| call->initFunction(callInfo.fun()); |
| |
| current->add(call); |
| return call; |
| } |
| |
| static bool |
| DOMCallNeedsBarrier(const JSJitInfo* jitinfo, TemporaryTypeSet* types) |
| { |
| MOZ_ASSERT(jitinfo->type() != JSJitInfo::InlinableNative); |
| |
| // If the return type of our DOM native is in "types" already, we don't |
| // actually need a barrier. |
| if (jitinfo->returnType() == JSVAL_TYPE_UNKNOWN) |
| return true; |
| |
| // JSVAL_TYPE_OBJECT doesn't tell us much; we still have to barrier on the |
| // actual type of the object. |
| if (jitinfo->returnType() == JSVAL_TYPE_OBJECT) |
| return true; |
| |
| // No need for a barrier if we're already expecting the type we'll produce. |
| return MIRTypeFromValueType(jitinfo->returnType()) != types->getKnownMIRType(); |
| } |
| |
| bool |
| IonBuilder::makeCall(JSFunction* target, CallInfo& callInfo) |
| { |
| // Constructor calls to non-constructors should throw. We don't want to use |
| // CallKnown in this case. |
| MOZ_ASSERT_IF(callInfo.constructing() && target, target->isConstructor()); |
| |
| MCall* call = makeCallHelper(target, callInfo); |
| if (!call) |
| return false; |
| |
| current->push(call); |
| if (call->isEffectful() && !resumeAfter(call)) |
| return false; |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| |
| if (call->isCallDOMNative()) |
| return pushDOMTypeBarrier(call, types, call->getSingleTarget()); |
| |
| return pushTypeBarrier(call, types, BarrierKind::TypeSet); |
| } |
| |
| bool |
| IonBuilder::jsop_eval(uint32_t argc) |
| { |
| int calleeDepth = -((int)argc + 2); |
| TemporaryTypeSet* calleeTypes = current->peek(calleeDepth)->resultTypeSet(); |
| |
| // Emit a normal call if the eval has never executed. This keeps us from |
| // disabling compilation for the script when testing with --ion-eager. |
| if (calleeTypes && calleeTypes->empty()) |
| return jsop_call(argc, /* constructing = */ false); |
| |
| JSFunction* singleton = getSingleCallTarget(calleeTypes); |
| if (!singleton) |
| return abort("No singleton callee for eval()"); |
| |
| if (script()->global().valueIsEval(ObjectValue(*singleton))) { |
| if (argc != 1) |
| return abort("Direct eval with more than one argument"); |
| |
| if (!info().funMaybeLazy()) |
| return abort("Direct eval in global code"); |
| |
| if (info().funMaybeLazy()->isArrow()) |
| return abort("Direct eval from arrow function"); |
| |
| CallInfo callInfo(alloc(), /* constructing = */ false); |
| if (!callInfo.init(current, argc)) |
| return false; |
| callInfo.setImplicitlyUsedUnchecked(); |
| |
| callInfo.fun()->setImplicitlyUsedUnchecked(); |
| |
| MDefinition* scopeChain = current->scopeChain(); |
| MDefinition* string = callInfo.getArg(0); |
| |
| // Direct eval acts as identity on non-string types according to |
| // ES5 15.1.2.1 step 1. |
| if (!string->mightBeType(MIRType_String)) { |
| current->push(string); |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| return pushTypeBarrier(string, types, BarrierKind::TypeSet); |
| } |
| |
| if (!jsop_newtarget()) |
| return false; |
| MDefinition* newTargetValue = current->pop(); |
| |
| // Try to pattern match 'eval(v + "()")'. In this case v is likely a |
| // name on the scope chain and the eval is performing a call on that |
| // value. Use a dynamic scope chain lookup rather than a full eval. |
| if (string->isConcat() && |
| string->getOperand(1)->isConstantValue() && |
| string->getOperand(1)->constantValue().isString()) |
| { |
| JSAtom* atom = &string->getOperand(1)->constantValue().toString()->asAtom(); |
| |
| if (StringEqualsAscii(atom, "()")) { |
| MDefinition* name = string->getOperand(0); |
| MInstruction* dynamicName = MGetDynamicName::New(alloc(), scopeChain, name); |
| current->add(dynamicName); |
| |
| current->push(dynamicName); |
| current->push(constant(UndefinedValue())); // thisv |
| |
| CallInfo evalCallInfo(alloc(), /* constructing = */ false); |
| if (!evalCallInfo.init(current, /* argc = */ 0)) |
| return false; |
| |
| return makeCall(nullptr, evalCallInfo); |
| } |
| } |
| |
| MInstruction* ins = MCallDirectEval::New(alloc(), scopeChain, string, |
| newTargetValue, pc); |
| current->add(ins); |
| current->push(ins); |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| return resumeAfter(ins) && pushTypeBarrier(ins, types, BarrierKind::TypeSet); |
| } |
| |
| return jsop_call(argc, /* constructing = */ false); |
| } |
| |
| bool |
| IonBuilder::jsop_compare(JSOp op) |
| { |
| MDefinition* right = current->pop(); |
| MDefinition* left = current->pop(); |
| |
| return jsop_compare(op, left, right); |
| } |
| |
| bool |
| IonBuilder::jsop_compare(JSOp op, MDefinition* left, MDefinition* right) |
| { |
| bool emitted = false; |
| |
| if (!forceInlineCaches()) { |
| if (!compareTrySpecialized(&emitted, op, left, right) || emitted) |
| return emitted; |
| if (!compareTryBitwise(&emitted, op, left, right) || emitted) |
| return emitted; |
| if (!compareTrySpecializedOnBaselineInspector(&emitted, op, left, right) || emitted) |
| return emitted; |
| } |
| |
| if (!compareTrySharedStub(&emitted, op, left, right) || emitted) |
| return emitted; |
| |
| // Not possible to optimize. Do a slow vm call. |
| MCompare* ins = MCompare::New(alloc(), left, right, op); |
| ins->cacheOperandMightEmulateUndefined(constraints()); |
| |
| current->add(ins); |
| current->push(ins); |
| if (ins->isEffectful() && !resumeAfter(ins)) |
| return false; |
| return true; |
| } |
| |
| static bool |
| ObjectOrSimplePrimitive(MDefinition* op) |
| { |
| // Return true if op is either undefined/null/boolean/int32 or an object. |
| return !op->mightBeType(MIRType_String) |
| && !op->mightBeType(MIRType_Symbol) |
| && !op->mightBeType(MIRType_Double) |
| && !op->mightBeType(MIRType_Float32) |
| && !op->mightBeType(MIRType_MagicOptimizedArguments) |
| && !op->mightBeType(MIRType_MagicHole) |
| && !op->mightBeType(MIRType_MagicIsConstructing); |
| } |
| |
| bool |
| IonBuilder::compareTrySpecialized(bool* emitted, JSOp op, MDefinition* left, MDefinition* right) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // Try to emit an compare based on the input types. |
| |
| MCompare::CompareType type = MCompare::determineCompareType(op, left, right); |
| if (type == MCompare::Compare_Unknown) |
| return true; |
| |
| MCompare* ins = MCompare::New(alloc(), left, right, op); |
| ins->setCompareType(type); |
| ins->cacheOperandMightEmulateUndefined(constraints()); |
| |
| // Some compare types need to have the specific type in the rhs. |
| // Swap operands if that is not the case. |
| if (type == MCompare::Compare_StrictString && right->type() != MIRType_String) |
| ins->swapOperands(); |
| else if (type == MCompare::Compare_Null && right->type() != MIRType_Null) |
| ins->swapOperands(); |
| else if (type == MCompare::Compare_Undefined && right->type() != MIRType_Undefined) |
| ins->swapOperands(); |
| else if (type == MCompare::Compare_Boolean && right->type() != MIRType_Boolean) |
| ins->swapOperands(); |
| |
| // Replace inputs with unsigned variants if needed. |
| if (type == MCompare::Compare_UInt32) |
| ins->replaceWithUnsignedOperands(); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| MOZ_ASSERT(!ins->isEffectful()); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::compareTryBitwise(bool* emitted, JSOp op, MDefinition* left, MDefinition* right) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // Try to emit a bitwise compare. Check if a bitwise compare equals the wanted |
| // result for all observed operand types. |
| |
| // Onlye allow loose and strict equality. |
| if (op != JSOP_EQ && op != JSOP_NE && op != JSOP_STRICTEQ && op != JSOP_STRICTNE) |
| return true; |
| |
| // Only primitive (not double/string) or objects are supported. |
| // I.e. Undefined/Null/Boolean/Int32 and Object |
| if (!ObjectOrSimplePrimitive(left) || !ObjectOrSimplePrimitive(right)) |
| return true; |
| |
| // Objects that emulate undefined are not supported. |
| if (left->maybeEmulatesUndefined(constraints()) || right->maybeEmulatesUndefined(constraints())) |
| return true; |
| |
| // In the loose comparison more values could be the same, |
| // but value comparison reporting otherwise. |
| if (op == JSOP_EQ || op == JSOP_NE) { |
| |
| // Undefined compared loosy to Null is not supported, |
| // because tag is different, but value can be the same (undefined == null). |
| if ((left->mightBeType(MIRType_Undefined) && right->mightBeType(MIRType_Null)) || |
| (left->mightBeType(MIRType_Null) && right->mightBeType(MIRType_Undefined))) |
| { |
| return true; |
| } |
| |
| // Int32 compared loosy to Boolean is not supported, |
| // because tag is different, but value can be the same (1 == true). |
| if ((left->mightBeType(MIRType_Int32) && right->mightBeType(MIRType_Boolean)) || |
| (left->mightBeType(MIRType_Boolean) && right->mightBeType(MIRType_Int32))) |
| { |
| return true; |
| } |
| |
| // For loosy comparison of an object with a Boolean/Number/String |
| // the valueOf the object is taken. Therefore not supported. |
| bool simpleLHS = left->mightBeType(MIRType_Boolean) || left->mightBeType(MIRType_Int32); |
| bool simpleRHS = right->mightBeType(MIRType_Boolean) || right->mightBeType(MIRType_Int32); |
| if ((left->mightBeType(MIRType_Object) && simpleRHS) || |
| (right->mightBeType(MIRType_Object) && simpleLHS)) |
| { |
| return true; |
| } |
| } |
| |
| MCompare* ins = MCompare::New(alloc(), left, right, op); |
| ins->setCompareType(MCompare::Compare_Bitwise); |
| ins->cacheOperandMightEmulateUndefined(constraints()); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| MOZ_ASSERT(!ins->isEffectful()); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::compareTrySpecializedOnBaselineInspector(bool* emitted, JSOp op, MDefinition* left, |
| MDefinition* right) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // Try to specialize based on any baseline caches that have been generated |
| // for the opcode. These will cause the instruction's type policy to insert |
| // fallible unboxes to the appropriate input types. |
| |
| // Strict equality isn't supported. |
| if (op == JSOP_STRICTEQ || op == JSOP_STRICTNE) |
| return true; |
| |
| MCompare::CompareType type = inspector->expectedCompareType(pc); |
| if (type == MCompare::Compare_Unknown) |
| return true; |
| |
| MCompare* ins = MCompare::New(alloc(), left, right, op); |
| ins->setCompareType(type); |
| ins->cacheOperandMightEmulateUndefined(constraints()); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| MOZ_ASSERT(!ins->isEffectful()); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::compareTrySharedStub(bool* emitted, JSOp op, MDefinition* left, MDefinition* right) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // Try to emit a shared stub cache. |
| |
| if (JitOptions.disableSharedStubs) |
| return true; |
| |
| if (JSOp(*pc) == JSOP_CASE) |
| return true; |
| |
| MBinarySharedStub* stub = MBinarySharedStub::New(alloc(), left, right); |
| current->add(stub); |
| current->push(stub); |
| if (!resumeAfter(stub)) |
| return false; |
| |
| MUnbox* unbox = MUnbox::New(alloc(), current->pop(), MIRType_Boolean, MUnbox::Infallible); |
| current->add(unbox); |
| current->push(unbox); |
| |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_newarray(uint32_t length) |
| { |
| JSObject* templateObject = inspector->getTemplateObject(pc); |
| gc::InitialHeap heap; |
| MConstant* templateConst; |
| |
| if (templateObject) { |
| heap = templateObject->group()->initialHeap(constraints()); |
| templateConst = MConstant::NewConstraintlessObject(alloc(), templateObject); |
| } else { |
| heap = gc::DefaultHeap; |
| templateConst = MConstant::New(alloc(), NullValue()); |
| } |
| current->add(templateConst); |
| |
| MNewArray* ins = MNewArray::New(alloc(), constraints(), length, templateConst, heap, pc); |
| current->add(ins); |
| current->push(ins); |
| |
| ObjectGroup* templateGroup = inspector->getTemplateObjectGroup(pc); |
| if (templateGroup) { |
| TemporaryTypeSet* types = MakeSingletonTypeSet(constraints(), templateGroup); |
| ins->setResultTypeSet(types); |
| } |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_newarray_copyonwrite() |
| { |
| ArrayObject* templateObject = ObjectGroup::getCopyOnWriteObject(script(), pc); |
| |
| // The baseline compiler should have ensured the template object has a type |
| // with the copy on write flag set already. During the arguments usage |
| // analysis the baseline compiler hasn't run yet, however, though in this |
| // case the template object's type doesn't matter. |
| MOZ_ASSERT_IF(info().analysisMode() != Analysis_ArgumentsUsage, |
| templateObject->group()->hasAnyFlags(OBJECT_FLAG_COPY_ON_WRITE)); |
| |
| MNewArrayCopyOnWrite* ins = |
| MNewArrayCopyOnWrite::New(alloc(), constraints(), templateObject, |
| templateObject->group()->initialHeap(constraints())); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_newobject() |
| { |
| JSObject* templateObject = inspector->getTemplateObject(pc); |
| gc::InitialHeap heap; |
| MConstant* templateConst; |
| |
| if (templateObject) { |
| heap = templateObject->group()->initialHeap(constraints()); |
| templateConst = MConstant::NewConstraintlessObject(alloc(), templateObject); |
| } else { |
| heap = gc::DefaultHeap; |
| templateConst = MConstant::New(alloc(), NullValue()); |
| } |
| |
| current->add(templateConst); |
| MNewObject* ins = MNewObject::New(alloc(), constraints(), templateConst, heap, |
| MNewObject::ObjectLiteral); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_initelem() |
| { |
| MDefinition* value = current->pop(); |
| MDefinition* id = current->pop(); |
| MDefinition* obj = current->peek(-1); |
| |
| MInitElem* initElem = MInitElem::New(alloc(), obj, id, value); |
| current->add(initElem); |
| |
| return resumeAfter(initElem); |
| } |
| |
| bool |
| IonBuilder::jsop_initelem_array() |
| { |
| MDefinition* value = current->pop(); |
| MDefinition* obj = current->peek(-1); |
| |
| // Make sure that arrays have the type being written to them by the |
| // intializer, and that arrays are marked as non-packed when writing holes |
| // to them during initialization. |
| bool needStub = false; |
| JSValueType unboxedType = JSVAL_TYPE_MAGIC; |
| if (shouldAbortOnPreliminaryGroups(obj)) { |
| needStub = true; |
| } else if (!obj->resultTypeSet() || |
| obj->resultTypeSet()->unknownObject() || |
| obj->resultTypeSet()->getObjectCount() != 1) |
| { |
| needStub = true; |
| } else { |
| MOZ_ASSERT(obj->resultTypeSet()->getObjectCount() == 1); |
| TypeSet::ObjectKey* initializer = obj->resultTypeSet()->getObject(0); |
| if (initializer->clasp() == &UnboxedArrayObject::class_) { |
| if (initializer->group()->unboxedLayout().nativeGroup()) |
| needStub = true; |
| else |
| unboxedType = initializer->group()->unboxedLayout().elementType(); |
| } |
| if (value->type() == MIRType_MagicHole) { |
| if (!initializer->hasFlags(constraints(), OBJECT_FLAG_NON_PACKED)) |
| needStub = true; |
| } else if (!initializer->unknownProperties()) { |
| HeapTypeSetKey elemTypes = initializer->property(JSID_VOID); |
| if (!TypeSetIncludes(elemTypes.maybeTypes(), value->type(), value->resultTypeSet())) { |
| elemTypes.freeze(constraints()); |
| needStub = true; |
| } |
| } |
| } |
| |
| uint32_t index = GET_UINT32(pc); |
| if (needStub) { |
| MCallInitElementArray* store = MCallInitElementArray::New(alloc(), obj, index, value); |
| current->add(store); |
| return resumeAfter(store); |
| } |
| |
| return initializeArrayElement(obj, index, value, unboxedType, /* addResumePoint = */ true); |
| } |
| |
| bool |
| IonBuilder::initializeArrayElement(MDefinition* obj, size_t index, MDefinition* value, |
| JSValueType unboxedType, |
| bool addResumePointAndIncrementInitializedLength) |
| { |
| MConstant* id = MConstant::New(alloc(), Int32Value(index)); |
| current->add(id); |
| |
| // Get the elements vector. |
| MElements* elements = MElements::New(alloc(), obj, unboxedType != JSVAL_TYPE_MAGIC); |
| current->add(elements); |
| |
| if (unboxedType != JSVAL_TYPE_MAGIC) { |
| // Note: storeUnboxedValue takes care of any post barriers on the value. |
| storeUnboxedValue(obj, elements, 0, id, unboxedType, value, /* preBarrier = */ false); |
| |
| if (addResumePointAndIncrementInitializedLength) { |
| MInstruction* increment = MIncrementUnboxedArrayInitializedLength::New(alloc(), obj); |
| current->add(increment); |
| |
| if (!resumeAfter(increment)) |
| return false; |
| } |
| } else { |
| if (NeedsPostBarrier(value)) |
| current->add(MPostWriteBarrier::New(alloc(), obj, value)); |
| |
| if (obj->toNewArray()->convertDoubleElements()) { |
| MInstruction* valueDouble = MToDouble::New(alloc(), value); |
| current->add(valueDouble); |
| value = valueDouble; |
| } |
| |
| // Store the value. |
| MStoreElement* store = MStoreElement::New(alloc(), elements, id, value, |
| /* needsHoleCheck = */ false); |
| current->add(store); |
| |
| if (addResumePointAndIncrementInitializedLength) { |
| // Update the initialized length. (The template object for this |
| // array has the array's ultimate length, so the length field is |
| // already correct: no updating needed.) |
| MSetInitializedLength* initLength = MSetInitializedLength::New(alloc(), elements, id); |
| current->add(initLength); |
| |
| if (!resumeAfter(initLength)) |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_mutateproto() |
| { |
| MDefinition* value = current->pop(); |
| MDefinition* obj = current->peek(-1); |
| |
| MMutateProto* mutate = MMutateProto::New(alloc(), obj, value); |
| current->add(mutate); |
| return resumeAfter(mutate); |
| } |
| |
| bool |
| IonBuilder::jsop_initprop(PropertyName* name) |
| { |
| bool useSlowPath = false; |
| |
| MDefinition* value = current->peek(-1); |
| MDefinition* obj = current->peek(-2); |
| if (obj->isLambda()) { |
| useSlowPath = true; |
| } else if (JSObject* templateObject = obj->toNewObject()->templateObject()) { |
| if (templateObject->is<PlainObject>()) { |
| if (!templateObject->as<PlainObject>().containsPure(name)) |
| useSlowPath = true; |
| } else { |
| MOZ_ASSERT(templateObject->as<UnboxedPlainObject>().layout().lookup(name)); |
| } |
| } else { |
| useSlowPath = true; |
| } |
| |
| if (useSlowPath) { |
| current->pop(); |
| MInitProp* init = MInitProp::New(alloc(), obj, name, value); |
| current->add(init); |
| return resumeAfter(init); |
| } |
| |
| MInstruction* last = *current->rbegin(); |
| |
| // This is definitely initializing an 'own' property of the object, treat |
| // it as an assignment. |
| if (!jsop_setprop(name)) |
| return false; |
| |
| // SETPROP pushed the value, instead of the object. Fix this on the stack, |
| // and check the most recent resume point to see if it needs updating too. |
| current->pop(); |
| current->push(obj); |
| for (MInstructionReverseIterator riter = current->rbegin(); *riter != last; riter++) { |
| if (MResumePoint* resumePoint = riter->resumePoint()) { |
| MOZ_ASSERT(resumePoint->pc() == pc); |
| if (resumePoint->mode() == MResumePoint::ResumeAfter) { |
| size_t index = resumePoint->numOperands() - 1; |
| resumePoint->replaceOperand(index, obj); |
| } |
| break; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_initprop_getter_setter(PropertyName* name) |
| { |
| MDefinition* value = current->pop(); |
| MDefinition* obj = current->peek(-1); |
| |
| MInitPropGetterSetter* init = MInitPropGetterSetter::New(alloc(), obj, name, value); |
| current->add(init); |
| return resumeAfter(init); |
| } |
| |
| bool |
| IonBuilder::jsop_initelem_getter_setter() |
| { |
| MDefinition* value = current->pop(); |
| MDefinition* id = current->pop(); |
| MDefinition* obj = current->peek(-1); |
| |
| MInitElemGetterSetter* init = MInitElemGetterSetter::New(alloc(), obj, id, value); |
| current->add(init); |
| return resumeAfter(init); |
| } |
| |
| MBasicBlock* |
| IonBuilder::addBlock(MBasicBlock* block, uint32_t loopDepth) |
| { |
| if (!block) |
| return nullptr; |
| if (block->pc() && script()->hasScriptCounts()) |
| block->setHitCount(script()->getHitCount(block->pc())); |
| graph().addBlock(block); |
| block->setLoopDepth(loopDepth); |
| return block; |
| } |
| |
| MBasicBlock* |
| IonBuilder::newBlock(MBasicBlock* predecessor, jsbytecode* pc) |
| { |
| MBasicBlock* block = MBasicBlock::New(graph(), &analysis(), info(), predecessor, |
| bytecodeSite(pc), MBasicBlock::NORMAL); |
| return addBlock(block, loopDepth_); |
| } |
| |
| MBasicBlock* |
| IonBuilder::newBlock(MBasicBlock* predecessor, jsbytecode* pc, MResumePoint* priorResumePoint) |
| { |
| MBasicBlock* block = MBasicBlock::NewWithResumePoint(graph(), info(), predecessor, |
| bytecodeSite(pc), priorResumePoint); |
| return addBlock(block, loopDepth_); |
| } |
| |
| MBasicBlock* |
| IonBuilder::newBlockPopN(MBasicBlock* predecessor, jsbytecode* pc, uint32_t popped) |
| { |
| MBasicBlock* block = MBasicBlock::NewPopN(graph(), info(), predecessor, bytecodeSite(pc), |
| MBasicBlock::NORMAL, popped); |
| return addBlock(block, loopDepth_); |
| } |
| |
| MBasicBlock* |
| IonBuilder::newBlockAfter(MBasicBlock* at, MBasicBlock* predecessor, jsbytecode* pc) |
| { |
| MBasicBlock* block = MBasicBlock::New(graph(), &analysis(), info(), predecessor, |
| bytecodeSite(pc), MBasicBlock::NORMAL); |
| if (!block) |
| return nullptr; |
| block->setHitCount(0); // osr block |
| graph().insertBlockAfter(at, block); |
| return block; |
| } |
| |
| MBasicBlock* |
| IonBuilder::newBlock(MBasicBlock* predecessor, jsbytecode* pc, uint32_t loopDepth) |
| { |
| MBasicBlock* block = MBasicBlock::New(graph(), &analysis(), info(), predecessor, |
| bytecodeSite(pc), MBasicBlock::NORMAL); |
| return addBlock(block, loopDepth); |
| } |
| |
| MBasicBlock* |
| IonBuilder::newOsrPreheader(MBasicBlock* predecessor, jsbytecode* loopEntry, jsbytecode* beforeLoopEntry) |
| { |
| MOZ_ASSERT(LoopEntryCanIonOsr(loopEntry)); |
| MOZ_ASSERT(loopEntry == info().osrPc()); |
| |
| // Create two blocks: one for the OSR entry with no predecessors, one for |
| // the preheader, which has the OSR entry block as a predecessor. The |
| // OSR block is always the second block (with id 1). |
| MBasicBlock* osrBlock = newBlockAfter(*graph().begin(), loopEntry); |
| MBasicBlock* preheader = newBlock(predecessor, loopEntry); |
| if (!osrBlock || !preheader) |
| return nullptr; |
| |
| // Give the pre-header the same hit count as the code before the loop. |
| if (script()->hasScriptCounts()) |
| preheader->setHitCount(script()->getHitCount(beforeLoopEntry)); |
| |
| MOsrEntry* entry = MOsrEntry::New(alloc()); |
| osrBlock->add(entry); |
| |
| // Initialize |scopeChain|. |
| { |
| uint32_t slot = info().scopeChainSlot(); |
| |
| MInstruction* scopev; |
| if (analysis().usesScopeChain()) { |
| scopev = MOsrScopeChain::New(alloc(), entry); |
| } else { |
| // Use an undefined value if the script does not need its scope |
| // chain, to match the type that is already being tracked for the |
| // slot. |
| scopev = MConstant::New(alloc(), UndefinedValue()); |
| } |
| |
| osrBlock->add(scopev); |
| osrBlock->initSlot(slot, scopev); |
| } |
| // Initialize |return value| |
| { |
| MInstruction* returnValue; |
| if (!script()->noScriptRval()) |
| returnValue = MOsrReturnValue::New(alloc(), entry); |
| else |
| returnValue = MConstant::New(alloc(), UndefinedValue()); |
| osrBlock->add(returnValue); |
| osrBlock->initSlot(info().returnValueSlot(), returnValue); |
| } |
| |
| // Initialize arguments object. |
| bool needsArgsObj = info().needsArgsObj(); |
| MInstruction* argsObj = nullptr; |
| if (info().hasArguments()) { |
| if (needsArgsObj) |
| argsObj = MOsrArgumentsObject::New(alloc(), entry); |
| else |
| argsObj = MConstant::New(alloc(), UndefinedValue()); |
| osrBlock->add(argsObj); |
| osrBlock->initSlot(info().argsObjSlot(), argsObj); |
| } |
| |
| if (info().funMaybeLazy()) { |
| // Initialize |this| parameter. |
| MParameter* thisv = MParameter::New(alloc(), MParameter::THIS_SLOT, nullptr); |
| osrBlock->add(thisv); |
| osrBlock->initSlot(info().thisSlot(), thisv); |
| |
| // Initialize arguments. |
| for (uint32_t i = 0; i < info().nargs(); i++) { |
| uint32_t slot = needsArgsObj ? info().argSlotUnchecked(i) : info().argSlot(i); |
| |
| // Only grab arguments from the arguments object if the arguments object |
| // aliases formals. If the argsobj does not alias formals, then the |
| // formals may have been assigned to during interpretation, and that change |
| // will not be reflected in the argsobj. |
| if (needsArgsObj && info().argsObjAliasesFormals()) { |
| MOZ_ASSERT(argsObj && argsObj->isOsrArgumentsObject()); |
| // If this is an aliased formal, then the arguments object |
| // contains a hole at this index. Any references to this |
| // variable in the jitcode will come from JSOP_*ALIASEDVAR |
| // opcodes, so the slot itself can be set to undefined. If |
| // it's not aliased, it must be retrieved from the arguments |
| // object. |
| MInstruction* osrv; |
| if (script()->formalIsAliased(i)) |
| osrv = MConstant::New(alloc(), UndefinedValue()); |
| else |
| osrv = MGetArgumentsObjectArg::New(alloc(), argsObj, i); |
| |
| osrBlock->add(osrv); |
| osrBlock->initSlot(slot, osrv); |
| } else { |
| MParameter* arg = MParameter::New(alloc(), i, nullptr); |
| osrBlock->add(arg); |
| osrBlock->initSlot(slot, arg); |
| } |
| } |
| } |
| |
| // Initialize locals. |
| for (uint32_t i = 0; i < info().nlocals(); i++) { |
| uint32_t slot = info().localSlot(i); |
| ptrdiff_t offset = BaselineFrame::reverseOffsetOfLocal(i); |
| |
| MOsrValue* osrv = MOsrValue::New(alloc(), entry, offset); |
| osrBlock->add(osrv); |
| osrBlock->initSlot(slot, osrv); |
| } |
| |
| // Initialize stack. |
| uint32_t numStackSlots = preheader->stackDepth() - info().firstStackSlot(); |
| for (uint32_t i = 0; i < numStackSlots; i++) { |
| uint32_t slot = info().stackSlot(i); |
| ptrdiff_t offset = BaselineFrame::reverseOffsetOfLocal(info().nlocals() + i); |
| |
| MOsrValue* osrv = MOsrValue::New(alloc(), entry, offset); |
| osrBlock->add(osrv); |
| osrBlock->initSlot(slot, osrv); |
| } |
| |
| // Create an MStart to hold the first valid MResumePoint. |
| MStart* start = MStart::New(alloc(), MStart::StartType_Osr); |
| osrBlock->add(start); |
| |
| // MOsrValue instructions are infallible, so the first MResumePoint must |
| // occur after they execute, at the point of the MStart. |
| if (!resumeAt(start, loopEntry)) |
| return nullptr; |
| |
| // Link the same MResumePoint from the MStart to each MOsrValue. |
| // This causes logic in ShouldSpecializeInput() to not replace Uses with |
| // Unboxes in the MResumePiont, so that the MStart always sees Values. |
| if (!osrBlock->linkOsrValues(start)) |
| return nullptr; |
| |
| // Clone types of the other predecessor of the pre-header to the osr block, |
| // such as pre-header phi's won't discard specialized type of the |
| // predecessor. |
| MOZ_ASSERT(predecessor->stackDepth() == osrBlock->stackDepth()); |
| MOZ_ASSERT(info().scopeChainSlot() == 0); |
| |
| // Treat the OSR values as having the same type as the existing values |
| // coming in to the loop. These will be fixed up with appropriate |
| // unboxing and type barriers in finishLoop, once the possible types |
| // at the loop header are known. |
| for (uint32_t i = info().startArgSlot(); i < osrBlock->stackDepth(); i++) { |
| MDefinition* existing = current->getSlot(i); |
| MDefinition* def = osrBlock->getSlot(i); |
| MOZ_ASSERT_IF(!needsArgsObj || !info().isSlotAliasedAtOsr(i), def->type() == MIRType_Value); |
| |
| // Aliased slots are never accessed, since they need to go through |
| // the callobject. No need to type them here. |
| if (info().isSlotAliasedAtOsr(i)) |
| continue; |
| |
| def->setResultType(existing->type()); |
| def->setResultTypeSet(existing->resultTypeSet()); |
| } |
| |
| // Finish the osrBlock. |
| osrBlock->end(MGoto::New(alloc(), preheader)); |
| if (!preheader->addPredecessor(alloc(), osrBlock)) |
| return nullptr; |
| graph().setOsrBlock(osrBlock); |
| |
| return preheader; |
| } |
| |
| MBasicBlock* |
| IonBuilder::newPendingLoopHeader(MBasicBlock* predecessor, jsbytecode* pc, bool osr, bool canOsr, |
| unsigned stackPhiCount) |
| { |
| loopDepth_++; |
| // If this site can OSR, all values on the expression stack are part of the loop. |
| if (canOsr) |
| stackPhiCount = predecessor->stackDepth() - info().firstStackSlot(); |
| MBasicBlock* block = MBasicBlock::NewPendingLoopHeader(graph(), info(), predecessor, |
| bytecodeSite(pc), stackPhiCount); |
| if (!addBlock(block, loopDepth_)) |
| return nullptr; |
| |
| if (osr) { |
| // Incorporate type information from the OSR frame into the loop |
| // header. The OSR frame may have unexpected types due to type changes |
| // within the loop body or due to incomplete profiling information, |
| // in which case this may avoid restarts of loop analysis or bailouts |
| // during the OSR itself. |
| |
| // Unbox the MOsrValue if it is known to be unboxable. |
| for (uint32_t i = info().startArgSlot(); i < block->stackDepth(); i++) { |
| |
| // The value of aliased args and slots are in the callobject. So we can't |
| // the value from the baseline frame. |
| if (info().isSlotAliasedAtOsr(i)) |
| continue; |
| |
| // Don't bother with expression stack values. The stack should be |
| // empty except for let variables (not Ion-compiled) or iterators. |
| if (i >= info().firstStackSlot()) |
| continue; |
| |
| MPhi* phi = block->getSlot(i)->toPhi(); |
| |
| // Get the type from the baseline frame. |
| TypeSet::Type existingType = TypeSet::UndefinedType(); |
| uint32_t arg = i - info().firstArgSlot(); |
| uint32_t var = i - info().firstLocalSlot(); |
| if (info().funMaybeLazy() && i == info().thisSlot()) |
| existingType = baselineFrame_->thisType; |
| else if (arg < info().nargs()) |
| existingType = baselineFrame_->argTypes[arg]; |
| else |
| existingType = baselineFrame_->varTypes[var]; |
| |
| if (existingType.isSingletonUnchecked()) |
| checkNurseryObject(existingType.singleton()); |
| |
| // Extract typeset from value. |
| LifoAlloc* lifoAlloc = alloc().lifoAlloc(); |
| TemporaryTypeSet* typeSet = |
| lifoAlloc->new_<TemporaryTypeSet>(lifoAlloc, existingType); |
| if (!typeSet) |
| return nullptr; |
| MIRType type = typeSet->getKnownMIRType(); |
| if (!phi->addBackedgeType(type, typeSet)) |
| return nullptr; |
| } |
| } |
| |
| return block; |
| } |
| |
| MTest* |
| IonBuilder::newTest(MDefinition* ins, MBasicBlock* ifTrue, MBasicBlock* ifFalse) |
| { |
| MTest* test = MTest::New(alloc(), ins, ifTrue, ifFalse); |
| test->cacheOperandMightEmulateUndefined(constraints()); |
| return test; |
| } |
| |
| // A resume point is a mapping of stack slots to MDefinitions. It is used to |
| // capture the environment such that if a guard fails, and IonMonkey needs |
| // to exit back to the interpreter, the interpreter state can be |
| // reconstructed. |
| // |
| // We capture stack state at critical points: |
| // * (1) At the beginning of every basic block. |
| // * (2) After every effectful operation. |
| // |
| // As long as these two properties are maintained, instructions can |
| // be moved, hoisted, or, eliminated without problems, and ops without side |
| // effects do not need to worry about capturing state at precisely the |
| // right point in time. |
| // |
| // Effectful instructions, of course, need to capture state after completion, |
| // where the interpreter will not attempt to repeat the operation. For this, |
| // ResumeAfter must be used. The state is attached directly to the effectful |
| // instruction to ensure that no intermediate instructions could be injected |
| // in between by a future analysis pass. |
| // |
| // During LIR construction, if an instruction can bail back to the interpreter, |
| // we create an LSnapshot, which uses the last known resume point to request |
| // register/stack assignments for every live value. |
| bool |
| IonBuilder::resume(MInstruction* ins, jsbytecode* pc, MResumePoint::Mode mode) |
| { |
| MOZ_ASSERT(ins->isEffectful() || !ins->isMovable()); |
| |
| MResumePoint* resumePoint = MResumePoint::New(alloc(), ins->block(), pc, |
| mode); |
| if (!resumePoint) |
| return false; |
| ins->setResumePoint(resumePoint); |
| return true; |
| } |
| |
| bool |
| IonBuilder::resumeAt(MInstruction* ins, jsbytecode* pc) |
| { |
| return resume(ins, pc, MResumePoint::ResumeAt); |
| } |
| |
| bool |
| IonBuilder::resumeAfter(MInstruction* ins) |
| { |
| return resume(ins, pc, MResumePoint::ResumeAfter); |
| } |
| |
| bool |
| IonBuilder::maybeInsertResume() |
| { |
| // Create a resume point at the current position, without an existing |
| // effectful instruction. This resume point is not necessary for correct |
| // behavior (see above), but is added to avoid holding any values from the |
| // previous resume point which are now dead. This shortens the live ranges |
| // of such values and improves register allocation. |
| // |
| // This optimization is not performed outside of loop bodies, where good |
| // register allocation is not as critical, in order to avoid creating |
| // excessive resume points. |
| |
| if (loopDepth_ == 0) |
| return true; |
| |
| MNop* ins = MNop::New(alloc()); |
| current->add(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| void |
| IonBuilder::maybeMarkEmpty(MDefinition* ins) |
| { |
| MOZ_ASSERT(ins->type() == MIRType_Value); |
| |
| // When one of the operands has no type information, mark the output |
| // as having no possible types too. This is to avoid degrading |
| // subsequent analysis. |
| for (size_t i = 0; i < ins->numOperands(); i++) { |
| if (!ins->emptyResultTypeSet()) |
| continue; |
| |
| TemporaryTypeSet* types = alloc().lifoAlloc()->new_<TemporaryTypeSet>(); |
| if (types) |
| ins->setResultTypeSet(types); |
| } |
| } |
| |
| // Return whether property lookups can be performed effectlessly on clasp. |
| static bool |
| ClassHasEffectlessLookup(const Class* clasp) |
| { |
| return (clasp == &UnboxedPlainObject::class_) || |
| (clasp == &UnboxedArrayObject::class_) || |
| IsTypedObjectClass(clasp) || |
| (clasp->isNative() && !clasp->ops.lookupProperty); |
| } |
| |
| // Return whether an object might have a property for name which is not |
| // accounted for by type information. |
| static bool |
| ObjectHasExtraOwnProperty(CompileCompartment* comp, TypeSet::ObjectKey* object, jsid id) |
| { |
| // Some typed object properties are not reflected in type information. |
| if (object->isGroup() && object->group()->maybeTypeDescr()) |
| return object->group()->typeDescr().hasProperty(comp->runtime()->names(), id); |
| |
| const Class* clasp = object->clasp(); |
| |
| // Array |length| properties are not reflected in type information. |
| if (clasp == &ArrayObject::class_) |
| return JSID_IS_ATOM(id, comp->runtime()->names().length); |
| |
| // Resolve hooks can install new properties on objects on demand. |
| JSObject* singleton = object->isSingleton() ? object->singleton() : nullptr; |
| return ClassMayResolveId(comp->runtime()->names(), clasp, id, singleton); |
| } |
| |
| void |
| IonBuilder::insertRecompileCheck() |
| { |
| // No need for recompile checks if this is the highest optimization level. |
| OptimizationLevel curLevel = optimizationInfo().level(); |
| if (IonOptimizations.isLastLevel(curLevel)) |
| return; |
| |
| // Add recompile check. |
| |
| // Get the topmost builder. The topmost script will get recompiled when |
| // warm-up counter is high enough to justify a higher optimization level. |
| IonBuilder* topBuilder = outermostBuilder(); |
| |
| // Add recompile check to recompile when the warm-up count reaches the |
| // threshold of the next optimization level. |
| OptimizationLevel nextLevel = IonOptimizations.nextLevel(curLevel); |
| const OptimizationInfo* info = IonOptimizations.get(nextLevel); |
| uint32_t warmUpThreshold = info->compilerWarmUpThreshold(topBuilder->script()); |
| MRecompileCheck* check = MRecompileCheck::New(alloc(), topBuilder->script(), warmUpThreshold, |
| MRecompileCheck::RecompileCheck_OptimizationLevel); |
| current->add(check); |
| } |
| |
| JSObject* |
| IonBuilder::testSingletonProperty(JSObject* obj, jsid id) |
| { |
| // We would like to completely no-op property/global accesses which can |
| // produce only a particular JSObject. When indicating the access result is |
| // definitely an object, type inference does not account for the |
| // possibility that the property is entirely missing from the input object |
| // and its prototypes (if this happens, a semantic trigger would be hit and |
| // the pushed types updated, even if there is no type barrier). |
| // |
| // If the access definitely goes through obj, either directly or on the |
| // prototype chain, and the object has singleton type, then the type |
| // information for that property reflects the value that will definitely be |
| // read on accesses to the object. If the property is later deleted or |
| // reconfigured as a getter/setter then the type information for the |
| // property will change and trigger invalidation. |
| |
| while (obj) { |
| if (!ClassHasEffectlessLookup(obj->getClass())) |
| return nullptr; |
| |
| TypeSet::ObjectKey* objKey = TypeSet::ObjectKey::get(obj); |
| if (analysisContext) |
| objKey->ensureTrackedProperty(analysisContext, id); |
| |
| if (objKey->unknownProperties()) |
| return nullptr; |
| |
| HeapTypeSetKey property = objKey->property(id); |
| if (property.isOwnProperty(constraints())) { |
| if (obj->isSingleton()) |
| return property.singleton(constraints()); |
| return nullptr; |
| } |
| |
| if (ObjectHasExtraOwnProperty(compartment, objKey, id)) |
| return nullptr; |
| |
| obj = checkNurseryObject(obj->getProto()); |
| } |
| |
| return nullptr; |
| } |
| |
| JSObject* |
| IonBuilder::testSingletonPropertyTypes(MDefinition* obj, jsid id) |
| { |
| // As for TestSingletonProperty, but the input is any value in a type set |
| // rather than a specific object. |
| |
| TemporaryTypeSet* types = obj->resultTypeSet(); |
| if (types && types->unknownObject()) |
| return nullptr; |
| |
| JSObject* objectSingleton = types ? types->maybeSingleton() : nullptr; |
| if (objectSingleton) |
| return testSingletonProperty(objectSingleton, id); |
| |
| MIRType objType = obj->type(); |
| if (objType == MIRType_Value && types) |
| objType = types->getKnownMIRType(); |
| |
| JSProtoKey key; |
| switch (objType) { |
| case MIRType_String: |
| key = JSProto_String; |
| break; |
| |
| case MIRType_Symbol: |
| key = JSProto_Symbol; |
| break; |
| |
| case MIRType_Int32: |
| case MIRType_Double: |
| key = JSProto_Number; |
| break; |
| |
| case MIRType_Boolean: |
| key = JSProto_Boolean; |
| break; |
| |
| case MIRType_Object: { |
| if (!types) |
| return nullptr; |
| |
| // For property accesses which may be on many objects, we just need to |
| // find a prototype common to all the objects; if that prototype |
| // has the singleton property, the access will not be on a missing property. |
| JSObject* singleton = nullptr; |
| for (unsigned i = 0; i < types->getObjectCount(); i++) { |
| TypeSet::ObjectKey* key = types->getObject(i); |
| if (!key) |
| continue; |
| if (analysisContext) |
| key->ensureTrackedProperty(analysisContext, id); |
| |
| const Class* clasp = key->clasp(); |
| if (!ClassHasEffectlessLookup(clasp) || ObjectHasExtraOwnProperty(compartment, key, id)) |
| return nullptr; |
| if (key->unknownProperties()) |
| return nullptr; |
| HeapTypeSetKey property = key->property(id); |
| if (property.isOwnProperty(constraints())) |
| return nullptr; |
| |
| if (JSObject* proto = checkNurseryObject(key->proto().toObjectOrNull())) { |
| // Test this type. |
| JSObject* thisSingleton = testSingletonProperty(proto, id); |
| if (!thisSingleton) |
| return nullptr; |
| if (singleton) { |
| if (thisSingleton != singleton) |
| return nullptr; |
| } else { |
| singleton = thisSingleton; |
| } |
| } else { |
| // Can't be on the prototype chain with no prototypes... |
| return nullptr; |
| } |
| } |
| return singleton; |
| } |
| default: |
| return nullptr; |
| } |
| |
| JSObject* proto = GetBuiltinPrototypePure(&script()->global(), key); |
| if (proto) |
| return testSingletonProperty(proto, id); |
| |
| return nullptr; |
| } |
| |
| bool |
| IonBuilder::pushTypeBarrier(MDefinition* def, TemporaryTypeSet* observed, BarrierKind kind) |
| { |
| MOZ_ASSERT(def == current->peek(-1)); |
| |
| MDefinition* replace = addTypeBarrier(current->pop(), observed, kind); |
| if (!replace) |
| return false; |
| |
| current->push(replace); |
| return true; |
| } |
| |
| // Given an observed type set, annotates the IR as much as possible: |
| // (1) If no type information is provided, the given value is returned. |
| // (2) If a single type definitely exists, and no type barrier is needed, |
| // then an infallible unbox instruction is returned. |
| // (3) If a type barrier is needed, but has an unknown type set, the given |
| // value is returned. |
| // (4) Lastly, a type barrier instruction is added and returned. |
| MDefinition* |
| IonBuilder::addTypeBarrier(MDefinition* def, TemporaryTypeSet* observed, BarrierKind kind, |
| MTypeBarrier** pbarrier) |
| { |
| // Barriers are never needed for instructions whose result will not be used. |
| if (BytecodeIsPopped(pc)) |
| return def; |
| |
| // If the instruction has no side effects, we'll resume the entire operation. |
| // The actual type barrier will occur in the interpreter. If the |
| // instruction is effectful, even if it has a singleton type, there |
| // must be a resume point capturing the original def, and resuming |
| // to that point will explicitly monitor the new type. |
| if (kind == BarrierKind::NoBarrier) { |
| MDefinition* replace = ensureDefiniteType(def, observed->getKnownMIRType()); |
| replace->setResultTypeSet(observed); |
| return replace; |
| } |
| |
| if (observed->unknown()) |
| return def; |
| |
| MTypeBarrier* barrier = MTypeBarrier::New(alloc(), def, observed, kind); |
| current->add(barrier); |
| |
| if (pbarrier) |
| *pbarrier = barrier; |
| |
| if (barrier->type() == MIRType_Undefined) |
| return constant(UndefinedValue()); |
| if (barrier->type() == MIRType_Null) |
| return constant(NullValue()); |
| |
| return barrier; |
| } |
| |
| bool |
| IonBuilder::pushDOMTypeBarrier(MInstruction* ins, TemporaryTypeSet* observed, JSFunction* func) |
| { |
| MOZ_ASSERT(func && func->isNative() && func->jitInfo()); |
| |
| const JSJitInfo* jitinfo = func->jitInfo(); |
| bool barrier = DOMCallNeedsBarrier(jitinfo, observed); |
| // Need to be a bit careful: if jitinfo->returnType is JSVAL_TYPE_DOUBLE but |
| // types->getKnownMIRType() is MIRType_Int32, then don't unconditionally |
| // unbox as a double. Instead, go ahead and barrier on having an int type, |
| // since we know we need a barrier anyway due to the type mismatch. This is |
| // the only situation in which TI actually has more information about the |
| // JSValueType than codegen can, short of jitinfo->returnType just being |
| // JSVAL_TYPE_UNKNOWN. |
| MDefinition* replace = ins; |
| if (jitinfo->returnType() != JSVAL_TYPE_DOUBLE || |
| observed->getKnownMIRType() != MIRType_Int32) { |
| replace = ensureDefiniteType(ins, MIRTypeFromValueType(jitinfo->returnType())); |
| if (replace != ins) { |
| current->pop(); |
| current->push(replace); |
| } |
| } else { |
| MOZ_ASSERT(barrier); |
| } |
| |
| return pushTypeBarrier(replace, observed, |
| barrier ? BarrierKind::TypeSet : BarrierKind::NoBarrier); |
| } |
| |
| MDefinition* |
| IonBuilder::ensureDefiniteType(MDefinition* def, MIRType definiteType) |
| { |
| MInstruction* replace; |
| switch (definiteType) { |
| case MIRType_Undefined: |
| def->setImplicitlyUsedUnchecked(); |
| replace = MConstant::New(alloc(), UndefinedValue()); |
| break; |
| |
| case MIRType_Null: |
| def->setImplicitlyUsedUnchecked(); |
| replace = MConstant::New(alloc(), NullValue()); |
| break; |
| |
| case MIRType_Value: |
| return def; |
| |
| default: { |
| if (def->type() != MIRType_Value) { |
| if (def->type() == MIRType_Int32 && definiteType == MIRType_Double) { |
| replace = MToDouble::New(alloc(), def); |
| break; |
| } |
| MOZ_ASSERT(def->type() == definiteType); |
| return def; |
| } |
| replace = MUnbox::New(alloc(), def, definiteType, MUnbox::Infallible); |
| break; |
| } |
| } |
| |
| current->add(replace); |
| return replace; |
| } |
| |
| MDefinition* |
| IonBuilder::ensureDefiniteTypeSet(MDefinition* def, TemporaryTypeSet* types) |
| { |
| // We cannot arbitrarily add a typeset to a definition. It can be shared |
| // in another path. So we always need to create a new MIR. |
| |
| // Use ensureDefiniteType to do unboxing. If that happened the type can |
| // be added on the newly created unbox operation. |
| MDefinition* replace = ensureDefiniteType(def, types->getKnownMIRType()); |
| if (replace != def) { |
| replace->setResultTypeSet(types); |
| return replace; |
| } |
| |
| // Don't replace if input type is more accurate than given typeset. |
| if (def->type() != types->getKnownMIRType()) { |
| MOZ_ASSERT(types->getKnownMIRType() == MIRType_Value); |
| return def; |
| } |
| |
| // Create a NOP mir instruction to filter the typeset. |
| MFilterTypeSet* filter = MFilterTypeSet::New(alloc(), def, types); |
| current->add(filter); |
| return filter; |
| } |
| |
| static size_t |
| NumFixedSlots(JSObject* object) |
| { |
| // Note: we can't use object->numFixedSlots() here, as this will read the |
| // shape and can race with the main thread if we are building off thread. |
| // The allocation kind and object class (which goes through the type) can |
| // be read freely, however. |
| gc::AllocKind kind = object->asTenured().getAllocKind(); |
| return gc::GetGCKindSlots(kind, object->getClass()); |
| } |
| |
| static bool |
| IsUninitializedGlobalLexicalSlot(JSObject* obj, PropertyName* name) |
| { |
| ClonedBlockObject &globalLexical = obj->as<ClonedBlockObject>(); |
| MOZ_ASSERT(globalLexical.isGlobal()); |
| Shape* shape = globalLexical.lookupPure(name); |
| if (!shape) |
| return false; |
| return globalLexical.getSlot(shape->slot()).isMagic(JS_UNINITIALIZED_LEXICAL); |
| } |
| |
| bool |
| IonBuilder::getStaticName(JSObject* staticObject, PropertyName* name, bool* psucceeded, |
| MDefinition* lexicalCheck) |
| { |
| MOZ_ASSERT(*psucceeded == false); |
| |
| jsid id = NameToId(name); |
| |
| bool isGlobalLexical = staticObject->is<ClonedBlockObject>() && |
| staticObject->as<ClonedBlockObject>().isGlobal(); |
| MOZ_ASSERT(isGlobalLexical || |
| staticObject->is<GlobalObject>() || |
| staticObject->is<LexicalScopeBase>()); |
| MOZ_ASSERT(staticObject->isSingleton()); |
| |
| *psucceeded = true; |
| |
| if (staticObject->is<GlobalObject>()) { |
| // Known values on the global definitely don't need TDZ checks. |
| if (lexicalCheck) |
| lexicalCheck->setNotGuardUnchecked(); |
| |
| // Optimize undefined, NaN, and Infinity. |
| if (name == names().undefined) |
| return pushConstant(UndefinedValue()); |
| if (name == names().NaN) |
| return pushConstant(compartment->runtime()->NaNValue()); |
| if (name == names().Infinity) |
| return pushConstant(compartment->runtime()->positiveInfinityValue()); |
| } |
| |
| // When not loading a known value on the global with a lexical check, |
| // always emit the lexical check. This could be optimized, but is |
| // currently not for simplicity's sake. |
| if (lexicalCheck) { |
| *psucceeded = false; |
| return true; |
| } |
| |
| TypeSet::ObjectKey* staticKey = TypeSet::ObjectKey::get(staticObject); |
| if (analysisContext) |
| staticKey->ensureTrackedProperty(analysisContext, NameToId(name)); |
| |
| if (staticKey->unknownProperties()) { |
| *psucceeded = false; |
| return true; |
| } |
| |
| HeapTypeSetKey property = staticKey->property(id); |
| if (!property.maybeTypes() || |
| !property.maybeTypes()->definiteProperty() || |
| property.nonData(constraints())) |
| { |
| // The property has been reconfigured as non-configurable, non-enumerable |
| // or non-writable. |
| *psucceeded = false; |
| return true; |
| } |
| |
| // Don't optimize global lexical bindings if they aren't initialized at |
| // compile time. |
| if (isGlobalLexical && IsUninitializedGlobalLexicalSlot(staticObject, name)) { |
| *psucceeded = false; |
| return true; |
| } |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| BarrierKind barrier = PropertyReadNeedsTypeBarrier(analysisContext, constraints(), staticKey, |
| name, types, /* updateObserved = */ true); |
| |
| JSObject* singleton = types->maybeSingleton(); |
| |
| MIRType knownType = types->getKnownMIRType(); |
| if (barrier == BarrierKind::NoBarrier) { |
| // Try to inline properties holding a known constant object. |
| if (singleton) { |
| if (testSingletonProperty(staticObject, id) == singleton) |
| return pushConstant(ObjectValue(*singleton)); |
| } |
| |
| // Try to inline properties that have never been overwritten. |
| Value constantValue; |
| if (property.constant(constraints(), &constantValue)) |
| return pushConstant(constantValue); |
| |
| // Try to inline properties that can only have one value. |
| if (knownType == MIRType_Undefined) |
| return pushConstant(UndefinedValue()); |
| if (knownType == MIRType_Null) |
| return pushConstant(NullValue()); |
| } |
| |
| MInstruction* obj = constant(ObjectValue(*staticObject)); |
| |
| MIRType rvalType = types->getKnownMIRType(); |
| if (barrier != BarrierKind::NoBarrier) |
| rvalType = MIRType_Value; |
| |
| if (!loadSlot(obj, property.maybeTypes()->definiteSlot(), NumFixedSlots(staticObject), |
| rvalType, barrier, types)) { |
| *psucceeded = false; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Whether a write of the given value may need a post-write barrier for GC purposes. |
| bool |
| jit::NeedsPostBarrier(MDefinition* value) |
| { |
| if (!GetJitContext()->runtime->gcNursery().exists()) |
| return false; |
| return value->mightBeType(MIRType_Object); |
| } |
| |
| bool |
| IonBuilder::setStaticName(JSObject* staticObject, PropertyName* name) |
| { |
| jsid id = NameToId(name); |
| |
| bool isGlobalLexical = staticObject->is<ClonedBlockObject>() && |
| staticObject->as<ClonedBlockObject>().isGlobal(); |
| MOZ_ASSERT(isGlobalLexical || |
| staticObject->is<GlobalObject>() || |
| staticObject->is<CallObject>()); |
| |
| MDefinition* value = current->peek(-1); |
| |
| TypeSet::ObjectKey* staticKey = TypeSet::ObjectKey::get(staticObject); |
| if (staticKey->unknownProperties()) |
| return jsop_setprop(name); |
| |
| HeapTypeSetKey property = staticKey->property(id); |
| if (!property.maybeTypes() || |
| !property.maybeTypes()->definiteProperty() || |
| property.nonData(constraints()) || |
| property.nonWritable(constraints())) |
| { |
| // The property has been reconfigured as non-configurable, non-enumerable |
| // or non-writable. |
| return jsop_setprop(name); |
| } |
| |
| if (!CanWriteProperty(alloc(), constraints(), property, value)) |
| return jsop_setprop(name); |
| |
| // Don't optimize global lexical bindings if they aren't initialized at |
| // compile time. |
| if (isGlobalLexical && IsUninitializedGlobalLexicalSlot(staticObject, name)) |
| return jsop_setprop(name); |
| |
| current->pop(); |
| |
| // Pop the bound object on the stack. |
| MDefinition* obj = current->pop(); |
| MOZ_ASSERT(&obj->toConstant()->value().toObject() == staticObject); |
| |
| if (NeedsPostBarrier(value)) |
| current->add(MPostWriteBarrier::New(alloc(), obj, value)); |
| |
| // If the property has a known type, we may be able to optimize typed stores by not |
| // storing the type tag. |
| MIRType slotType = MIRType_None; |
| MIRType knownType = property.knownMIRType(constraints()); |
| if (knownType != MIRType_Value) |
| slotType = knownType; |
| |
| bool needsBarrier = property.needsBarrier(constraints()); |
| return storeSlot(obj, property.maybeTypes()->definiteSlot(), NumFixedSlots(staticObject), |
| value, needsBarrier, slotType); |
| } |
| |
| JSObject* |
| IonBuilder::testGlobalLexicalBinding(PropertyName* name) |
| { |
| MOZ_ASSERT(JSOp(*pc) == JSOP_BINDGNAME || |
| JSOp(*pc) == JSOP_GETGNAME || |
| JSOp(*pc) == JSOP_SETGNAME || |
| JSOp(*pc) == JSOP_STRICTSETGNAME); |
| |
| // The global isn't the global lexical scope's prototype, but its |
| // enclosing scope. Test for the existence of |name| manually on the |
| // global lexical scope. If it is not found, look for it on the global |
| // itself. |
| |
| NativeObject* obj = &script()->global().lexicalScope(); |
| TypeSet::ObjectKey* lexicalKey = TypeSet::ObjectKey::get(obj); |
| jsid id = NameToId(name); |
| if (analysisContext) |
| lexicalKey->ensureTrackedProperty(analysisContext, id); |
| if (!lexicalKey->unknownProperties()) { |
| // If the property is not found on the global lexical scope but it is |
| // found on the global and is configurable, freeze the typeset for its |
| // non-existence. |
| // |
| // In the case that it is found on the global but is non-configurable, |
| // the binding cannot be shadowed by a global lexical binding. |
| HeapTypeSetKey lexicalProperty = lexicalKey->property(id); |
| Shape* shape = obj->lookupPure(name); |
| if (shape) { |
| if ((JSOp(*pc) != JSOP_GETGNAME && !shape->writable()) || |
| obj->getSlot(shape->slot()).isMagic(JS_UNINITIALIZED_LEXICAL)) |
| { |
| return nullptr; |
| } |
| } else { |
| shape = script()->global().lookupPure(name); |
| if (!shape || shape->configurable()) |
| MOZ_ALWAYS_FALSE(lexicalProperty.isOwnProperty(constraints())); |
| obj = &script()->global(); |
| } |
| } |
| |
| return obj; |
| } |
| |
| bool |
| IonBuilder::jsop_getgname(PropertyName* name) |
| { |
| if (JSObject* obj = testGlobalLexicalBinding(name)) { |
| bool emitted = false; |
| if (!getStaticName(obj, name, &emitted) || emitted) |
| return emitted; |
| |
| if (!forceInlineCaches() && obj->is<GlobalObject>()) { |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| MDefinition* globalObj = constant(ObjectValue(*obj)); |
| if (!getPropTryCommonGetter(&emitted, globalObj, name, types) || emitted) |
| return emitted; |
| } |
| } |
| |
| return jsop_getname(name); |
| } |
| |
| bool |
| IonBuilder::jsop_getname(PropertyName* name) |
| { |
| MDefinition* object; |
| if (IsGlobalOp(JSOp(*pc)) && !script()->hasNonSyntacticScope()) { |
| MInstruction* global = constant(ObjectValue(script()->global().lexicalScope())); |
| object = global; |
| } else { |
| current->push(current->scopeChain()); |
| object = current->pop(); |
| } |
| |
| MGetNameCache* ins; |
| if (JSOp(*GetNextPc(pc)) == JSOP_TYPEOF) |
| ins = MGetNameCache::New(alloc(), object, name, MGetNameCache::NAMETYPEOF); |
| else |
| ins = MGetNameCache::New(alloc(), object, name, MGetNameCache::NAME); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| if (!resumeAfter(ins)) |
| return false; |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| return pushTypeBarrier(ins, types, BarrierKind::TypeSet); |
| } |
| |
| bool |
| IonBuilder::jsop_intrinsic(PropertyName* name) |
| { |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| |
| // If we haven't executed this opcode yet, we need to get the intrinsic |
| // value and monitor the result. |
| if (types->empty()) { |
| MCallGetIntrinsicValue* ins = MCallGetIntrinsicValue::New(alloc(), name); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| if (!resumeAfter(ins)) |
| return false; |
| |
| return pushTypeBarrier(ins, types, BarrierKind::TypeSet); |
| } |
| |
| // Bake in the intrinsic, guaranteed to exist because a non-empty typeset |
| // means the intrinsic was successfully gotten in the VM call above. |
| // Assert that TI agrees with us on the type. |
| Value vp = script()->global().existingIntrinsicValue(name); |
| MOZ_ASSERT(types->hasType(TypeSet::GetValueType(vp))); |
| |
| pushConstant(vp); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_getimport(PropertyName* name) |
| { |
| ModuleEnvironmentObject* env = GetModuleEnvironmentForScript(script()); |
| MOZ_ASSERT(env); |
| |
| Shape* shape; |
| ModuleEnvironmentObject* targetEnv; |
| MOZ_ALWAYS_TRUE(env->lookupImport(NameToId(name), &targetEnv, &shape)); |
| |
| PropertyName* localName = JSID_TO_STRING(shape->propid())->asAtom().asPropertyName(); |
| bool emitted = false; |
| if (!getStaticName(targetEnv, localName, &emitted)) |
| return false; |
| |
| MOZ_ASSERT(emitted); |
| |
| // In the rare case where this import hasn't been initialized already (we |
| // have an import cycle where modules reference each other's imports), emit |
| // a check. |
| if (targetEnv->getSlot(shape->slot()).isMagic(JS_UNINITIALIZED_LEXICAL)) { |
| MDefinition* checked = addLexicalCheck(current->pop()); |
| current->push(checked); |
| } |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_bindname(PropertyName* name) |
| { |
| MDefinition* scopeChain; |
| if (analysis().usesScopeChain()) { |
| scopeChain = current->scopeChain(); |
| } else { |
| // We take the slow path when trying to BINDGNAME a name that resolves |
| // to a 'const' or an uninitialized binding. |
| MOZ_ASSERT(JSOp(*pc) == JSOP_BINDGNAME); |
| scopeChain = constant(ObjectValue(script()->global().lexicalScope())); |
| } |
| MBindNameCache* ins = MBindNameCache::New(alloc(), scopeChain, name, script(), pc); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| static MIRType |
| GetElemKnownType(bool needsHoleCheck, TemporaryTypeSet* types) |
| { |
| MIRType knownType = types->getKnownMIRType(); |
| |
| // Null and undefined have no payload so they can't be specialized. |
| // Since folding null/undefined while building SSA is not safe (see the |
| // comment in IsPhiObservable), we just add an untyped load instruction |
| // and rely on pushTypeBarrier and DCE to replace it with a null/undefined |
| // constant. |
| if (knownType == MIRType_Undefined || knownType == MIRType_Null) |
| knownType = MIRType_Value; |
| |
| // Different architectures may want typed element reads which require |
| // hole checks to be done as either value or typed reads. |
| if (needsHoleCheck && !LIRGenerator::allowTypedElementHoleCheck()) |
| knownType = MIRType_Value; |
| |
| return knownType; |
| } |
| |
| bool |
| IonBuilder::jsop_getelem() |
| { |
| startTrackingOptimizations(); |
| |
| MDefinition* index = current->pop(); |
| MDefinition* obj = current->pop(); |
| |
| trackTypeInfo(TrackedTypeSite::Receiver, obj->type(), obj->resultTypeSet()); |
| trackTypeInfo(TrackedTypeSite::Index, index->type(), index->resultTypeSet()); |
| |
| // Always use a call if we are performing analysis and not actually |
| // emitting code, to simplify later analysis. |
| if (info().isAnalysis() || shouldAbortOnPreliminaryGroups(obj)) { |
| MInstruction* ins = MCallGetElement::New(alloc(), obj, index); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| if (!resumeAfter(ins)) |
| return false; |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| return pushTypeBarrier(ins, types, BarrierKind::TypeSet); |
| } |
| |
| obj = maybeUnboxForPropertyAccess(obj); |
| if (obj->type() == MIRType_Object) |
| obj = convertUnboxedObjects(obj); |
| |
| bool emitted = false; |
| |
| if (!forceInlineCaches()) { |
| trackOptimizationAttempt(TrackedStrategy::GetElem_TypedObject); |
| if (!getElemTryTypedObject(&emitted, obj, index) || emitted) |
| return emitted; |
| |
| // Note: no trackOptimizationAttempt call is needed, getElemTryGetProp |
| // will call it. |
| if (!getElemTryGetProp(&emitted, obj, index) || emitted) |
| return emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::GetElem_Dense); |
| if (!getElemTryDense(&emitted, obj, index) || emitted) |
| return emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::GetElem_TypedStatic); |
| if (!getElemTryTypedStatic(&emitted, obj, index) || emitted) |
| return emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::GetElem_TypedArray); |
| if (!getElemTryTypedArray(&emitted, obj, index) || emitted) |
| return emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::GetElem_String); |
| if (!getElemTryString(&emitted, obj, index) || emitted) |
| return emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::GetElem_Arguments); |
| if (!getElemTryArguments(&emitted, obj, index) || emitted) |
| return emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::GetElem_ArgumentsInlined); |
| if (!getElemTryArgumentsInlined(&emitted, obj, index) || emitted) |
| return emitted; |
| } |
| |
| if (script()->argumentsHasVarBinding() && obj->mightBeType(MIRType_MagicOptimizedArguments)) |
| return abort("Type is not definitely lazy arguments."); |
| |
| trackOptimizationAttempt(TrackedStrategy::GetElem_InlineCache); |
| if (!getElemTryCache(&emitted, obj, index) || emitted) |
| return emitted; |
| |
| // Emit call. |
| MInstruction* ins = MCallGetElement::New(alloc(), obj, index); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| if (!resumeAfter(ins)) |
| return false; |
| |
| if (*pc == JSOP_CALLELEM && IsNullOrUndefined(obj->type())) { |
| // Due to inlining, it's possible the observed TypeSet is non-empty, |
| // even though we know |obj| is null/undefined and the MCallGetElement |
| // will throw. Don't push a TypeBarrier in this case, to avoid |
| // inlining the following (unreachable) JSOP_CALL. |
| return true; |
| } |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| return pushTypeBarrier(ins, types, BarrierKind::TypeSet); |
| } |
| |
| bool |
| IonBuilder::getElemTryTypedObject(bool* emitted, MDefinition* obj, MDefinition* index) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // The next several failures are all due to types not predicting that we |
| // are definitely doing a getelem access on a typed object. |
| trackOptimizationOutcome(TrackedOutcome::AccessNotTypedObject); |
| |
| TypedObjectPrediction objPrediction = typedObjectPrediction(obj); |
| if (objPrediction.isUseless()) |
| return true; |
| |
| if (!objPrediction.ofArrayKind()) |
| return true; |
| |
| TypedObjectPrediction elemPrediction = objPrediction.arrayElementType(); |
| if (elemPrediction.isUseless()) |
| return true; |
| |
| int32_t elemSize; |
| if (!elemPrediction.hasKnownSize(&elemSize)) |
| return true; |
| |
| switch (elemPrediction.kind()) { |
| case type::Simd: |
| // FIXME (bug 894105): load into a MIRType_float32x4 etc |
| trackOptimizationOutcome(TrackedOutcome::GenericFailure); |
| return true; |
| |
| case type::Struct: |
| case type::Array: |
| return getElemTryComplexElemOfTypedObject(emitted, |
| obj, |
| index, |
| objPrediction, |
| elemPrediction, |
| elemSize); |
| case type::Scalar: |
| return getElemTryScalarElemOfTypedObject(emitted, |
| obj, |
| index, |
| objPrediction, |
| elemPrediction, |
| elemSize); |
| |
| case type::Reference: |
| return getElemTryReferenceElemOfTypedObject(emitted, |
| obj, |
| index, |
| objPrediction, |
| elemPrediction); |
| } |
| |
| MOZ_CRASH("Bad kind"); |
| } |
| |
| static MIRType |
| MIRTypeForTypedArrayRead(Scalar::Type arrayType, bool observedDouble); |
| |
| bool |
| IonBuilder::checkTypedObjectIndexInBounds(int32_t elemSize, |
| MDefinition* obj, |
| MDefinition* index, |
| TypedObjectPrediction objPrediction, |
| LinearSum* indexAsByteOffset) |
| { |
| // Ensure index is an integer. |
| MInstruction* idInt32 = MToInt32::New(alloc(), index); |
| current->add(idInt32); |
| |
| // If we know the length statically from the type, just embed it. |
| // Otherwise, load it from the appropriate reserved slot on the |
| // typed object. We know it's an int32, so we can convert from |
| // Value to int32 using truncation. |
| int32_t lenOfAll; |
| MDefinition* length; |
| if (objPrediction.hasKnownArrayLength(&lenOfAll)) { |
| length = constantInt(lenOfAll); |
| |
| // If we are not loading the length from the object itself, only |
| // optimize if the array buffer can't have been neutered. |
| TypeSet::ObjectKey* globalKey = TypeSet::ObjectKey::get(&script()->global()); |
| if (globalKey->hasFlags(constraints(), OBJECT_FLAG_TYPED_OBJECT_NEUTERED)) { |
| trackOptimizationOutcome(TrackedOutcome::TypedObjectNeutered); |
| return false; |
| } |
| } else { |
| trackOptimizationOutcome(TrackedOutcome::TypedObjectArrayRange); |
| return false; |
| } |
| |
| index = addBoundsCheck(idInt32, length); |
| |
| return indexAsByteOffset->add(index, elemSize); |
| } |
| |
| bool |
| IonBuilder::getElemTryScalarElemOfTypedObject(bool* emitted, |
| MDefinition* obj, |
| MDefinition* index, |
| TypedObjectPrediction objPrediction, |
| TypedObjectPrediction elemPrediction, |
| int32_t elemSize) |
| { |
| MOZ_ASSERT(objPrediction.ofArrayKind()); |
| |
| // Must always be loading the same scalar type |
| ScalarTypeDescr::Type elemType = elemPrediction.scalarType(); |
| MOZ_ASSERT(elemSize == ScalarTypeDescr::alignment(elemType)); |
| |
| LinearSum indexAsByteOffset(alloc()); |
| if (!checkTypedObjectIndexInBounds(elemSize, obj, index, objPrediction, &indexAsByteOffset)) |
| return true; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| |
| return pushScalarLoadFromTypedObject(obj, indexAsByteOffset, elemType); |
| } |
| |
| bool |
| IonBuilder::getElemTryReferenceElemOfTypedObject(bool* emitted, |
| MDefinition* obj, |
| MDefinition* index, |
| TypedObjectPrediction objPrediction, |
| TypedObjectPrediction elemPrediction) |
| { |
| MOZ_ASSERT(objPrediction.ofArrayKind()); |
| |
| ReferenceTypeDescr::Type elemType = elemPrediction.referenceType(); |
| size_t elemSize = ReferenceTypeDescr::size(elemType); |
| |
| LinearSum indexAsByteOffset(alloc()); |
| if (!checkTypedObjectIndexInBounds(elemSize, obj, index, objPrediction, &indexAsByteOffset)) |
| return true; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| |
| return pushReferenceLoadFromTypedObject(obj, indexAsByteOffset, elemType, nullptr); |
| } |
| |
| bool |
| IonBuilder::pushScalarLoadFromTypedObject(MDefinition* obj, |
| const LinearSum& byteOffset, |
| ScalarTypeDescr::Type elemType) |
| { |
| int32_t size = ScalarTypeDescr::size(elemType); |
| MOZ_ASSERT(size == ScalarTypeDescr::alignment(elemType)); |
| |
| // Find location within the owner object. |
| MDefinition* elements; |
| MDefinition* scaledOffset; |
| int32_t adjustment; |
| loadTypedObjectElements(obj, byteOffset, size, &elements, &scaledOffset, &adjustment); |
| |
| // Load the element. |
| MLoadUnboxedScalar* load = MLoadUnboxedScalar::New(alloc(), elements, scaledOffset, |
| elemType, |
| DoesNotRequireMemoryBarrier, |
| adjustment); |
| current->add(load); |
| current->push(load); |
| |
| // If we are reading in-bounds elements, we can use knowledge about |
| // the array type to determine the result type, even if the opcode has |
| // never executed. The known pushed type is only used to distinguish |
| // uint32 reads that may produce either doubles or integers. |
| TemporaryTypeSet* resultTypes = bytecodeTypes(pc); |
| bool allowDouble = resultTypes->hasType(TypeSet::DoubleType()); |
| |
| // Note: knownType is not necessarily in resultTypes; e.g. if we |
| // have only observed integers coming out of float array. |
| MIRType knownType = MIRTypeForTypedArrayRead(elemType, allowDouble); |
| |
| // Note: we can ignore the type barrier here, we know the type must |
| // be valid and unbarriered. Also, need not set resultTypeSet, |
| // because knownType is scalar and a resultTypeSet would provide |
| // no useful additional info. |
| load->setResultType(knownType); |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::pushReferenceLoadFromTypedObject(MDefinition* typedObj, |
| const LinearSum& byteOffset, |
| ReferenceTypeDescr::Type type, |
| PropertyName* name) |
| { |
| // Find location within the owner object. |
| MDefinition* elements; |
| MDefinition* scaledOffset; |
| int32_t adjustment; |
| size_t alignment = ReferenceTypeDescr::alignment(type); |
| loadTypedObjectElements(typedObj, byteOffset, alignment, &elements, &scaledOffset, &adjustment); |
| |
| TemporaryTypeSet* observedTypes = bytecodeTypes(pc); |
| |
| MInstruction* load = nullptr; // initialize to silence GCC warning |
| BarrierKind barrier = PropertyReadNeedsTypeBarrier(analysisContext, constraints(), |
| typedObj, name, observedTypes); |
| |
| switch (type) { |
| case ReferenceTypeDescr::TYPE_ANY: { |
| // Make sure the barrier reflects the possibility of reading undefined. |
| bool bailOnUndefined = barrier == BarrierKind::NoBarrier && |
| !observedTypes->hasType(TypeSet::UndefinedType()); |
| if (bailOnUndefined) |
| barrier = BarrierKind::TypeTagOnly; |
| load = MLoadElement::New(alloc(), elements, scaledOffset, false, false, adjustment); |
| break; |
| } |
| case ReferenceTypeDescr::TYPE_OBJECT: { |
| // Make sure the barrier reflects the possibility of reading null. When |
| // there is no other barrier needed we include the null bailout with |
| // MLoadUnboxedObjectOrNull, which avoids the need to box the result |
| // for a type barrier instruction. |
| MLoadUnboxedObjectOrNull::NullBehavior nullBehavior; |
| if (barrier == BarrierKind::NoBarrier && !observedTypes->hasType(TypeSet::NullType())) |
| nullBehavior = MLoadUnboxedObjectOrNull::BailOnNull; |
| else |
| nullBehavior = MLoadUnboxedObjectOrNull::HandleNull; |
| load = MLoadUnboxedObjectOrNull::New(alloc(), elements, scaledOffset, nullBehavior, |
| adjustment); |
| break; |
| } |
| case ReferenceTypeDescr::TYPE_STRING: { |
| load = MLoadUnboxedString::New(alloc(), elements, scaledOffset, adjustment); |
| observedTypes->addType(TypeSet::StringType(), alloc().lifoAlloc()); |
| break; |
| } |
| } |
| |
| current->add(load); |
| current->push(load); |
| |
| return pushTypeBarrier(load, observedTypes, barrier); |
| } |
| |
| bool |
| IonBuilder::getElemTryComplexElemOfTypedObject(bool* emitted, |
| MDefinition* obj, |
| MDefinition* index, |
| TypedObjectPrediction objPrediction, |
| TypedObjectPrediction elemPrediction, |
| int32_t elemSize) |
| { |
| MOZ_ASSERT(objPrediction.ofArrayKind()); |
| |
| MDefinition* type = loadTypedObjectType(obj); |
| MDefinition* elemTypeObj = typeObjectForElementFromArrayStructType(type); |
| |
| LinearSum indexAsByteOffset(alloc()); |
| if (!checkTypedObjectIndexInBounds(elemSize, obj, index, objPrediction, &indexAsByteOffset)) |
| return true; |
| |
| return pushDerivedTypedObject(emitted, obj, indexAsByteOffset, |
| elemPrediction, elemTypeObj); |
| } |
| |
| bool |
| IonBuilder::pushDerivedTypedObject(bool* emitted, |
| MDefinition* obj, |
| const LinearSum& baseByteOffset, |
| TypedObjectPrediction derivedPrediction, |
| MDefinition* derivedTypeObj) |
| { |
| // Find location within the owner object. |
| MDefinition* owner; |
| LinearSum ownerByteOffset(alloc()); |
| loadTypedObjectData(obj, &owner, &ownerByteOffset); |
| |
| if (!ownerByteOffset.add(baseByteOffset, 1)) |
| setForceAbort(); |
| |
| MDefinition* offset = ConvertLinearSum(alloc(), current, ownerByteOffset, |
| /* convertConstant = */ true); |
| |
| // Create the derived typed object. |
| MInstruction* derivedTypedObj = MNewDerivedTypedObject::New(alloc(), |
| derivedPrediction, |
| derivedTypeObj, |
| owner, |
| offset); |
| current->add(derivedTypedObj); |
| current->push(derivedTypedObj); |
| |
| // Determine (if possible) the class/proto that `derivedTypedObj` will |
| // have. For derived typed objects, the opacity will be the same as the |
| // incoming object from which the derived typed object is, well, derived. |
| // The prototype will be determined based on the type descriptor (and is |
| // immutable). |
| TemporaryTypeSet* objTypes = obj->resultTypeSet(); |
| const Class* expectedClass = nullptr; |
| if (const Class* objClass = objTypes ? objTypes->getKnownClass(constraints()) : nullptr) { |
| MOZ_ASSERT(IsTypedObjectClass(objClass)); |
| expectedClass = GetOutlineTypedObjectClass(IsOpaqueTypedObjectClass(objClass)); |
| } |
| const TypedProto* expectedProto = derivedPrediction.getKnownPrototype(); |
| MOZ_ASSERT_IF(expectedClass, IsTypedObjectClass(expectedClass)); |
| |
| // Determine (if possible) the class/proto that the observed type set |
| // describes. |
| TemporaryTypeSet* observedTypes = bytecodeTypes(pc); |
| const Class* observedClass = observedTypes->getKnownClass(constraints()); |
| |
| // If expectedClass/expectedProto are both non-null (and hence known), we |
| // can predict precisely what object group derivedTypedObj will have. |
| // Therefore, if we observe that this group is already contained in the set |
| // of observedTypes, we can skip the barrier. |
| // |
| // Barriers still wind up being needed in some relatively |
| // rare cases: |
| // |
| // - if multiple kinds of typed objects flow into this point, |
| // in which case we will not be able to predict expectedClass |
| // nor expectedProto. |
| // |
| // - if the code has never executed, in which case the set of |
| // observed types will be incomplete. |
| // |
| // Barriers are particularly expensive here because they prevent |
| // us from optimizing the MNewDerivedTypedObject away. |
| JSObject* observedProto; |
| if (observedTypes->getCommonPrototype(constraints(), &observedProto) && |
| observedClass && observedProto && observedClass == expectedClass && |
| observedProto == expectedProto) |
| { |
| derivedTypedObj->setResultTypeSet(observedTypes); |
| } else { |
| if (!pushTypeBarrier(derivedTypedObj, observedTypes, BarrierKind::TypeSet)) |
| return false; |
| } |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::getElemTryGetProp(bool* emitted, MDefinition* obj, MDefinition* index) |
| { |
| // If index is a constant string or symbol, try to optimize this GETELEM |
| // as a GETPROP. |
| |
| MOZ_ASSERT(*emitted == false); |
| |
| if (!index->isConstantValue()) |
| return true; |
| |
| jsid id; |
| if (!ValueToIdPure(index->constantValue(), &id)) |
| return true; |
| |
| if (id != IdToTypeId(id)) |
| return true; |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| |
| trackOptimizationAttempt(TrackedStrategy::GetProp_Constant); |
| if (!getPropTryConstant(emitted, obj, id, types) || *emitted) { |
| if (*emitted) |
| index->setImplicitlyUsedUnchecked(); |
| return *emitted; |
| } |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::getElemTryDense(bool* emitted, MDefinition* obj, MDefinition* index) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| JSValueType unboxedType = UnboxedArrayElementType(constraints(), obj, index); |
| if (unboxedType == JSVAL_TYPE_MAGIC) { |
| if (!ElementAccessIsDenseNative(constraints(), obj, index)) { |
| trackOptimizationOutcome(TrackedOutcome::AccessNotDense); |
| return true; |
| } |
| } |
| |
| // Don't generate a fast path if there have been bounds check failures |
| // and this access might be on a sparse property. |
| if (ElementAccessHasExtraIndexedProperty(this, obj) && failedBoundsCheck_) { |
| trackOptimizationOutcome(TrackedOutcome::ProtoIndexedProps); |
| return true; |
| } |
| |
| // Don't generate a fast path if this pc has seen negative indexes accessed, |
| // which will not appear to be extra indexed properties. |
| if (inspector->hasSeenNegativeIndexGetElement(pc)) { |
| trackOptimizationOutcome(TrackedOutcome::ArraySeenNegativeIndex); |
| return true; |
| } |
| |
| if (!jsop_getelem_dense(obj, index, unboxedType)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| JSObject* |
| IonBuilder::getStaticTypedArrayObject(MDefinition* obj, MDefinition* index) |
| { |
| Scalar::Type arrayType; |
| if (!ElementAccessIsAnyTypedArray(constraints(), obj, index, &arrayType)) { |
| trackOptimizationOutcome(TrackedOutcome::AccessNotTypedArray); |
| return nullptr; |
| } |
| |
| if (!LIRGenerator::allowStaticTypedArrayAccesses()) { |
| trackOptimizationOutcome(TrackedOutcome::Disabled); |
| return nullptr; |
| } |
| |
| if (ElementAccessHasExtraIndexedProperty(this, obj)) { |
| trackOptimizationOutcome(TrackedOutcome::ProtoIndexedProps); |
| return nullptr; |
| } |
| |
| if (!obj->resultTypeSet()) { |
| trackOptimizationOutcome(TrackedOutcome::NoTypeInfo); |
| return nullptr; |
| } |
| |
| JSObject* tarrObj = obj->resultTypeSet()->maybeSingleton(); |
| if (!tarrObj) { |
| trackOptimizationOutcome(TrackedOutcome::NotSingleton); |
| return nullptr; |
| } |
| |
| TypeSet::ObjectKey* tarrKey = TypeSet::ObjectKey::get(tarrObj); |
| if (tarrKey->unknownProperties()) { |
| trackOptimizationOutcome(TrackedOutcome::UnknownProperties); |
| return nullptr; |
| } |
| |
| return tarrObj; |
| } |
| |
| bool |
| IonBuilder::getElemTryTypedStatic(bool* emitted, MDefinition* obj, MDefinition* index) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| JSObject* tarrObj = getStaticTypedArrayObject(obj, index); |
| if (!tarrObj) |
| return true; |
| |
| // LoadTypedArrayElementStatic currently treats uint32 arrays as int32. |
| Scalar::Type viewType = AnyTypedArrayType(tarrObj); |
| if (viewType == Scalar::Uint32) { |
| trackOptimizationOutcome(TrackedOutcome::StaticTypedArrayUint32); |
| return true; |
| } |
| |
| MDefinition* ptr = convertShiftToMaskForStaticTypedArray(index, viewType); |
| if (!ptr) |
| return true; |
| |
| // Emit LoadTypedArrayElementStatic. |
| |
| if (tarrObj->is<TypedArrayObject>()) { |
| TypeSet::ObjectKey* tarrKey = TypeSet::ObjectKey::get(tarrObj); |
| tarrKey->watchStateChangeForTypedArrayData(constraints()); |
| } |
| |
| obj->setImplicitlyUsedUnchecked(); |
| index->setImplicitlyUsedUnchecked(); |
| |
| MLoadTypedArrayElementStatic* load = MLoadTypedArrayElementStatic::New(alloc(), tarrObj, ptr); |
| current->add(load); |
| current->push(load); |
| |
| // The load is infallible if an undefined result will be coerced to the |
| // appropriate numeric type if the read is out of bounds. The truncation |
| // analysis picks up some of these cases, but is incomplete with respect |
| // to others. For now, sniff the bytecode for simple patterns following |
| // the load which guarantee a truncation or numeric conversion. |
| if (viewType == Scalar::Float32 || viewType == Scalar::Float64) { |
| jsbytecode* next = pc + JSOP_GETELEM_LENGTH; |
| if (*next == JSOP_POS) |
| load->setInfallible(); |
| } else { |
| jsbytecode* next = pc + JSOP_GETELEM_LENGTH; |
| if (*next == JSOP_ZERO && *(next + JSOP_ZERO_LENGTH) == JSOP_BITOR) |
| load->setInfallible(); |
| } |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::getElemTryTypedArray(bool* emitted, MDefinition* obj, MDefinition* index) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| Scalar::Type arrayType; |
| if (!ElementAccessIsAnyTypedArray(constraints(), obj, index, &arrayType)) { |
| trackOptimizationOutcome(TrackedOutcome::AccessNotTypedArray); |
| return true; |
| } |
| |
| // Emit typed getelem variant. |
| if (!jsop_getelem_typed(obj, index, arrayType)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::getElemTryString(bool* emitted, MDefinition* obj, MDefinition* index) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| if (obj->type() != MIRType_String || !IsNumberType(index->type())) { |
| trackOptimizationOutcome(TrackedOutcome::AccessNotString); |
| return true; |
| } |
| |
| // If the index is expected to be out-of-bounds, don't optimize to avoid |
| // frequent bailouts. |
| if (bytecodeTypes(pc)->hasType(TypeSet::UndefinedType())) { |
| trackOptimizationOutcome(TrackedOutcome::OutOfBounds); |
| return true; |
| } |
| |
| // Emit fast path for string[index]. |
| MInstruction* idInt32 = MToInt32::New(alloc(), index); |
| current->add(idInt32); |
| index = idInt32; |
| |
| MStringLength* length = MStringLength::New(alloc(), obj); |
| current->add(length); |
| |
| index = addBoundsCheck(index, length); |
| |
| MCharCodeAt* charCode = MCharCodeAt::New(alloc(), obj, index); |
| current->add(charCode); |
| |
| MFromCharCode* result = MFromCharCode::New(alloc(), charCode); |
| current->add(result); |
| current->push(result); |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::getElemTryArguments(bool* emitted, MDefinition* obj, MDefinition* index) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| if (inliningDepth_ > 0) |
| return true; |
| |
| if (obj->type() != MIRType_MagicOptimizedArguments) |
| return true; |
| |
| // Emit GetFrameArgument. |
| |
| MOZ_ASSERT(!info().argsObjAliasesFormals()); |
| |
| // Type Inference has guaranteed this is an optimized arguments object. |
| obj->setImplicitlyUsedUnchecked(); |
| |
| // To ensure that we are not looking above the number of actual arguments. |
| MArgumentsLength* length = MArgumentsLength::New(alloc()); |
| current->add(length); |
| |
| // Ensure index is an integer. |
| MInstruction* idInt32 = MToInt32::New(alloc(), index); |
| current->add(idInt32); |
| index = idInt32; |
| |
| // Bailouts if we read more than the number of actual arguments. |
| index = addBoundsCheck(index, length); |
| |
| // Load the argument from the actual arguments. |
| MGetFrameArgument* load = MGetFrameArgument::New(alloc(), index, analysis_.hasSetArg()); |
| current->add(load); |
| current->push(load); |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| if (!pushTypeBarrier(load, types, BarrierKind::TypeSet)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::getElemTryArgumentsInlined(bool* emitted, MDefinition* obj, MDefinition* index) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| if (inliningDepth_ == 0) |
| return true; |
| |
| if (obj->type() != MIRType_MagicOptimizedArguments) |
| return true; |
| |
| // Emit inlined arguments. |
| obj->setImplicitlyUsedUnchecked(); |
| |
| MOZ_ASSERT(!info().argsObjAliasesFormals()); |
| |
| // When the id is constant, we can just return the corresponding inlined argument |
| if (index->isConstantValue() && index->constantValue().isInt32()) { |
| MOZ_ASSERT(inliningDepth_ > 0); |
| |
| int32_t id = index->constantValue().toInt32(); |
| index->setImplicitlyUsedUnchecked(); |
| |
| if (id < (int32_t)inlineCallInfo_->argc() && id >= 0) |
| current->push(inlineCallInfo_->getArg(id)); |
| else |
| pushConstant(UndefinedValue()); |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| // inlined not constant not supported, yet. |
| return abort("NYI inlined not constant get argument element"); |
| } |
| |
| bool |
| IonBuilder::getElemTryCache(bool* emitted, MDefinition* obj, MDefinition* index) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // Make sure we have at least an object. |
| if (!obj->mightBeType(MIRType_Object)) { |
| trackOptimizationOutcome(TrackedOutcome::NotObject); |
| return true; |
| } |
| |
| // Don't cache for strings. |
| if (obj->mightBeType(MIRType_String)) { |
| trackOptimizationOutcome(TrackedOutcome::GetElemStringNotCached); |
| return true; |
| } |
| |
| // Index should be integer, string, or symbol |
| if (!index->mightBeType(MIRType_Int32) && |
| !index->mightBeType(MIRType_String) && |
| !index->mightBeType(MIRType_Symbol)) |
| { |
| trackOptimizationOutcome(TrackedOutcome::IndexType); |
| return true; |
| } |
| |
| // Turn off cacheing if the element is int32 and we've seen non-native objects as the target |
| // of this getelem. |
| bool nonNativeGetElement = inspector->hasSeenNonNativeGetElement(pc); |
| if (index->mightBeType(MIRType_Int32) && nonNativeGetElement) { |
| trackOptimizationOutcome(TrackedOutcome::NonNativeReceiver); |
| return true; |
| } |
| |
| // Emit GetElementCache. |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| BarrierKind barrier = PropertyReadNeedsTypeBarrier(analysisContext, constraints(), obj, |
| nullptr, types); |
| |
| // Always add a barrier if the index might be a string or symbol, so that |
| // the cache can attach stubs for particular properties. |
| if (index->mightBeType(MIRType_String) || index->mightBeType(MIRType_Symbol)) |
| barrier = BarrierKind::TypeSet; |
| |
| MGetPropertyCache* ins = MGetPropertyCache::New(alloc(), obj, index, |
| barrier == BarrierKind::TypeSet); |
| current->add(ins); |
| current->push(ins); |
| |
| if (!resumeAfter(ins)) |
| return false; |
| |
| // Spice up type information. |
| if (index->type() == MIRType_Int32 && barrier == BarrierKind::NoBarrier) { |
| bool needHoleCheck = !ElementAccessIsPacked(constraints(), obj); |
| MIRType knownType = GetElemKnownType(needHoleCheck, types); |
| |
| if (knownType != MIRType_Value && knownType != MIRType_Double) |
| ins->setResultType(knownType); |
| } |
| |
| if (!pushTypeBarrier(ins, types, barrier)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| TemporaryTypeSet* |
| IonBuilder::computeHeapType(const TemporaryTypeSet* objTypes, const jsid id) |
| { |
| if (objTypes->unknownObject() || objTypes->getObjectCount() == 0) |
| return nullptr; |
| |
| TemporaryTypeSet empty; |
| TemporaryTypeSet* acc = ∅ |
| LifoAlloc* lifoAlloc = alloc().lifoAlloc(); |
| |
| Vector<HeapTypeSetKey, 4, SystemAllocPolicy> properties; |
| if (!properties.reserve(objTypes->getObjectCount())) |
| return nullptr; |
| |
| for (unsigned i = 0; i < objTypes->getObjectCount(); i++) { |
| TypeSet::ObjectKey* key = objTypes->getObject(i); |
| |
| if (key->unknownProperties()) |
| return nullptr; |
| |
| HeapTypeSetKey property = key->property(id); |
| HeapTypeSet* currentSet = property.maybeTypes(); |
| |
| if (!currentSet || currentSet->unknown()) |
| return nullptr; |
| |
| properties.infallibleAppend(property); |
| acc = TypeSet::unionSets(acc, currentSet, lifoAlloc); |
| if (!acc) |
| return nullptr; |
| } |
| |
| // Freeze all the properties associated with the refined type set. |
| for (HeapTypeSetKey* i = properties.begin(); i != properties.end(); i++) |
| i->freeze(constraints()); |
| |
| return acc; |
| } |
| |
| bool |
| IonBuilder::jsop_getelem_dense(MDefinition* obj, MDefinition* index, JSValueType unboxedType) |
| { |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| |
| MOZ_ASSERT(index->type() == MIRType_Int32 || index->type() == MIRType_Double); |
| if (JSOp(*pc) == JSOP_CALLELEM) { |
| // Indexed call on an element of an array. Populate the observed types |
| // with any objects that could be in the array, to avoid extraneous |
| // type barriers. |
| AddObjectsForPropertyRead(obj, nullptr, types); |
| } |
| |
| BarrierKind barrier = PropertyReadNeedsTypeBarrier(analysisContext, constraints(), obj, |
| nullptr, types); |
| bool needsHoleCheck = !ElementAccessIsPacked(constraints(), obj); |
| |
| // Reads which are on holes in the object do not have to bail out if |
| // undefined values have been observed at this access site and the access |
| // cannot hit another indexed property on the object or its prototypes. |
| bool readOutOfBounds = |
| types->hasType(TypeSet::UndefinedType()) && |
| !ElementAccessHasExtraIndexedProperty(this, obj); |
| |
| MIRType knownType = MIRType_Value; |
| if (unboxedType == JSVAL_TYPE_MAGIC && barrier == BarrierKind::NoBarrier) |
| knownType = GetElemKnownType(needsHoleCheck, types); |
| |
| // Ensure index is an integer. |
| MInstruction* idInt32 = MToInt32::New(alloc(), index); |
| current->add(idInt32); |
| index = idInt32; |
| |
| // Get the elements vector. |
| MInstruction* elements = MElements::New(alloc(), obj, unboxedType != JSVAL_TYPE_MAGIC); |
| current->add(elements); |
| |
| // Note: to help GVN, use the original MElements instruction and not |
| // MConvertElementsToDoubles as operand. This is fine because converting |
| // elements to double does not change the initialized length. |
| MInstruction* initLength = initializedLength(obj, elements, unboxedType); |
| |
| // If we can load the element as a definite double, make sure to check that |
| // the array has been converted to homogenous doubles first. |
| TemporaryTypeSet* objTypes = obj->resultTypeSet(); |
| bool inBounds = !readOutOfBounds && !needsHoleCheck; |
| |
| if (inBounds) { |
| TemporaryTypeSet* heapTypes = computeHeapType(objTypes, JSID_VOID); |
| if (heapTypes && heapTypes->isSubset(types)) { |
| knownType = heapTypes->getKnownMIRType(); |
| types = heapTypes; |
| } |
| } |
| |
| bool loadDouble = |
| unboxedType == JSVAL_TYPE_MAGIC && |
| barrier == BarrierKind::NoBarrier && |
| loopDepth_ && |
| inBounds && |
| knownType == MIRType_Double && |
| objTypes && |
| objTypes->convertDoubleElements(constraints()) == TemporaryTypeSet::AlwaysConvertToDoubles; |
| if (loadDouble) |
| elements = addConvertElementsToDoubles(elements); |
| |
| MInstruction* load; |
| |
| if (!readOutOfBounds) { |
| // This load should not return undefined, so likely we're reading |
| // in-bounds elements, and the array is packed or its holes are not |
| // read. This is the best case: we can separate the bounds check for |
| // hoisting. |
| index = addBoundsCheck(index, initLength); |
| |
| if (unboxedType != JSVAL_TYPE_MAGIC) { |
| load = loadUnboxedValue(elements, 0, index, unboxedType, barrier, types); |
| } else { |
| load = MLoadElement::New(alloc(), elements, index, needsHoleCheck, loadDouble); |
| current->add(load); |
| } |
| } else { |
| // This load may return undefined, so assume that we *can* read holes, |
| // or that we can read out-of-bounds accesses. In this case, the bounds |
| // check is part of the opcode. |
| load = MLoadElementHole::New(alloc(), elements, index, initLength, |
| unboxedType, needsHoleCheck); |
| current->add(load); |
| |
| // If maybeUndefined was true, the typeset must have undefined, and |
| // then either additional types or a barrier. This means we should |
| // never have a typed version of LoadElementHole. |
| MOZ_ASSERT(knownType == MIRType_Value); |
| } |
| |
| if (knownType != MIRType_Value) { |
| load->setResultType(knownType); |
| load->setResultTypeSet(types); |
| } |
| |
| current->push(load); |
| return pushTypeBarrier(load, types, barrier); |
| } |
| |
| void |
| IonBuilder::addTypedArrayLengthAndData(MDefinition* obj, |
| BoundsChecking checking, |
| MDefinition** index, |
| MInstruction** length, MInstruction** elements) |
| { |
| MOZ_ASSERT((index != nullptr) == (elements != nullptr)); |
| JSObject* tarr = nullptr; |
| |
| if (obj->isConstantValue() && obj->constantValue().isObject()) |
| tarr = &obj->constantValue().toObject(); |
| else if (obj->resultTypeSet()) |
| tarr = obj->resultTypeSet()->maybeSingleton(); |
| |
| if (tarr) { |
| SharedMem<void*> data = AnyTypedArrayViewData(tarr); |
| // Bug 979449 - Optimistically embed the elements and use TI to |
| // invalidate if we move them. |
| bool isTenured = !tarr->runtimeFromMainThread()->gc.nursery.isInside(data); |
| if (isTenured && tarr->isSingleton()) { |
| // The 'data' pointer of TypedArrayObject can change in rare circumstances |
| // (ArrayBufferObject::changeContents). |
| TypeSet::ObjectKey* tarrKey = TypeSet::ObjectKey::get(tarr); |
| if (!tarrKey->unknownProperties()) { |
| if (tarr->is<TypedArrayObject>()) |
| tarrKey->watchStateChangeForTypedArrayData(constraints()); |
| |
| obj->setImplicitlyUsedUnchecked(); |
| |
| int32_t len = AssertedCast<int32_t>(AnyTypedArrayLength(tarr)); |
| *length = MConstant::New(alloc(), Int32Value(len)); |
| current->add(*length); |
| |
| if (index) { |
| if (checking == DoBoundsCheck) |
| *index = addBoundsCheck(*index, *length); |
| |
| *elements = MConstantElements::New(alloc(), data); |
| current->add(*elements); |
| } |
| return; |
| } |
| } |
| } |
| |
| *length = MTypedArrayLength::New(alloc(), obj); |
| current->add(*length); |
| |
| if (index) { |
| if (checking == DoBoundsCheck) |
| *index = addBoundsCheck(*index, *length); |
| |
| *elements = MTypedArrayElements::New(alloc(), obj); |
| current->add(*elements); |
| } |
| } |
| |
| MDefinition* |
| IonBuilder::convertShiftToMaskForStaticTypedArray(MDefinition* id, |
| Scalar::Type viewType) |
| { |
| trackOptimizationOutcome(TrackedOutcome::StaticTypedArrayCantComputeMask); |
| |
| // No shifting is necessary if the typed array has single byte elements. |
| if (TypedArrayShift(viewType) == 0) |
| return id; |
| |
| // If the index is an already shifted constant, undo the shift to get the |
| // absolute offset being accessed. |
| if (id->isConstantValue() && id->constantValue().isInt32()) { |
| int32_t index = id->constantValue().toInt32(); |
| MConstant* offset = MConstant::New(alloc(), Int32Value(index << TypedArrayShift(viewType))); |
| current->add(offset); |
| return offset; |
| } |
| |
| if (!id->isRsh() || id->isEffectful()) |
| return nullptr; |
| if (!id->getOperand(1)->isConstantValue()) |
| return nullptr; |
| const Value& value = id->getOperand(1)->constantValue(); |
| if (!value.isInt32() || uint32_t(value.toInt32()) != TypedArrayShift(viewType)) |
| return nullptr; |
| |
| // Instead of shifting, mask off the low bits of the index so that |
| // a non-scaled access on the typed array can be performed. |
| MConstant* mask = MConstant::New(alloc(), Int32Value(~((1 << value.toInt32()) - 1))); |
| MBitAnd* ptr = MBitAnd::New(alloc(), id->getOperand(0), mask); |
| |
| ptr->infer(nullptr, nullptr); |
| MOZ_ASSERT(!ptr->isEffectful()); |
| |
| current->add(mask); |
| current->add(ptr); |
| |
| return ptr; |
| } |
| |
| static MIRType |
| MIRTypeForTypedArrayRead(Scalar::Type arrayType, bool observedDouble) |
| { |
| switch (arrayType) { |
| case Scalar::Int8: |
| case Scalar::Uint8: |
| case Scalar::Uint8Clamped: |
| case Scalar::Int16: |
| case Scalar::Uint16: |
| case Scalar::Int32: |
| return MIRType_Int32; |
| case Scalar::Uint32: |
| return observedDouble ? MIRType_Double : MIRType_Int32; |
| case Scalar::Float32: |
| return MIRType_Float32; |
| case Scalar::Float64: |
| return MIRType_Double; |
| default: |
| break; |
| } |
| MOZ_CRASH("Unknown typed array type"); |
| } |
| |
| bool |
| IonBuilder::jsop_getelem_typed(MDefinition* obj, MDefinition* index, |
| Scalar::Type arrayType) |
| { |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| |
| bool maybeUndefined = types->hasType(TypeSet::UndefinedType()); |
| |
| // Reading from an Uint32Array will result in a double for values |
| // that don't fit in an int32. We have to bailout if this happens |
| // and the instruction is not known to return a double. |
| bool allowDouble = types->hasType(TypeSet::DoubleType()); |
| |
| // Ensure id is an integer. |
| MInstruction* idInt32 = MToInt32::New(alloc(), index); |
| current->add(idInt32); |
| index = idInt32; |
| |
| if (!maybeUndefined) { |
| // Assume the index is in range, so that we can hoist the length, |
| // elements vector and bounds check. |
| |
| // If we are reading in-bounds elements, we can use knowledge about |
| // the array type to determine the result type, even if the opcode has |
| // never executed. The known pushed type is only used to distinguish |
| // uint32 reads that may produce either doubles or integers. |
| MIRType knownType = MIRTypeForTypedArrayRead(arrayType, allowDouble); |
| |
| // Get length, bounds-check, then get elements, and add all instructions. |
| MInstruction* length; |
| MInstruction* elements; |
| addTypedArrayLengthAndData(obj, DoBoundsCheck, &index, &length, &elements); |
| |
| // Load the element. |
| MLoadUnboxedScalar* load = MLoadUnboxedScalar::New(alloc(), elements, index, arrayType); |
| current->add(load); |
| current->push(load); |
| |
| // Note: we can ignore the type barrier here, we know the type must |
| // be valid and unbarriered. |
| load->setResultType(knownType); |
| return true; |
| } else { |
| // We need a type barrier if the array's element type has never been |
| // observed (we've only read out-of-bounds values). Note that for |
| // Uint32Array, we only check for int32: if allowDouble is false we |
| // will bailout when we read a double. |
| BarrierKind barrier = BarrierKind::TypeSet; |
| switch (arrayType) { |
| case Scalar::Int8: |
| case Scalar::Uint8: |
| case Scalar::Uint8Clamped: |
| case Scalar::Int16: |
| case Scalar::Uint16: |
| case Scalar::Int32: |
| case Scalar::Uint32: |
| if (types->hasType(TypeSet::Int32Type())) |
| barrier = BarrierKind::NoBarrier; |
| break; |
| case Scalar::Float32: |
| case Scalar::Float64: |
| if (allowDouble) |
| barrier = BarrierKind::NoBarrier; |
| break; |
| default: |
| MOZ_CRASH("Unknown typed array type"); |
| } |
| |
| // Assume we will read out-of-bound values. In this case the |
| // bounds check will be part of the instruction, and the instruction |
| // will always return a Value. |
| MLoadTypedArrayElementHole* load = |
| MLoadTypedArrayElementHole::New(alloc(), obj, index, arrayType, allowDouble); |
| current->add(load); |
| current->push(load); |
| |
| return pushTypeBarrier(load, types, barrier); |
| } |
| } |
| |
| bool |
| IonBuilder::jsop_setelem() |
| { |
| bool emitted = false; |
| startTrackingOptimizations(); |
| |
| MDefinition* value = current->pop(); |
| MDefinition* index = current->pop(); |
| MDefinition* object = convertUnboxedObjects(current->pop()); |
| |
| trackTypeInfo(TrackedTypeSite::Receiver, object->type(), object->resultTypeSet()); |
| trackTypeInfo(TrackedTypeSite::Index, index->type(), index->resultTypeSet()); |
| trackTypeInfo(TrackedTypeSite::Value, value->type(), value->resultTypeSet()); |
| |
| if (shouldAbortOnPreliminaryGroups(object)) { |
| MInstruction* ins = MCallSetElement::New(alloc(), object, index, value, IsStrictSetPC(pc)); |
| current->add(ins); |
| current->push(value); |
| return resumeAfter(ins); |
| } |
| |
| if (!forceInlineCaches()) { |
| trackOptimizationAttempt(TrackedStrategy::SetElem_TypedObject); |
| if (!setElemTryTypedObject(&emitted, object, index, value) || emitted) |
| return emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::SetElem_TypedStatic); |
| if (!setElemTryTypedStatic(&emitted, object, index, value) || emitted) |
| return emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::SetElem_TypedArray); |
| if (!setElemTryTypedArray(&emitted, object, index, value) || emitted) |
| return emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::SetElem_Dense); |
| SetElemICInspector icInspect(inspector->setElemICInspector(pc)); |
| bool writeHole = icInspect.sawOOBDenseWrite(); |
| if (!setElemTryDense(&emitted, object, index, value, writeHole) || emitted) |
| return emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::SetElem_Arguments); |
| if (!setElemTryArguments(&emitted, object, index, value) || emitted) |
| return emitted; |
| } |
| |
| if (script()->argumentsHasVarBinding() && |
| object->mightBeType(MIRType_MagicOptimizedArguments) && |
| info().analysisMode() != Analysis_ArgumentsUsage) |
| { |
| return abort("Type is not definitely lazy arguments."); |
| } |
| |
| trackOptimizationAttempt(TrackedStrategy::SetElem_InlineCache); |
| if (!setElemTryCache(&emitted, object, index, value) || emitted) |
| return emitted; |
| |
| // Emit call. |
| MInstruction* ins = MCallSetElement::New(alloc(), object, index, value, IsStrictSetPC(pc)); |
| current->add(ins); |
| current->push(value); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::setElemTryTypedObject(bool* emitted, MDefinition* obj, |
| MDefinition* index, MDefinition* value) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // The next several failures are all due to types not predicting that we |
| // are definitely doing a getelem access on a typed object. |
| trackOptimizationOutcome(TrackedOutcome::AccessNotTypedObject); |
| |
| TypedObjectPrediction objPrediction = typedObjectPrediction(obj); |
| if (objPrediction.isUseless()) |
| return true; |
| |
| if (!objPrediction.ofArrayKind()) |
| return true; |
| |
| TypedObjectPrediction elemPrediction = objPrediction.arrayElementType(); |
| if (elemPrediction.isUseless()) |
| return true; |
| |
| int32_t elemSize; |
| if (!elemPrediction.hasKnownSize(&elemSize)) |
| return true; |
| |
| switch (elemPrediction.kind()) { |
| case type::Simd: |
| // FIXME (bug 894105): store a MIRType_float32x4 etc |
| trackOptimizationOutcome(TrackedOutcome::GenericFailure); |
| return true; |
| |
| case type::Reference: |
| return setElemTryReferenceElemOfTypedObject(emitted, obj, index, |
| objPrediction, value, elemPrediction); |
| |
| case type::Scalar: |
| return setElemTryScalarElemOfTypedObject(emitted, |
| obj, |
| index, |
| objPrediction, |
| value, |
| elemPrediction, |
| elemSize); |
| |
| case type::Struct: |
| case type::Array: |
| // Not yet optimized. |
| trackOptimizationOutcome(TrackedOutcome::GenericFailure); |
| return true; |
| } |
| |
| MOZ_CRASH("Bad kind"); |
| } |
| |
| bool |
| IonBuilder::setElemTryReferenceElemOfTypedObject(bool* emitted, |
| MDefinition* obj, |
| MDefinition* index, |
| TypedObjectPrediction objPrediction, |
| MDefinition* value, |
| TypedObjectPrediction elemPrediction) |
| { |
| ReferenceTypeDescr::Type elemType = elemPrediction.referenceType(); |
| size_t elemSize = ReferenceTypeDescr::size(elemType); |
| |
| LinearSum indexAsByteOffset(alloc()); |
| if (!checkTypedObjectIndexInBounds(elemSize, obj, index, objPrediction, &indexAsByteOffset)) |
| return true; |
| |
| if (!storeReferenceTypedObjectValue(obj, indexAsByteOffset, elemType, value, nullptr)) |
| return true; |
| |
| current->push(value); |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::setElemTryScalarElemOfTypedObject(bool* emitted, |
| MDefinition* obj, |
| MDefinition* index, |
| TypedObjectPrediction objPrediction, |
| MDefinition* value, |
| TypedObjectPrediction elemPrediction, |
| int32_t elemSize) |
| { |
| // Must always be loading the same scalar type |
| ScalarTypeDescr::Type elemType = elemPrediction.scalarType(); |
| MOZ_ASSERT(elemSize == ScalarTypeDescr::alignment(elemType)); |
| |
| LinearSum indexAsByteOffset(alloc()); |
| if (!checkTypedObjectIndexInBounds(elemSize, obj, index, objPrediction, &indexAsByteOffset)) |
| return true; |
| |
| // Store the element |
| if (!storeScalarTypedObjectValue(obj, indexAsByteOffset, elemType, value)) |
| return false; |
| |
| current->push(value); |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::setElemTryTypedStatic(bool* emitted, MDefinition* object, |
| MDefinition* index, MDefinition* value) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| JSObject* tarrObj = getStaticTypedArrayObject(object, index); |
| if (!tarrObj) |
| return true; |
| |
| if (tarrObj->runtimeFromMainThread()->gc.nursery.isInside(AnyTypedArrayViewData(tarrObj))) |
| return true; |
| |
| Scalar::Type viewType = AnyTypedArrayType(tarrObj); |
| MDefinition* ptr = convertShiftToMaskForStaticTypedArray(index, viewType); |
| if (!ptr) |
| return true; |
| |
| // Emit StoreTypedArrayElementStatic. |
| |
| if (tarrObj->is<TypedArrayObject>()) { |
| TypeSet::ObjectKey* tarrKey = TypeSet::ObjectKey::get(tarrObj); |
| tarrKey->watchStateChangeForTypedArrayData(constraints()); |
| } |
| |
| object->setImplicitlyUsedUnchecked(); |
| index->setImplicitlyUsedUnchecked(); |
| |
| // Clamp value to [0, 255] for Uint8ClampedArray. |
| MDefinition* toWrite = value; |
| if (viewType == Scalar::Uint8Clamped) { |
| toWrite = MClampToUint8::New(alloc(), value); |
| current->add(toWrite->toInstruction()); |
| } |
| |
| MInstruction* store = MStoreTypedArrayElementStatic::New(alloc(), tarrObj, ptr, toWrite); |
| current->add(store); |
| current->push(value); |
| |
| if (!resumeAfter(store)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::setElemTryTypedArray(bool* emitted, MDefinition* object, |
| MDefinition* index, MDefinition* value) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| Scalar::Type arrayType; |
| if (!ElementAccessIsAnyTypedArray(constraints(), object, index, &arrayType)) { |
| trackOptimizationOutcome(TrackedOutcome::AccessNotTypedArray); |
| return true; |
| } |
| |
| // Emit typed setelem variant. |
| if (!jsop_setelem_typed(arrayType, object, index, value)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::setElemTryDense(bool* emitted, MDefinition* object, |
| MDefinition* index, MDefinition* value, bool writeHole) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| JSValueType unboxedType = UnboxedArrayElementType(constraints(), object, index); |
| if (unboxedType == JSVAL_TYPE_MAGIC) { |
| if (!ElementAccessIsDenseNative(constraints(), object, index)) { |
| trackOptimizationOutcome(TrackedOutcome::AccessNotDense); |
| return true; |
| } |
| } |
| |
| if (PropertyWriteNeedsTypeBarrier(alloc(), constraints(), current, |
| &object, nullptr, &value, /* canModify = */ true)) |
| { |
| trackOptimizationOutcome(TrackedOutcome::NeedsTypeBarrier); |
| return true; |
| } |
| |
| if (!object->resultTypeSet()) { |
| trackOptimizationOutcome(TrackedOutcome::NoTypeInfo); |
| return true; |
| } |
| |
| TemporaryTypeSet::DoubleConversion conversion = |
| object->resultTypeSet()->convertDoubleElements(constraints()); |
| |
| // If AmbiguousDoubleConversion, only handle int32 values for now. |
| if (conversion == TemporaryTypeSet::AmbiguousDoubleConversion && |
| value->type() != MIRType_Int32) |
| { |
| trackOptimizationOutcome(TrackedOutcome::ArrayDoubleConversion); |
| return true; |
| } |
| |
| // Don't generate a fast path if there have been bounds check failures |
| // and this access might be on a sparse property. |
| if (ElementAccessHasExtraIndexedProperty(this, object) && failedBoundsCheck_) { |
| trackOptimizationOutcome(TrackedOutcome::ProtoIndexedProps); |
| return true; |
| } |
| |
| // Emit dense setelem variant. |
| if (!jsop_setelem_dense(conversion, object, index, value, unboxedType, writeHole)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::setElemTryArguments(bool* emitted, MDefinition* object, |
| MDefinition* index, MDefinition* value) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| if (object->type() != MIRType_MagicOptimizedArguments) |
| return true; |
| |
| // Arguments are not supported yet. |
| return abort("NYI arguments[]="); |
| } |
| |
| bool |
| IonBuilder::setElemTryCache(bool* emitted, MDefinition* object, |
| MDefinition* index, MDefinition* value) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| if (!object->mightBeType(MIRType_Object)) { |
| trackOptimizationOutcome(TrackedOutcome::NotObject); |
| return true; |
| } |
| |
| if (!index->mightBeType(MIRType_Int32) && |
| !index->mightBeType(MIRType_String) && |
| !index->mightBeType(MIRType_Symbol)) |
| { |
| trackOptimizationOutcome(TrackedOutcome::IndexType); |
| return true; |
| } |
| |
| bool barrier = true; |
| |
| if (index->type() == MIRType_Int32 && |
| !PropertyWriteNeedsTypeBarrier(alloc(), constraints(), current, |
| &object, nullptr, &value, /* canModify = */ true)) |
| { |
| barrier = false; |
| } |
| |
| // We can avoid worrying about holes in the IC if we know a priori we are safe |
| // from them. If TI can guard that there are no indexed properties on the prototype |
| // chain, we know that we anen't missing any setters by overwriting the hole with |
| // another value. |
| bool guardHoles = ElementAccessHasExtraIndexedProperty(this, object); |
| |
| // Make sure the object being written to doesn't have copy on write elements. |
| const Class* clasp = object->resultTypeSet() ? object->resultTypeSet()->getKnownClass(constraints()) : nullptr; |
| bool checkNative = !clasp || !clasp->isNative(); |
| object = addMaybeCopyElementsForWrite(object, checkNative); |
| |
| if (NeedsPostBarrier(value)) |
| current->add(MPostWriteBarrier::New(alloc(), object, value)); |
| |
| // Emit SetPropertyCache. |
| bool strict = JSOp(*pc) == JSOP_STRICTSETELEM; |
| MSetPropertyCache* ins = |
| MSetPropertyCache::New(alloc(), object, index, value, strict, barrier, guardHoles); |
| current->add(ins); |
| current->push(value); |
| |
| if (!resumeAfter(ins)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_setelem_dense(TemporaryTypeSet::DoubleConversion conversion, |
| MDefinition* obj, MDefinition* id, MDefinition* value, |
| JSValueType unboxedType, bool writeHole) |
| { |
| MIRType elementType = MIRType_None; |
| if (unboxedType == JSVAL_TYPE_MAGIC) |
| elementType = DenseNativeElementType(constraints(), obj); |
| bool packed = ElementAccessIsPacked(constraints(), obj); |
| |
| // Writes which are on holes in the object do not have to bail out if they |
| // cannot hit another indexed property on the object or its prototypes. |
| bool writeOutOfBounds = !ElementAccessHasExtraIndexedProperty(this, obj); |
| |
| if (NeedsPostBarrier(value)) |
| current->add(MPostWriteBarrier::New(alloc(), obj, value)); |
| |
| // Ensure id is an integer. |
| MInstruction* idInt32 = MToInt32::New(alloc(), id); |
| current->add(idInt32); |
| id = idInt32; |
| |
| // Copy the elements vector if necessary. |
| obj = addMaybeCopyElementsForWrite(obj, /* checkNative = */ false); |
| |
| // Get the elements vector. |
| MElements* elements = MElements::New(alloc(), obj, unboxedType != JSVAL_TYPE_MAGIC); |
| current->add(elements); |
| |
| // Ensure the value is a double, if double conversion might be needed. |
| MDefinition* newValue = value; |
| switch (conversion) { |
| case TemporaryTypeSet::AlwaysConvertToDoubles: |
| case TemporaryTypeSet::MaybeConvertToDoubles: { |
| MInstruction* valueDouble = MToDouble::New(alloc(), value); |
| current->add(valueDouble); |
| newValue = valueDouble; |
| break; |
| } |
| |
| case TemporaryTypeSet::AmbiguousDoubleConversion: { |
| MOZ_ASSERT(value->type() == MIRType_Int32); |
| MInstruction* maybeDouble = MMaybeToDoubleElement::New(alloc(), elements, value); |
| current->add(maybeDouble); |
| newValue = maybeDouble; |
| break; |
| } |
| |
| case TemporaryTypeSet::DontConvertToDoubles: |
| break; |
| |
| default: |
| MOZ_CRASH("Unknown double conversion"); |
| } |
| |
| // Use MStoreElementHole if this SETELEM has written to out-of-bounds |
| // indexes in the past. Otherwise, use MStoreElement so that we can hoist |
| // the initialized length and bounds check. |
| MInstruction* store; |
| MStoreElementCommon *common = nullptr; |
| if (writeHole && writeOutOfBounds) { |
| MStoreElementHole* ins = MStoreElementHole::New(alloc(), obj, elements, id, newValue, unboxedType); |
| store = ins; |
| common = ins; |
| |
| current->add(ins); |
| current->push(value); |
| } else { |
| MInstruction* initLength = initializedLength(obj, elements, unboxedType); |
| |
| id = addBoundsCheck(id, initLength); |
| bool needsHoleCheck = !packed && !writeOutOfBounds; |
| |
| if (unboxedType != JSVAL_TYPE_MAGIC) { |
| store = storeUnboxedValue(obj, elements, 0, id, unboxedType, newValue); |
| } else { |
| MStoreElement* ins = MStoreElement::New(alloc(), elements, id, newValue, needsHoleCheck); |
| store = ins; |
| common = ins; |
| |
| current->add(store); |
| } |
| |
| current->push(value); |
| } |
| |
| if (!resumeAfter(store)) |
| return false; |
| |
| if (common) { |
| // Determine whether a write barrier is required. |
| if (obj->resultTypeSet()->propertyNeedsBarrier(constraints(), JSID_VOID)) |
| common->setNeedsBarrier(); |
| |
| if (elementType != MIRType_None && packed) |
| common->setElementType(elementType); |
| } |
| |
| return true; |
| } |
| |
| |
| bool |
| IonBuilder::jsop_setelem_typed(Scalar::Type arrayType, |
| MDefinition* obj, MDefinition* id, MDefinition* value) |
| { |
| SetElemICInspector icInspect(inspector->setElemICInspector(pc)); |
| bool expectOOB = icInspect.sawOOBTypedArrayWrite(); |
| |
| if (expectOOB) |
| spew("Emitting OOB TypedArray SetElem"); |
| |
| // Ensure id is an integer. |
| MInstruction* idInt32 = MToInt32::New(alloc(), id); |
| current->add(idInt32); |
| id = idInt32; |
| |
| // Get length, bounds-check, then get elements, and add all instructions. |
| MInstruction* length; |
| MInstruction* elements; |
| BoundsChecking checking = expectOOB ? SkipBoundsCheck : DoBoundsCheck; |
| addTypedArrayLengthAndData(obj, checking, &id, &length, &elements); |
| |
| // Clamp value to [0, 255] for Uint8ClampedArray. |
| MDefinition* toWrite = value; |
| if (arrayType == Scalar::Uint8Clamped) { |
| toWrite = MClampToUint8::New(alloc(), value); |
| current->add(toWrite->toInstruction()); |
| } |
| |
| // Store the value. |
| MInstruction* ins; |
| if (expectOOB) { |
| ins = MStoreTypedArrayElementHole::New(alloc(), elements, length, id, toWrite, arrayType); |
| } else { |
| MStoreUnboxedScalar* store = |
| MStoreUnboxedScalar::New(alloc(), elements, id, toWrite, arrayType, |
| MStoreUnboxedScalar::TruncateInput); |
| ins = store; |
| } |
| |
| current->add(ins); |
| current->push(value); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_length() |
| { |
| if (jsop_length_fastPath()) |
| return true; |
| |
| PropertyName* name = info().getAtom(pc)->asPropertyName(); |
| return jsop_getprop(name); |
| } |
| |
| bool |
| IonBuilder::jsop_length_fastPath() |
| { |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| |
| if (types->getKnownMIRType() != MIRType_Int32) |
| return false; |
| |
| MDefinition* obj = current->peek(-1); |
| |
| if (shouldAbortOnPreliminaryGroups(obj)) |
| return false; |
| |
| if (obj->mightBeType(MIRType_String)) { |
| if (obj->mightBeType(MIRType_Object)) |
| return false; |
| current->pop(); |
| MStringLength* ins = MStringLength::New(alloc(), obj); |
| current->add(ins); |
| current->push(ins); |
| return true; |
| } |
| |
| if (obj->mightBeType(MIRType_Object)) { |
| TemporaryTypeSet* objTypes = obj->resultTypeSet(); |
| |
| // Compute the length for array objects. |
| if (objTypes && |
| objTypes->getKnownClass(constraints()) == &ArrayObject::class_ && |
| !objTypes->hasObjectFlags(constraints(), OBJECT_FLAG_LENGTH_OVERFLOW)) |
| { |
| current->pop(); |
| MElements* elements = MElements::New(alloc(), obj); |
| current->add(elements); |
| |
| // Read length. |
| MArrayLength* length = MArrayLength::New(alloc(), elements); |
| current->add(length); |
| current->push(length); |
| return true; |
| } |
| |
| // Compute the length for unboxed array objects. |
| if (UnboxedArrayElementType(constraints(), obj, nullptr) != JSVAL_TYPE_MAGIC && |
| !objTypes->hasObjectFlags(constraints(), OBJECT_FLAG_LENGTH_OVERFLOW)) |
| { |
| current->pop(); |
| |
| MUnboxedArrayLength* length = MUnboxedArrayLength::New(alloc(), obj); |
| current->add(length); |
| current->push(length); |
| return true; |
| } |
| |
| // Compute the length for array typed objects. |
| TypedObjectPrediction prediction = typedObjectPrediction(obj); |
| if (!prediction.isUseless()) { |
| TypeSet::ObjectKey* globalKey = TypeSet::ObjectKey::get(&script()->global()); |
| if (globalKey->hasFlags(constraints(), OBJECT_FLAG_TYPED_OBJECT_NEUTERED)) |
| return false; |
| |
| MInstruction* length; |
| int32_t sizedLength; |
| if (prediction.hasKnownArrayLength(&sizedLength)) { |
| obj->setImplicitlyUsedUnchecked(); |
| length = MConstant::New(alloc(), Int32Value(sizedLength)); |
| } else { |
| return false; |
| } |
| |
| current->pop(); |
| current->add(length); |
| current->push(length); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| bool |
| IonBuilder::jsop_arguments() |
| { |
| if (info().needsArgsObj()) { |
| current->push(current->argumentsObject()); |
| return true; |
| } |
| MOZ_ASSERT(lazyArguments_); |
| current->push(lazyArguments_); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_newtarget() |
| { |
| if (!info().funMaybeLazy()) { |
| MOZ_ASSERT(!info().script()->isForEval()); |
| pushConstant(NullValue()); |
| return true; |
| } |
| |
| MOZ_ASSERT(info().funMaybeLazy()); |
| |
| if (info().funMaybeLazy()->isArrow()) { |
| MArrowNewTarget* arrowNewTarget = MArrowNewTarget::New(alloc(), getCallee()); |
| current->add(arrowNewTarget); |
| current->push(arrowNewTarget); |
| return true; |
| } |
| |
| if (inliningDepth_ == 0) { |
| MNewTarget* newTarget = MNewTarget::New(alloc()); |
| current->add(newTarget); |
| current->push(newTarget); |
| return true; |
| } |
| |
| if (!info().constructing()) { |
| pushConstant(UndefinedValue()); |
| return true; |
| } |
| |
| current->push(inlineCallInfo_->getNewTarget()); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_rest() |
| { |
| if (info().analysisMode() == Analysis_ArgumentsUsage) { |
| // There's no BaselineScript with the template object. Just push a |
| // dummy value, it does not affect the arguments analysis. |
| MUnknownValue* unknown = MUnknownValue::New(alloc()); |
| current->add(unknown); |
| current->push(unknown); |
| return true; |
| } |
| |
| ArrayObject* templateObject = &inspector->getTemplateObject(pc)->as<ArrayObject>(); |
| |
| if (inliningDepth_ == 0) { |
| // We don't know anything about the callee. |
| MArgumentsLength* numActuals = MArgumentsLength::New(alloc()); |
| current->add(numActuals); |
| |
| // Pass in the number of actual arguments, the number of formals (not |
| // including the rest parameter slot itself), and the template object. |
| MRest* rest = MRest::New(alloc(), constraints(), numActuals, info().nargs() - 1, |
| templateObject); |
| current->add(rest); |
| current->push(rest); |
| return true; |
| } |
| |
| // We know the exact number of arguments the callee pushed. |
| unsigned numActuals = inlineCallInfo_->argc(); |
| unsigned numFormals = info().nargs() - 1; |
| unsigned numRest = numActuals > numFormals ? numActuals - numFormals : 0; |
| |
| MConstant* templateConst = MConstant::NewConstraintlessObject(alloc(), templateObject); |
| current->add(templateConst); |
| |
| MNewArray* array = MNewArray::New(alloc(), constraints(), numRest, templateConst, |
| templateObject->group()->initialHeap(constraints()), pc); |
| current->add(array); |
| |
| if (numRest == 0) { |
| // No more updating to do. (Note that in this one case the length from |
| // the template object is already correct.) |
| current->push(array); |
| return true; |
| } |
| |
| MElements* elements = MElements::New(alloc(), array); |
| current->add(elements); |
| |
| // Unroll the argument copy loop. We don't need to do any bounds or hole |
| // checking here. |
| MConstant* index = nullptr; |
| for (unsigned i = numFormals; i < numActuals; i++) { |
| index = MConstant::New(alloc(), Int32Value(i - numFormals)); |
| current->add(index); |
| |
| MDefinition* arg = inlineCallInfo_->argv()[i]; |
| MStoreElement* store = MStoreElement::New(alloc(), elements, index, arg, |
| /* needsHoleCheck = */ false); |
| current->add(store); |
| |
| if (NeedsPostBarrier(arg)) |
| current->add(MPostWriteBarrier::New(alloc(), array, arg)); |
| } |
| |
| // The array's length is incorrectly 0 now, from the template object |
| // created by BaselineCompiler::emit_JSOP_REST() before the actual argument |
| // count was known. Set the correct length now that we know that count. |
| MSetArrayLength* length = MSetArrayLength::New(alloc(), elements, index); |
| current->add(length); |
| |
| // Update the initialized length for all the (necessarily non-hole) |
| // elements added. |
| MSetInitializedLength* initLength = MSetInitializedLength::New(alloc(), elements, index); |
| current->add(initLength); |
| |
| current->push(array); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_checkobjcoercible() |
| { |
| MDefinition* toCheck = current->peek(-1); |
| |
| if (!toCheck->mightBeType(MIRType_Undefined) && |
| !toCheck->mightBeType(MIRType_Null)) |
| { |
| toCheck->setImplicitlyUsedUnchecked(); |
| return true; |
| } |
| |
| MOZ_ASSERT(toCheck->type() == MIRType_Value || |
| toCheck->type() == MIRType_Null || |
| toCheck->type() == MIRType_Undefined); |
| |
| // If we want to squeeze more perf here, we can throw without checking, |
| // if IsNullOrUndefined(toCheck->type()). Since this is a failure case, |
| // it should be OK. |
| MCheckObjCoercible* check = MCheckObjCoercible::New(alloc(), current->pop()); |
| current->add(check); |
| current->push(check); |
| return resumeAfter(check); |
| } |
| |
| uint32_t |
| IonBuilder::getDefiniteSlot(TemporaryTypeSet* types, PropertyName* name, uint32_t* pnfixed) |
| { |
| if (!types || types->unknownObject()) { |
| trackOptimizationOutcome(TrackedOutcome::NoTypeInfo); |
| return UINT32_MAX; |
| } |
| |
| uint32_t slot = UINT32_MAX; |
| |
| for (size_t i = 0; i < types->getObjectCount(); i++) { |
| TypeSet::ObjectKey* key = types->getObject(i); |
| if (!key) |
| continue; |
| |
| if (key->unknownProperties()) { |
| trackOptimizationOutcome(TrackedOutcome::UnknownProperties); |
| return UINT32_MAX; |
| } |
| |
| if (key->isSingleton()) { |
| trackOptimizationOutcome(TrackedOutcome::Singleton); |
| return UINT32_MAX; |
| } |
| |
| HeapTypeSetKey property = key->property(NameToId(name)); |
| if (!property.maybeTypes() || |
| !property.maybeTypes()->definiteProperty() || |
| property.nonData(constraints())) |
| { |
| trackOptimizationOutcome(TrackedOutcome::NotFixedSlot); |
| return UINT32_MAX; |
| } |
| |
| // Definite slots will always be fixed slots when they are in the |
| // allowable range for fixed slots, except for objects which were |
| // converted from unboxed objects and have a smaller allocation size. |
| size_t nfixed = NativeObject::MAX_FIXED_SLOTS; |
| if (ObjectGroup* group = key->group()->maybeOriginalUnboxedGroup()) |
| nfixed = gc::GetGCKindSlots(group->unboxedLayout().getAllocKind()); |
| |
| uint32_t propertySlot = property.maybeTypes()->definiteSlot(); |
| if (slot == UINT32_MAX) { |
| slot = propertySlot; |
| *pnfixed = nfixed; |
| } else if (slot != propertySlot || nfixed != *pnfixed) { |
| trackOptimizationOutcome(TrackedOutcome::InconsistentFixedSlot); |
| return UINT32_MAX; |
| } |
| } |
| |
| return slot; |
| } |
| |
| uint32_t |
| IonBuilder::getUnboxedOffset(TemporaryTypeSet* types, PropertyName* name, JSValueType* punboxedType) |
| { |
| if (!types || types->unknownObject()) { |
| trackOptimizationOutcome(TrackedOutcome::NoTypeInfo); |
| return UINT32_MAX; |
| } |
| |
| uint32_t offset = UINT32_MAX; |
| |
| for (size_t i = 0; i < types->getObjectCount(); i++) { |
| TypeSet::ObjectKey* key = types->getObject(i); |
| if (!key) |
| continue; |
| |
| if (key->unknownProperties()) { |
| trackOptimizationOutcome(TrackedOutcome::UnknownProperties); |
| return UINT32_MAX; |
| } |
| |
| if (key->isSingleton()) { |
| trackOptimizationOutcome(TrackedOutcome::Singleton); |
| return UINT32_MAX; |
| } |
| |
| UnboxedLayout* layout = key->group()->maybeUnboxedLayout(); |
| if (!layout) { |
| trackOptimizationOutcome(TrackedOutcome::NotUnboxed); |
| return UINT32_MAX; |
| } |
| |
| const UnboxedLayout::Property* property = layout->lookup(name); |
| if (!property) { |
| trackOptimizationOutcome(TrackedOutcome::StructNoField); |
| return UINT32_MAX; |
| } |
| |
| if (layout->nativeGroup()) { |
| trackOptimizationOutcome(TrackedOutcome::UnboxedConvertedToNative); |
| return UINT32_MAX; |
| } |
| |
| key->watchStateChangeForUnboxedConvertedToNative(constraints()); |
| |
| if (offset == UINT32_MAX) { |
| offset = property->offset; |
| *punboxedType = property->type; |
| } else if (offset != property->offset) { |
| trackOptimizationOutcome(TrackedOutcome::InconsistentFieldOffset); |
| return UINT32_MAX; |
| } else if (*punboxedType != property->type) { |
| trackOptimizationOutcome(TrackedOutcome::InconsistentFieldType); |
| return UINT32_MAX; |
| } |
| } |
| |
| return offset; |
| } |
| |
| bool |
| IonBuilder::jsop_runonce() |
| { |
| MRunOncePrologue* ins = MRunOncePrologue::New(alloc()); |
| current->add(ins); |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_not() |
| { |
| MDefinition* value = current->pop(); |
| |
| MNot* ins = MNot::New(alloc(), value, constraints()); |
| current->add(ins); |
| current->push(ins); |
| return true; |
| } |
| |
| bool |
| IonBuilder::objectsHaveCommonPrototype(TemporaryTypeSet* types, PropertyName* name, |
| bool isGetter, JSObject* foundProto, bool* guardGlobal) |
| { |
| // With foundProto a prototype with a getter or setter for name, return |
| // whether looking up name on any object in |types| will go through |
| // foundProto, i.e. all the objects have foundProto on their prototype |
| // chain and do not have a property for name before reaching foundProto. |
| |
| // No sense looking if we don't know what's going on. |
| if (!types || types->unknownObject()) |
| return false; |
| *guardGlobal = false; |
| |
| for (unsigned i = 0; i < types->getObjectCount(); i++) { |
| if (types->getSingleton(i) == foundProto) |
| continue; |
| |
| TypeSet::ObjectKey* key = types->getObject(i); |
| if (!key) |
| continue; |
| |
| while (key) { |
| if (key->unknownProperties()) |
| return false; |
| |
| const Class* clasp = key->clasp(); |
| if (!ClassHasEffectlessLookup(clasp)) |
| return false; |
| JSObject* singleton = key->isSingleton() ? key->singleton() : nullptr; |
| if (ObjectHasExtraOwnProperty(compartment, key, NameToId(name))) { |
| if (!singleton || !singleton->is<GlobalObject>()) |
| return false; |
| *guardGlobal = true; |
| } |
| |
| // Look for a getter/setter on the class itself which may need |
| // to be called. |
| if (isGetter && clasp->ops.getProperty) |
| return false; |
| if (!isGetter && clasp->ops.setProperty) |
| return false; |
| |
| // Test for isOwnProperty() without freezing. If we end up |
| // optimizing, freezePropertiesForCommonPropFunc will freeze the |
| // property type sets later on. |
| HeapTypeSetKey property = key->property(NameToId(name)); |
| if (TypeSet* types = property.maybeTypes()) { |
| if (!types->empty() || types->nonDataProperty()) |
| return false; |
| } |
| if (singleton) { |
| if (CanHaveEmptyPropertyTypesForOwnProperty(singleton)) { |
| MOZ_ASSERT(singleton->is<GlobalObject>()); |
| *guardGlobal = true; |
| } |
| } |
| |
| JSObject* proto = checkNurseryObject(key->proto().toObjectOrNull()); |
| |
| if (proto == foundProto) |
| break; |
| if (!proto) { |
| // The foundProto being searched for did not show up on the |
| // object's prototype chain. |
| return false; |
| } |
| key = TypeSet::ObjectKey::get(proto); |
| } |
| } |
| |
| return true; |
| } |
| |
| void |
| IonBuilder::freezePropertiesForCommonPrototype(TemporaryTypeSet* types, PropertyName* name, |
| JSObject* foundProto, |
| bool allowEmptyTypesforGlobal/* = false*/) |
| { |
| for (unsigned i = 0; i < types->getObjectCount(); i++) { |
| // If we found a Singleton object's own-property, there's nothing to |
| // freeze. |
| if (types->getSingleton(i) == foundProto) |
| continue; |
| |
| TypeSet::ObjectKey* key = types->getObject(i); |
| if (!key) |
| continue; |
| |
| while (true) { |
| HeapTypeSetKey property = key->property(NameToId(name)); |
| JS_ALWAYS_TRUE(!property.isOwnProperty(constraints(), allowEmptyTypesforGlobal)); |
| |
| // Don't mark the proto. It will be held down by the shape |
| // guard. This allows us to use properties found on prototypes |
| // with properties unknown to TI. |
| if (key->proto() == TaggedProto(foundProto)) |
| break; |
| key = TypeSet::ObjectKey::get(key->proto().toObjectOrNull()); |
| } |
| } |
| } |
| |
| bool |
| IonBuilder::testCommonGetterSetter(TemporaryTypeSet* types, PropertyName* name, |
| bool isGetter, JSObject* foundProto, Shape* lastProperty, |
| JSFunction* getterOrSetter, |
| MDefinition** guard, |
| Shape* globalShape/* = nullptr*/, |
| MDefinition** globalGuard/* = nullptr */) |
| { |
| MOZ_ASSERT_IF(globalShape, globalGuard); |
| bool guardGlobal; |
| |
| // Check if all objects being accessed will lookup the name through foundProto. |
| if (!objectsHaveCommonPrototype(types, name, isGetter, foundProto, &guardGlobal) || |
| (guardGlobal && !globalShape)) |
| { |
| trackOptimizationOutcome(TrackedOutcome::MultiProtoPaths); |
| return false; |
| } |
| |
| // We can optimize the getter/setter, so freeze all involved properties to |
| // ensure there isn't a lower shadowing getter or setter installed in the |
| // future. |
| freezePropertiesForCommonPrototype(types, name, foundProto, guardGlobal); |
| |
| // Add a shape guard on the prototype we found the property on. The rest of |
| // the prototype chain is guarded by TI freezes, except when name is a global |
| // name. In this case, we also have to guard on the globals shape to be able |
| // to optimize, because the way global property sets are handled means |
| // freezing doesn't work for what we want here. Note that a shape guard is |
| // good enough here, even in the proxy case, because we have ensured there |
| // are no lookup hooks for this property. |
| if (guardGlobal) { |
| JSObject* obj = &script()->global(); |
| MDefinition* globalObj = constant(ObjectValue(*obj)); |
| *globalGuard = addShapeGuard(globalObj, globalShape, Bailout_ShapeGuard); |
| } |
| |
| if (foundProto->isNative()) { |
| NativeObject& nativeProto = foundProto->as<NativeObject>(); |
| if (nativeProto.lastProperty() == lastProperty) { |
| // The proto shape is the same as it was at the point when we |
| // created the baseline IC, so looking up the prop on the object as |
| // it is now should be safe. |
| Shape* propShape = nativeProto.lookupPure(name); |
| MOZ_ASSERT_IF(isGetter, propShape->getterObject() == getterOrSetter); |
| MOZ_ASSERT_IF(!isGetter, propShape->setterObject() == getterOrSetter); |
| if (propShape && !propShape->configurable()) |
| return true; |
| } |
| } |
| |
| MInstruction* wrapper = constant(ObjectValue(*foundProto)); |
| *guard = addShapeGuard(wrapper, lastProperty, Bailout_ShapeGuard); |
| return true; |
| } |
| |
| void |
| IonBuilder::replaceMaybeFallbackFunctionGetter(MGetPropertyCache* cache) |
| { |
| // Discard the last prior resume point of the previous MGetPropertyCache. |
| WrapMGetPropertyCache rai(maybeFallbackFunctionGetter_); |
| maybeFallbackFunctionGetter_ = cache; |
| } |
| |
| bool |
| IonBuilder::annotateGetPropertyCache(MDefinition* obj, PropertyName* name, |
| MGetPropertyCache* getPropCache, TemporaryTypeSet* objTypes, |
| TemporaryTypeSet* pushedTypes) |
| { |
| // Ensure every pushed value is a singleton. |
| if (pushedTypes->unknownObject() || pushedTypes->baseFlags() != 0) |
| return true; |
| |
| for (unsigned i = 0; i < pushedTypes->getObjectCount(); i++) { |
| if (pushedTypes->getGroup(i) != nullptr) |
| return true; |
| } |
| |
| // Object's typeset should be a proper object |
| if (!objTypes || objTypes->baseFlags() || objTypes->unknownObject()) |
| return true; |
| |
| unsigned int objCount = objTypes->getObjectCount(); |
| if (objCount == 0) |
| return true; |
| |
| InlinePropertyTable* inlinePropTable = getPropCache->initInlinePropertyTable(alloc(), pc); |
| if (!inlinePropTable) |
| return false; |
| |
| // Ensure that the relevant property typeset for each group is |
| // is a single-object typeset containing a JSFunction |
| for (unsigned int i = 0; i < objCount; i++) { |
| ObjectGroup* group = objTypes->getGroup(i); |
| if (!group) |
| continue; |
| TypeSet::ObjectKey* key = TypeSet::ObjectKey::get(group); |
| if (key->unknownProperties() || !key->proto().isObject()) |
| continue; |
| JSObject* proto = checkNurseryObject(key->proto().toObject()); |
| |
| const Class* clasp = key->clasp(); |
| if (!ClassHasEffectlessLookup(clasp) || ObjectHasExtraOwnProperty(compartment, key, NameToId(name))) |
| continue; |
| |
| HeapTypeSetKey ownTypes = key->property(NameToId(name)); |
| if (ownTypes.isOwnProperty(constraints())) |
| continue; |
| |
| JSObject* singleton = testSingletonProperty(proto, NameToId(name)); |
| if (!singleton || !singleton->is<JSFunction>()) |
| continue; |
| |
| // Don't add cases corresponding to non-observed pushes |
| if (!pushedTypes->hasType(TypeSet::ObjectType(singleton))) |
| continue; |
| |
| if (!inlinePropTable->addEntry(alloc(), group, &singleton->as<JSFunction>())) |
| return false; |
| } |
| |
| if (inlinePropTable->numEntries() == 0) { |
| getPropCache->clearInlinePropertyTable(); |
| return true; |
| } |
| |
| #ifdef JS_JITSPEW |
| if (inlinePropTable->numEntries() > 0) |
| JitSpew(JitSpew_Inlining, "Annotated GetPropertyCache with %d/%d inline cases", |
| (int) inlinePropTable->numEntries(), (int) objCount); |
| #endif |
| |
| // If we successfully annotated the GetPropertyCache and there are inline cases, |
| // then keep a resume point of the state right before this instruction for use |
| // later when we have to bail out to this point in the fallback case of a |
| // PolyInlineDispatch. |
| if (inlinePropTable->numEntries() > 0) { |
| // Push the object back onto the stack temporarily to capture the resume point. |
| current->push(obj); |
| MResumePoint* resumePoint = MResumePoint::New(alloc(), current, pc, |
| MResumePoint::ResumeAt); |
| if (!resumePoint) |
| return false; |
| inlinePropTable->setPriorResumePoint(resumePoint); |
| replaceMaybeFallbackFunctionGetter(getPropCache); |
| current->pop(); |
| } |
| return true; |
| } |
| |
| // Returns true if an idempotent cache has ever invalidated this script |
| // or an outer script. |
| bool |
| IonBuilder::invalidatedIdempotentCache() |
| { |
| IonBuilder* builder = this; |
| do { |
| if (builder->script()->invalidatedIdempotentCache()) |
| return true; |
| builder = builder->callerBuilder_; |
| } while (builder); |
| |
| return false; |
| } |
| |
| bool |
| IonBuilder::loadSlot(MDefinition* obj, size_t slot, size_t nfixed, MIRType rvalType, |
| BarrierKind barrier, TemporaryTypeSet* types) |
| { |
| if (slot < nfixed) { |
| MLoadFixedSlot* load = MLoadFixedSlot::New(alloc(), obj, slot); |
| current->add(load); |
| current->push(load); |
| |
| load->setResultType(rvalType); |
| return pushTypeBarrier(load, types, barrier); |
| } |
| |
| MSlots* slots = MSlots::New(alloc(), obj); |
| current->add(slots); |
| |
| MLoadSlot* load = MLoadSlot::New(alloc(), slots, slot - nfixed); |
| current->add(load); |
| current->push(load); |
| |
| load->setResultType(rvalType); |
| return pushTypeBarrier(load, types, barrier); |
| } |
| |
| bool |
| IonBuilder::loadSlot(MDefinition* obj, Shape* shape, MIRType rvalType, |
| BarrierKind barrier, TemporaryTypeSet* types) |
| { |
| return loadSlot(obj, shape->slot(), shape->numFixedSlots(), rvalType, barrier, types); |
| } |
| |
| bool |
| IonBuilder::storeSlot(MDefinition* obj, size_t slot, size_t nfixed, |
| MDefinition* value, bool needsBarrier, |
| MIRType slotType /* = MIRType_None */) |
| { |
| if (slot < nfixed) { |
| MStoreFixedSlot* store = MStoreFixedSlot::New(alloc(), obj, slot, value); |
| current->add(store); |
| current->push(value); |
| if (needsBarrier) |
| store->setNeedsBarrier(); |
| return resumeAfter(store); |
| } |
| |
| MSlots* slots = MSlots::New(alloc(), obj); |
| current->add(slots); |
| |
| MStoreSlot* store = MStoreSlot::New(alloc(), slots, slot - nfixed, value); |
| current->add(store); |
| current->push(value); |
| if (needsBarrier) |
| store->setNeedsBarrier(); |
| if (slotType != MIRType_None) |
| store->setSlotType(slotType); |
| return resumeAfter(store); |
| } |
| |
| bool |
| IonBuilder::storeSlot(MDefinition* obj, Shape* shape, MDefinition* value, bool needsBarrier, |
| MIRType slotType /* = MIRType_None */) |
| { |
| MOZ_ASSERT(shape->writable()); |
| return storeSlot(obj, shape->slot(), shape->numFixedSlots(), value, needsBarrier, slotType); |
| } |
| |
| bool |
| IonBuilder::shouldAbortOnPreliminaryGroups(MDefinition *obj) |
| { |
| // Watch for groups which still have preliminary object information and |
| // have not had the new script properties or unboxed layout analyses |
| // performed. Normally this is done after a small number of the objects |
| // have been created, but if only a few have been created we can still |
| // perform the analysis with a smaller object population. The analysis can |
| // have side effects so we will end up aborting compilation after building |
| // finishes and retrying later. |
| TemporaryTypeSet *types = obj->resultTypeSet(); |
| if (!types || types->unknownObject()) |
| return false; |
| |
| bool preliminary = false; |
| for (size_t i = 0; i < types->getObjectCount(); i++) { |
| TypeSet::ObjectKey* key = types->getObject(i); |
| if (!key) |
| continue; |
| |
| if (ObjectGroup* group = key->maybeGroup()) { |
| if (group->hasUnanalyzedPreliminaryObjects()) { |
| addAbortedPreliminaryGroup(group); |
| preliminary = true; |
| } |
| } |
| } |
| |
| return preliminary; |
| } |
| |
| MDefinition* |
| IonBuilder::maybeUnboxForPropertyAccess(MDefinition* def) |
| { |
| if (def->type() != MIRType_Value) |
| return def; |
| |
| MIRType type = inspector->expectedPropertyAccessInputType(pc); |
| if (type == MIRType_Value || !def->mightBeType(type)) |
| return def; |
| |
| MUnbox* unbox = MUnbox::New(alloc(), def, type, MUnbox::Fallible); |
| current->add(unbox); |
| |
| // Fixup type information for a common case where a property call |
| // is converted to the following bytecodes |
| // |
| // a.foo() |
| // ================= Compiles to ================ |
| // LOAD "a" |
| // DUP |
| // CALLPROP "foo" |
| // SWAP |
| // CALL 0 |
| // |
| // If we have better type information to unbox the first copy going into |
| // the CALLPROP operation, we can replace the duplicated copy on the |
| // stack as well. |
| if (*pc == JSOP_CALLPROP || *pc == JSOP_CALLELEM) { |
| uint32_t idx = current->stackDepth() - 1; |
| MOZ_ASSERT(current->getSlot(idx) == def); |
| current->setSlot(idx, unbox); |
| } |
| |
| return unbox; |
| } |
| |
| bool |
| IonBuilder::jsop_getprop(PropertyName* name) |
| { |
| bool emitted = false; |
| startTrackingOptimizations(); |
| |
| MDefinition* obj = current->pop(); |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| |
| trackTypeInfo(TrackedTypeSite::Receiver, obj->type(), obj->resultTypeSet()); |
| |
| if (!info().isAnalysis()) { |
| // The calls below can abort compilation, so we only try this if we're |
| // not analyzing. |
| // Try to optimize arguments.length. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_ArgumentsLength); |
| if (!getPropTryArgumentsLength(&emitted, obj) || emitted) |
| return emitted; |
| |
| // Try to optimize arguments.callee. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_ArgumentsCallee); |
| if (!getPropTryArgumentsCallee(&emitted, obj, name) || emitted) |
| return emitted; |
| } |
| |
| obj = maybeUnboxForPropertyAccess(obj); |
| if (obj->type() == MIRType_Object) |
| obj = convertUnboxedObjects(obj); |
| |
| BarrierKind barrier = PropertyReadNeedsTypeBarrier(analysisContext, constraints(), |
| obj, name, types); |
| |
| // Try to optimize to a specific constant. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_InferredConstant); |
| if (barrier == BarrierKind::NoBarrier) { |
| if (!getPropTryInferredConstant(&emitted, obj, name, types) || emitted) |
| return emitted; |
| } else { |
| trackOptimizationOutcome(TrackedOutcome::NeedsTypeBarrier); |
| } |
| |
| // Always use a call if we are performing analysis and |
| // not actually emitting code, to simplify later analysis. Also skip deeper |
| // analysis if there are no known types for this operation, as it will |
| // always invalidate when executing. |
| if (info().isAnalysis() || types->empty() || shouldAbortOnPreliminaryGroups(obj)) { |
| if (types->empty()) { |
| // Since no further optimizations will be tried, use the IC |
| // strategy, which would have been the last one to be tried, as a |
| // sentinel value for why everything failed. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_InlineCache); |
| trackOptimizationOutcome(TrackedOutcome::NoTypeInfo); |
| } |
| |
| MCallGetProperty* call = MCallGetProperty::New(alloc(), obj, name); |
| current->add(call); |
| |
| // During the definite properties analysis we can still try to bake in |
| // constants read off the prototype chain, to allow inlining later on. |
| // In this case we still need the getprop call so that the later |
| // analysis knows when the |this| value has been read from. |
| if (info().isAnalysis()) { |
| if (!getPropTryConstant(&emitted, obj, NameToId(name), types) || emitted) |
| return emitted; |
| } |
| |
| current->push(call); |
| return resumeAfter(call) && pushTypeBarrier(call, types, BarrierKind::TypeSet); |
| } |
| |
| // Try to optimize accesses on outer window proxies, for example window.foo. |
| // This needs to come before the various strategies getPropTryInnerize tries |
| // internally, since some of those strategies will "succeed" in silly ways |
| // even for an outer object. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_Innerize); |
| if (!getPropTryInnerize(&emitted, obj, name, types) || emitted) |
| return emitted; |
| |
| if (!forceInlineCaches()) { |
| // Try to hardcode known constants. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_Constant); |
| if (!getPropTryConstant(&emitted, obj, NameToId(name), types) || emitted) |
| return emitted; |
| |
| // Try to emit SIMD getter loads |
| trackOptimizationAttempt(TrackedStrategy::GetProp_SimdGetter); |
| if (!getPropTrySimdGetter(&emitted, obj, name) || emitted) |
| return emitted; |
| |
| // Try to emit loads from known binary data blocks |
| trackOptimizationAttempt(TrackedStrategy::GetProp_TypedObject); |
| if (!getPropTryTypedObject(&emitted, obj, name) || emitted) |
| return emitted; |
| |
| // Try to emit loads from definite slots. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_DefiniteSlot); |
| if (!getPropTryDefiniteSlot(&emitted, obj, name, barrier, types) || emitted) |
| return emitted; |
| |
| // Try to emit loads from unboxed objects. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_Unboxed); |
| if (!getPropTryUnboxed(&emitted, obj, name, barrier, types) || emitted) |
| return emitted; |
| |
| // Try to inline a common property getter, or make a call. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_CommonGetter); |
| if (!getPropTryCommonGetter(&emitted, obj, name, types) || emitted) |
| return emitted; |
| |
| // Try to emit a monomorphic/polymorphic access based on baseline caches. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_InlineAccess); |
| if (!getPropTryInlineAccess(&emitted, obj, name, barrier, types) || emitted) |
| return emitted; |
| |
| // Try to emit loads from a module namespace. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_ModuleNamespace); |
| if (!getPropTryModuleNamespace(&emitted, obj, name, barrier, types) || emitted) |
| return emitted; |
| } |
| |
| // Try to emit a polymorphic cache. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_InlineCache); |
| if (!getPropTryCache(&emitted, obj, name, barrier, types) || emitted) |
| return emitted; |
| |
| // Try to emit a shared stub. |
| trackOptimizationAttempt(TrackedStrategy::GetProp_SharedCache); |
| if (!getPropTrySharedStub(&emitted, obj) || emitted) |
| return emitted; |
| |
| // Emit a call. |
| MCallGetProperty* call = MCallGetProperty::New(alloc(), obj, name); |
| current->add(call); |
| current->push(call); |
| if (!resumeAfter(call)) |
| return false; |
| |
| if (*pc == JSOP_CALLPROP && IsNullOrUndefined(obj->type())) { |
| // Due to inlining, it's possible the observed TypeSet is non-empty, |
| // even though we know |obj| is null/undefined and the MCallGetProperty |
| // will throw. Don't push a TypeBarrier in this case, to avoid |
| // inlining the following (unreachable) JSOP_CALL. |
| return true; |
| } |
| |
| return pushTypeBarrier(call, types, BarrierKind::TypeSet); |
| } |
| |
| bool |
| IonBuilder::improveThisTypesForCall() |
| { |
| // After a CALLPROP (or CALLELEM) for obj.prop(), the this-value and callee |
| // for the call are on top of the stack: |
| // |
| // ... [this: obj], [callee: obj.prop] |
| // |
| // If obj is null or undefined, obj.prop would have thrown an exception so |
| // at this point we can remove null and undefined from obj's TypeSet, to |
| // improve type information for the call that will follow. |
| |
| MOZ_ASSERT(*pc == JSOP_CALLPROP || *pc == JSOP_CALLELEM); |
| |
| // Ensure |this| has types {object, null/undefined}. |
| MDefinition* thisDef = current->peek(-2); |
| if (thisDef->type() != MIRType_Value || |
| !thisDef->mightBeType(MIRType_Object) || |
| !thisDef->resultTypeSet() || |
| !thisDef->resultTypeSet()->objectOrSentinel()) |
| { |
| return true; |
| } |
| |
| // Remove null/undefined from the TypeSet. |
| TemporaryTypeSet* types = thisDef->resultTypeSet()->cloneObjectsOnly(alloc_->lifoAlloc()); |
| if (!types) |
| return false; |
| |
| MFilterTypeSet* filter = MFilterTypeSet::New(alloc(), thisDef, types); |
| current->add(filter); |
| current->rewriteAtDepth(-2, filter); |
| |
| // FilterTypeSetPolicy::adjustInputs will insert an infallible Unbox(Object) |
| // for the input. Don't hoist this unbox above the getprop or getelem |
| // operation. |
| filter->setDependency(current->peek(-1)->toInstruction()); |
| return true; |
| } |
| |
| bool |
| IonBuilder::checkIsDefinitelyOptimizedArguments(MDefinition* obj, bool* isOptimizedArgs) |
| { |
| if (obj->type() != MIRType_MagicOptimizedArguments) { |
| if (script()->argumentsHasVarBinding() && |
| obj->mightBeType(MIRType_MagicOptimizedArguments)) |
| { |
| return abort("Type is not definitely lazy arguments."); |
| } |
| |
| *isOptimizedArgs = false; |
| return true; |
| } |
| |
| *isOptimizedArgs = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::getPropTryInferredConstant(bool* emitted, MDefinition* obj, PropertyName* name, |
| TemporaryTypeSet* types) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // Need a result typeset to optimize. |
| TemporaryTypeSet* objTypes = obj->resultTypeSet(); |
| if (!objTypes) { |
| trackOptimizationOutcome(TrackedOutcome::NoTypeInfo); |
| return true; |
| } |
| |
| JSObject* singleton = objTypes->maybeSingleton(); |
| if (!singleton) { |
| trackOptimizationOutcome(TrackedOutcome::NotSingleton); |
| return true; |
| } |
| |
| TypeSet::ObjectKey* key = TypeSet::ObjectKey::get(singleton); |
| if (key->unknownProperties()) { |
| trackOptimizationOutcome(TrackedOutcome::UnknownProperties); |
| return true; |
| } |
| |
| HeapTypeSetKey property = key->property(NameToId(name)); |
| |
| Value constantValue = UndefinedValue(); |
| if (property.constant(constraints(), &constantValue)) { |
| spew("Optimized constant property"); |
| obj->setImplicitlyUsedUnchecked(); |
| if (!pushConstant(constantValue)) |
| return false; |
| types->addType(TypeSet::GetValueType(constantValue), alloc_->lifoAlloc()); |
| trackOptimizationSuccess(); |
| *emitted = true; |
| } |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::getPropTryArgumentsLength(bool* emitted, MDefinition* obj) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| bool isOptimizedArgs = false; |
| if (!checkIsDefinitelyOptimizedArguments(obj, &isOptimizedArgs)) |
| return false; |
| if (!isOptimizedArgs) |
| return true; |
| |
| if (JSOp(*pc) != JSOP_LENGTH) |
| return true; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| |
| obj->setImplicitlyUsedUnchecked(); |
| |
| // We don't know anything from the callee |
| if (inliningDepth_ == 0) { |
| MInstruction* ins = MArgumentsLength::New(alloc()); |
| current->add(ins); |
| current->push(ins); |
| return true; |
| } |
| |
| // We are inlining and know the number of arguments the callee pushed |
| return pushConstant(Int32Value(inlineCallInfo_->argv().length())); |
| } |
| |
| bool |
| IonBuilder::getPropTryArgumentsCallee(bool* emitted, MDefinition* obj, PropertyName* name) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| bool isOptimizedArgs = false; |
| if (!checkIsDefinitelyOptimizedArguments(obj, &isOptimizedArgs)) |
| return false; |
| if (!isOptimizedArgs) |
| return true; |
| |
| if (name != names().callee) |
| return true; |
| |
| MOZ_ASSERT(script()->hasMappedArgsObj()); |
| |
| obj->setImplicitlyUsedUnchecked(); |
| current->push(getCallee()); |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::getPropTryConstant(bool* emitted, MDefinition* obj, jsid id, TemporaryTypeSet* types) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| if (!types->mightBeMIRType(MIRType_Object)) { |
| // If we have not observed an object result here, don't look for a |
| // singleton constant. |
| trackOptimizationOutcome(TrackedOutcome::NotObject); |
| return true; |
| } |
| |
| JSObject* singleton = testSingletonPropertyTypes(obj, id); |
| if (!singleton) { |
| trackOptimizationOutcome(TrackedOutcome::NotSingleton); |
| return true; |
| } |
| |
| // Property access is a known constant -- safe to emit. |
| obj->setImplicitlyUsedUnchecked(); |
| |
| pushConstant(ObjectValue(*singleton)); |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| MIRType |
| IonBuilder::SimdTypeDescrToMIRType(SimdTypeDescr::Type type) |
| { |
| switch (type) { |
| case SimdTypeDescr::Int32x4: return MIRType_Int32x4; |
| case SimdTypeDescr::Float32x4: return MIRType_Float32x4; |
| case SimdTypeDescr::Int8x16: |
| case SimdTypeDescr::Int16x8: |
| case SimdTypeDescr::Float64x2: return MIRType_Undefined; |
| } |
| MOZ_CRASH("unimplemented MIR type for a SimdTypeDescr::Type"); |
| } |
| |
| bool |
| IonBuilder::getPropTrySimdGetter(bool* emitted, MDefinition* obj, PropertyName* name) |
| { |
| MOZ_ASSERT(!*emitted); |
| |
| if (!JitSupportsSimd()) { |
| trackOptimizationOutcome(TrackedOutcome::NoSimdJitSupport); |
| return true; |
| } |
| |
| TypedObjectPrediction objPrediction = typedObjectPrediction(obj); |
| if (objPrediction.isUseless()) { |
| trackOptimizationOutcome(TrackedOutcome::AccessNotTypedObject); |
| return true; |
| } |
| |
| if (objPrediction.kind() != type::Simd) { |
| trackOptimizationOutcome(TrackedOutcome::AccessNotSimdObject); |
| return true; |
| } |
| |
| MIRType type = SimdTypeDescrToMIRType(objPrediction.simdType()); |
| if (type == MIRType_Undefined) { |
| trackOptimizationOutcome(TrackedOutcome::SimdTypeNotOptimized); |
| return true; |
| } |
| |
| const JSAtomState& names = compartment->runtime()->names(); |
| |
| // Reading the signMask property. |
| if (name != names.signMask) { |
| // Unknown getprop access on a SIMD value |
| trackOptimizationOutcome(TrackedOutcome::UnknownSimdProperty); |
| return true; |
| } |
| |
| MSimdSignMask* ins = MSimdSignMask::New(alloc(), obj, type); |
| current->add(ins); |
| current->push(ins); |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::getPropTryTypedObject(bool* emitted, |
| MDefinition* obj, |
| PropertyName* name) |
| { |
| TypedObjectPrediction fieldPrediction; |
| size_t fieldOffset; |
| size_t fieldIndex; |
| if (!typedObjectHasField(obj, name, &fieldOffset, &fieldPrediction, &fieldIndex)) |
| return true; |
| |
| switch (fieldPrediction.kind()) { |
| case type::Simd: |
| // FIXME (bug 894104): load into a MIRType_float32x4 etc |
| return true; |
| |
| case type::Struct: |
| case type::Array: |
| return getPropTryComplexPropOfTypedObject(emitted, |
| obj, |
| fieldOffset, |
| fieldPrediction, |
| fieldIndex); |
| |
| case type::Reference: |
| return getPropTryReferencePropOfTypedObject(emitted, |
| obj, |
| fieldOffset, |
| fieldPrediction, |
| name); |
| |
| case type::Scalar: |
| return getPropTryScalarPropOfTypedObject(emitted, |
| obj, |
| fieldOffset, |
| fieldPrediction); |
| } |
| |
| MOZ_CRASH("Bad kind"); |
| } |
| |
| bool |
| IonBuilder::getPropTryScalarPropOfTypedObject(bool* emitted, MDefinition* typedObj, |
| int32_t fieldOffset, |
| TypedObjectPrediction fieldPrediction) |
| { |
| // Must always be loading the same scalar type |
| Scalar::Type fieldType = fieldPrediction.scalarType(); |
| |
| // Don't optimize if the typed object might be neutered. |
| TypeSet::ObjectKey* globalKey = TypeSet::ObjectKey::get(&script()->global()); |
| if (globalKey->hasFlags(constraints(), OBJECT_FLAG_TYPED_OBJECT_NEUTERED)) |
| return true; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| |
| LinearSum byteOffset(alloc()); |
| if (!byteOffset.add(fieldOffset)) |
| setForceAbort(); |
| |
| return pushScalarLoadFromTypedObject(typedObj, byteOffset, fieldType); |
| } |
| |
| bool |
| IonBuilder::getPropTryReferencePropOfTypedObject(bool* emitted, MDefinition* typedObj, |
| int32_t fieldOffset, |
| TypedObjectPrediction fieldPrediction, |
| PropertyName* name) |
| { |
| ReferenceTypeDescr::Type fieldType = fieldPrediction.referenceType(); |
| |
| TypeSet::ObjectKey* globalKey = TypeSet::ObjectKey::get(&script()->global()); |
| if (globalKey->hasFlags(constraints(), OBJECT_FLAG_TYPED_OBJECT_NEUTERED)) |
| return true; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| |
| LinearSum byteOffset(alloc()); |
| if (!byteOffset.add(fieldOffset)) |
| setForceAbort(); |
| |
| return pushReferenceLoadFromTypedObject(typedObj, byteOffset, fieldType, name); |
| } |
| |
| bool |
| IonBuilder::getPropTryComplexPropOfTypedObject(bool* emitted, |
| MDefinition* typedObj, |
| int32_t fieldOffset, |
| TypedObjectPrediction fieldPrediction, |
| size_t fieldIndex) |
| { |
| // Don't optimize if the typed object might be neutered. |
| TypeSet::ObjectKey* globalKey = TypeSet::ObjectKey::get(&script()->global()); |
| if (globalKey->hasFlags(constraints(), OBJECT_FLAG_TYPED_OBJECT_NEUTERED)) |
| return true; |
| |
| // OK, perform the optimization |
| |
| // Identify the type object for the field. |
| MDefinition* type = loadTypedObjectType(typedObj); |
| MDefinition* fieldTypeObj = typeObjectForFieldFromStructType(type, fieldIndex); |
| |
| LinearSum byteOffset(alloc()); |
| if (!byteOffset.add(fieldOffset)) |
| setForceAbort(); |
| |
| return pushDerivedTypedObject(emitted, typedObj, byteOffset, |
| fieldPrediction, fieldTypeObj); |
| } |
| |
| MDefinition* |
| IonBuilder::convertUnboxedObjects(MDefinition* obj) |
| { |
| // If obj might be in any particular unboxed group which should be |
| // converted to a native representation, perform that conversion. This does |
| // not guarantee the object will not have such a group afterwards, if the |
| // object's possible groups are not precisely known. |
| TemporaryTypeSet* types = obj->resultTypeSet(); |
| if (!types || types->unknownObject()) |
| return obj; |
| |
| BaselineInspector::ObjectGroupVector list(alloc()); |
| for (size_t i = 0; i < types->getObjectCount(); i++) { |
| TypeSet::ObjectKey* key = obj->resultTypeSet()->getObject(i); |
| if (!key || !key->isGroup()) |
| continue; |
| |
| if (UnboxedLayout* layout = key->group()->maybeUnboxedLayout()) { |
| AutoEnterOOMUnsafeRegion oomUnsafe; |
| if (layout->nativeGroup() && !list.append(key->group())) |
| oomUnsafe.crash("IonBuilder::convertUnboxedObjects"); |
| } |
| } |
| |
| return convertUnboxedObjects(obj, list); |
| } |
| |
| MDefinition* |
| IonBuilder::convertUnboxedObjects(MDefinition* obj, |
| const BaselineInspector::ObjectGroupVector& list) |
| { |
| for (size_t i = 0; i < list.length(); i++) { |
| ObjectGroup* group = list[i]; |
| if (TemporaryTypeSet* types = obj->resultTypeSet()) { |
| if (!types->hasType(TypeSet::ObjectType(group))) |
| continue; |
| } |
| obj = MConvertUnboxedObjectToNative::New(alloc(), obj, group); |
| current->add(obj->toInstruction()); |
| } |
| return obj; |
| } |
| |
| bool |
| IonBuilder::getPropTryDefiniteSlot(bool* emitted, MDefinition* obj, PropertyName* name, |
| BarrierKind barrier, TemporaryTypeSet* types) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| uint32_t nfixed; |
| uint32_t slot = getDefiniteSlot(obj->resultTypeSet(), name, &nfixed); |
| if (slot == UINT32_MAX) |
| return true; |
| |
| if (obj->type() != MIRType_Object) { |
| MGuardObject* guard = MGuardObject::New(alloc(), obj); |
| current->add(guard); |
| obj = guard; |
| } |
| |
| MInstruction* load; |
| if (slot < nfixed) { |
| load = MLoadFixedSlot::New(alloc(), obj, slot); |
| } else { |
| MInstruction* slots = MSlots::New(alloc(), obj); |
| current->add(slots); |
| |
| load = MLoadSlot::New(alloc(), slots, slot - nfixed); |
| } |
| |
| if (barrier == BarrierKind::NoBarrier) |
| load->setResultType(types->getKnownMIRType()); |
| |
| current->add(load); |
| current->push(load); |
| |
| if (!pushTypeBarrier(load, types, barrier)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::getPropTryModuleNamespace(bool* emitted, MDefinition* obj, PropertyName* name, |
| BarrierKind barrier, TemporaryTypeSet* types) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| TemporaryTypeSet* objTypes = obj->resultTypeSet(); |
| if (!objTypes) { |
| trackOptimizationOutcome(TrackedOutcome::NoTypeInfo); |
| return true; |
| } |
| |
| JSObject* singleton = objTypes->maybeSingleton(); |
| if (!singleton) { |
| trackOptimizationOutcome(TrackedOutcome::NotSingleton); |
| return true; |
| } |
| |
| if (!singleton->is<ModuleNamespaceObject>()) { |
| trackOptimizationOutcome(TrackedOutcome::NotModuleNamespace); |
| return true; |
| } |
| |
| ModuleNamespaceObject* ns = &singleton->as<ModuleNamespaceObject>(); |
| ModuleEnvironmentObject* env; |
| Shape* shape; |
| if (!ns->bindings().lookup(NameToId(name), &env, &shape)) { |
| trackOptimizationOutcome(TrackedOutcome::UnknownProperty); |
| return true; |
| } |
| |
| obj->setImplicitlyUsedUnchecked(); |
| MConstant* envConst = constant(ObjectValue(*env)); |
| uint32_t slot = shape->slot(); |
| uint32_t nfixed = env->numFixedSlots(); |
| if (!loadSlot(envConst, slot, nfixed, types->getKnownMIRType(), barrier, types)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| MInstruction* |
| IonBuilder::loadUnboxedProperty(MDefinition* obj, size_t offset, JSValueType unboxedType, |
| BarrierKind barrier, TemporaryTypeSet* types) |
| { |
| // loadUnboxedValue is designed to load any value as if it were contained in |
| // an array. Thus a property offset is converted to an index, when the |
| // object is reinterpreted as an array of properties of the same size. |
| size_t index = offset / UnboxedTypeSize(unboxedType); |
| MInstruction* indexConstant = MConstant::New(alloc(), Int32Value(index)); |
| current->add(indexConstant); |
| |
| return loadUnboxedValue(obj, UnboxedPlainObject::offsetOfData(), |
| indexConstant, unboxedType, barrier, types); |
| } |
| |
| MInstruction* |
| IonBuilder::loadUnboxedValue(MDefinition* elements, size_t elementsOffset, |
| MDefinition* index, JSValueType unboxedType, |
| BarrierKind barrier, TemporaryTypeSet* types) |
| { |
| MInstruction* load; |
| switch (unboxedType) { |
| case JSVAL_TYPE_BOOLEAN: |
| load = MLoadUnboxedScalar::New(alloc(), elements, index, Scalar::Uint8, |
| DoesNotRequireMemoryBarrier, elementsOffset); |
| load->setResultType(MIRType_Boolean); |
| break; |
| |
| case JSVAL_TYPE_INT32: |
| load = MLoadUnboxedScalar::New(alloc(), elements, index, Scalar::Int32, |
| DoesNotRequireMemoryBarrier, elementsOffset); |
| load->setResultType(MIRType_Int32); |
| break; |
| |
| case JSVAL_TYPE_DOUBLE: |
| load = MLoadUnboxedScalar::New(alloc(), elements, index, Scalar::Float64, |
| DoesNotRequireMemoryBarrier, elementsOffset, |
| /* canonicalizeDoubles = */ false); |
| load->setResultType(MIRType_Double); |
| break; |
| |
| case JSVAL_TYPE_STRING: |
| load = MLoadUnboxedString::New(alloc(), elements, index, elementsOffset); |
| break; |
| |
| case JSVAL_TYPE_OBJECT: { |
| MLoadUnboxedObjectOrNull::NullBehavior nullBehavior; |
| if (types->hasType(TypeSet::NullType()) || barrier != BarrierKind::NoBarrier) |
| nullBehavior = MLoadUnboxedObjectOrNull::HandleNull; |
| else |
| nullBehavior = MLoadUnboxedObjectOrNull::NullNotPossible; |
| load = MLoadUnboxedObjectOrNull::New(alloc(), elements, index, nullBehavior, |
| elementsOffset); |
| break; |
| } |
| |
| default: |
| MOZ_CRASH(); |
| } |
| |
| current->add(load); |
| return load; |
| } |
| |
| bool |
| IonBuilder::getPropTryUnboxed(bool* emitted, MDefinition* obj, PropertyName* name, |
| BarrierKind barrier, TemporaryTypeSet* types) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| JSValueType unboxedType; |
| uint32_t offset = getUnboxedOffset(obj->resultTypeSet(), name, &unboxedType); |
| if (offset == UINT32_MAX) |
| return true; |
| |
| if (obj->type() != MIRType_Object) { |
| MGuardObject* guard = MGuardObject::New(alloc(), obj); |
| current->add(guard); |
| obj = guard; |
| } |
| |
| MInstruction* load = loadUnboxedProperty(obj, offset, unboxedType, barrier, types); |
| current->push(load); |
| |
| if (!pushTypeBarrier(load, types, barrier)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| MDefinition* |
| IonBuilder::addShapeGuardsForGetterSetter(MDefinition* obj, JSObject* holder, Shape* holderShape, |
| const BaselineInspector::ReceiverVector& receivers, |
| const BaselineInspector::ObjectGroupVector& convertUnboxedGroups, |
| bool isOwnProperty) |
| { |
| MOZ_ASSERT(holder); |
| MOZ_ASSERT(holderShape); |
| |
| obj = convertUnboxedObjects(obj, convertUnboxedGroups); |
| |
| if (isOwnProperty) { |
| MOZ_ASSERT(receivers.empty()); |
| return addShapeGuard(obj, holderShape, Bailout_ShapeGuard); |
| } |
| |
| MDefinition* holderDef = constant(ObjectValue(*holder)); |
| addShapeGuard(holderDef, holderShape, Bailout_ShapeGuard); |
| |
| return addGuardReceiverPolymorphic(obj, receivers); |
| } |
| |
| bool |
| IonBuilder::getPropTryCommonGetter(bool* emitted, MDefinition* obj, PropertyName* name, |
| TemporaryTypeSet* types) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| Shape* lastProperty = nullptr; |
| JSFunction* commonGetter = nullptr; |
| Shape* globalShape = nullptr; |
| JSObject* foundProto = nullptr; |
| bool isOwnProperty = false; |
| BaselineInspector::ReceiverVector receivers(alloc()); |
| BaselineInspector::ObjectGroupVector convertUnboxedGroups(alloc()); |
| if (!inspector->commonGetPropFunction(pc, &foundProto, &lastProperty, &commonGetter, |
| &globalShape, &isOwnProperty, |
| receivers, convertUnboxedGroups)) |
| { |
| return true; |
| } |
| |
| TemporaryTypeSet* objTypes = obj->resultTypeSet(); |
| MDefinition* guard = nullptr; |
| MDefinition* globalGuard = nullptr; |
| bool canUseTIForGetter = |
| testCommonGetterSetter(objTypes, name, /* isGetter = */ true, |
| foundProto, lastProperty, commonGetter, &guard, |
| globalShape, &globalGuard); |
| if (!canUseTIForGetter) { |
| // If type information is bad, we can still optimize the getter if we |
| // shape guard. |
| obj = addShapeGuardsForGetterSetter(obj, foundProto, lastProperty, |
| receivers, convertUnboxedGroups, |
| isOwnProperty); |
| if (!obj) |
| return false; |
| } |
| |
| bool isDOM = objTypes && objTypes->isDOMClass(constraints()); |
| |
| if (isDOM && testShouldDOMCall(objTypes, commonGetter, JSJitInfo::Getter)) { |
| const JSJitInfo* jitinfo = commonGetter->jitInfo(); |
| MInstruction* get; |
| if (jitinfo->isAlwaysInSlot) { |
| // If our object is a singleton and we know the property is |
| // constant (which is true if and only if the get doesn't alias |
| // anything), we can just read the slot here and use that constant. |
| JSObject* singleton = objTypes->maybeSingleton(); |
| if (singleton && jitinfo->aliasSet() == JSJitInfo::AliasNone) { |
| size_t slot = jitinfo->slotIndex; |
| *emitted = true; |
| return pushConstant(GetReservedSlot(singleton, slot)); |
| } |
| |
| // We can't use MLoadFixedSlot here because it might not have the |
| // right aliasing behavior; we want to alias DOM setters as needed. |
| get = MGetDOMMember::New(alloc(), jitinfo, obj, guard, globalGuard); |
| } else { |
| get = MGetDOMProperty::New(alloc(), jitinfo, obj, guard, globalGuard); |
| } |
| if (!get) { |
| return false; |
| } |
| current->add(get); |
| current->push(get); |
| |
| if (get->isEffectful() && !resumeAfter(get)) |
| return false; |
| |
| if (!pushDOMTypeBarrier(get, types, commonGetter)) |
| return false; |
| |
| trackOptimizationOutcome(TrackedOutcome::DOM); |
| *emitted = true; |
| return true; |
| } |
| |
| // Don't call the getter with a primitive value. |
| if (obj->type() != MIRType_Object) { |
| MGuardObject* guardObj = MGuardObject::New(alloc(), obj); |
| current->add(guardObj); |
| obj = guardObj; |
| } |
| |
| // Spoof stack to expected state for call. |
| |
| // Make sure there's enough room |
| if (!current->ensureHasSlots(2)) |
| return false; |
| current->push(constant(ObjectValue(*commonGetter))); |
| |
| current->push(obj); |
| |
| CallInfo callInfo(alloc(), false); |
| if (!callInfo.init(current, 0)) |
| return false; |
| |
| if (commonGetter->isNative()) { |
| InliningStatus status = inlineNativeGetter(callInfo, commonGetter); |
| switch (status) { |
| case InliningStatus_Error: |
| return false; |
| case InliningStatus_WarmUpCountTooLow: |
| case InliningStatus_NotInlined: |
| break; |
| case InliningStatus_Inlined: |
| trackOptimizationOutcome(TrackedOutcome::Inlined); |
| *emitted = true; |
| return true; |
| } |
| } |
| |
| // Inline if we can, otherwise, forget it and just generate a call. |
| if (commonGetter->isInterpreted()) { |
| InliningDecision decision = makeInliningDecision(commonGetter, callInfo); |
| switch (decision) { |
| case InliningDecision_Error: |
| return false; |
| case InliningDecision_DontInline: |
| case InliningDecision_WarmUpCountTooLow: |
| break; |
| case InliningDecision_Inline: |
| if (!inlineScriptedCall(callInfo, commonGetter)) |
| return false; |
| *emitted = true; |
| return true; |
| } |
| } |
| |
| if (!makeCall(commonGetter, callInfo)) |
| return false; |
| |
| // If the getter could have been inlined, don't track success. The call to |
| // makeInliningDecision above would have tracked a specific reason why we |
| // couldn't inline. |
| if (!commonGetter->isInterpreted()) |
| trackOptimizationSuccess(); |
| |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::canInlinePropertyOpShapes(const BaselineInspector::ReceiverVector& receivers) |
| { |
| if (receivers.empty()) { |
| trackOptimizationOutcome(TrackedOutcome::NoShapeInfo); |
| return false; |
| } |
| |
| for (size_t i = 0; i < receivers.length(); i++) { |
| // We inline the property access as long as the shape is not in |
| // dictionary mode. We cannot be sure that the shape is still a |
| // lastProperty, and calling Shape::search() on dictionary mode |
| // shapes that aren't lastProperty is invalid. |
| if (receivers[i].shape && receivers[i].shape->inDictionary()) { |
| trackOptimizationOutcome(TrackedOutcome::InDictionaryMode); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| static Shape* |
| PropertyShapesHaveSameSlot(const BaselineInspector::ReceiverVector& receivers, jsid id) |
| { |
| Shape* firstShape = nullptr; |
| for (size_t i = 0; i < receivers.length(); i++) { |
| if (receivers[i].group) |
| return nullptr; |
| |
| Shape* shape = receivers[i].shape->searchLinear(id); |
| MOZ_ASSERT(shape); |
| |
| if (i == 0) { |
| firstShape = shape; |
| } else if (shape->slot() != firstShape->slot() || |
| shape->numFixedSlots() != firstShape->numFixedSlots()) |
| { |
| return nullptr; |
| } |
| } |
| |
| return firstShape; |
| } |
| |
| bool |
| IonBuilder::getPropTryInlineAccess(bool* emitted, MDefinition* obj, PropertyName* name, |
| BarrierKind barrier, TemporaryTypeSet* types) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| BaselineInspector::ReceiverVector receivers(alloc()); |
| BaselineInspector::ObjectGroupVector convertUnboxedGroups(alloc()); |
| if (!inspector->maybeInfoForPropertyOp(pc, receivers, convertUnboxedGroups)) |
| return false; |
| |
| if (!canInlinePropertyOpShapes(receivers)) |
| return true; |
| |
| obj = convertUnboxedObjects(obj, convertUnboxedGroups); |
| |
| MIRType rvalType = types->getKnownMIRType(); |
| if (barrier != BarrierKind::NoBarrier || IsNullOrUndefined(rvalType)) |
| rvalType = MIRType_Value; |
| |
| if (receivers.length() == 1) { |
| if (!receivers[0].group) { |
| // Monomorphic load from a native object. |
| spew("Inlining monomorphic native GETPROP"); |
| |
| obj = addShapeGuard(obj, receivers[0].shape, Bailout_ShapeGuard); |
| |
| Shape* shape = receivers[0].shape->searchLinear(NameToId(name)); |
| MOZ_ASSERT(shape); |
| |
| if (!loadSlot(obj, shape, rvalType, barrier, types)) |
| return false; |
| |
| trackOptimizationOutcome(TrackedOutcome::Monomorphic); |
| *emitted = true; |
| return true; |
| } |
| |
| if (receivers[0].shape) { |
| // Monomorphic load from an unboxed object expando. |
| spew("Inlining monomorphic unboxed expando GETPROP"); |
| |
| obj = addGroupGuard(obj, receivers[0].group, Bailout_ShapeGuard); |
| obj = addUnboxedExpandoGuard(obj, /* hasExpando = */ true, Bailout_ShapeGuard); |
| |
| MInstruction* expando = MLoadUnboxedExpando::New(alloc(), obj); |
| current->add(expando); |
| |
| expando = addShapeGuard(expando, receivers[0].shape, Bailout_ShapeGuard); |
| |
| Shape* shape = receivers[0].shape->searchLinear(NameToId(name)); |
| MOZ_ASSERT(shape); |
| |
| if (!loadSlot(expando, shape, rvalType, barrier, types)) |
| return false; |
| |
| trackOptimizationOutcome(TrackedOutcome::Monomorphic); |
| *emitted = true; |
| return true; |
| } |
| |
| // Monomorphic load from an unboxed object. |
| obj = addGroupGuard(obj, receivers[0].group, Bailout_ShapeGuard); |
| |
| const UnboxedLayout::Property* property = receivers[0].group->unboxedLayout().lookup(name); |
| MInstruction* load = loadUnboxedProperty(obj, property->offset, property->type, barrier, types); |
| current->push(load); |
| |
| if (!pushTypeBarrier(load, types, barrier)) |
| return false; |
| |
| trackOptimizationOutcome(TrackedOutcome::Monomorphic); |
| *emitted = true; |
| return true; |
| } |
| |
| MOZ_ASSERT(receivers.length() > 1); |
| spew("Inlining polymorphic GETPROP"); |
| |
| if (Shape* propShape = PropertyShapesHaveSameSlot(receivers, NameToId(name))) { |
| obj = addGuardReceiverPolymorphic(obj, receivers); |
| if (!obj) |
| return false; |
| |
| if (!loadSlot(obj, propShape, rvalType, barrier, types)) |
| return false; |
| |
| trackOptimizationOutcome(TrackedOutcome::Polymorphic); |
| *emitted = true; |
| return true; |
| } |
| |
| MGetPropertyPolymorphic* load = MGetPropertyPolymorphic::New(alloc(), obj, name); |
| current->add(load); |
| current->push(load); |
| |
| for (size_t i = 0; i < receivers.length(); i++) { |
| Shape* propShape = nullptr; |
| if (receivers[i].shape) { |
| propShape = receivers[i].shape->searchLinear(NameToId(name)); |
| MOZ_ASSERT(propShape); |
| } |
| if (!load->addReceiver(receivers[i], propShape)) |
| return false; |
| } |
| |
| if (failedShapeGuard_) |
| load->setNotMovable(); |
| |
| load->setResultType(rvalType); |
| if (!pushTypeBarrier(load, types, barrier)) |
| return false; |
| |
| trackOptimizationOutcome(TrackedOutcome::Polymorphic); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::getPropTryCache(bool* emitted, MDefinition* obj, PropertyName* name, |
| BarrierKind barrier, TemporaryTypeSet* types) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // The input value must either be an object, or we should have strong suspicions |
| // that it can be safely unboxed to an object. |
| if (obj->type() != MIRType_Object) { |
| TemporaryTypeSet* types = obj->resultTypeSet(); |
| if (!types || !types->objectOrSentinel()) { |
| trackOptimizationOutcome(TrackedOutcome::NoTypeInfo); |
| return true; |
| } |
| } |
| |
| // Since getters have no guaranteed return values, we must barrier in order to be |
| // able to attach stubs for them. |
| if (inspector->hasSeenAccessedGetter(pc)) |
| barrier = BarrierKind::TypeSet; |
| |
| // Caches can read values from prototypes, so update the barrier to |
| // reflect such possible values. |
| if (barrier != BarrierKind::TypeSet) { |
| BarrierKind protoBarrier = |
| PropertyReadOnPrototypeNeedsTypeBarrier(this, obj, name, types); |
| if (protoBarrier != BarrierKind::NoBarrier) { |
| MOZ_ASSERT(barrier <= protoBarrier); |
| barrier = protoBarrier; |
| } |
| } |
| |
| MConstant* id = constant(StringValue(name)); |
| MGetPropertyCache* load = MGetPropertyCache::New(alloc(), obj, id, |
| barrier == BarrierKind::TypeSet); |
| |
| // Try to mark the cache as idempotent. |
| if (obj->type() == MIRType_Object && !invalidatedIdempotentCache()) { |
| if (PropertyReadIsIdempotent(constraints(), obj, name)) |
| load->setIdempotent(); |
| } |
| |
| // When we are in the context of making a call from the value returned from |
| // a property, we query the typeObject for the given property name to fill |
| // the InlinePropertyTable of the GetPropertyCache. This information is |
| // then used in inlineCallsite and inlineCalls, if the "this" definition is |
| // matching the "object" definition of the GetPropertyCache (see |
| // CanInlineGetPropertyCache). |
| // |
| // If this GetPropertyCache is idempotent, then we can dispatch to the right |
| // function only by checking the typed object, instead of querying the value |
| // of the property. Thus this GetPropertyCache can be moved into the |
| // fallback path (see inlineObjectGroupFallback). Otherwise, we always have |
| // to do the GetPropertyCache, and we can dispatch based on the JSFunction |
| // value. |
| if (JSOp(*pc) == JSOP_CALLPROP && load->idempotent()) { |
| if (!annotateGetPropertyCache(obj, name, load, obj->resultTypeSet(), types)) |
| return false; |
| } |
| |
| current->add(load); |
| current->push(load); |
| |
| if (load->isEffectful() && !resumeAfter(load)) |
| return false; |
| |
| MIRType rvalType = types->getKnownMIRType(); |
| if (barrier != BarrierKind::NoBarrier || IsNullOrUndefined(rvalType)) |
| rvalType = MIRType_Value; |
| load->setResultType(rvalType); |
| |
| if (!pushTypeBarrier(load, types, barrier)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::getPropTrySharedStub(bool* emitted, MDefinition* obj) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| // Try to emit a shared stub cache. |
| |
| if (JitOptions.disableSharedStubs) |
| return true; |
| |
| MInstruction* stub = MUnarySharedStub::New(alloc(), obj); |
| current->add(stub); |
| current->push(stub); |
| |
| if (!resumeAfter(stub)) |
| return false; |
| |
| *emitted = true; |
| return true; |
| } |
| |
| MDefinition* |
| IonBuilder::tryInnerizeWindow(MDefinition* obj) |
| { |
| // Try to optimize accesses on outer window proxies (window.foo, for |
| // example) to go directly to the inner window, the global. |
| // |
| // Callers should be careful not to pass the inner object to getters or |
| // setters that require outerization. |
| |
| if (obj->type() != MIRType_Object) |
| return obj; |
| |
| TemporaryTypeSet* types = obj->resultTypeSet(); |
| if (!types) |
| return obj; |
| |
| JSObject* singleton = types->maybeSingleton(); |
| if (!singleton) |
| return obj; |
| |
| if (!IsWindowProxy(singleton)) |
| return obj; |
| |
| // This must be a WindowProxy for the current Window/global. Else it'd be |
| // a cross-compartment wrapper and IsWindowProxy returns false for those. |
| MOZ_ASSERT(ToWindowIfWindowProxy(singleton) == &script()->global()); |
| |
| // When we navigate, the WindowProxy is brain transplanted and we'll mark |
| // its ObjectGroup as having unknown properties. The type constraint we add |
| // here will invalidate JIT code when this happens. |
| TypeSet::ObjectKey* key = TypeSet::ObjectKey::get(singleton); |
| if (key->hasFlags(constraints(), OBJECT_FLAG_UNKNOWN_PROPERTIES)) |
| return obj; |
| |
| obj->setImplicitlyUsedUnchecked(); |
| return constant(ObjectValue(script()->global())); |
| } |
| |
| bool |
| IonBuilder::getPropTryInnerize(bool* emitted, MDefinition* obj, PropertyName* name, |
| TemporaryTypeSet* types) |
| { |
| // See the comment in tryInnerizeWindow for how this works. |
| |
| // Note that it's important that we do this _before_ we'd try to |
| // do the optimizations below on obj normally, since some of those |
| // optimizations have fallback paths that are slower than the path |
| // we'd produce here. |
| |
| MOZ_ASSERT(*emitted == false); |
| |
| MDefinition* inner = tryInnerizeWindow(obj); |
| if (inner == obj) |
| return true; |
| |
| if (!forceInlineCaches()) { |
| trackOptimizationAttempt(TrackedStrategy::GetProp_Constant); |
| if (!getPropTryConstant(emitted, inner, NameToId(name), types) || *emitted) |
| return *emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::GetProp_StaticName); |
| if (!getStaticName(&script()->global(), name, emitted) || *emitted) |
| return *emitted; |
| |
| trackOptimizationAttempt(TrackedStrategy::GetProp_CommonGetter); |
| if (!getPropTryCommonGetter(emitted, inner, name, types) || *emitted) |
| return *emitted; |
| } |
| |
| // Passing the inner object to GetProperty IC is safe, see the |
| // needsOuterizedThisObject check in IsCacheableGetPropCallNative. |
| BarrierKind barrier = PropertyReadNeedsTypeBarrier(analysisContext, constraints(), |
| inner, name, types); |
| trackOptimizationAttempt(TrackedStrategy::GetProp_InlineCache); |
| if (!getPropTryCache(emitted, inner, name, barrier, types) || *emitted) |
| return *emitted; |
| |
| MOZ_ASSERT(*emitted == false); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_setprop(PropertyName* name) |
| { |
| MDefinition* value = current->pop(); |
| MDefinition* obj = convertUnboxedObjects(current->pop()); |
| |
| bool emitted = false; |
| startTrackingOptimizations(); |
| trackTypeInfo(TrackedTypeSite::Receiver, obj->type(), obj->resultTypeSet()); |
| trackTypeInfo(TrackedTypeSite::Value, value->type(), value->resultTypeSet()); |
| |
| // Always use a call if we are doing the definite properties analysis and |
| // not actually emitting code, to simplify later analysis. |
| if (info().isAnalysis() || shouldAbortOnPreliminaryGroups(obj)) { |
| bool strict = IsStrictSetPC(pc); |
| MInstruction* ins = MCallSetProperty::New(alloc(), obj, value, name, strict); |
| current->add(ins); |
| current->push(value); |
| return resumeAfter(ins); |
| } |
| |
| if (!forceInlineCaches()) { |
| // Try to inline a common property setter, or make a call. |
| trackOptimizationAttempt(TrackedStrategy::SetProp_CommonSetter); |
| if (!setPropTryCommonSetter(&emitted, obj, name, value) || emitted) |
| return emitted; |
| |
| // Try to emit stores to known binary data blocks |
| trackOptimizationAttempt(TrackedStrategy::SetProp_TypedObject); |
| if (!setPropTryTypedObject(&emitted, obj, name, value) || emitted) |
| return emitted; |
| } |
| |
| TemporaryTypeSet* objTypes = obj->resultTypeSet(); |
| bool barrier = PropertyWriteNeedsTypeBarrier(alloc(), constraints(), current, &obj, name, &value, |
| /* canModify = */ true); |
| |
| if (!forceInlineCaches()) { |
| // Try to emit stores to unboxed objects. |
| trackOptimizationAttempt(TrackedStrategy::SetProp_Unboxed); |
| if (!setPropTryUnboxed(&emitted, obj, name, value, barrier, objTypes) || emitted) |
| return emitted; |
| } |
| |
| // Add post barrier if needed. The instructions above manage any post |
| // barriers they need directly. |
| if (NeedsPostBarrier(value)) |
| current->add(MPostWriteBarrier::New(alloc(), obj, value)); |
| |
| if (!forceInlineCaches()) { |
| // Try to emit store from definite slots. |
| trackOptimizationAttempt(TrackedStrategy::SetProp_DefiniteSlot); |
| if (!setPropTryDefiniteSlot(&emitted, obj, name, value, barrier, objTypes) || emitted) |
| return emitted; |
| |
| // Try to emit a monomorphic/polymorphic store based on baseline caches. |
| trackOptimizationAttempt(TrackedStrategy::SetProp_InlineAccess); |
| if (!setPropTryInlineAccess(&emitted, obj, name, value, barrier, objTypes) || emitted) |
| return emitted; |
| } |
| |
| // Emit a polymorphic cache. |
| trackOptimizationAttempt(TrackedStrategy::SetProp_InlineCache); |
| return setPropTryCache(&emitted, obj, name, value, barrier, objTypes); |
| } |
| |
| bool |
| IonBuilder::setPropTryCommonSetter(bool* emitted, MDefinition* obj, |
| PropertyName* name, MDefinition* value) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| Shape* lastProperty = nullptr; |
| JSFunction* commonSetter = nullptr; |
| JSObject* foundProto = nullptr; |
| bool isOwnProperty; |
| BaselineInspector::ReceiverVector receivers(alloc()); |
| BaselineInspector::ObjectGroupVector convertUnboxedGroups(alloc()); |
| if (!inspector->commonSetPropFunction(pc, &foundProto, &lastProperty, &commonSetter, |
| &isOwnProperty, |
| receivers, convertUnboxedGroups)) |
| { |
| trackOptimizationOutcome(TrackedOutcome::NoProtoFound); |
| return true; |
| } |
| |
| TemporaryTypeSet* objTypes = obj->resultTypeSet(); |
| MDefinition* guard = nullptr; |
| bool canUseTIForSetter = |
| testCommonGetterSetter(objTypes, name, /* isGetter = */ false, |
| foundProto, lastProperty, commonSetter, &guard); |
| if (!canUseTIForSetter) { |
| // If type information is bad, we can still optimize the setter if we |
| // shape guard. |
| obj = addShapeGuardsForGetterSetter(obj, foundProto, lastProperty, |
| receivers, convertUnboxedGroups, |
| isOwnProperty); |
| if (!obj) |
| return false; |
| } |
| |
| // Emit common setter. |
| |
| // Setters can be called even if the property write needs a type |
| // barrier, as calling the setter does not actually write any data |
| // properties. |
| |
| // Try emitting dom call. |
| if (!setPropTryCommonDOMSetter(emitted, obj, value, commonSetter, objTypes)) |
| return false; |
| |
| if (*emitted) { |
| trackOptimizationOutcome(TrackedOutcome::DOM); |
| return true; |
| } |
| |
| // Don't call the setter with a primitive value. |
| if (obj->type() != MIRType_Object) { |
| MGuardObject* guardObj = MGuardObject::New(alloc(), obj); |
| current->add(guardObj); |
| obj = guardObj; |
| } |
| |
| // Dummy up the stack, as in getprop. We are pushing an extra value, so |
| // ensure there is enough space. |
| if (!current->ensureHasSlots(3)) |
| return false; |
| |
| current->push(constant(ObjectValue(*commonSetter))); |
| current->push(obj); |
| current->push(value); |
| |
| // Call the setter. Note that we have to push the original value, not |
| // the setter's return value. |
| CallInfo callInfo(alloc(), false); |
| if (!callInfo.init(current, 1)) |
| return false; |
| |
| // Ensure that we know we are calling a setter in case we inline it. |
| callInfo.markAsSetter(); |
| |
| // Inline the setter if we can. |
| if (commonSetter->isInterpreted()) { |
| InliningDecision decision = makeInliningDecision(commonSetter, callInfo); |
| switch (decision) { |
| case InliningDecision_Error: |
| return false; |
| case InliningDecision_DontInline: |
| case InliningDecision_WarmUpCountTooLow: |
| break; |
| case InliningDecision_Inline: |
| if (!inlineScriptedCall(callInfo, commonSetter)) |
| return false; |
| *emitted = true; |
| return true; |
| } |
| } |
| |
| MCall* call = makeCallHelper(commonSetter, callInfo); |
| if (!call) |
| return false; |
| |
| current->push(value); |
| if (!resumeAfter(call)) |
| return false; |
| |
| // If the setter could have been inlined, don't track success. The call to |
| // makeInliningDecision above would have tracked a specific reason why we |
| // couldn't inline. |
| if (!commonSetter->isInterpreted()) |
| trackOptimizationSuccess(); |
| |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::setPropTryCommonDOMSetter(bool* emitted, MDefinition* obj, |
| MDefinition* value, JSFunction* setter, |
| TemporaryTypeSet* objTypes) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| if (!objTypes || !objTypes->isDOMClass(constraints())) |
| return true; |
| |
| if (!testShouldDOMCall(objTypes, setter, JSJitInfo::Setter)) |
| return true; |
| |
| // Emit SetDOMProperty. |
| MOZ_ASSERT(setter->jitInfo()->type() == JSJitInfo::Setter); |
| MSetDOMProperty* set = MSetDOMProperty::New(alloc(), setter->jitInfo()->setter, obj, value); |
| |
| current->add(set); |
| current->push(value); |
| |
| if (!resumeAfter(set)) |
| return false; |
| |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::setPropTryTypedObject(bool* emitted, MDefinition* obj, |
| PropertyName* name, MDefinition* value) |
| { |
| TypedObjectPrediction fieldPrediction; |
| size_t fieldOffset; |
| size_t fieldIndex; |
| if (!typedObjectHasField(obj, name, &fieldOffset, &fieldPrediction, &fieldIndex)) |
| return true; |
| |
| switch (fieldPrediction.kind()) { |
| case type::Simd: |
| // FIXME (bug 894104): store into a MIRType_float32x4 etc |
| return true; |
| |
| case type::Reference: |
| return setPropTryReferencePropOfTypedObject(emitted, obj, fieldOffset, |
| value, fieldPrediction, name); |
| |
| case type::Scalar: |
| return setPropTryScalarPropOfTypedObject(emitted, obj, fieldOffset, |
| value, fieldPrediction); |
| |
| case type::Struct: |
| case type::Array: |
| return true; |
| } |
| |
| MOZ_CRASH("Unknown kind"); |
| } |
| |
| bool |
| IonBuilder::setPropTryReferencePropOfTypedObject(bool* emitted, |
| MDefinition* obj, |
| int32_t fieldOffset, |
| MDefinition* value, |
| TypedObjectPrediction fieldPrediction, |
| PropertyName* name) |
| { |
| ReferenceTypeDescr::Type fieldType = fieldPrediction.referenceType(); |
| |
| TypeSet::ObjectKey* globalKey = TypeSet::ObjectKey::get(&script()->global()); |
| if (globalKey->hasFlags(constraints(), OBJECT_FLAG_TYPED_OBJECT_NEUTERED)) |
| return true; |
| |
| LinearSum byteOffset(alloc()); |
| if (!byteOffset.add(fieldOffset)) |
| setForceAbort(); |
| |
| if (!storeReferenceTypedObjectValue(obj, byteOffset, fieldType, value, name)) |
| return true; |
| |
| current->push(value); |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::setPropTryScalarPropOfTypedObject(bool* emitted, |
| MDefinition* obj, |
| int32_t fieldOffset, |
| MDefinition* value, |
| TypedObjectPrediction fieldPrediction) |
| { |
| // Must always be loading the same scalar type |
| Scalar::Type fieldType = fieldPrediction.scalarType(); |
| |
| // Don't optimize if the typed object might be neutered. |
| TypeSet::ObjectKey* globalKey = TypeSet::ObjectKey::get(&script()->global()); |
| if (globalKey->hasFlags(constraints(), OBJECT_FLAG_TYPED_OBJECT_NEUTERED)) |
| return true; |
| |
| LinearSum byteOffset(alloc()); |
| if (!byteOffset.add(fieldOffset)) |
| setForceAbort(); |
| |
| if (!storeScalarTypedObjectValue(obj, byteOffset, fieldType, value)) |
| return false; |
| |
| current->push(value); |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::setPropTryDefiniteSlot(bool* emitted, MDefinition* obj, |
| PropertyName* name, MDefinition* value, |
| bool barrier, TemporaryTypeSet* objTypes) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| if (barrier) { |
| trackOptimizationOutcome(TrackedOutcome::NeedsTypeBarrier); |
| return true; |
| } |
| |
| uint32_t nfixed; |
| uint32_t slot = getDefiniteSlot(obj->resultTypeSet(), name, &nfixed); |
| if (slot == UINT32_MAX) |
| return true; |
| |
| bool writeBarrier = false; |
| for (size_t i = 0; i < obj->resultTypeSet()->getObjectCount(); i++) { |
| TypeSet::ObjectKey* key = obj->resultTypeSet()->getObject(i); |
| if (!key) |
| continue; |
| |
| HeapTypeSetKey property = key->property(NameToId(name)); |
| if (property.nonWritable(constraints())) { |
| trackOptimizationOutcome(TrackedOutcome::NonWritableProperty); |
| return true; |
| } |
| writeBarrier |= property.needsBarrier(constraints()); |
| } |
| |
| MInstruction* store; |
| if (slot < nfixed) { |
| store = MStoreFixedSlot::New(alloc(), obj, slot, value); |
| if (writeBarrier) |
| store->toStoreFixedSlot()->setNeedsBarrier(); |
| } else { |
| MInstruction* slots = MSlots::New(alloc(), obj); |
| current->add(slots); |
| |
| store = MStoreSlot::New(alloc(), slots, slot - nfixed, value); |
| if (writeBarrier) |
| store->toStoreSlot()->setNeedsBarrier(); |
| } |
| |
| current->add(store); |
| current->push(value); |
| |
| if (!resumeAfter(store)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| MInstruction* |
| IonBuilder::storeUnboxedProperty(MDefinition* obj, size_t offset, JSValueType unboxedType, |
| MDefinition* value) |
| { |
| size_t scaledOffsetConstant = offset / UnboxedTypeSize(unboxedType); |
| MInstruction* scaledOffset = MConstant::New(alloc(), Int32Value(scaledOffsetConstant)); |
| current->add(scaledOffset); |
| |
| return storeUnboxedValue(obj, obj, UnboxedPlainObject::offsetOfData(), |
| scaledOffset, unboxedType, value); |
| } |
| |
| MInstruction* |
| IonBuilder::storeUnboxedValue(MDefinition* obj, MDefinition* elements, int32_t elementsOffset, |
| MDefinition* scaledOffset, JSValueType unboxedType, |
| MDefinition* value, bool preBarrier /* = true */) |
| { |
| MInstruction* store; |
| switch (unboxedType) { |
| case JSVAL_TYPE_BOOLEAN: |
| store = MStoreUnboxedScalar::New(alloc(), elements, scaledOffset, value, Scalar::Uint8, |
| MStoreUnboxedScalar::DontTruncateInput, |
| DoesNotRequireMemoryBarrier, elementsOffset); |
| break; |
| |
| case JSVAL_TYPE_INT32: |
| store = MStoreUnboxedScalar::New(alloc(), elements, scaledOffset, value, Scalar::Int32, |
| MStoreUnboxedScalar::DontTruncateInput, |
| DoesNotRequireMemoryBarrier, elementsOffset); |
| break; |
| |
| case JSVAL_TYPE_DOUBLE: |
| store = MStoreUnboxedScalar::New(alloc(), elements, scaledOffset, value, Scalar::Float64, |
| MStoreUnboxedScalar::DontTruncateInput, |
| DoesNotRequireMemoryBarrier, elementsOffset); |
| break; |
| |
| case JSVAL_TYPE_STRING: |
| store = MStoreUnboxedString::New(alloc(), elements, scaledOffset, value, |
| elementsOffset, preBarrier); |
| break; |
| |
| case JSVAL_TYPE_OBJECT: |
| store = MStoreUnboxedObjectOrNull::New(alloc(), elements, scaledOffset, value, obj, |
| elementsOffset, preBarrier); |
| break; |
| |
| default: |
| MOZ_CRASH(); |
| } |
| |
| current->add(store); |
| return store; |
| } |
| |
| bool |
| IonBuilder::setPropTryUnboxed(bool* emitted, MDefinition* obj, |
| PropertyName* name, MDefinition* value, |
| bool barrier, TemporaryTypeSet* objTypes) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| if (barrier) { |
| trackOptimizationOutcome(TrackedOutcome::NeedsTypeBarrier); |
| return true; |
| } |
| |
| JSValueType unboxedType; |
| uint32_t offset = getUnboxedOffset(obj->resultTypeSet(), name, &unboxedType); |
| if (offset == UINT32_MAX) |
| return true; |
| |
| if (obj->type() != MIRType_Object) { |
| MGuardObject* guard = MGuardObject::New(alloc(), obj); |
| current->add(guard); |
| obj = guard; |
| } |
| |
| MInstruction* store = storeUnboxedProperty(obj, offset, unboxedType, value); |
| |
| current->push(value); |
| |
| if (!resumeAfter(store)) |
| return false; |
| |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::setPropTryInlineAccess(bool* emitted, MDefinition* obj, |
| PropertyName* name, MDefinition* value, |
| bool barrier, TemporaryTypeSet* objTypes) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| if (barrier) { |
| trackOptimizationOutcome(TrackedOutcome::NeedsTypeBarrier); |
| return true; |
| } |
| |
| BaselineInspector::ReceiverVector receivers(alloc()); |
| BaselineInspector::ObjectGroupVector convertUnboxedGroups(alloc()); |
| if (!inspector->maybeInfoForPropertyOp(pc, receivers, convertUnboxedGroups)) |
| return false; |
| |
| if (!canInlinePropertyOpShapes(receivers)) |
| return true; |
| |
| obj = convertUnboxedObjects(obj, convertUnboxedGroups); |
| |
| if (receivers.length() == 1) { |
| if (!receivers[0].group) { |
| // Monomorphic store to a native object. |
| spew("Inlining monomorphic native SETPROP"); |
| |
| obj = addShapeGuard(obj, receivers[0].shape, Bailout_ShapeGuard); |
| |
| Shape* shape = receivers[0].shape->searchLinear(NameToId(name)); |
| MOZ_ASSERT(shape); |
| |
| bool needsBarrier = objTypes->propertyNeedsBarrier(constraints(), NameToId(name)); |
| if (!storeSlot(obj, shape, value, needsBarrier)) |
| return false; |
| |
| trackOptimizationOutcome(TrackedOutcome::Monomorphic); |
| *emitted = true; |
| return true; |
| } |
| |
| if (receivers[0].shape) { |
| // Monomorphic store to an unboxed object expando. |
| spew("Inlining monomorphic unboxed expando SETPROP"); |
| |
| obj = addGroupGuard(obj, receivers[0].group, Bailout_ShapeGuard); |
| obj = addUnboxedExpandoGuard(obj, /* hasExpando = */ true, Bailout_ShapeGuard); |
| |
| MInstruction* expando = MLoadUnboxedExpando::New(alloc(), obj); |
| current->add(expando); |
| |
| expando = addShapeGuard(expando, receivers[0].shape, Bailout_ShapeGuard); |
| |
| Shape* shape = receivers[0].shape->searchLinear(NameToId(name)); |
| MOZ_ASSERT(shape); |
| |
| bool needsBarrier = objTypes->propertyNeedsBarrier(constraints(), NameToId(name)); |
| if (!storeSlot(expando, shape, value, needsBarrier)) |
| return false; |
| |
| trackOptimizationOutcome(TrackedOutcome::Monomorphic); |
| *emitted = true; |
| return true; |
| } |
| |
| // Monomorphic store to an unboxed object. |
| spew("Inlining monomorphic unboxed SETPROP"); |
| |
| ObjectGroup* group = receivers[0].group; |
| obj = addGroupGuard(obj, group, Bailout_ShapeGuard); |
| |
| const UnboxedLayout::Property* property = group->unboxedLayout().lookup(name); |
| storeUnboxedProperty(obj, property->offset, property->type, value); |
| |
| current->push(value); |
| |
| trackOptimizationOutcome(TrackedOutcome::Monomorphic); |
| *emitted = true; |
| return true; |
| } |
| |
| MOZ_ASSERT(receivers.length() > 1); |
| spew("Inlining polymorphic SETPROP"); |
| |
| if (Shape* propShape = PropertyShapesHaveSameSlot(receivers, NameToId(name))) { |
| obj = addGuardReceiverPolymorphic(obj, receivers); |
| if (!obj) |
| return false; |
| |
| bool needsBarrier = objTypes->propertyNeedsBarrier(constraints(), NameToId(name)); |
| if (!storeSlot(obj, propShape, value, needsBarrier)) |
| return false; |
| |
| trackOptimizationOutcome(TrackedOutcome::Polymorphic); |
| *emitted = true; |
| return true; |
| } |
| |
| MSetPropertyPolymorphic* ins = MSetPropertyPolymorphic::New(alloc(), obj, value, name); |
| current->add(ins); |
| current->push(value); |
| |
| for (size_t i = 0; i < receivers.length(); i++) { |
| Shape* propShape = nullptr; |
| if (receivers[i].shape) { |
| propShape = receivers[i].shape->searchLinear(NameToId(name)); |
| MOZ_ASSERT(propShape); |
| } |
| if (!ins->addReceiver(receivers[i], propShape)) |
| return false; |
| } |
| |
| if (objTypes->propertyNeedsBarrier(constraints(), NameToId(name))) |
| ins->setNeedsBarrier(); |
| |
| if (!resumeAfter(ins)) |
| return false; |
| |
| trackOptimizationOutcome(TrackedOutcome::Polymorphic); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::setPropTryCache(bool* emitted, MDefinition* obj, |
| PropertyName* name, MDefinition* value, |
| bool barrier, TemporaryTypeSet* objTypes) |
| { |
| MOZ_ASSERT(*emitted == false); |
| |
| bool strict = IsStrictSetPC(pc); |
| |
| MConstant* id = constant(StringValue(name)); |
| MSetPropertyCache* ins = MSetPropertyCache::New(alloc(), obj, id, value, strict, barrier, |
| /* guardHoles = */ false); |
| current->add(ins); |
| current->push(value); |
| |
| if (!resumeAfter(ins)) |
| return false; |
| |
| trackOptimizationSuccess(); |
| *emitted = true; |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_delprop(PropertyName* name) |
| { |
| MDefinition* obj = current->pop(); |
| |
| bool strict = JSOp(*pc) == JSOP_STRICTDELPROP; |
| MInstruction* ins = MDeleteProperty::New(alloc(), obj, name, strict); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_delelem() |
| { |
| MDefinition* index = current->pop(); |
| MDefinition* obj = current->pop(); |
| |
| bool strict = JSOp(*pc) == JSOP_STRICTDELELEM; |
| MDeleteElement* ins = MDeleteElement::New(alloc(), obj, index, strict); |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_regexp(RegExpObject* reobj) |
| { |
| // JS semantics require regular expression literals to create different |
| // objects every time they execute. We only need to do this cloning if the |
| // script could actually observe the effect of such cloning, for instance |
| // by getting or setting properties on it. |
| // |
| // First, make sure the regex is one we can safely optimize. Lowering can |
| // then check if this regex object only flows into known natives and can |
| // avoid cloning in this case. |
| |
| bool mustClone = true; |
| TypeSet::ObjectKey* globalKey = TypeSet::ObjectKey::get(&script()->global()); |
| if (!globalKey->hasFlags(constraints(), OBJECT_FLAG_REGEXP_FLAGS_SET)) { |
| #ifdef DEBUG |
| // Only compare the statics if the one on script()->global() has been |
| // instantiated. |
| if (script()->global().hasRegExpStatics()) { |
| RegExpStatics* res = script()->global().getAlreadyCreatedRegExpStatics(); |
| MOZ_ASSERT(res); |
| uint32_t origFlags = reobj->getFlags(); |
| uint32_t staticsFlags = res->getFlags(); |
| MOZ_ASSERT((origFlags & staticsFlags) == staticsFlags); |
| } |
| #endif |
| |
| if (!reobj->global() && !reobj->sticky()) |
| mustClone = false; |
| } |
| |
| MRegExp* regexp = MRegExp::New(alloc(), constraints(), reobj, mustClone); |
| current->add(regexp); |
| current->push(regexp); |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_object(JSObject* obj) |
| { |
| if (options.cloneSingletons()) { |
| MCloneLiteral* clone = MCloneLiteral::New(alloc(), constant(ObjectValue(*obj))); |
| current->add(clone); |
| current->push(clone); |
| return resumeAfter(clone); |
| } |
| |
| compartment->setSingletonsAsValues(); |
| pushConstant(ObjectValue(*obj)); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_lambda(JSFunction* fun) |
| { |
| MOZ_ASSERT(analysis().usesScopeChain()); |
| MOZ_ASSERT(!fun->isArrow()); |
| |
| if (fun->isNative() && IsAsmJSModuleNative(fun->native())) |
| return abort("asm.js module function"); |
| |
| MConstant* cst = MConstant::NewConstraintlessObject(alloc(), fun); |
| current->add(cst); |
| MLambda* ins = MLambda::New(alloc(), constraints(), current->scopeChain(), cst); |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_lambda_arrow(JSFunction* fun) |
| { |
| MOZ_ASSERT(analysis().usesScopeChain()); |
| MOZ_ASSERT(fun->isArrow()); |
| MOZ_ASSERT(!fun->isNative()); |
| |
| MDefinition* newTargetDef = current->pop(); |
| MLambdaArrow* ins = MLambdaArrow::New(alloc(), constraints(), current->scopeChain(), |
| newTargetDef, fun); |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_setarg(uint32_t arg) |
| { |
| // To handle this case, we should spill the arguments to the space where |
| // actual arguments are stored. The tricky part is that if we add a MIR |
| // to wrap the spilling action, we don't want the spilling to be |
| // captured by the GETARG and by the resume point, only by |
| // MGetFrameArgument. |
| MOZ_ASSERT(analysis_.hasSetArg()); |
| MDefinition* val = current->peek(-1); |
| |
| // If an arguments object is in use, and it aliases formals, then all SETARGs |
| // must go through the arguments object. |
| if (info().argsObjAliasesFormals()) { |
| if (NeedsPostBarrier(val)) |
| current->add(MPostWriteBarrier::New(alloc(), current->argumentsObject(), val)); |
| current->add(MSetArgumentsObjectArg::New(alloc(), current->argumentsObject(), |
| GET_ARGNO(pc), val)); |
| return true; |
| } |
| |
| // :TODO: if hasArguments() is true, and the script has a JSOP_SETARG, then |
| // convert all arg accesses to go through the arguments object. (see Bug 957475) |
| if (info().hasArguments()) |
| return abort("NYI: arguments & setarg."); |
| |
| // Otherwise, if a magic arguments is in use, and it aliases formals, and there exist |
| // arguments[...] GETELEM expressions in the script, then SetFrameArgument must be used. |
| // If no arguments[...] GETELEM expressions are in the script, and an argsobj is not |
| // required, then it means that any aliased argument set can never be observed, and |
| // the frame does not actually need to be updated with the new arg value. |
| if (info().argumentsAliasesFormals()) { |
| // JSOP_SETARG with magic arguments within inline frames is not yet supported. |
| MOZ_ASSERT(script()->uninlineable() && !isInlineBuilder()); |
| |
| MSetFrameArgument* store = MSetFrameArgument::New(alloc(), arg, val); |
| modifiesFrameArguments_ = true; |
| current->add(store); |
| current->setArg(arg); |
| return true; |
| } |
| |
| // If this assignment is at the start of the function and is coercing |
| // the original value for the argument which was passed in, loosen |
| // the type information for that original argument if it is currently |
| // empty due to originally executing in the interpreter. |
| if (graph().numBlocks() == 1 && |
| (val->isBitOr() || val->isBitAnd() || val->isMul() /* for JSOP_POS */)) |
| { |
| for (size_t i = 0; i < val->numOperands(); i++) { |
| MDefinition* op = val->getOperand(i); |
| if (op->isParameter() && |
| op->toParameter()->index() == (int32_t)arg && |
| op->resultTypeSet() && |
| op->resultTypeSet()->empty()) |
| { |
| bool otherUses = false; |
| for (MUseDefIterator iter(op); iter; iter++) { |
| MDefinition* def = iter.def(); |
| if (def == val) |
| continue; |
| otherUses = true; |
| } |
| if (!otherUses) { |
| MOZ_ASSERT(op->resultTypeSet() == &argTypes[arg]); |
| argTypes[arg].addType(TypeSet::UnknownType(), alloc_->lifoAlloc()); |
| if (val->isMul()) { |
| val->setResultType(MIRType_Double); |
| val->toMul()->setSpecialization(MIRType_Double); |
| } else { |
| MOZ_ASSERT(val->type() == MIRType_Int32); |
| } |
| val->setResultTypeSet(nullptr); |
| } |
| } |
| } |
| } |
| |
| current->setArg(arg); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_defvar(uint32_t index) |
| { |
| MOZ_ASSERT(JSOp(*pc) == JSOP_DEFVAR); |
| |
| PropertyName* name = script()->getName(index); |
| |
| // Bake in attrs. |
| unsigned attrs = JSPROP_ENUMERATE | JSPROP_PERMANENT; |
| MOZ_ASSERT(!script()->isForEval()); |
| |
| // Pass the ScopeChain. |
| MOZ_ASSERT(analysis().usesScopeChain()); |
| |
| // Bake the name pointer into the MDefVar. |
| MDefVar* defvar = MDefVar::New(alloc(), name, attrs, current->scopeChain()); |
| current->add(defvar); |
| |
| return resumeAfter(defvar); |
| } |
| |
| bool |
| IonBuilder::jsop_deflexical(uint32_t index) |
| { |
| MOZ_ASSERT(!script()->hasNonSyntacticScope()); |
| MOZ_ASSERT(JSOp(*pc) == JSOP_DEFLET || JSOp(*pc) == JSOP_DEFCONST); |
| |
| PropertyName* name = script()->getName(index); |
| unsigned attrs = JSPROP_ENUMERATE | JSPROP_PERMANENT; |
| if (JSOp(*pc) == JSOP_DEFCONST) |
| attrs |= JSPROP_READONLY; |
| |
| MDefLexical* deflex = MDefLexical::New(alloc(), name, attrs); |
| current->add(deflex); |
| |
| return resumeAfter(deflex); |
| } |
| |
| bool |
| IonBuilder::jsop_deffun(uint32_t index) |
| { |
| JSFunction* fun = script()->getFunction(index); |
| if (fun->isNative() && IsAsmJSModuleNative(fun->native())) |
| return abort("asm.js module function"); |
| |
| MOZ_ASSERT(analysis().usesScopeChain()); |
| |
| MDefFun* deffun = MDefFun::New(alloc(), fun, current->scopeChain()); |
| current->add(deffun); |
| |
| return resumeAfter(deffun); |
| } |
| |
| bool |
| IonBuilder::jsop_throwsetconst() |
| { |
| current->peek(-1)->setImplicitlyUsedUnchecked(); |
| MInstruction* lexicalError = MThrowRuntimeLexicalError::New(alloc(), JSMSG_BAD_CONST_ASSIGN); |
| current->add(lexicalError); |
| return resumeAfter(lexicalError); |
| } |
| |
| bool |
| IonBuilder::jsop_checklexical() |
| { |
| uint32_t slot = info().localSlot(GET_LOCALNO(pc)); |
| MDefinition* lexical = addLexicalCheck(current->getSlot(slot)); |
| if (!lexical) |
| return false; |
| current->setSlot(slot, lexical); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_checkaliasedlet(ScopeCoordinate sc) |
| { |
| MDefinition* let = addLexicalCheck(getAliasedVar(sc)); |
| if (!let) |
| return false; |
| |
| jsbytecode* nextPc = pc + JSOP_CHECKALIASEDLEXICAL_LENGTH; |
| MOZ_ASSERT(JSOp(*nextPc) == JSOP_GETALIASEDVAR || |
| JSOp(*nextPc) == JSOP_SETALIASEDVAR || |
| JSOp(*nextPc) == JSOP_THROWSETALIASEDCONST); |
| MOZ_ASSERT(sc == ScopeCoordinate(nextPc)); |
| |
| // If we are checking for a load, push the checked let so that the load |
| // can use it. |
| if (JSOp(*nextPc) == JSOP_GETALIASEDVAR) |
| setLexicalCheck(let); |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_functionthis() |
| { |
| MOZ_ASSERT(info().funMaybeLazy()); |
| MOZ_ASSERT(!info().funMaybeLazy()->isArrow()); |
| |
| if (script()->strict() || info().funMaybeLazy()->isSelfHostedBuiltin()) { |
| // No need to wrap primitive |this| in strict mode or self-hosted code. |
| current->pushSlot(info().thisSlot()); |
| return true; |
| } |
| |
| if (thisTypes && (thisTypes->getKnownMIRType() == MIRType_Object || |
| (thisTypes->empty() && baselineFrame_ && baselineFrame_->thisType.isSomeObject()))) |
| { |
| // This is safe, because if the entry type of |this| is an object, it |
| // will necessarily be an object throughout the entire function. OSR |
| // can introduce a phi, but this phi will be specialized. |
| current->pushSlot(info().thisSlot()); |
| return true; |
| } |
| |
| // If we are doing an analysis, we might not yet know the type of |this|. |
| // Instead of bailing out just push the |this| slot, as this code won't |
| // actually execute and it does not matter whether |this| is primitive. |
| if (info().isAnalysis()) { |
| current->pushSlot(info().thisSlot()); |
| return true; |
| } |
| |
| // Hard case: |this| may be a primitive we have to wrap. |
| MDefinition* def = current->getSlot(info().thisSlot()); |
| |
| if (def->type() == MIRType_Object) { |
| current->push(def); |
| return true; |
| } |
| |
| MComputeThis* thisObj = MComputeThis::New(alloc(), def); |
| current->add(thisObj); |
| current->push(thisObj); |
| |
| return resumeAfter(thisObj); |
| } |
| |
| bool |
| IonBuilder::jsop_globalthis() |
| { |
| if (script()->hasNonSyntacticScope()) { |
| // Ion does not compile global scripts with a non-syntactic scope, but |
| // we can end up here when we're compiling an arrow function. |
| return abort("JSOP_GLOBALTHIS in script with non-syntactic scope"); |
| } |
| |
| ClonedBlockObject* globalLexical = &script()->global().lexicalScope(); |
| pushConstant(globalLexical->thisValue()); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_typeof() |
| { |
| MDefinition* input = current->pop(); |
| MTypeOf* ins = MTypeOf::New(alloc(), input, input->type()); |
| |
| ins->cacheInputMaybeCallableOrEmulatesUndefined(constraints()); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_toid() |
| { |
| // No-op if the index is an integer. |
| if (current->peek(-1)->type() == MIRType_Int32) |
| return true; |
| |
| MDefinition* index = current->pop(); |
| MToId* ins = MToId::New(alloc(), index); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_iter(uint8_t flags) |
| { |
| if (flags != JSITER_ENUMERATE) |
| nonStringIteration_ = true; |
| |
| MDefinition* obj = current->pop(); |
| MInstruction* ins = MIteratorStart::New(alloc(), obj, flags); |
| |
| if (!iterators_.append(ins)) |
| return false; |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_itermore() |
| { |
| MDefinition* iter = current->peek(-1); |
| MInstruction* ins = MIteratorMore::New(alloc(), iter); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_isnoiter() |
| { |
| MDefinition* def = current->peek(-1); |
| MOZ_ASSERT(def->isIteratorMore()); |
| |
| MInstruction* ins = MIsNoIter::New(alloc(), def); |
| current->add(ins); |
| current->push(ins); |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_iterend() |
| { |
| MDefinition* iter = current->pop(); |
| MInstruction* ins = MIteratorEnd::New(alloc(), iter); |
| |
| current->add(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| MDefinition* |
| IonBuilder::walkScopeChain(unsigned hops) |
| { |
| MDefinition* scope = current->getSlot(info().scopeChainSlot()); |
| |
| for (unsigned i = 0; i < hops; i++) { |
| MInstruction* ins = MEnclosingScope::New(alloc(), scope); |
| current->add(ins); |
| scope = ins; |
| } |
| |
| return scope; |
| } |
| |
| bool |
| IonBuilder::hasStaticScopeObject(ScopeCoordinate sc, JSObject** pcall) |
| { |
| JSScript* outerScript = ScopeCoordinateFunctionScript(script(), pc); |
| if (!outerScript || !outerScript->treatAsRunOnce()) |
| return false; |
| |
| TypeSet::ObjectKey* funKey = |
| TypeSet::ObjectKey::get(outerScript->functionNonDelazifying()); |
| if (funKey->hasFlags(constraints(), OBJECT_FLAG_RUNONCE_INVALIDATED)) |
| return false; |
| |
| // The script this aliased var operation is accessing will run only once, |
| // so there will be only one call object and the aliased var access can be |
| // compiled in the same manner as a global access. We still need to find |
| // the call object though. |
| |
| // Look for the call object on the current script's function's scope chain. |
| // If the current script is inner to the outer script and the function has |
| // singleton type then it should show up here. |
| |
| MDefinition* scope = current->getSlot(info().scopeChainSlot()); |
| scope->setImplicitlyUsedUnchecked(); |
| |
| JSObject* environment = script()->functionNonDelazifying()->environment(); |
| while (environment && !environment->is<GlobalObject>()) { |
| if (environment->is<CallObject>() && |
| !environment->as<CallObject>().isForEval() && |
| environment->as<CallObject>().callee().nonLazyScript() == outerScript) |
| { |
| MOZ_ASSERT(environment->isSingleton()); |
| *pcall = environment; |
| return true; |
| } |
| environment = environment->enclosingScope(); |
| } |
| |
| // Look for the call object on the current frame, if we are compiling the |
| // outer script itself. Don't do this if we are at entry to the outer |
| // script, as the call object we see will not be the real one --- after |
| // entering the Ion code a different call object will be created. |
| |
| if (script() == outerScript && baselineFrame_ && info().osrPc()) { |
| JSObject* singletonScope = baselineFrame_->singletonScopeChain; |
| if (singletonScope && |
| singletonScope->is<CallObject>() && |
| singletonScope->as<CallObject>().callee().nonLazyScript() == outerScript) |
| { |
| MOZ_ASSERT(singletonScope->isSingleton()); |
| *pcall = singletonScope; |
| return true; |
| } |
| } |
| |
| return true; |
| } |
| |
| MDefinition* |
| IonBuilder::getAliasedVar(ScopeCoordinate sc) |
| { |
| MDefinition* obj = walkScopeChain(sc.hops()); |
| |
| Shape* shape = ScopeCoordinateToStaticScopeShape(script(), pc); |
| |
| MInstruction* load; |
| if (shape->numFixedSlots() <= sc.slot()) { |
| MInstruction* slots = MSlots::New(alloc(), obj); |
| current->add(slots); |
| |
| load = MLoadSlot::New(alloc(), slots, sc.slot() - shape->numFixedSlots()); |
| } else { |
| load = MLoadFixedSlot::New(alloc(), obj, sc.slot()); |
| } |
| |
| current->add(load); |
| return load; |
| } |
| |
| bool |
| IonBuilder::jsop_getaliasedvar(ScopeCoordinate sc) |
| { |
| JSObject* call = nullptr; |
| if (hasStaticScopeObject(sc, &call) && call) { |
| PropertyName* name = ScopeCoordinateName(scopeCoordinateNameCache, script(), pc); |
| bool emitted = false; |
| if (!getStaticName(call, name, &emitted, takeLexicalCheck()) || emitted) |
| return emitted; |
| } |
| |
| // See jsop_checkaliasedlet. |
| MDefinition* load = takeLexicalCheck(); |
| if (!load) |
| load = getAliasedVar(sc); |
| current->push(load); |
| |
| TemporaryTypeSet* types = bytecodeTypes(pc); |
| return pushTypeBarrier(load, types, BarrierKind::TypeSet); |
| } |
| |
| bool |
| IonBuilder::jsop_setaliasedvar(ScopeCoordinate sc) |
| { |
| JSObject* call = nullptr; |
| if (hasStaticScopeObject(sc, &call)) { |
| uint32_t depth = current->stackDepth() + 1; |
| if (depth > current->nslots()) { |
| if (!current->increaseSlots(depth - current->nslots())) |
| return false; |
| } |
| MDefinition* value = current->pop(); |
| PropertyName* name = ScopeCoordinateName(scopeCoordinateNameCache, script(), pc); |
| |
| if (call) { |
| // Push the object on the stack to match the bound object expected in |
| // the global and property set cases. |
| pushConstant(ObjectValue(*call)); |
| current->push(value); |
| return setStaticName(call, name); |
| } |
| |
| // The call object has type information we need to respect but we |
| // couldn't find it. Just do a normal property assign. |
| MDefinition* obj = walkScopeChain(sc.hops()); |
| current->push(obj); |
| current->push(value); |
| return jsop_setprop(name); |
| } |
| |
| MDefinition* rval = current->peek(-1); |
| MDefinition* obj = walkScopeChain(sc.hops()); |
| |
| Shape* shape = ScopeCoordinateToStaticScopeShape(script(), pc); |
| |
| if (NeedsPostBarrier(rval)) |
| current->add(MPostWriteBarrier::New(alloc(), obj, rval)); |
| |
| MInstruction* store; |
| if (shape->numFixedSlots() <= sc.slot()) { |
| MInstruction* slots = MSlots::New(alloc(), obj); |
| current->add(slots); |
| |
| store = MStoreSlot::NewBarriered(alloc(), slots, sc.slot() - shape->numFixedSlots(), rval); |
| } else { |
| store = MStoreFixedSlot::NewBarriered(alloc(), obj, sc.slot(), rval); |
| } |
| |
| current->add(store); |
| return resumeAfter(store); |
| } |
| |
| bool |
| IonBuilder::jsop_in() |
| { |
| MDefinition* obj = convertUnboxedObjects(current->pop()); |
| MDefinition* id = current->pop(); |
| |
| do { |
| if (shouldAbortOnPreliminaryGroups(obj)) |
| break; |
| |
| JSValueType unboxedType = UnboxedArrayElementType(constraints(), obj, id); |
| if (unboxedType == JSVAL_TYPE_MAGIC) { |
| if (!ElementAccessIsDenseNative(constraints(), obj, id)) |
| break; |
| } |
| |
| if (ElementAccessHasExtraIndexedProperty(this, obj)) |
| break; |
| |
| return jsop_in_dense(obj, id, unboxedType); |
| } while (false); |
| |
| MIn* ins = MIn::New(alloc(), id, obj); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_in_dense(MDefinition* obj, MDefinition* id, JSValueType unboxedType) |
| { |
| bool needsHoleCheck = !ElementAccessIsPacked(constraints(), obj); |
| |
| // Ensure id is an integer. |
| MInstruction* idInt32 = MToInt32::New(alloc(), id); |
| current->add(idInt32); |
| id = idInt32; |
| |
| // Get the elements vector. |
| MElements* elements = MElements::New(alloc(), obj, unboxedType != JSVAL_TYPE_MAGIC); |
| current->add(elements); |
| |
| MInstruction* initLength = initializedLength(obj, elements, unboxedType); |
| |
| // If there are no holes, speculate the InArray check will not fail. |
| if (!needsHoleCheck && !failedBoundsCheck_) { |
| addBoundsCheck(idInt32, initLength); |
| return pushConstant(BooleanValue(true)); |
| } |
| |
| // Check if id < initLength and elem[id] not a hole. |
| MInArray* ins = MInArray::New(alloc(), elements, id, initLength, obj, needsHoleCheck, |
| unboxedType); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::hasOnProtoChain(TypeSet::ObjectKey* key, JSObject* protoObject, bool* hasOnProto) |
| { |
| MOZ_ASSERT(protoObject); |
| |
| while (true) { |
| if (!key->hasStableClassAndProto(constraints()) || !key->clasp()->isNative()) |
| return false; |
| |
| JSObject* proto = checkNurseryObject(key->proto().toObjectOrNull()); |
| if (!proto) { |
| *hasOnProto = false; |
| return true; |
| } |
| |
| if (proto == protoObject) { |
| *hasOnProto = true; |
| return true; |
| } |
| |
| key = TypeSet::ObjectKey::get(proto); |
| } |
| |
| MOZ_CRASH("Unreachable"); |
| } |
| |
| bool |
| IonBuilder::tryFoldInstanceOf(MDefinition* lhs, JSObject* protoObject) |
| { |
| // Try to fold the js::IsDelegate part of the instanceof operation. |
| |
| if (!lhs->mightBeType(MIRType_Object)) { |
| // If the lhs is a primitive, the result is false. |
| lhs->setImplicitlyUsedUnchecked(); |
| pushConstant(BooleanValue(false)); |
| return true; |
| } |
| |
| TemporaryTypeSet* lhsTypes = lhs->resultTypeSet(); |
| if (!lhsTypes || lhsTypes->unknownObject()) |
| return false; |
| |
| // We can fold if either all objects have protoObject on their proto chain |
| // or none have. |
| bool isFirst = true; |
| bool knownIsInstance = false; |
| |
| for (unsigned i = 0; i < lhsTypes->getObjectCount(); i++) { |
| TypeSet::ObjectKey* key = lhsTypes->getObject(i); |
| if (!key) |
| continue; |
| |
| bool isInstance; |
| if (!hasOnProtoChain(key, protoObject, &isInstance)) |
| return false; |
| |
| if (isFirst) { |
| knownIsInstance = isInstance; |
| isFirst = false; |
| } else if (knownIsInstance != isInstance) { |
| // Some of the objects have protoObject on their proto chain and |
| // others don't, so we can't optimize this. |
| return false; |
| } |
| } |
| |
| if (knownIsInstance && lhsTypes->getKnownMIRType() != MIRType_Object) { |
| // The result is true for all objects, but the lhs might be a primitive. |
| // We can't fold this completely but we can use a much faster IsObject |
| // test. |
| MIsObject* isObject = MIsObject::New(alloc(), lhs); |
| current->add(isObject); |
| current->push(isObject); |
| return true; |
| } |
| |
| lhs->setImplicitlyUsedUnchecked(); |
| pushConstant(BooleanValue(knownIsInstance)); |
| return true; |
| } |
| |
| bool |
| IonBuilder::jsop_instanceof() |
| { |
| MDefinition* rhs = current->pop(); |
| MDefinition* obj = current->pop(); |
| |
| // If this is an 'x instanceof function' operation and we can determine the |
| // exact function and prototype object being tested for, use a typed path. |
| do { |
| TemporaryTypeSet* rhsTypes = rhs->resultTypeSet(); |
| JSObject* rhsObject = rhsTypes ? rhsTypes->maybeSingleton() : nullptr; |
| if (!rhsObject || !rhsObject->is<JSFunction>() || rhsObject->isBoundFunction()) |
| break; |
| |
| TypeSet::ObjectKey* rhsKey = TypeSet::ObjectKey::get(rhsObject); |
| if (rhsKey->unknownProperties()) |
| break; |
| |
| HeapTypeSetKey protoProperty = |
| rhsKey->property(NameToId(names().prototype)); |
| JSObject* protoObject = protoProperty.singleton(constraints()); |
| if (!protoObject) |
| break; |
| |
| rhs->setImplicitlyUsedUnchecked(); |
| |
| if (tryFoldInstanceOf(obj, protoObject)) |
| return true; |
| |
| MInstanceOf* ins = MInstanceOf::New(alloc(), obj, protoObject); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } while (false); |
| |
| // Try to inline a fast path based on Baseline ICs. |
| do { |
| Shape* shape; |
| uint32_t slot; |
| JSObject* protoObject; |
| if (!inspector->instanceOfData(pc, &shape, &slot, &protoObject)) |
| break; |
| |
| // Shape guard. |
| rhs = addShapeGuard(rhs, shape, Bailout_ShapeGuard); |
| |
| // Guard .prototype == protoObject. |
| MOZ_ASSERT(shape->numFixedSlots() == 0, "Must be a dynamic slot"); |
| MSlots* slots = MSlots::New(alloc(), rhs); |
| current->add(slots); |
| MLoadSlot* prototype = MLoadSlot::New(alloc(), slots, slot); |
| current->add(prototype); |
| MConstant* protoConst = MConstant::NewConstraintlessObject(alloc(), protoObject); |
| current->add(protoConst); |
| MGuardObjectIdentity* guard = MGuardObjectIdentity::New(alloc(), prototype, protoConst, |
| /* bailOnEquality = */ false); |
| current->add(guard); |
| |
| if (tryFoldInstanceOf(obj, protoObject)) |
| return true; |
| |
| MInstanceOf* ins = MInstanceOf::New(alloc(), obj, protoObject); |
| current->add(ins); |
| current->push(ins); |
| return resumeAfter(ins); |
| } while (false); |
| |
| MCallInstanceOf* ins = MCallInstanceOf::New(alloc(), obj, rhs); |
| |
| current->add(ins); |
| current->push(ins); |
| |
| return resumeAfter(ins); |
| } |
| |
| bool |
| IonBuilder::jsop_debugger() |
| { |
| MDebugger* debugger = MDebugger::New(alloc()); |
| current->add(debugger); |
| |
| // The |debugger;| statement will always bail out to baseline if |
| // cx->compartment()->isDebuggee(). Resume in-place and have baseline |
| // handle the details. |
| return resumeAt(debugger, pc); |
| } |
| |
| MInstruction* |
| IonBuilder::addConvertElementsToDoubles(MDefinition* elements) |
| { |
| MInstruction* convert = MConvertElementsToDoubles::New(alloc(), elements); |
| current->add(convert); |
| return convert; |
| } |
| |
| MDefinition* |
| IonBuilder::addMaybeCopyElementsForWrite(MDefinition* object, bool checkNative) |
| { |
| if (!ElementAccessMightBeCopyOnWrite(constraints(), object)) |
| return object; |
| MInstruction* copy = MMaybeCopyElementsForWrite::New(alloc(), object, checkNative); |
| current->add(copy); |
| return copy; |
| } |
| |
| MInstruction* |
| IonBuilder::addBoundsCheck(MDefinition* index, MDefinition* length) |
| { |
| MInstruction* check = MBoundsCheck::New(alloc(), index, length); |
| current->add(check); |
| |
| // If a bounds check failed in the past, don't optimize bounds checks. |
| if (failedBoundsCheck_) |
| check->setNotMovable(); |
| |
| return check; |
| } |
| |
| MInstruction* |
| IonBuilder::addShapeGuard(MDefinition* obj, Shape* const shape, BailoutKind bailoutKind) |
| { |
| MGuardShape* guard = MGuardShape::New(alloc(), obj, shape, bailoutKind); |
| current->add(guard); |
| |
| // If a shape guard failed in the past, don't optimize shape guard. |
| if (failedShapeGuard_) |
| guard->setNotMovable(); |
| |
| return guard; |
| } |
| |
| MInstruction* |
| IonBuilder::addGroupGuard(MDefinition* obj, ObjectGroup* group, BailoutKind bailoutKind) |
| { |
| MGuardObjectGroup* guard = MGuardObjectGroup::New(alloc(), obj, group, |
| /* bailOnEquality = */ false, bailoutKind); |
| current->add(guard); |
| |
| // If a shape guard failed in the past, don't optimize group guards. |
| if (failedShapeGuard_) |
| guard->setNotMovable(); |
| |
| LifoAlloc* lifoAlloc = alloc().lifoAlloc(); |
| guard->setResultTypeSet(lifoAlloc->new_<TemporaryTypeSet>(lifoAlloc, |
| TypeSet::ObjectType(group))); |
| |
| return guard; |
| } |
| |
| MInstruction* |
| IonBuilder::addUnboxedExpandoGuard(MDefinition* obj, bool hasExpando, BailoutKind bailoutKind) |
| { |
| MGuardUnboxedExpando* guard = MGuardUnboxedExpando::New(alloc(), obj, hasExpando, bailoutKind); |
| current->add(guard); |
| |
| // If a shape guard failed in the past, don't optimize group guards. |
| if (failedShapeGuard_) |
| guard->setNotMovable(); |
| |
| return guard; |
| } |
| |
| MInstruction* |
| IonBuilder::addGuardReceiverPolymorphic(MDefinition* obj, |
| const BaselineInspector::ReceiverVector& receivers) |
| { |
| if (receivers.length() == 1) { |
| if (!receivers[0].group) { |
| // Monomorphic guard on a native object. |
| return addShapeGuard(obj, receivers[0].shape, Bailout_ShapeGuard); |
| } |
| |
| if (!receivers[0].shape) { |
| // Guard on an unboxed object that does not have an expando. |
| obj = addGroupGuard(obj, receivers[0].group, Bailout_ShapeGuard); |
| return addUnboxedExpandoGuard(obj, /* hasExpando = */ false, Bailout_ShapeGuard); |
| } |
| |
| // Monomorphic receiver guards are not yet supported when the receiver |
| // is an unboxed object with an expando. |
| } |
| |
| MGuardReceiverPolymorphic* guard = MGuardReceiverPolymorphic::New(alloc(), obj); |
| current->add(guard); |
| |
| if (failedShapeGuard_) |
| guard->setNotMovable(); |
| |
| for (size_t i = 0; i < receivers.length(); i++) { |
| if (!guard->addReceiver(receivers[i])) |
| return nullptr; |
| } |
| |
| return guard; |
| } |
| |
| MInstruction* |
| IonBuilder::addSharedTypedArrayGuard(MDefinition* obj) |
| { |
| MGuardSharedTypedArray* guard = MGuardSharedTypedArray::New(alloc(), obj); |
| current->add(guard); |
| return guard; |
| } |
| |
| TemporaryTypeSet* |
| IonBuilder::bytecodeTypes(jsbytecode* pc) |
| { |
| return TypeScript::BytecodeTypes(script(), pc, bytecodeTypeMap, &typeArrayHint, typeArray); |
| } |
| |
| TypedObjectPrediction |
| IonBuilder::typedObjectPrediction(MDefinition* typedObj) |
| { |
| // Extract TypedObjectPrediction directly if we can |
| if (typedObj->isNewDerivedTypedObject()) { |
| return typedObj->toNewDerivedTypedObject()->prediction(); |
| } |
| |
| TemporaryTypeSet* types = typedObj->resultTypeSet(); |
| return typedObjectPrediction(types); |
| } |
| |
| TypedObjectPrediction |
| IonBuilder::typedObjectPrediction(TemporaryTypeSet* types) |
| { |
| // Type set must be known to be an object. |
| if (!types || types->getKnownMIRType() != MIRType_Object) |
| return TypedObjectPrediction(); |
| |
| // And only known objects. |
| if (types->unknownObject()) |
| return TypedObjectPrediction(); |
| |
| TypedObjectPrediction out; |
| for (uint32_t i = 0; i < types->getObjectCount(); i++) { |
| ObjectGroup* group = types->getGroup(i); |
| if (!group || !TypeSet::ObjectKey::get(group)->hasStableClassAndProto(constraints())) |
| return TypedObjectPrediction(); |
| |
| if (!IsTypedObjectClass(group->clasp())) |
| return TypedObjectPrediction(); |
| |
| out.addDescr(group->typeDescr()); |
| } |
| |
| return out; |
| } |
| |
| MDefinition* |
| IonBuilder::loadTypedObjectType(MDefinition* typedObj) |
| { |
| // Shortcircuit derived type objects, meaning the intermediate |
| // objects created to represent `a.b` in an expression like |
| // `a.b.c`. In that case, the type object can be simply pulled |
| // from the operands of that instruction. |
| if (typedObj->isNewDerivedTypedObject()) |
| return typedObj->toNewDerivedTypedObject()->type(); |
| |
| MInstruction* descr = MTypedObjectDescr::New(alloc(), typedObj); |
| current->add(descr); |
| |
| return descr; |
| } |
| |
| // Given a typed object `typedObj` and an offset `offset` into that |
| // object's data, returns another typed object and adusted offset |
| // where the data can be found. Often, these returned values are the |
| // same as the inputs, but in cases where intermediate derived type |
| // objects have been created, the return values will remove |
| // intermediate layers (often rendering those derived type objects |
| // into dead code). |
| void |
| IonBuilder::loadTypedObjectData(MDefinition* typedObj, |
| MDefinition** owner, |
| LinearSum* ownerOffset) |
| { |
| MOZ_ASSERT(typedObj->type() == MIRType_Object); |
| |
| // Shortcircuit derived type objects, meaning the intermediate |
| // objects created to represent `a.b` in an expression like |
| // `a.b.c`. In that case, the owned and a base offset can be |
| // pulled from the operands of the instruction and combined with |
| // `offset`. |
| if (typedObj->isNewDerivedTypedObject()) { |
| MNewDerivedTypedObject* ins = typedObj->toNewDerivedTypedObject(); |
| |
| SimpleLinearSum base = ExtractLinearSum(ins->offset()); |
| if (!ownerOffset->add(base)) |
| setForceAbort(); |
| |
| *owner = ins->owner(); |
| return; |
| } |
| |
| *owner = typedObj; |
| } |
| |
| // Takes as input a typed object, an offset into that typed object's |
| // memory, and the type repr of the data found at that offset. Returns |
| // the elements pointer and a scaled offset. The scaled offset is |
| // expressed in units of `unit`; when working with typed array MIR, |
| // this is typically the alignment. |
| void |
| IonBuilder::loadTypedObjectElements(MDefinition* typedObj, |
| const LinearSum& baseByteOffset, |
| int32_t scale, |
| MDefinition** ownerElements, |
| MDefinition** ownerScaledOffset, |
| int32_t* ownerByteAdjustment) |
| { |
| MDefinition* owner; |
| LinearSum ownerByteOffset(alloc()); |
| loadTypedObjectData(typedObj, &owner, &ownerByteOffset); |
| |
| if (!ownerByteOffset.add(baseByteOffset)) |
| setForceAbort(); |
| |
| TemporaryTypeSet* ownerTypes = owner->resultTypeSet(); |
| const Class* clasp = ownerTypes ? ownerTypes->getKnownClass(constraints()) : nullptr; |
| if (clasp && IsInlineTypedObjectClass(clasp)) { |
| // Perform the load directly from the owner pointer. |
| if (!ownerByteOffset.add(InlineTypedObject::offsetOfDataStart())) |
| setForceAbort(); |
| *ownerElements = owner; |
| } else { |
| bool definitelyOutline = clasp && IsOutlineTypedObjectClass(clasp); |
| *ownerElements = MTypedObjectElements::New(alloc(), owner, definitelyOutline); |
| current->add((*ownerElements)->toInstruction()); |
| } |
| |
| // Extract the constant adjustment from the byte offset. |
| *ownerByteAdjustment = ownerByteOffset.constant(); |
| int32_t negativeAdjustment; |
| if (!SafeSub(0, *ownerByteAdjustment, &negativeAdjustment)) |
| setForceAbort(); |
| if (!ownerByteOffset.add(negativeAdjustment)) |
| setForceAbort(); |
| |
| // Scale the byte offset if required by the MIR node which will access the |
| // typed object. In principle we should always be able to cleanly divide |
| // the terms in this lienar sum due to alignment restrictions, but due to |
| // limitations of ExtractLinearSum when applied to the terms in derived |
| // typed objects this isn't always be possible. In these cases, fall back |
| // on an explicit division operation. |
| if (ownerByteOffset.divide(scale)) { |
| *ownerScaledOffset = ConvertLinearSum(alloc(), current, ownerByteOffset); |
| } else { |
| MDefinition* unscaledOffset = ConvertLinearSum(alloc(), current, ownerByteOffset); |
| *ownerScaledOffset = MDiv::NewAsmJS(alloc(), unscaledOffset, constantInt(scale), |
| MIRType_Int32, /* unsigned = */ false); |
| current->add((*ownerScaledOffset)->toInstruction()); |
| } |
| } |
| |
| // Looks up the offset/type-repr-set of the field `id`, given the type |
| // set `objTypes` of the field owner. If a field is found, returns true |
| // and sets *fieldOffset, *fieldPrediction, and *fieldIndex. Returns false |
| // otherwise. Infallible. |
| bool |
| IonBuilder::typedObjectHasField(MDefinition* typedObj, |
| PropertyName* name, |
| size_t* fieldOffset, |
| TypedObjectPrediction* fieldPrediction, |
| size_t* fieldIndex) |
| { |
| TypedObjectPrediction objPrediction = typedObjectPrediction(typedObj); |
| if (objPrediction.isUseless()) { |
| trackOptimizationOutcome(TrackedOutcome::AccessNotTypedObject); |
| return false; |
| } |
| |
| // Must be accessing a struct. |
| if (objPrediction.kind() != type::Struct) { |
| trackOptimizationOutcome(TrackedOutcome::NotStruct); |
| return false; |
| } |
| |
| // Determine the type/offset of the field `name`, if any. |
| if (!objPrediction.hasFieldNamed(NameToId(name), fieldOffset, |
| fieldPrediction, fieldIndex)) |
| { |
| trackOptimizationOutcome(TrackedOutcome::StructNoField); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| MDefinition* |
| IonBuilder::typeObjectForElementFromArrayStructType(MDefinition* typeObj) |
| { |
| MInstruction* elemType = MLoadFixedSlot::New(alloc(), typeObj, JS_DESCR_SLOT_ARRAY_ELEM_TYPE); |
| current->add(elemType); |
| |
| MInstruction* unboxElemType = MUnbox::New(alloc(), elemType, MIRType_Object, MUnbox::Infallible); |
| current->add(unboxElemType); |
| |
| return unboxElemType; |
| } |
| |
| MDefinition* |
| IonBuilder::typeObjectForFieldFromStructType(MDefinition* typeObj, |
| size_t fieldIndex) |
| { |
| // Load list of field type objects. |
| |
| MInstruction* fieldTypes = MLoadFixedSlot::New(alloc(), typeObj, JS_DESCR_SLOT_STRUCT_FIELD_TYPES); |
| current->add(fieldTypes); |
| |
| MInstruction* unboxFieldTypes = MUnbox::New(alloc(), fieldTypes, MIRType_Object, MUnbox::Infallible); |
| current->add(unboxFieldTypes); |
| |
| // Index into list with index of field. |
| |
| MInstruction* fieldTypesElements = MElements::New(alloc(), unboxFieldTypes); |
| current->add(fieldTypesElements); |
| |
| MConstant* fieldIndexDef = constantInt(fieldIndex); |
| |
| MInstruction* fieldType = MLoadElement::New(alloc(), fieldTypesElements, fieldIndexDef, false, false); |
| current->add(fieldType); |
| |
| MInstruction* unboxFieldType = MUnbox::New(alloc(), fieldType, MIRType_Object, MUnbox::Infallible); |
| current->add(unboxFieldType); |
| |
| return unboxFieldType; |
| } |
| |
| bool |
| IonBuilder::storeScalarTypedObjectValue(MDefinition* typedObj, |
| const LinearSum& byteOffset, |
| ScalarTypeDescr::Type type, |
| MDefinition* value) |
| { |
| // Find location within the owner object. |
| MDefinition* elements; |
| MDefinition* scaledOffset; |
| int32_t adjustment; |
| size_t alignment = ScalarTypeDescr::alignment(type); |
| loadTypedObjectElements(typedObj, byteOffset, alignment, &elements, &scaledOffset, &adjustment); |
| |
| // Clamp value to [0, 255] when type is Uint8Clamped |
| MDefinition* toWrite = value; |
| if (type == Scalar::Uint8Clamped) { |
| toWrite = MClampToUint8::New(alloc(), value); |
| current->add(toWrite->toInstruction()); |
| } |
| |
| MStoreUnboxedScalar* store = |
| MStoreUnboxedScalar::New(alloc(), elements, scaledOffset, toWrite, |
| type, MStoreUnboxedScalar::TruncateInput, |
| DoesNotRequireMemoryBarrier, adjustment); |
| current->add(store); |
| |
| return true; |
| } |
| |
| bool |
| IonBuilder::storeReferenceTypedObjectValue(MDefinition* typedObj, |
| const LinearSum& byteOffset, |
| ReferenceTypeDescr::Type type, |
| MDefinition* value, |
| PropertyName* name) |
| { |
| // Make sure we aren't adding new type information for writes of object and value |
| // references. |
| if (type != ReferenceTypeDescr::TYPE_STRING) { |
| MOZ_ASSERT(type == ReferenceTypeDescr::TYPE_ANY || |
| type == ReferenceTypeDescr::TYPE_OBJECT); |
| MIRType implicitType = |
| (type == ReferenceTypeDescr::TYPE_ANY) ? MIRType_Undefined : MIRType_Null; |
| |
| if (PropertyWriteNeedsTypeBarrier(alloc(), constraints(), current, &typedObj, name, &value, |
| /* canModify = */ true, implicitType)) |
| { |
| trackOptimizationOutcome(TrackedOutcome::NeedsTypeBarrier); |
| return false; |
| } |
| } |
| |
| // Find location within the owner object. |
| MDefinition* elements; |
| MDefinition* scaledOffset; |
| int32_t adjustment; |
| size_t alignment = ReferenceTypeDescr::alignment(type); |
| loadTypedObjectElements(typedObj, byteOffset, alignment, &elements, &scaledOffset, &adjustment); |
| |
| MInstruction* store = nullptr; // initialize to silence GCC warning |
| switch (type) { |
| case ReferenceTypeDescr::TYPE_ANY: |
| if (NeedsPostBarrier(value)) |
| current->add(MPostWriteBarrier::New(alloc(), typedObj, value)); |
| store = MStoreElement::New(alloc(), elements, scaledOffset, value, false, adjustment); |
| store->toStoreElement()->setNeedsBarrier(); |
| break; |
| case ReferenceTypeDescr::TYPE_OBJECT: |
| // Note: We cannot necessarily tell at this point whether a post |
| // barrier is needed, because the type policy may insert ToObjectOrNull |
| // instructions later, and those may require a post barrier. Therefore, |
| // defer the insertion of post barriers to the type policy. |
| store = MStoreUnboxedObjectOrNull::New(alloc(), elements, scaledOffset, value, typedObj, adjustment); |
| break; |
| case ReferenceTypeDescr::TYPE_STRING: |
| // Strings are not nursery allocated, so these writes do not need post |
| // barriers. |
| store = MStoreUnboxedString::New(alloc(), elements, scaledOffset, value, adjustment); |
| break; |
| } |
| |
| current->add(store); |
| return true; |
| } |
| |
| JSObject* |
| IonBuilder::checkNurseryObject(JSObject* obj) |
| { |
| // If we try to use any nursery pointers during compilation, make sure that |
| // the main thread will cancel this compilation before performing a minor |
| // GC. All constants used during compilation should either go through this |
| // function or should come from a type set (which has a similar barrier). |
| if (obj && IsInsideNursery(obj)) { |
| compartment->runtime()->setMinorGCShouldCancelIonCompilations(); |
| IonBuilder* builder = this; |
| while (builder) { |
| builder->setNotSafeForMinorGC(); |
| builder = builder->callerBuilder_; |
| } |
| } |
| |
| return obj; |
| } |
| |
| MConstant* |
| IonBuilder::constant(const Value& v) |
| { |
| MOZ_ASSERT(!v.isString() || v.toString()->isAtom(), |
| "Handle non-atomized strings outside IonBuilder."); |
| |
| if (v.isObject()) |
| checkNurseryObject(&v.toObject()); |
| |
| MConstant* c = MConstant::New(alloc(), v, constraints()); |
| current->add(c); |
| return c; |
| } |
| |
| MConstant* |
| IonBuilder::constantInt(int32_t i) |
| { |
| return constant(Int32Value(i)); |
| } |
| |
| MInstruction* |
| IonBuilder::initializedLength(MDefinition* obj, MDefinition* elements, JSValueType unboxedType) |
| { |
| MInstruction* res; |
| if (unboxedType != JSVAL_TYPE_MAGIC) |
| res = MUnboxedArrayInitializedLength::New(alloc(), obj); |
| else |
| res = MInitializedLength::New(alloc(), elements); |
| current->add(res); |
| return res; |
| } |
| |
| MInstruction* |
| IonBuilder::setInitializedLength(MDefinition* obj, JSValueType unboxedType, size_t count) |
| { |
| MOZ_ASSERT(count); |
| |
| MInstruction* res; |
| if (unboxedType != JSVAL_TYPE_MAGIC) { |
| res = MSetUnboxedArrayInitializedLength::New(alloc(), obj, constant(Int32Value(count))); |
| } else { |
| // MSetInitializedLength takes the index of the last element, rather |
| // than the count itself. |
| MInstruction* elements = MElements::New(alloc(), obj, /* unboxed = */ false); |
| current->add(elements); |
| res = MSetInitializedLength::New(alloc(), elements, constant(Int32Value(count - 1))); |
| } |
| current->add(res); |
| return res; |
| } |
| |
| MDefinition* |
| IonBuilder::getCallee() |
| { |
| if (inliningDepth_ == 0) { |
| MInstruction* callee = MCallee::New(alloc()); |
| current->add(callee); |
| return callee; |
| } |
| |
| return inlineCallInfo_->fun(); |
| } |
| |
| MDefinition* |
| IonBuilder::addLexicalCheck(MDefinition* input) |
| { |
| MOZ_ASSERT(JSOp(*pc) == JSOP_CHECKLEXICAL || |
| JSOp(*pc) == JSOP_CHECKALIASEDLEXICAL || |
| JSOp(*pc) == JSOP_GETIMPORT); |
| |
| MInstruction* lexicalCheck; |
| |
| // If we're guaranteed to not be JS_UNINITIALIZED_LEXICAL, no need to check. |
| if (input->type() == MIRType_MagicUninitializedLexical) { |
| // Mark the input as implicitly used so the JS_UNINITIALIZED_LEXICAL |
| // magic value will be preserved on bailout. |
| input->setImplicitlyUsedUnchecked(); |
| lexicalCheck = MThrowRuntimeLexicalError::New(alloc(), JSMSG_UNINITIALIZED_LEXICAL); |
| current->add(lexicalCheck); |
| if (!resumeAfter(lexicalCheck)) |
| return nullptr; |
| return constant(UndefinedValue()); |
| } |
| |
| if (input->type() == MIRType_Value) { |
| lexicalCheck = MLexicalCheck::New(alloc(), input); |
| current->add(lexicalCheck); |
| if (failedLexicalCheck_) |
| lexicalCheck->setNotMovableUnchecked(); |
| return lexicalCheck; |
| } |
| |
| return input; |
| } |