From 7d753c1a8f22f85f6279a3c016034ce8f8e740f7 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 24 Mar 2018 12:09:30 +0100 Subject: Bug 1147371: Implement IteratorClose for for-of Issue #74 --- js/src/builtin/TypedArray.js | 4 +- js/src/frontend/BytecodeEmitter.cpp | 619 +++++++++++++++------ js/src/frontend/BytecodeEmitter.h | 6 + js/src/jit/JitFrames.cpp | 39 +- js/src/js.msg | 2 +- js/src/jsiter.cpp | 52 ++ js/src/jsiter.h | 3 + js/src/jsscript.h | 3 +- js/src/shell/js.cpp | 9 +- .../ecma_6/Statements/for-of-iterator-close.js | 102 ++++ js/src/tests/ecma_6/shell.js | 48 +- js/src/vm/Interpreter.cpp | 19 +- js/src/vm/Interpreter.h | 1 + 13 files changed, 704 insertions(+), 203 deletions(-) create mode 100644 js/src/tests/ecma_6/Statements/for-of-iterator-close.js diff --git a/js/src/builtin/TypedArray.js b/js/src/builtin/TypedArray.js index 4a3f38365..a2205dc92 100644 --- a/js/src/builtin/TypedArray.js +++ b/js/src/builtin/TypedArray.js @@ -1428,7 +1428,7 @@ function TypedArrayStaticFrom(source, mapfn = undefined, thisArg = undefined) { // 22.2.2.1.1 IterableToList, step 4.a. var next = callContentFunction(iterator.next, iterator); if (!IsObject(next)) - ThrowTypeError(JSMSG_NEXT_RETURNED_PRIMITIVE); + ThrowTypeError(JSMSG_ITER_METHOD_RETURNED_PRIMITIVE, "next"); // 22.2.2.1.1 IterableToList, step 4.b. if (next.done) @@ -1555,7 +1555,7 @@ function IterableToList(items, method) { // Step 4.a. var next = callContentFunction(iterator.next, iterator); if (!IsObject(next)) - ThrowTypeError(JSMSG_NEXT_RETURNED_PRIMITIVE); + ThrowTypeError(JSMSG_ITER_METHOD_RETURNED_PRIMITIVE, "next"); // Step 4.b. if (next.done) diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index a4cfeb753..fb141e92d 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -58,6 +58,7 @@ using mozilla::Some; class BreakableControl; class LabelControl; class LoopControl; +class ForOfLoopControl; class TryFinallyControl; static bool @@ -150,6 +151,13 @@ BytecodeEmitter::NestableControl::is() const return StatementKindIsLoop(kind_); } +template <> +bool +BytecodeEmitter::NestableControl::is() const +{ + return kind_ == StatementKind::ForOfLoop; +} + template <> bool BytecodeEmitter::NestableControl::is() const @@ -270,6 +278,64 @@ class LoopControl : public BreakableControl } }; +class ForOfLoopControl : public LoopControl +{ + // The stack depth of the iterator. + int32_t iterDepth_; + + // for-of loops, when throwing from non-iterator code (i.e. from the body + // or from evaluating the LHS of the loop condition), need to call + // IteratorClose. If IteratorClose itself throws, we must not re-call + // IteratorClose. Since non-local jumps like break and return call + // IteratorClose, whenever a non-local jump is emitted, we must terminate + // the current JSTRY_ITERCLOSE note to skip the non-local jump code, then + // start a new one. + // + // Visually, + // + // for (x of y) { + // ... instantiate ForOfLoopControl + // ... + <-- iterCloseTryStart_ points to right before + // ... assignment to loop variable + // ... ^ + // ... | + // if (...) v + // + call finishIterCloseTryNote before |break| + // above range is noted with JSTRY_ITERCLOSE + // + // break; <-- break and IteratorClose are not inside + // JSTRY_ITERCLOSE note + // + // call startNewIterCloseTryNote after |break| + // + <-- next iterCloseTryStart_ points here + // ... | + // ... ~ + // } + ptrdiff_t iterCloseTryStart_; + + public: + ForOfLoopControl(BytecodeEmitter* bce, int32_t iterDepth) + : LoopControl(bce, StatementKind::ForOfLoop), + iterDepth_(iterDepth), + iterCloseTryStart_(-1) + { + MOZ_ASSERT(bce->stackDepth >= iterDepth); + } + + MOZ_MUST_USE bool finishIterCloseTryNote(BytecodeEmitter* bce) { + ptrdiff_t end = bce->offset(); + MOZ_ASSERT(end >= iterCloseTryStart_); + if (end != iterCloseTryStart_) + return bce->tryNoteList.append(JSTRY_ITERCLOSE, iterDepth_, iterCloseTryStart_, end); + return true; + } + + void startNewIterCloseTryNote(BytecodeEmitter* bce) { + MOZ_ASSERT(bce->offset() > iterCloseTryStart_); + iterCloseTryStart_ = bce->offset(); + } +}; + class TryFinallyControl : public BytecodeEmitter::NestableControl { bool emittingSubroutine_; @@ -1497,6 +1563,156 @@ BytecodeEmitter::TDZCheckCache::noteTDZCheck(BytecodeEmitter* bce, JSAtom* name, return true; } +class MOZ_STACK_CLASS IfThenElseEmitter +{ + BytecodeEmitter* bce_; + JumpList jumpAroundThen_; + JumpList jumpsAroundElse_; + unsigned noteIndex_; + int32_t thenDepth_; +#ifdef DEBUG + int32_t pushed_; + bool calculatedPushed_; +#endif + enum State { + Start, + If, + Cond, + IfElse, + Else, + End + }; + State state_; + + public: + explicit IfThenElseEmitter(BytecodeEmitter* bce) + : bce_(bce), + noteIndex_(-1), + thenDepth_(0), +#ifdef DEBUG + pushed_(0), + calculatedPushed_(false), +#endif + state_(Start) + {} + + ~IfThenElseEmitter() + {} + + private: + bool emitIf(State nextState) { + MOZ_ASSERT(state_ == Start || state_ == Else); + MOZ_ASSERT(nextState == If || nextState == IfElse || nextState == Cond); + + // Clear jumpAroundThen_ offset that points previous JSOP_IFEQ. + if (state_ == Else) + jumpAroundThen_ = JumpList(); + + // Emit an annotated branch-if-false around the then part. + SrcNoteType type = nextState == If ? SRC_IF : nextState == IfElse ? SRC_IF_ELSE : SRC_COND; + if (!bce_->newSrcNote(type, ¬eIndex_)) + return false; + if (!bce_->emitJump(JSOP_IFEQ, &jumpAroundThen_)) + return false; + + // To restore stack depth in else part, save depth of the then part. +#ifdef DEBUG + // If DEBUG, this is also necessary to calculate |pushed_|. + thenDepth_ = bce_->stackDepth; +#else + if (nextState == IfElse || nextState == Cond) + thenDepth_ = bce_->stackDepth; +#endif + state_ = nextState; + return true; + } + + public: + bool emitIf() { + return emitIf(If); + } + + bool emitCond() { + return emitIf(Cond); + } + + bool emitIfElse() { + return emitIf(IfElse); + } + + bool emitElse() { + MOZ_ASSERT(state_ == IfElse || state_ == Cond); + + calculateOrCheckPushed(); + + // Emit a jump from the end of our then part around the else part. The + // patchJumpsToTarget call at the bottom of this function will fix up + // the offset with jumpsAroundElse value. + if (!bce_->emitJump(JSOP_GOTO, &jumpsAroundElse_)) + return false; + + // Ensure the branch-if-false comes here, then emit the else. + if (!bce_->emitJumpTargetAndPatch(jumpAroundThen_)) + return false; + + // Annotate SRC_IF_ELSE or SRC_COND with the offset from branch to + // jump, for IonMonkey's benefit. We can't just "back up" from the pc + // of the else clause, because we don't know whether an extended + // jump was required to leap from the end of the then clause over + // the else clause. + if (!bce_->setSrcNoteOffset(noteIndex_, 0, + jumpsAroundElse_.offset - jumpAroundThen_.offset)) + { + return false; + } + + // Restore stack depth of the then part. + bce_->stackDepth = thenDepth_; + state_ = Else; + return true; + } + + bool emitEnd() { + MOZ_ASSERT(state_ == If || state_ == Else); + + calculateOrCheckPushed(); + + if (state_ == If) { + // No else part, fixup the branch-if-false to come here. + if (!bce_->emitJumpTargetAndPatch(jumpAroundThen_)) + return false; + } + + // Patch all the jumps around else parts. + if (!bce_->emitJumpTargetAndPatch(jumpsAroundElse_)) + return false; + + state_ = End; + return true; + } + + void calculateOrCheckPushed() { +#ifdef DEBUG + if (!calculatedPushed_) { + pushed_ = bce_->stackDepth - thenDepth_; + calculatedPushed_ = true; + } else { + MOZ_ASSERT(pushed_ == bce_->stackDepth - thenDepth_); + } +#endif + } + +#ifdef DEBUG + int32_t pushed() const { + return pushed_; + } + + int32_t popped() const { + return -pushed_; + } +#endif +}; + BytecodeEmitter::BytecodeEmitter(BytecodeEmitter* parent, Parser* parser, SharedContext* sc, HandleScript script, Handle lazyScript, @@ -2013,22 +2229,43 @@ BytecodeEmitter::flushPops(int* npops) namespace { -class NonLocalExitControl { +class NonLocalExitControl +{ + public: + enum Kind + { + // IteratorClose is handled especially inside the exception unwinder. + Throw, + + // A 'continue' statement does not call IteratorClose for the loop it + // is continuing, i.e. excluding the target loop. + Continue, + + // A 'break' or 'return' statement does call IteratorClose for the + // loop it is breaking out of or returning from, i.e. including the + // target loop. + Break, + Return + }; + + private: BytecodeEmitter* bce_; const uint32_t savedScopeNoteIndex_; const int savedDepth_; uint32_t openScopeNoteIndex_; + Kind kind_; NonLocalExitControl(const NonLocalExitControl&) = delete; MOZ_MUST_USE bool leaveScope(BytecodeEmitter::EmitterScope* scope); public: - explicit NonLocalExitControl(BytecodeEmitter* bce) + NonLocalExitControl(BytecodeEmitter* bce, Kind kind) : bce_(bce), savedScopeNoteIndex_(bce->scopeNoteList.length()), savedDepth_(bce->stackDepth), - openScopeNoteIndex_(bce->innermostEmitterScope->noteIndex()) + openScopeNoteIndex_(bce->innermostEmitterScope->noteIndex()), + kind_(kind) { } ~NonLocalExitControl() { @@ -2075,6 +2312,14 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta EmitterScope* es = bce_->innermostEmitterScope; int npops = 0; + bool hasForOfLoopsWithIteratorClose = false; + + // IteratorClose is handled specially in the exception unwinder. For + // 'continue', 'break', and 'return' statements, emit IteratorClose + // bytecode inline. 'continue' statements do not call IteratorClose for + // the loop they are continuing. + bool emitIteratorClose = kind_ == Continue || kind_ == Break || kind_ == Return; + bool emitIteratorCloseAtTarget = emitIteratorClose && kind_ != Continue; auto flushPops = [&npops](BytecodeEmitter* bce) { if (npops && !bce->flushPops(&npops)) @@ -2114,15 +2359,29 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta } case StatementKind::ForOfLoop: - npops += 2; + // The iterator and the current value are on the stack. + // + if (emitIteratorClose) { + hasForOfLoopsWithIteratorClose = true; + if (!control->as().finishIterCloseTryNote(bce_)) + return false; + if (!bce_->emit1(JSOP_POP)) // ... ITER + return false; + if (!bce_->emitIteratorClose()) // ... + return false; + } else { + if (!bce_->emit1(JSOP_POP)) // ... ITER + return false; + if (!bce_->emit1(JSOP_POP)) // ... + return false; + } break; case StatementKind::ForInLoop: - /* The iterator and the current value are on the stack. */ - npops += 1; - if (!flushPops(bce_)) + // The iterator and the current value are on the stack. + if (!bce_->emit1(JSOP_POP)) // ... ITER return false; - if (!bce_->emit1(JSOP_ENDITER)) + if (!bce_->emit1(JSOP_ENDITER)) // ... return false; break; @@ -2131,13 +2390,45 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta } } + if (target && target->is() && emitIteratorCloseAtTarget) { + hasForOfLoopsWithIteratorClose = true; + if (!target->as().finishIterCloseTryNote(bce_)) + return false; + + // The iterator and the current value are on the stack. At the level + // of the target block, there's bytecode after the loop that will pop + // the iterator and the value, so duplicate the iterator and call + // IteratorClose. + if (!bce_->emitDupAt(1)) // ... ITER RESULT ITER + return false; + if (!bce_->emitIteratorClose()) // ... ITER RESULT + return false; + } + EmitterScope* targetEmitterScope = target ? target->emitterScope() : bce_->varEmitterScope; for (; es != targetEmitterScope; es = es->enclosingInFrame()) { if (!leaveScope(es)) return false; } - return flushPops(bce_); + if (!flushPops(bce_)) + return false; + + // See comment in ForOfLoopControl. + if (hasForOfLoopsWithIteratorClose) { + for (NestableControl* control = bce_->innermostNestableControl; + control != target; + control = control->enclosing()) + { + if (control->is()) + control->as().startNewIterCloseTryNote(bce_); + } + + if (target && target->is() && emitIteratorCloseAtTarget) + target->as().startNewIterCloseTryNote(bce_); + } + + return true; } } // anonymous namespace @@ -2145,7 +2436,9 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta bool BytecodeEmitter::emitGoto(NestableControl* target, JumpList* jumplist, SrcNoteType noteType) { - NonLocalExitControl nle(this); + NonLocalExitControl nle(this, noteType == SRC_CONTINUE + ? NonLocalExitControl::Continue + : NonLocalExitControl::Break); if (!nle.prepareForNonLocalJump(target)) return false; @@ -4529,6 +4822,144 @@ BytecodeEmitter::emitIteratorNext(ParseNode* pn, bool allowSelfHosted) return true; } +bool +BytecodeEmitter::emitIteratorClose(Maybe yieldStarTryStart, bool allowSelfHosted) +{ + MOZ_ASSERT(allowSelfHosted || emitterMode != BytecodeEmitter::SelfHosting, + ".close() on iterators is prohibited in self-hosted code because it " + "can run user-modifiable iteration code"); + + // Generate inline logic corresponding to IteratorClose (ES 7.4.6). + // + // Callers need to ensure that the iterator object is at the top of the + // stack. + + if (!emit1(JSOP_DUP)) // ... ITER ITER + return false; + + // Step 3. + // + // Get the "return" method. + if (!emitAtomOp(cx->names().return_, JSOP_CALLPROP)) // ... ITER RET + return false; + + // Step 4. + // + // Do nothing if "return" is null or undefined. + IfThenElseEmitter ifReturnMethodIsDefined(this); + if (!emit1(JSOP_DUP)) // ... ITER RET RET + return false; + if (!emit1(JSOP_UNDEFINED)) // ... ITER RET RET UNDEFINED + return false; + if (!emit1(JSOP_NE)) // ... ITER RET ?NEQL + return false; + if (!ifReturnMethodIsDefined.emitIfElse()) + return false; + + // Steps 5, 8. + // + // Call "return" if it is not undefined or null, and check that it returns + // an Object. + if (!emit1(JSOP_SWAP)) // ... RET ITER + return false; + + // ES 14.4.13, yield * AssignmentExpression, step 5.c + // + // When emitting iterator.return() for yield* forced return, we need to + // pass the argument passed to Generator.prototype.return to the return + // method. + if (yieldStarTryStart) { + IfThenElseEmitter ifGeneratorClosing(this); + if (!emitDupAt(2)) // ... FTYPE FVALUE RET ITER FVALUE + return false; + if (!emit1(JSOP_ISGENCLOSING)) // ... FTYPE FVALUE RET ITER FVALUE CLOSING + return false; + if (!emit1(JSOP_SWAP)) // ... FTYPE FVALUE RET ITER CLOSING FVALUE + return false; + if (!emit1(JSOP_POP)) // ... FTYPE FVALUE RET ITER CLOSING + return false; + if (!ifGeneratorClosing.emitIfElse()) // ... FTYPE FVALUE RET ITER + return false; + + if (!emit1(JSOP_GETRVAL)) // ... FTYPE FVALUE RET ITER RVAL + return false; + if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // ... FTYPE FVALUE RET ITER VALUE + return false; + if (!emitCall(JSOP_CALL, 1)) // ... FTYPE FVALUE RESULT + return false; + checkTypeSet(JSOP_CALL); + if (!emitCheckIsObj(CheckIsObjectKind::IteratorReturn)) // ... FTYPE FVALUE RESULT + return false; + + IfThenElseEmitter ifReturnDone(this); + if (!emit1(JSOP_DUP)) // ITER OLDRESULT FTYPE FVALUE RESULT RESULT + return false; + if (!emitAtomOp(cx->names().done, JSOP_GETPROP)) // ITER OLDRESULT FTYPE FVALUE RESULT DONE + return false; + if (!ifReturnDone.emitIfElse()) // ITER OLDRESULT FTYPE FVALUE RESULT + return false; + if (!emit1(JSOP_DUP)) // ITER OLDRESULT FTYPE FVALUE RESULT RESULT + return false; + if (!emit1(JSOP_SETRVAL)) // ITER OLDRESULT FTYPE FVALUE RESULT + return false; + if (!ifReturnDone.emitElse()) // ITER OLDRESULT FTYPE FVALUE RESULT + return false; + int32_t savedDepth = this->stackDepth; + if (!emit2(JSOP_UNPICK, 3)) // ITER RESULT OLDRESULT FTYPE FVALUE + return false; + if (!emitUint16Operand(JSOP_POPN, 3)) // ITER RESULT + return false; + JumpList beq; + JumpTarget breakTarget{ -1 }; + if (!emitBackwardJump(JSOP_GOTO, *yieldStarTryStart, &beq, &breakTarget)) // ITER RESULT + return false; + this->stackDepth = savedDepth; + if (!ifReturnDone.emitEnd()) + return false; + + if (!ifGeneratorClosing.emitElse()) // ... FTYPE FVALUE RET ITER + return false; + if (!emitCall(JSOP_CALL, 0)) // ... FTYPE FVALUE RESULT + return false; + checkTypeSet(JSOP_CALL); + if (!emitCheckIsObj(CheckIsObjectKind::IteratorReturn)) // ... FTYPE FVALUE RESULT + return false; + + if (!ifGeneratorClosing.emitEnd()) + return false; + } else { + if (!emitCall(JSOP_CALL, 0)) // ... RESULT + return false; + checkTypeSet(JSOP_CALL); + if (!emitCheckIsObj(CheckIsObjectKind::IteratorReturn)) // ... RESULT + return false; + } + + if (!ifReturnMethodIsDefined.emitElse()) + return false; + if (!emit1(JSOP_POP)) // ... ITER + return false; + if (!ifReturnMethodIsDefined.emitEnd()) + return false; + + return emit1(JSOP_POP); // ... +} + +template +bool +BytecodeEmitter::wrapWithIteratorCloseTryNote(int32_t iterDepth, InnerEmitter emitter) +{ + MOZ_ASSERT(this->stackDepth >= iterDepth); + + ptrdiff_t start = offset(); + if (!emitter(this)) + return false; + ptrdiff_t end = offset(); + if (start != end) + return tryNoteList.append(JSTRY_ITERCLOSE, iterDepth, start, end); + return true; +} + bool BytecodeEmitter::emitDefault(ParseNode* defaultExpr, ParseNode* pattern) { @@ -4617,156 +5048,6 @@ BytecodeEmitter::emitInitializerInBranch(ParseNode* initializer, ParseNode* patt return emitInitializer(initializer, pattern); } -class MOZ_STACK_CLASS IfThenElseEmitter -{ - BytecodeEmitter* bce_; - JumpList jumpAroundThen_; - JumpList jumpsAroundElse_; - unsigned noteIndex_; - int32_t thenDepth_; -#ifdef DEBUG - int32_t pushed_; - bool calculatedPushed_; -#endif - enum State { - Start, - If, - Cond, - IfElse, - Else, - End - }; - State state_; - - public: - explicit IfThenElseEmitter(BytecodeEmitter* bce) - : bce_(bce), - noteIndex_(-1), - thenDepth_(0), -#ifdef DEBUG - pushed_(0), - calculatedPushed_(false), -#endif - state_(Start) - {} - - ~IfThenElseEmitter() - {} - - private: - bool emitIf(State nextState) { - MOZ_ASSERT(state_ == Start || state_ == Else); - MOZ_ASSERT(nextState == If || nextState == IfElse || nextState == Cond); - - // Clear jumpAroundThen_ offset that points previous JSOP_IFEQ. - if (state_ == Else) - jumpAroundThen_ = JumpList(); - - // Emit an annotated branch-if-false around the then part. - SrcNoteType type = nextState == If ? SRC_IF : nextState == IfElse ? SRC_IF_ELSE : SRC_COND; - if (!bce_->newSrcNote(type, ¬eIndex_)) - return false; - if (!bce_->emitJump(JSOP_IFEQ, &jumpAroundThen_)) - return false; - - // To restore stack depth in else part, save depth of the then part. -#ifdef DEBUG - // If DEBUG, this is also necessary to calculate |pushed_|. - thenDepth_ = bce_->stackDepth; -#else - if (nextState == IfElse || nextState == Cond) - thenDepth_ = bce_->stackDepth; -#endif - state_ = nextState; - return true; - } - - public: - bool emitIf() { - return emitIf(If); - } - - bool emitCond() { - return emitIf(Cond); - } - - bool emitIfElse() { - return emitIf(IfElse); - } - - bool emitElse() { - MOZ_ASSERT(state_ == IfElse || state_ == Cond); - - calculateOrCheckPushed(); - - // Emit a jump from the end of our then part around the else part. The - // patchJumpsToTarget call at the bottom of this function will fix up - // the offset with jumpsAroundElse value. - if (!bce_->emitJump(JSOP_GOTO, &jumpsAroundElse_)) - return false; - - // Ensure the branch-if-false comes here, then emit the else. - if (!bce_->emitJumpTargetAndPatch(jumpAroundThen_)) - return false; - - // Annotate SRC_IF_ELSE or SRC_COND with the offset from branch to - // jump, for IonMonkey's benefit. We can't just "back up" from the pc - // of the else clause, because we don't know whether an extended - // jump was required to leap from the end of the then clause over - // the else clause. - if (!bce_->setSrcNoteOffset(noteIndex_, 0, - jumpsAroundElse_.offset - jumpAroundThen_.offset)) - { - return false; - } - - // Restore stack depth of the then part. - bce_->stackDepth = thenDepth_; - state_ = Else; - return true; - } - - bool emitEnd() { - MOZ_ASSERT(state_ == If || state_ == Else); - - calculateOrCheckPushed(); - - if (state_ == If) { - // No else part, fixup the branch-if-false to come here. - if (!bce_->emitJumpTargetAndPatch(jumpAroundThen_)) - return false; - } - - // Patch all the jumps around else parts. - if (!bce_->emitJumpTargetAndPatch(jumpsAroundElse_)) - return false; - - state_ = End; - return true; - } - - void calculateOrCheckPushed() { -#ifdef DEBUG - if (!calculatedPushed_) { - pushed_ = bce_->stackDepth - thenDepth_; - calculatedPushed_ = true; - } else { - MOZ_ASSERT(pushed_ == bce_->stackDepth - thenDepth_); - } -#endif - } - -#ifdef DEBUG - int32_t pushed() const { - return pushed_; - } - - int32_t popped() const { - return -pushed_; - } -#endif -}; - bool BytecodeEmitter::emitDestructuringOpsArray(ParseNode* pattern, DestructuringFlavor flav) { @@ -5710,7 +5991,7 @@ BytecodeEmitter::emitCatch(ParseNode* pn) return false; { - NonLocalExitControl nle(this); + NonLocalExitControl nle(this, NonLocalExitControl::Throw); // Move exception back to cx->exception to prepare for // the next catch. @@ -6303,12 +6584,14 @@ BytecodeEmitter::emitForOf(ParseNode* forOfLoop, EmitterScope* headLexicalEmitte if (!emitIterator()) // ITER return false; + int32_t iterDepth = stackDepth; + // For-of loops have both the iterator and the value on the stack. Push // undefined to balance the stack. if (!emit1(JSOP_UNDEFINED)) // ITER RESULT return false; - LoopControl loopInfo(this, StatementKind::ForOfLoop); + ForOfLoopControl loopInfo(this, iterDepth); // Annotate so IonMonkey can find the loop-closing jump. unsigned noteIndex; @@ -6354,18 +6637,23 @@ BytecodeEmitter::emitForOf(ParseNode* forOfLoop, EmitterScope* headLexicalEmitte #endif // Emit code to assign result.value to the iteration variable. + // + // Note that ES 13.7.5.13, step 5.c says getting result.value does not + // call IteratorClose, so start JSTRY_ITERCLOSE after the GETPROP. if (!emit1(JSOP_DUP)) // ITER RESULT RESULT return false; if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // ITER RESULT VALUE return false; + loopInfo.startNewIterCloseTryNote(this); + if (!emitInitializeForInOrOfTarget(forOfHead)) // ITER RESULT VALUE return false; if (!emit1(JSOP_POP)) // ITER RESULT return false; - MOZ_ASSERT(this->stackDepth == loopDepth, + MOZ_ASSERT(stackDepth == loopDepth, "the stack must be balanced around the initializing " "operation"); @@ -6374,6 +6662,9 @@ BytecodeEmitter::emitForOf(ParseNode* forOfLoop, EmitterScope* headLexicalEmitte if (!emitTree(forBody)) // ITER RESULT return false; + if (!loopInfo.finishIterCloseTryNote(this)) + return false; + // Set offset for continues. loopInfo.continueTarget = { offset() }; @@ -7609,7 +7900,7 @@ BytecodeEmitter::emitReturn(ParseNode* pn) return false; } - NonLocalExitControl nle(this); + NonLocalExitControl nle(this, NonLocalExitControl::Return); if (!nle.prepareForNonLocalJumpToOutermost()) return false; diff --git a/js/src/frontend/BytecodeEmitter.h b/js/src/frontend/BytecodeEmitter.h index 066c06672..6b39069de 100644 --- a/js/src/frontend/BytecodeEmitter.h +++ b/js/src/frontend/BytecodeEmitter.h @@ -679,6 +679,12 @@ struct MOZ_STACK_CLASS BytecodeEmitter // Pops iterator from the top of the stack. Pushes the result of |.next()| // onto the stack. MOZ_MUST_USE bool emitIteratorNext(ParseNode* pn, bool allowSelfHosted = false); + MOZ_MUST_USE bool emitIteratorClose( + mozilla::Maybe yieldStarTryStart = mozilla::Nothing(), + bool allowSelfHosted = false); + + template + MOZ_MUST_USE bool wrapWithIteratorCloseTryNote(int32_t iterDepth, InnerEmitter emitter); // Check if the value on top of the stack is "undefined". If so, replace // that value on the stack with the value defined by |defaultExpr|. diff --git a/js/src/jit/JitFrames.cpp b/js/src/jit/JitFrames.cpp index 646442b4c..d6c6fc4c9 100644 --- a/js/src/jit/JitFrames.cpp +++ b/js/src/jit/JitFrames.cpp @@ -328,11 +328,15 @@ NumArgAndLocalSlots(const InlineFrameIterator& frame) } static void -CloseLiveIteratorIon(JSContext* cx, const InlineFrameIterator& frame, uint32_t stackSlot) +CloseLiveIteratorIon(JSContext* cx, const InlineFrameIterator& frame, JSTryNote* tn) { + MOZ_ASSERT(tn->kind == JSTRY_FOR_IN || tn->kind == JSTRY_ITERCLOSE); + MOZ_ASSERT(tn->stackDepth > 0); + SnapshotIterator si = frame.snapshotIterator(); // Skip stack slots until we reach the iterator object. + uint32_t stackSlot = tn->stackDepth; uint32_t skipSlots = NumArgAndLocalSlots(frame) + stackSlot - 1; for (unsigned i = 0; i < skipSlots; i++) @@ -341,10 +345,14 @@ CloseLiveIteratorIon(JSContext* cx, const InlineFrameIterator& frame, uint32_t s Value v = si.read(); RootedObject obj(cx, &v.toObject()); - if (cx->isExceptionPending()) - UnwindIteratorForException(cx, obj); - else + if (cx->isExceptionPending()) { + if (tn->kind == JSTRY_FOR_IN) + UnwindIteratorForException(cx, obj); + else + IteratorCloseForException(cx, obj); + } else { UnwindIteratorForUncatchableException(cx, obj); + } } class IonFrameStackDepthOp @@ -417,12 +425,13 @@ HandleExceptionIon(JSContext* cx, const InlineFrameIterator& frame, ResumeFromEx JSTryNote* tn = *tni; switch (tn->kind) { - case JSTRY_FOR_IN: { - MOZ_ASSERT(JSOp(*(script->main() + tn->start + tn->length)) == JSOP_ENDITER); + case JSTRY_FOR_IN: + case JSTRY_ITERCLOSE: { + MOZ_ASSERT_IF(tn->kind == JSTRY_FOR_IN, + JSOp(*(script->main() + tn->start + tn->length)) == JSOP_ENDITER); MOZ_ASSERT(tn->stackDepth > 0); - uint32_t localSlot = tn->stackDepth; - CloseLiveIteratorIon(cx, frame, localSlot); + CloseLiveIteratorIon(cx, frame, tn); break; } @@ -598,17 +607,23 @@ ProcessTryNotesBaseline(JSContext* cx, const JitFrameIterator& frame, Environmen return true; } - case JSTRY_FOR_IN: { + case JSTRY_FOR_IN: + case JSTRY_ITERCLOSE: { uint8_t* framePointer; uint8_t* stackPointer; BaselineFrameAndStackPointersFromTryNote(tn, frame, &framePointer, &stackPointer); - Value iterValue(*(Value*) stackPointer); + Value iterValue(*(reinterpret_cast(stackPointer))); RootedObject iterObject(cx, &iterValue.toObject()); - if (!UnwindIteratorForException(cx, iterObject)) { + bool ok; + if (tn->kind == JSTRY_FOR_IN) + ok = UnwindIteratorForException(cx, iterObject); + else + ok = IteratorCloseForException(cx, iterObject); + if (!ok) { // See comment in the JSTRY_FOR_IN case in Interpreter.cpp's // ProcessTryNotes. SettleOnTryNote(cx, tn, frame, ei, rfe, pc); - MOZ_ASSERT(**pc == JSOP_ENDITER); + MOZ_ASSERT_IF(tn->kind == JSTRY_FOR_IN, **pc == JSOP_ENDITER); return false; } break; diff --git a/js/src/js.msg b/js/src/js.msg index 8766bfbf5..30c0d3035 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -96,7 +96,7 @@ MSG_DEF(JSMSG_NOT_ITERABLE, 1, JSEXN_TYPEERR, "{0} is not iterable") MSG_DEF(JSMSG_NOT_ITERATOR, 1, JSEXN_TYPEERR, "{0} is not iterator") MSG_DEF(JSMSG_ALREADY_HAS_PRAGMA, 2, JSEXN_WARN, "{0} is being assigned a {1}, but already has one") MSG_DEF(JSMSG_GET_ITER_RETURNED_PRIMITIVE, 0, JSEXN_TYPEERR, "[Symbol.iterator]() returned a non-object value") -MSG_DEF(JSMSG_NEXT_RETURNED_PRIMITIVE, 0, JSEXN_TYPEERR, "iterator.next() returned a non-object value") +MSG_DEF(JSMSG_ITER_METHOD_RETURNED_PRIMITIVE, 1, JSEXN_TYPEERR, "iterator.{0}() returned a non-object value") MSG_DEF(JSMSG_CANT_SET_PROTO, 0, JSEXN_TYPEERR, "can't set prototype of this object") MSG_DEF(JSMSG_CANT_SET_PROTO_OF, 1, JSEXN_TYPEERR, "can't set prototype of {0}") MSG_DEF(JSMSG_CANT_SET_PROTO_CYCLE, 0, JSEXN_TYPEERR, "can't set prototype: it would cause a prototype chain cycle") diff --git a/js/src/jsiter.cpp b/js/src/jsiter.cpp index 63c81e6af..c1ae5dc15 100644 --- a/js/src/jsiter.cpp +++ b/js/src/jsiter.cpp @@ -12,6 +12,7 @@ #include "mozilla/Maybe.h" #include "mozilla/MemoryReporting.h" #include "mozilla/PodOperations.h" +#include "mozilla/Unused.h" #include "jsarray.h" #include "jsatom.h" @@ -1229,6 +1230,7 @@ js::CloseIterator(JSContext* cx, HandleObject obj) } return LegacyGeneratorObject::close(cx, obj); } + return true; } @@ -1246,6 +1248,56 @@ js::UnwindIteratorForException(JSContext* cx, HandleObject obj) return true; } +bool +js::IteratorCloseForException(JSContext* cx, HandleObject obj) +{ + MOZ_ASSERT(cx->isExceptionPending()); + + bool isClosingGenerator = cx->isClosingGenerator(); + JS::AutoSaveExceptionState savedExc(cx); + + // Implements IteratorClose (ES 7.4.6) for exception unwinding. See + // also the bytecode generated by BytecodeEmitter::emitIteratorClose. + + // Step 3. + // + // Get the "return" method. + RootedValue returnMethod(cx); + if (!GetProperty(cx, obj, obj, cx->names().return_, &returnMethod)) + return false; + + // Step 4. + // + // Do nothing if "return" is null or undefined. Throw a TypeError if the + // method is not IsCallable. + if (returnMethod.isNullOrUndefined()) + return true; + if (!IsCallable(returnMethod)) + return ReportIsNotFunction(cx, returnMethod); + + // Step 5, 6, 8. + // + // Call "return" if it is not null or undefined. + RootedValue rval(cx); + bool ok = Call(cx, returnMethod, obj, &rval); + if (isClosingGenerator) { + // Closing an iterator is implemented as an exception, but in spec + // terms it is a Completion value with [[Type]] return. In this case + // we *do* care if the call threw and if it returned an object. + if (!ok) + return false; + if (!rval.isObject()) + return ThrowCheckIsObject(cx, CheckIsObjectKind::IteratorReturn); + } else { + // We don't care if the call threw or that it returned an Object, as + // Step 6 says if IteratorClose is being called during a throw, the + // original throw has primacy. + savedExc.restore(); + } + + return true; +} + void js::UnwindIteratorForUncatchableException(JSContext* cx, JSObject* obj) { diff --git a/js/src/jsiter.h b/js/src/jsiter.h index 35d7ef118..f11f09b55 100644 --- a/js/src/jsiter.h +++ b/js/src/jsiter.h @@ -189,6 +189,9 @@ CloseIterator(JSContext* cx, HandleObject iterObj); bool UnwindIteratorForException(JSContext* cx, HandleObject obj); +bool +IteratorCloseForException(JSContext* cx, HandleObject obj); + void UnwindIteratorForUncatchableException(JSContext* cx, JSObject* obj); diff --git a/js/src/jsscript.h b/js/src/jsscript.h index 690bc225d..b48d48686 100644 --- a/js/src/jsscript.h +++ b/js/src/jsscript.h @@ -84,7 +84,8 @@ enum JSTryNoteKind { JSTRY_FINALLY, JSTRY_FOR_IN, JSTRY_FOR_OF, - JSTRY_LOOP + JSTRY_LOOP, + JSTRY_ITERCLOSE }; /* diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 3f56981cd..294dd935d 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -2589,7 +2589,8 @@ JS_STATIC_ASSERT(JSTRY_CATCH == 0); JS_STATIC_ASSERT(JSTRY_FINALLY == 1); JS_STATIC_ASSERT(JSTRY_FOR_IN == 2); -static const char* const TryNoteNames[] = { "catch", "finally", "for-in", "for-of", "loop" }; +static const char* const TryNoteNames[] = { "catch", "finally", "for-in", "for-of", "loop", + "iterclose" }; static MOZ_MUST_USE bool TryNotes(JSContext* cx, HandleScript script, Sprinter* sp) @@ -2597,15 +2598,15 @@ TryNotes(JSContext* cx, HandleScript script, Sprinter* sp) if (!script->hasTrynotes()) return true; - if (sp->put("\nException table:\nkind stack start end\n") < 0) + if (sp->put("\nException table:\nkind stack start end\n") < 0) return false; JSTryNote* tn = script->trynotes()->vector; JSTryNote* tnlimit = tn + script->trynotes()->length; do { MOZ_ASSERT(tn->kind < ArrayLength(TryNoteNames)); - uint8_t startOff = script->pcToOffset(script->main()) + tn->start; - if (!sp->jsprintf(" %-7s %6u %8u %8u\n", + uint32_t startOff = script->pcToOffset(script->main()) + tn->start; + if (!sp->jsprintf(" %-9s %6u %8u %8u\n", TryNoteNames[tn->kind], tn->stackDepth, startOff, startOff + tn->length)) { diff --git a/js/src/tests/ecma_6/Statements/for-of-iterator-close.js b/js/src/tests/ecma_6/Statements/for-of-iterator-close.js new file mode 100644 index 000000000..b28895d8f --- /dev/null +++ b/js/src/tests/ecma_6/Statements/for-of-iterator-close.js @@ -0,0 +1,102 @@ +// Tests that IteratorReturn is called when a for-of loop has an abrupt +// completion value during non-iterator code. + +function test() { + var returnCalled = 0; + var returnCalledExpected = 0; + var iterable = {}; + iterable[Symbol.iterator] = makeIterator({ + ret: function() { + returnCalled++; + return {}; + } + }); + + // break calls iter.return + for (var x of iterable) + break; + assertEq(returnCalled, ++returnCalledExpected); + + // throw in body calls iter.return + assertThrowsValue(function() { + for (var x of iterable) + throw "in body"; + }, "in body"); + assertEq(returnCalled, ++returnCalledExpected); + + // throw in lhs ref calls iter.return + function throwlhs() { + throw "in lhs"; + } + assertThrowsValue(function() { + for ((throwlhs()) of iterable) + continue; + }, "in lhs"); + assertEq(returnCalled, ++returnCalledExpected); + + // throw in iter.return doesn't re-call iter.return + iterable[Symbol.iterator] = makeIterator({ + ret: function() { + returnCalled++; + throw "in iter.return"; + } + }); + assertThrowsValue(function() { + for (var x of iterable) + break; + }, "in iter.return"); + assertEq(returnCalled, ++returnCalledExpected); + + // throw in iter.next doesn't call IteratorClose + iterable[Symbol.iterator] = makeIterator({ + next: function() { + throw "in next"; + } + }); + assertThrowsValue(function() { + for (var x of iterable) + break; + }, "in next"); + assertEq(returnCalled, returnCalledExpected); + + // "return" must return an Object. + iterable[Symbol.iterator] = makeIterator({ + ret: function() { + returnCalled++; + return 42; + } + }); + assertThrowsInstanceOf(function() { + for (var x of iterable) + break; + }, TypeError); + assertEq(returnCalled, ++returnCalledExpected); + + // continue doesn't call iter.return for the loop it's continuing + var i = 0; + iterable[Symbol.iterator] = makeIterator({ + next: function() { + return { done: i++ > 5 }; + }, + ret: function() { + returnCalled++; + return {}; + } + }); + for (var x of iterable) + continue; + assertEq(returnCalled, returnCalledExpected); + + // continue does call iter.return for loops it skips + i = 0; + L: do { + for (var x of iterable) + continue L; + } while (false); + assertEq(returnCalled, ++returnCalledExpected); +} + +test(); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/shell.js b/js/src/tests/ecma_6/shell.js index 06be6f2db..1d067a282 100644 --- a/js/src/tests/ecma_6/shell.js +++ b/js/src/tests/ecma_6/shell.js @@ -3,21 +3,39 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ (function(global) { - /** Yield every permutation of the elements in some array. */ - global.Permutations = function* Permutations(items) { - if (items.length == 0) { - yield []; - } else { - items = items.slice(0); - for (let i = 0; i < items.length; i++) { - let swap = items[0]; - items[0] = items[i]; - items[i] = swap; - for (let e of Permutations(items.slice(1, items.length))) - yield [items[0]].concat(e); - } - } - }; + /** Yield every permutation of the elements in some array. */ + global.Permutations = function* Permutations(items) { + if (items.length == 0) { + yield []; + } else { + items = items.slice(0); + for (let i = 0; i < items.length; i++) { + let swap = items[0]; + items[0] = items[i]; + items[i] = swap; + for (let e of Permutations(items.slice(1, items.length))) + yield [items[0]].concat(e); + } + } + }; + + /** Make an iterator with a return method. */ + global.makeIterator = function makeIterator(overrides) { + var iterator = { + next: function() { + if (overrides && overrides.next) + return overrides.next(); + return { done: false }; + }, + return: function() { + if (overrides && overrides.ret) + return overrides.ret(); + return { done: true }; + } + }; + + return function() { return iterator; }; + }; })(this); if (typeof assertThrowsInstanceOf === 'undefined') { diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 8ae9c43b0..489d0a155 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -1171,13 +1171,19 @@ ProcessTryNotes(JSContext* cx, EnvironmentIter& ei, InterpreterRegs& regs) SettleOnTryNote(cx, tn, ei, regs); return FinallyContinuation; - case JSTRY_FOR_IN: { + case JSTRY_FOR_IN: + case JSTRY_ITERCLOSE: { /* This is similar to JSOP_ENDITER in the interpreter loop. */ DebugOnly pc = regs.fp()->script()->main() + tn->start + tn->length; - MOZ_ASSERT(JSOp(*pc) == JSOP_ENDITER); + MOZ_ASSERT_IF(tn->kind == JSTRY_FOR_IN, JSOp(*pc) == JSOP_ENDITER); Value* sp = regs.spForStackDepth(tn->stackDepth); RootedObject obj(cx, &sp[-1].toObject()); - if (!UnwindIteratorForException(cx, obj)) { + bool ok; + if (tn->kind == JSTRY_FOR_IN) + ok = UnwindIteratorForException(cx, obj); + else + ok = IteratorCloseForException(cx, obj); + if (!ok) { // We should only settle on the note only if // UnwindIteratorForException itself threw, as // onExceptionUnwind should be called anew with the new @@ -5040,7 +5046,12 @@ js::ThrowCheckIsObject(JSContext* cx, CheckIsObjectKind kind) { switch (kind) { case CheckIsObjectKind::IteratorNext: - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NEXT_RETURNED_PRIMITIVE); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_ITER_METHOD_RETURNED_PRIMITIVE, "next"); + break; + case CheckIsObjectKind::IteratorReturn: + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_ITER_METHOD_RETURNED_PRIMITIVE, "return"); break; case CheckIsObjectKind::GetIterator: JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_GET_ITER_RETURNED_PRIMITIVE); diff --git a/js/src/vm/Interpreter.h b/js/src/vm/Interpreter.h index 1ffe1fdca..3a24240fc 100644 --- a/js/src/vm/Interpreter.h +++ b/js/src/vm/Interpreter.h @@ -562,6 +562,7 @@ ReportRuntimeRedeclaration(JSContext* cx, HandlePropertyName name, const char* r enum class CheckIsObjectKind : uint8_t { IteratorNext, + IteratorReturn, GetIterator }; -- cgit v1.2.3 From 2d2a60cdae0fb5ac13eb544e54495f54ac886c6c Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 24 Mar 2018 12:11:23 +0100 Subject: Bug 1147371: Rename allowContentSpread to allowContentIter Issue #74 --- js/src/frontend/BytecodeEmitter.cpp | 30 ++++++++++++++++++++---------- js/src/frontend/BytecodeEmitter.h | 2 +- js/src/vm/CommonPropertyNames.h | 2 +- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index fb141e92d..2934f63a5 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -6676,7 +6676,17 @@ BytecodeEmitter::emitForOf(ParseNode* forOfLoop, EmitterScope* headLexicalEmitte if (!emit1(JSOP_DUP)) // ITER ITER return false; - if (!emitIteratorNext(forOfHead)) // ITER RESULT + // Certain builtins (e.g. Array.from) are implemented in self-hosting + // as for-of loops. + bool allowSelfHostedIter = false; + if (emitterMode == BytecodeEmitter::SelfHosting && + forHeadExpr->isKind(PNK_CALL) && + forHeadExpr->pn_head->name() == cx->names().allowContentIter) + { + allowSelfHostedIter = true; + } + + if (!emitIteratorNext(forOfHead, allowSelfHostedIter)) // ITER RESULT return false; if (!emit1(JSOP_DUP)) // ITER RESULT RESULT return false; @@ -8438,10 +8448,10 @@ BytecodeEmitter::emitSelfHostedForceInterpreter(ParseNode* pn) } bool -BytecodeEmitter::emitSelfHostedAllowContentSpread(ParseNode* pn) +BytecodeEmitter::emitSelfHostedAllowContentIter(ParseNode* pn) { if (pn->pn_count != 2) { - reportError(pn, JSMSG_MORE_ARGS_NEEDED, "allowContentSpread", "1", ""); + reportError(pn, JSMSG_MORE_ARGS_NEEDED, "allowContentIter", "1", ""); return false; } @@ -8467,7 +8477,7 @@ BytecodeEmitter::isRestParameter(ParseNode* pn, bool* result) if (!pn->isKind(PNK_NAME)) { if (emitterMode == BytecodeEmitter::SelfHosting && pn->isKind(PNK_CALL)) { ParseNode* pn2 = pn->pn_head; - if (pn2->getKind() == PNK_NAME && pn2->name() == cx->names().allowContentSpread) + if (pn2->getKind() == PNK_NAME && pn2->name() == cx->names().allowContentIter) return isRestParameter(pn2->pn_next, result); } *result = false; @@ -8575,8 +8585,8 @@ BytecodeEmitter::emitCallOrNew(ParseNode* pn) return emitSelfHostedResumeGenerator(pn); if (pn2->name() == cx->names().forceInterpreter) return emitSelfHostedForceInterpreter(pn); - if (pn2->name() == cx->names().allowContentSpread) - return emitSelfHostedAllowContentSpread(pn); + if (pn2->name() == cx->names().allowContentIter) + return emitSelfHostedAllowContentIter(pn); // Fall through. } if (!emitGetName(pn2, callop)) @@ -9231,7 +9241,7 @@ BytecodeEmitter::emitArray(ParseNode* pn, uint32_t count, JSOp op) if (!updateSourceCoordNotes(pn2->pn_pos.begin)) return false; - bool allowSelfHostedSpread = false; + bool allowSelfHostedIter = false; if (pn2->isKind(PNK_ELISION)) { if (!emit1(JSOP_HOLE)) return false; @@ -9242,9 +9252,9 @@ BytecodeEmitter::emitArray(ParseNode* pn, uint32_t count, JSOp op) if (emitterMode == BytecodeEmitter::SelfHosting && expr->isKind(PNK_CALL) && - expr->pn_head->name() == cx->names().allowContentSpread) + expr->pn_head->name() == cx->names().allowContentIter) { - allowSelfHostedSpread = true; + allowSelfHostedIter = true; } } else { expr = pn2; @@ -9259,7 +9269,7 @@ BytecodeEmitter::emitArray(ParseNode* pn, uint32_t count, JSOp op) return false; if (!emit2(JSOP_PICK, 2)) // ITER ARRAY INDEX return false; - if (!emitSpread(allowSelfHostedSpread)) // ARRAY INDEX + if (!emitSpread(allowSelfHostedIter)) // ARRAY INDEX return false; } else if (afterSpread) { if (!emit1(JSOP_INITELEM_INC)) diff --git a/js/src/frontend/BytecodeEmitter.h b/js/src/frontend/BytecodeEmitter.h index 6b39069de..156abedbe 100644 --- a/js/src/frontend/BytecodeEmitter.h +++ b/js/src/frontend/BytecodeEmitter.h @@ -732,7 +732,7 @@ struct MOZ_STACK_CLASS BytecodeEmitter MOZ_MUST_USE bool emitSelfHostedCallFunction(ParseNode* pn); MOZ_MUST_USE bool emitSelfHostedResumeGenerator(ParseNode* pn); MOZ_MUST_USE bool emitSelfHostedForceInterpreter(ParseNode* pn); - MOZ_MUST_USE bool emitSelfHostedAllowContentSpread(ParseNode* pn); + MOZ_MUST_USE bool emitSelfHostedAllowContentIter(ParseNode* pn); MOZ_MUST_USE bool emitComprehensionFor(ParseNode* compFor); MOZ_MUST_USE bool emitComprehensionForIn(ParseNode* pn); diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index bd0705446..e971dc844 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -13,7 +13,7 @@ #define FOR_EACH_COMMON_PROPERTYNAME(macro) \ macro(add, add, "add") \ - macro(allowContentSpread, allowContentSpread, "allowContentSpread") \ + macro(allowContentIter, allowContentIter, "allowContentIter") \ macro(anonymous, anonymous, "anonymous") \ macro(Any, Any, "Any") \ macro(apply, apply, "apply") \ -- cgit v1.2.3 From 4b487efb58bfba4c3f67d898e86b9f6daaab59b2 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 24 Mar 2018 12:15:02 +0100 Subject: Bug 1147371: Convert self-hosted code that need to call IteratorClose to use for-of Issue #74 --- js/src/builtin/Array.js | 46 ++++++++++++--------------------------------- js/src/builtin/Classes.js | 2 +- js/src/builtin/Map.js | 38 ++++--------------------------------- js/src/builtin/Set.js | 36 +++-------------------------------- js/src/builtin/Utilities.js | 23 ----------------------- js/src/builtin/WeakMap.js | 38 ++++--------------------------------- js/src/builtin/WeakSet.js | 36 +++-------------------------------- 7 files changed, 27 insertions(+), 192 deletions(-) diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js index 5ab0b71be..45f90a7b8 100644 --- a/js/src/builtin/Array.js +++ b/js/src/builtin/Array.js @@ -803,54 +803,32 @@ function ArrayFrom(items, mapfn=undefined, thisArg=undefined) { // Steps 5.a-c. var A = IsConstructor(C) ? new C() : []; - // Step 5.c. - var iterator = GetIterator(items, usingIterator); - // Step 5.d. var k = 0; - // Step 5.e. - // These steps cannot be implemented using a for-of loop. - // See . - while (true) { + // Step 5.c, 5.e. + var iteratorWrapper = { [std_iterator]() { return GetIterator(items, usingIterator); } }; + for (var nextValue of allowContentIter(iteratorWrapper)) { // Step 5.e.i. // Disabled for performance reason. We won't hit this case on // normal array, since _DefineDataProperty will throw before it. // We could hit this when |A| is a proxy and it ignores // |_DefineDataProperty|, but it happens only after too long loop. /* - if (k >= 0x1fffffffffffff) { - IteratorCloseThrow(iterator); + if (k >= 0x1fffffffffffff) ThrowTypeError(JSMSG_TOO_LONG_ARRAY); - } */ - // Step 5.e.iii. - var next = callContentFunction(iterator.next, iterator); - if (!IsObject(next)) - ThrowTypeError(JSMSG_NEXT_RETURNED_PRIMITIVE); - - // Step 5.e.iv. - if (next.done) { - A.length = k; - return A; - } - - // Steps 5.e.v. - var nextValue = next.value; - // Steps 5.e.vi-vii. - try { - var mappedValue = mapping ? callContentFunction(mapfn, thisArg, nextValue, k) : nextValue; - - // Steps 5.e.ii (reordered), 5.e.viii. - _DefineDataProperty(A, k++, mappedValue); - } catch (e) { - // Steps 5.e.vi.2, 5.e.ix. - IteratorCloseThrow(iterator); - throw e; - } + var mappedValue = mapping ? callContentFunction(mapfn, thisArg, nextValue, k) : nextValue; + + // Steps 5.e.ii (reordered), 5.e.viii. + _DefineDataProperty(A, k++, mappedValue); } + + // Step 5.e.iv. + A.length = k; + return A; } // Step 7. diff --git a/js/src/builtin/Classes.js b/js/src/builtin/Classes.js index d0f20b5fd..24841d605 100644 --- a/js/src/builtin/Classes.js +++ b/js/src/builtin/Classes.js @@ -5,7 +5,7 @@ var DefaultDerivedClassConstructor = class extends null { constructor(...args) { - super(...allowContentSpread(args)); + super(...allowContentIter(args)); } }; MakeDefaultConstructor(DefaultDerivedClassConstructor); diff --git a/js/src/builtin/Map.js b/js/src/builtin/Map.js index 27a12bfff..580629a13 100644 --- a/js/src/builtin/Map.js +++ b/js/src/builtin/Map.js @@ -14,44 +14,14 @@ function MapConstructorInit(iterable) { if (!IsCallable(adder)) ThrowTypeError(JSMSG_NOT_FUNCTION, typeof adder); - // Step 6.c. - var iterFn = iterable[std_iterator]; - if (!IsCallable(iterFn)) - ThrowTypeError(JSMSG_NOT_ITERABLE, DecompileArg(0, iterable)); - - var iter = callContentFunction(iterFn, iterable); - if (!IsObject(iter)) - ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, typeof iter); - - // Step 7 (not applicable). - - // Step 8. - while (true) { - // Step 8.a. - var next = callContentFunction(iter.next, iter); - if (!IsObject(next)) - ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, typeof next); - - // Step 8.b. - if (next.done) - return; - - // Step 8.c. - var nextItem = next.value; - + // Steps 6.c-8. + for (var nextItem of allowContentIter(iterable)) { // Step 8.d. - if (!IsObject(nextItem)) { - IteratorCloseThrow(iter); + if (!IsObject(nextItem)) ThrowTypeError(JSMSG_INVALID_MAP_ITERABLE, "Map"); - } // Steps 8.e-j. - try { - callContentFunction(adder, map, nextItem[0], nextItem[1]); - } catch (e) { - IteratorCloseThrow(iter); - throw e; - } + callContentFunction(adder, map, nextItem[0], nextItem[1]); } } diff --git a/js/src/builtin/Set.js b/js/src/builtin/Set.js index c61a49ef8..9af6cf8d1 100644 --- a/js/src/builtin/Set.js +++ b/js/src/builtin/Set.js @@ -14,39 +14,9 @@ function SetConstructorInit(iterable) { if (!IsCallable(adder)) ThrowTypeError(JSMSG_NOT_FUNCTION, typeof adder); - // Step 6.c. - var iterFn = iterable[std_iterator]; - if (!IsCallable(iterFn)) - ThrowTypeError(JSMSG_NOT_ITERABLE, DecompileArg(0, iterable)); - - var iter = callContentFunction(iterFn, iterable); - if (!IsObject(iter)) - ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, typeof iter); - - // Step 7 (not applicable). - - // Step 8. - while (true) { - // Step 8.a. - var next = callContentFunction(iter.next, iter); - if (!IsObject(next)) - ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, typeof next); - - // Step 8.b. - if (next.done) - return; - - // Step 8.c. - var nextValue = next.value; - - // Steps 8.d-e. - try { - callContentFunction(adder, set, nextValue); - } catch (e) { - IteratorCloseThrow(iter); - throw e; - } - } + // Steps 6.c-8. + for (var nextValue of allowContentIter(iterable)) + callContentFunction(adder, set, nextValue); } /* ES6 20121122 draft 15.16.4.6. */ diff --git a/js/src/builtin/Utilities.js b/js/src/builtin/Utilities.js index 2dece3801..bfb1fe7f4 100644 --- a/js/src/builtin/Utilities.js +++ b/js/src/builtin/Utilities.js @@ -154,29 +154,6 @@ function GetIterator(obj, method) { return iterator; } -// ES2017 draft rev 7.4.6. -// When completion.[[Type]] is throw. -function IteratorCloseThrow(iter) { - // Steps 1-2 (implicit) - - // Step 3. - var returnMethod = GetMethod(iter, "return"); - - // Step 4 (done in caller). - if (returnMethod === undefined) - return; - - try { - // Step 5. - callContentFunction(returnMethod, iter); - } catch (e) { - } - - // Step 6 (done in caller). - - // Steps 7-9 (skipped). -} - var _builtinCtorsCache = {__proto__: null}; function GetBuiltinConstructor(builtinName) { diff --git a/js/src/builtin/WeakMap.js b/js/src/builtin/WeakMap.js index 708be8424..6755b7a7b 100644 --- a/js/src/builtin/WeakMap.js +++ b/js/src/builtin/WeakMap.js @@ -14,43 +14,13 @@ function WeakMapConstructorInit(iterable) { if (!IsCallable(adder)) ThrowTypeError(JSMSG_NOT_FUNCTION, typeof adder); - // Step 6.c. - var iterFn = iterable[std_iterator]; - if (!IsCallable(iterFn)) - ThrowTypeError(JSMSG_NOT_ITERABLE, DecompileArg(0, iterable)); - - var iter = callContentFunction(iterFn, iterable); - if (!IsObject(iter)) - ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, typeof iter); - - // Step 7 (not applicable). - - // Step 8. - while (true) { - // Step 8.a. - var next = callContentFunction(iter.next, iter); - if (!IsObject(next)) - ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, typeof next); - - // Step 8.b. - if (next.done) - return; - - // Step 8.c. - var nextItem = next.value; - + // Steps 6.c-8. + for (var nextItem of allowContentIter(iterable)) { // Step 8.d. - if (!IsObject(nextItem)) { - IteratorCloseThrow(iter); + if (!IsObject(nextItem)) ThrowTypeError(JSMSG_INVALID_MAP_ITERABLE, "WeakMap"); - } // Steps 8.e-j. - try { - callContentFunction(adder, map, nextItem[0], nextItem[1]); - } catch (e) { - IteratorCloseThrow(iter); - throw e; - } + callContentFunction(adder, map, nextItem[0], nextItem[1]); } } diff --git a/js/src/builtin/WeakSet.js b/js/src/builtin/WeakSet.js index 8589f9dc6..b16b4634d 100644 --- a/js/src/builtin/WeakSet.js +++ b/js/src/builtin/WeakSet.js @@ -14,39 +14,9 @@ function WeakSetConstructorInit(iterable) { if (!IsCallable(adder)) ThrowTypeError(JSMSG_NOT_FUNCTION, typeof adder); - // Step 6.c. - var iterFn = iterable[std_iterator]; - if (!IsCallable(iterFn)) - ThrowTypeError(JSMSG_NOT_ITERABLE, DecompileArg(0, iterable)); - - var iter = callContentFunction(iterFn, iterable); - if (!IsObject(iter)) - ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, typeof iter); - - // Step 7 (not applicable). - - // Step 8. - while (true) { - // Step 8.a. - var next = callContentFunction(iter.next, iter); - if (!IsObject(next)) - ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, typeof next); - - // Step 8.b. - if (next.done) - return; - - // Step 8.c. - var nextValue = next.value; - - // Steps 8.d-e. - try { - callContentFunction(adder, set, nextValue); - } catch (e) { - IteratorCloseThrow(iter); - throw e; - } - } + // Steps 6.c-8. + for (var nextValue of allowContentIter(iterable)) + callContentFunction(adder, set, nextValue); } // 23.4.3.1 -- cgit v1.2.3 From 2bb0252ab48a97a72c33cef9cbe54e86563f15c9 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 24 Mar 2018 12:23:14 +0100 Subject: Bug 1147371: Implement IteratorClose for array destructuring Issue #74 --- js/src/frontend/BytecodeEmitter.cpp | 218 ++++++++++++--------- js/src/frontend/BytecodeEmitter.h | 3 +- js/src/jit/JitFrames.cpp | 82 +++++--- js/src/jsscript.h | 3 +- js/src/shell/js.cpp | 35 +++- .../ecma_6/Destructuring/array-iterator-close.js | 77 ++++++++ js/src/vm/Interpreter.cpp | 39 +++- 7 files changed, 322 insertions(+), 135 deletions(-) create mode 100644 js/src/tests/ecma_6/Destructuring/array-iterator-close.js diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index 2934f63a5..e2c906850 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -4947,7 +4947,7 @@ BytecodeEmitter::emitIteratorClose(Maybe yieldStarTryStart, bool all template bool -BytecodeEmitter::wrapWithIteratorCloseTryNote(int32_t iterDepth, InnerEmitter emitter) +BytecodeEmitter::wrapWithDestructuringIteratorCloseTryNote(int32_t iterDepth, InnerEmitter emitter) { MOZ_ASSERT(this->stackDepth >= iterDepth); @@ -4956,7 +4956,7 @@ BytecodeEmitter::wrapWithIteratorCloseTryNote(int32_t iterDepth, InnerEmitter em return false; ptrdiff_t end = offset(); if (start != end) - return tryNoteList.append(JSTRY_ITERCLOSE, iterDepth, start, end); + return tryNoteList.append(JSTRY_DESTRUCTURING_ITERCLOSE, iterDepth, start, end); return true; } @@ -5057,6 +5057,9 @@ BytecodeEmitter::emitDestructuringOpsArray(ParseNode* pattern, DestructuringFlav // Here's pseudo code for |let [a, b, , c=y, ...d] = x;| // + // Lines that are annotated "covered by trynote" mean that upon throwing + // an exception, IteratorClose is called on iter only if done is false. + // // let x, y; // let a, b, c, d; // let iter, lref, result, done, value; // stack values @@ -5064,7 +5067,7 @@ BytecodeEmitter::emitDestructuringOpsArray(ParseNode* pattern, DestructuringFlav // iter = x[Symbol.iterator](); // // // ==== emitted by loop for a ==== - // lref = GetReference(a); + // lref = GetReference(a); // covered by trynote // // result = iter.next(); // done = result.done; @@ -5074,10 +5077,10 @@ BytecodeEmitter::emitDestructuringOpsArray(ParseNode* pattern, DestructuringFlav // else // value = result.value; // - // SetOrInitialize(lref, value); + // SetOrInitialize(lref, value); // covered by trynote // // // ==== emitted by loop for b ==== - // lref = GetReference(b); + // lref = GetReference(b); // covered by trynote // // if (done) { // value = undefined; @@ -5090,7 +5093,7 @@ BytecodeEmitter::emitDestructuringOpsArray(ParseNode* pattern, DestructuringFlav // value = result.value; // } // - // SetOrInitialize(lref, value); + // SetOrInitialize(lref, value); // covered by trynote // // // ==== emitted by loop for elision ==== // if (done) { @@ -5105,7 +5108,7 @@ BytecodeEmitter::emitDestructuringOpsArray(ParseNode* pattern, DestructuringFlav // } // // // ==== emitted by loop for c ==== - // lref = GetReference(c); + // lref = GetReference(c); // covered by trynote // // if (done) { // value = undefined; @@ -5119,19 +5122,23 @@ BytecodeEmitter::emitDestructuringOpsArray(ParseNode* pattern, DestructuringFlav // } // // if (value === undefined) - // value = y; + // value = y; // covered by trynote // - // SetOrInitialize(lref, value); + // SetOrInitialize(lref, value); // covered by trynote // // // ==== emitted by loop for d ==== - // lref = GetReference(d); + // lref = GetReference(d); // covered by trynote // // if (done) // value = []; // else // value = [...iter]; // - // SetOrInitialize(lref, value); + // SetOrInitialize(lref, value); // covered by trynote + // + // // === emitted after loop === + // if (!done) + // IteratorClose(iter); // Use an iterator to destructure the RHS, instead of index lookup. We // must leave the *original* value on the stack. @@ -5140,25 +5147,61 @@ BytecodeEmitter::emitDestructuringOpsArray(ParseNode* pattern, DestructuringFlav if (!emitIterator()) // ... OBJ ITER return false; + // For an empty pattern [], call IteratorClose unconditionally. Nothing + // else needs to be done. + if (!pattern->pn_head) + return emitIteratorClose(); // ... OBJ + + // Push an initial FALSE value for DONE. + if (!emit1(JSOP_FALSE)) // ... OBJ ITER FALSE + return false; + + // JSTRY_DESTRUCTURING_ITERCLOSE expects the iterator and the done value + // to be the second to top and the top of the stack, respectively. + // IteratorClose is called upon exception only if done is false. + int32_t tryNoteDepth = stackDepth; + for (ParseNode* member = pattern->pn_head; member; member = member->pn_next) { - bool isHead = member == pattern->pn_head; - bool hasNext = !!member->pn_next; + bool isFirst = member == pattern->pn_head; + DebugOnly hasNext = !!member->pn_next; - if (member->isKind(PNK_SPREAD)) { - size_t emitted = 0; - if (!emitDestructuringLHSRef(member, &emitted)) // ... OBJ ITER ?DONE *LREF + size_t emitted = 0; + + // Spec requires LHS reference to be evaluated first. + ParseNode* lhsPattern = member; + if (lhsPattern->isKind(PNK_ASSIGN)) + lhsPattern = lhsPattern->pn_left; + + bool isElision = lhsPattern->isKind(PNK_ELISION); + if (!isElision) { + auto emitLHSRef = [lhsPattern, &emitted](BytecodeEmitter* bce) { + return bce->emitDestructuringLHSRef(lhsPattern, &emitted); // ... OBJ ITER DONE *LREF + }; + if (!wrapWithDestructuringIteratorCloseTryNote(tryNoteDepth, emitLHSRef)) + return false; + } + + // Pick the DONE value to the top of the stack. + if (emitted) { + if (!emit2(JSOP_PICK, emitted)) // ... OBJ ITER *LREF DONE return false; + } + if (isFirst) { + // If this element is the first, DONE is always FALSE, so pop it. + // + // Non-first elements should emit if-else depending on the + // member pattern, below. + if (!emit1(JSOP_POP)) // ... OBJ ITER *LREF + return false; + } + + if (member->isKind(PNK_SPREAD)) { IfThenElseEmitter ifThenElse(this); - if (!isHead) { + if (!isFirst) { // If spread is not the first element of the pattern, // iterator can already be completed. - // ... OBJ ITER DONE *LREF - if (emitted) { - if (!emit2(JSOP_PICK, emitted)) // ... OBJ ITER *LREF DONE - return false; - } - + // ... OBJ ITER *LREF DONE if (!ifThenElse.emitIfElse()) // ... OBJ ITER *LREF return false; @@ -5181,13 +5224,22 @@ BytecodeEmitter::emitDestructuringOpsArray(ParseNode* pattern, DestructuringFlav if (!emit1(JSOP_POP)) // ... OBJ ITER *LREF ARRAY return false; - if (!isHead) { + if (!isFirst) { if (!ifThenElse.emitEnd()) return false; MOZ_ASSERT(ifThenElse.pushed() == 1); } - if (!emitSetOrInitializeDestructuring(member, flav)) // ... OBJ ITER + // At this point the iterator is done. Unpick a TRUE value for DONE above ITER. + if (!emit1(JSOP_TRUE)) // ... OBJ ITER *LREF ARRAY TRUE + return false; + if (!emit2(JSOP_UNPICK, emitted + 1)) // ... OBJ ITER TRUE *LREF ARRAY + return false; + + auto emitAssignment = [member, flav](BytecodeEmitter* bce) { + return bce->emitSetOrInitializeDestructuring(member, flav); // ... OBJ ITER TRUE + }; + if (!wrapWithDestructuringIteratorCloseTryNote(tryNoteDepth, emitAssignment)) return false; MOZ_ASSERT(!hasNext); @@ -5195,63 +5247,30 @@ BytecodeEmitter::emitDestructuringOpsArray(ParseNode* pattern, DestructuringFlav } ParseNode* pndefault = nullptr; - ParseNode* subpattern = member; - if (subpattern->isKind(PNK_ASSIGN)) { - pndefault = subpattern->pn_right; - subpattern = subpattern->pn_left; - } - - bool isElision = subpattern->isKind(PNK_ELISION); - - MOZ_ASSERT(!subpattern->isKind(PNK_SPREAD)); + if (member->isKind(PNK_ASSIGN)) + pndefault = member->pn_right; - size_t emitted = 0; - if (!isElision) { - if (!emitDestructuringLHSRef(subpattern, &emitted)) // ... OBJ ITER ?DONE *LREF - return false; - } + MOZ_ASSERT(!member->isKind(PNK_SPREAD)); IfThenElseEmitter ifAlreadyDone(this); - if (!isHead) { - // If this element is not the first element of the pattern, - // iterator can already be completed. - // ... OBJ ITER DONE *LREF - if (emitted) { - if (hasNext) { - if (!emitDupAt(emitted)) // ... OBJ ITER DONE *LREF DONE - return false; - } else { - if (!emit2(JSOP_PICK, emitted)) // ... OBJ ITER *LREF DONE - return false; - } - } else { - if (hasNext) { - // The position of LREF in the following stack comment - // isn't accurate for the operation, but it's equivalent - // since LREF is nothing - if (!emit1(JSOP_DUP)) // ... OBJ ITER DONE *LREF DONE - return false; - } - } - if (!ifAlreadyDone.emitIfElse()) // ... OBJ ITER ?DONE *LREF + if (!isFirst) { + // ... OBJ ITER *LREF DONE + if (!ifAlreadyDone.emitIfElse()) // ... OBJ ITER *LREF return false; - if (!emit1(JSOP_UNDEFINED)) // ... OBJ ITER ?DONE *LREF UNDEF + if (!emit1(JSOP_UNDEFINED)) // ... OBJ ITER *LREF UNDEF return false; - if (!emit1(JSOP_NOP_DESTRUCTURING)) // ... OBJ ITER ?DONE *LREF UNDEF + if (!emit1(JSOP_NOP_DESTRUCTURING)) // ... OBJ ITER *LREF UNDEF return false; - if (!ifAlreadyDone.emitElse()) // ... OBJ ITER ?DONE *LREF + // The iterator is done. Unpick a TRUE value for DONE above ITER. + if (!emit1(JSOP_TRUE)) // ... OBJ ITER *LREF UNDEF TRUE + return false; + if (!emit2(JSOP_UNPICK, emitted + 1)) // ... OBJ ITER TRUE *LREF UNDEF return false; - if (hasNext) { - if (emitted) { - if (!emit2(JSOP_PICK, emitted)) // ... OBJ ITER *LREF DONE - return false; - } - if (!emit1(JSOP_POP)) // ... OBJ ITER *LREF - return false; - } + if (!ifAlreadyDone.emitElse()) // ... OBJ ITER *LREF + return false; } if (emitted) { @@ -5268,59 +5287,74 @@ BytecodeEmitter::emitDestructuringOpsArray(ParseNode* pattern, DestructuringFlav if (!emitAtomOp(cx->names().done, JSOP_GETPROP)) // ... OBJ ITER *LREF RESULT DONE return false; - if (hasNext) { - if (!emit1(JSOP_DUP)) // ... OBJ ITER *LREF RESULT DONE DONE - return false; - if (!emit2(JSOP_UNPICK, emitted + 2)) // ... OBJ ITER DONE *LREF RESULT DONE - return false; - } + if (!emit1(JSOP_DUP)) // ... OBJ ITER *LREF RESULT DONE DONE + return false; + if (!emit2(JSOP_UNPICK, emitted + 2)) // ... OBJ ITER DONE *LREF RESULT DONE + return false; IfThenElseEmitter ifDone(this); - if (!ifDone.emitIfElse()) // ... OBJ ITER ?DONE *LREF RESULT + if (!ifDone.emitIfElse()) // ... OBJ ITER DONE *LREF RESULT return false; - if (!emit1(JSOP_POP)) // ... OBJ ITER ?DONE *LREF + if (!emit1(JSOP_POP)) // ... OBJ ITER DONE *LREF return false; - if (!emit1(JSOP_UNDEFINED)) // ... OBJ ITER ?DONE *LREF UNDEF + if (!emit1(JSOP_UNDEFINED)) // ... OBJ ITER DONE *LREF UNDEF return false; - if (!emit1(JSOP_NOP_DESTRUCTURING)) // ... OBJ ITER ?DONE *LREF UNDEF + if (!emit1(JSOP_NOP_DESTRUCTURING)) // ... OBJ ITER DONE *LREF UNDEF return false; - if (!ifDone.emitElse()) // ... OBJ ITER ?DONE *LREF RESULT + if (!ifDone.emitElse()) // ... OBJ ITER DONE *LREF RESULT return false; - if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // ... OBJ ITER ?DONE *LREF VALUE + if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // ... OBJ ITER DONE *LREF VALUE return false; if (!ifDone.emitEnd()) return false; MOZ_ASSERT(ifDone.pushed() == 0); - if (!isHead) { + if (!isFirst) { if (!ifAlreadyDone.emitEnd()) return false; - MOZ_ASSERT(ifAlreadyDone.pushed() == 1); + MOZ_ASSERT(ifAlreadyDone.pushed() == 2); } if (pndefault) { - if (!emitDefault(pndefault, subpattern)) // ... OBJ ITER ?DONE *LREF VALUE + auto emitDefault = [pndefault, lhsPattern](BytecodeEmitter* bce) { + return bce->emitDefault(pndefault, lhsPattern); // ... OBJ ITER DONE *LREF VALUE + }; + + if (!wrapWithDestructuringIteratorCloseTryNote(tryNoteDepth, emitDefault)) return false; } if (!isElision) { - if (!emitSetOrInitializeDestructuring(subpattern, - flav)) // ... OBJ ITER ?DONE - { + auto emitAssignment = [lhsPattern, flav](BytecodeEmitter* bce) { + return bce->emitSetOrInitializeDestructuring(lhsPattern, flav); // ... OBJ ITER DONE + }; + + if (!wrapWithDestructuringIteratorCloseTryNote(tryNoteDepth, emitAssignment)) return false; - } } else { - if (!emit1(JSOP_POP)) // ... OBJ ITER ?DONE + if (!emit1(JSOP_POP)) // ... OBJ ITER DONE return false; } } + // The last DONE value is on top of the stack. If not DONE, call + // IteratorClose. + // ... OBJ ITER DONE + IfThenElseEmitter ifDone(this); + if (!ifDone.emitIfElse()) // ... OBJ ITER + return false; if (!emit1(JSOP_POP)) // ... OBJ return false; + if (!ifDone.emitElse()) // ... OBJ ITER + return false; + if (!emitIteratorClose()) // ... OBJ + return false; + if (!ifDone.emitEnd()) + return false; return true; } diff --git a/js/src/frontend/BytecodeEmitter.h b/js/src/frontend/BytecodeEmitter.h index 156abedbe..78eb3510c 100644 --- a/js/src/frontend/BytecodeEmitter.h +++ b/js/src/frontend/BytecodeEmitter.h @@ -684,7 +684,8 @@ struct MOZ_STACK_CLASS BytecodeEmitter bool allowSelfHosted = false); template - MOZ_MUST_USE bool wrapWithIteratorCloseTryNote(int32_t iterDepth, InnerEmitter emitter); + MOZ_MUST_USE bool wrapWithDestructuringIteratorCloseTryNote(int32_t iterDepth, + InnerEmitter emitter); // Check if the value on top of the stack is "undefined". If so, replace // that value on the stack with the value defined by |defaultExpr|. diff --git a/js/src/jit/JitFrames.cpp b/js/src/jit/JitFrames.cpp index d6c6fc4c9..30bbd0b9b 100644 --- a/js/src/jit/JitFrames.cpp +++ b/js/src/jit/JitFrames.cpp @@ -330,28 +330,44 @@ NumArgAndLocalSlots(const InlineFrameIterator& frame) static void CloseLiveIteratorIon(JSContext* cx, const InlineFrameIterator& frame, JSTryNote* tn) { - MOZ_ASSERT(tn->kind == JSTRY_FOR_IN || tn->kind == JSTRY_ITERCLOSE); - MOZ_ASSERT(tn->stackDepth > 0); + MOZ_ASSERT(tn->kind == JSTRY_FOR_IN || + tn->kind == JSTRY_ITERCLOSE || + tn->kind == JSTRY_DESTRUCTURING_ITERCLOSE); + + bool isDestructuring = tn->kind == JSTRY_DESTRUCTURING_ITERCLOSE; + MOZ_ASSERT_IF(!isDestructuring, tn->stackDepth > 0); + MOZ_ASSERT_IF(isDestructuring, tn->stackDepth > 1); SnapshotIterator si = frame.snapshotIterator(); - // Skip stack slots until we reach the iterator object. + // Skip stack slots until we reach the iterator object on the stack. For + // the destructuring case, we also need to get the "done" value. uint32_t stackSlot = tn->stackDepth; - uint32_t skipSlots = NumArgAndLocalSlots(frame) + stackSlot - 1; + uint32_t adjust = isDestructuring ? 2 : 1; + uint32_t skipSlots = NumArgAndLocalSlots(frame) + stackSlot - adjust; for (unsigned i = 0; i < skipSlots; i++) si.skip(); Value v = si.read(); - RootedObject obj(cx, &v.toObject()); + RootedObject iterObject(cx, &v.toObject()); + + if (isDestructuring) { + Value v = si.read(); + MOZ_ASSERT(v.isBoolean()); + // Do not call IteratorClose if the destructuring iterator is already + // done. + if (v.isTrue()) + return; + } if (cx->isExceptionPending()) { if (tn->kind == JSTRY_FOR_IN) - UnwindIteratorForException(cx, obj); + UnwindIteratorForException(cx, iterObject); else - IteratorCloseForException(cx, obj); + IteratorCloseForException(cx, iterObject); } else { - UnwindIteratorForUncatchableException(cx, obj); + UnwindIteratorForUncatchableException(cx, iterObject); } } @@ -426,14 +442,12 @@ HandleExceptionIon(JSContext* cx, const InlineFrameIterator& frame, ResumeFromEx switch (tn->kind) { case JSTRY_FOR_IN: - case JSTRY_ITERCLOSE: { + case JSTRY_ITERCLOSE: + case JSTRY_DESTRUCTURING_ITERCLOSE: MOZ_ASSERT_IF(tn->kind == JSTRY_FOR_IN, JSOp(*(script->main() + tn->start + tn->length)) == JSOP_ENDITER); - MOZ_ASSERT(tn->stackDepth > 0); - CloseLiveIteratorIon(cx, frame, tn); break; - } case JSTRY_FOR_OF: case JSTRY_LOOP: @@ -607,28 +621,52 @@ ProcessTryNotesBaseline(JSContext* cx, const JitFrameIterator& frame, Environmen return true; } - case JSTRY_FOR_IN: - case JSTRY_ITERCLOSE: { + case JSTRY_FOR_IN: { uint8_t* framePointer; uint8_t* stackPointer; BaselineFrameAndStackPointersFromTryNote(tn, frame, &framePointer, &stackPointer); - Value iterValue(*(reinterpret_cast(stackPointer))); + Value iterValue(*reinterpret_cast(stackPointer)); RootedObject iterObject(cx, &iterValue.toObject()); - bool ok; - if (tn->kind == JSTRY_FOR_IN) - ok = UnwindIteratorForException(cx, iterObject); - else - ok = IteratorCloseForException(cx, iterObject); - if (!ok) { + if (!UnwindIteratorForException(cx, iterObject)) { // See comment in the JSTRY_FOR_IN case in Interpreter.cpp's // ProcessTryNotes. SettleOnTryNote(cx, tn, frame, ei, rfe, pc); - MOZ_ASSERT_IF(tn->kind == JSTRY_FOR_IN, **pc == JSOP_ENDITER); + MOZ_ASSERT(**pc == JSOP_ENDITER); + return false; + } + break; + } + + case JSTRY_ITERCLOSE: { + uint8_t* framePointer; + uint8_t* stackPointer; + BaselineFrameAndStackPointersFromTryNote(tn, frame, &framePointer, &stackPointer); + Value iterValue(*reinterpret_cast(stackPointer)); + RootedObject iterObject(cx, &iterValue.toObject()); + if (!IteratorCloseForException(cx, iterObject)) { + SettleOnTryNote(cx, tn, frame, ei, rfe, pc); return false; } break; } + case JSTRY_DESTRUCTURING_ITERCLOSE: { + uint8_t* framePointer; + uint8_t* stackPointer; + BaselineFrameAndStackPointersFromTryNote(tn, frame, &framePointer, &stackPointer); + Value doneValue(*(reinterpret_cast(stackPointer))); + MOZ_ASSERT(doneValue.isBoolean()); + if (doneValue.isFalse()) { + Value iterValue(*(reinterpret_cast(stackPointer) + 1)); + RootedObject iterObject(cx, &iterValue.toObject()); + if (!IteratorCloseForException(cx, iterObject)) { + SettleOnTryNote(cx, tn, frame, ei, rfe, pc); + return false; + } + } + break; + } + case JSTRY_FOR_OF: case JSTRY_LOOP: break; diff --git a/js/src/jsscript.h b/js/src/jsscript.h index b48d48686..8bba3ec39 100644 --- a/js/src/jsscript.h +++ b/js/src/jsscript.h @@ -85,7 +85,8 @@ enum JSTryNoteKind { JSTRY_FOR_IN, JSTRY_FOR_OF, JSTRY_LOOP, - JSTRY_ITERCLOSE + JSTRY_ITERCLOSE, + JSTRY_DESTRUCTURING_ITERCLOSE }; /* diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 294dd935d..6fe01de22 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -2585,12 +2585,28 @@ Notes(JSContext* cx, unsigned argc, Value* vp) return true; } -JS_STATIC_ASSERT(JSTRY_CATCH == 0); -JS_STATIC_ASSERT(JSTRY_FINALLY == 1); -JS_STATIC_ASSERT(JSTRY_FOR_IN == 2); +static const char* +TryNoteName(JSTryNoteKind kind) +{ + switch (kind) { + case JSTRY_CATCH: + return "catch"; + case JSTRY_FINALLY: + return "finally"; + case JSTRY_FOR_IN: + return "for-in"; + case JSTRY_FOR_OF: + return "for-of"; + case JSTRY_LOOP: + return "loop"; + case JSTRY_ITERCLOSE: + return "iterclose"; + case JSTRY_DESTRUCTURING_ITERCLOSE: + return "dstr-iterclose"; + } -static const char* const TryNoteNames[] = { "catch", "finally", "for-in", "for-of", "loop", - "iterclose" }; + MOZ_CRASH("Bad JSTryNoteKind"); +} static MOZ_MUST_USE bool TryNotes(JSContext* cx, HandleScript script, Sprinter* sp) @@ -2598,17 +2614,16 @@ TryNotes(JSContext* cx, HandleScript script, Sprinter* sp) if (!script->hasTrynotes()) return true; - if (sp->put("\nException table:\nkind stack start end\n") < 0) + if (sp->put("\nException table:\nkind stack start end\n") < 0) return false; JSTryNote* tn = script->trynotes()->vector; JSTryNote* tnlimit = tn + script->trynotes()->length; do { - MOZ_ASSERT(tn->kind < ArrayLength(TryNoteNames)); uint32_t startOff = script->pcToOffset(script->main()) + tn->start; - if (!sp->jsprintf(" %-9s %6u %8u %8u\n", - TryNoteNames[tn->kind], tn->stackDepth, - startOff, startOff + tn->length)) + if (!sp->jsprintf(" %-14s %6u %8u %8u\n", + TryNoteName(static_cast(tn->kind)), + tn->stackDepth, startOff, startOff + tn->length)) { return false; } diff --git a/js/src/tests/ecma_6/Destructuring/array-iterator-close.js b/js/src/tests/ecma_6/Destructuring/array-iterator-close.js new file mode 100644 index 000000000..f7805540d --- /dev/null +++ b/js/src/tests/ecma_6/Destructuring/array-iterator-close.js @@ -0,0 +1,77 @@ +// Tests that IteratorClose is called in array destructuring patterns. + +function test() { + var returnCalled = 0; + var returnCalledExpected = 0; + var iterable = {}; + + // empty [] calls IteratorClose regardless of "done" on the result. + iterable[Symbol.iterator] = makeIterator({ + next: function() { + return { done: true }; + }, + ret: function() { + returnCalled++; + return {}; + } + }); + var [] = iterable; + assertEq(returnCalled, ++returnCalledExpected); + + iterable[Symbol.iterator] = makeIterator({ + ret: function() { + returnCalled++; + return {}; + } + }); + var [] = iterable; + assertEq(returnCalled, ++returnCalledExpected); + + // Non-empty destructuring calls IteratorClose if iterator is not done by + // the end of destructuring. + var [a,b] = iterable; + assertEq(returnCalled, ++returnCalledExpected); + var [c,] = iterable; + assertEq(returnCalled, ++returnCalledExpected); + + // throw in lhs ref calls IteratorClose + function throwlhs() { + throw "in lhs"; + } + assertThrowsValue(function() { + 0, [...{}[throwlhs()]] = iterable; + }, "in lhs"); + assertEq(returnCalled, ++returnCalledExpected); + + // throw in iter.next doesn't call IteratorClose + iterable[Symbol.iterator] = makeIterator({ + next: function() { + throw "in next"; + }, + ret: function() { + returnCalled++; + return {}; + } + }); + assertThrowsValue(function() { + var [d] = iterable; + }, "in next"); + assertEq(returnCalled, returnCalledExpected); + + // "return" must return an Object. + iterable[Symbol.iterator] = makeIterator({ + ret: function() { + returnCalled++; + return 42; + } + }); + assertThrowsInstanceOf(function() { + var [] = iterable; + }, TypeError); + assertEq(returnCalled, ++returnCalledExpected); +} + +test(); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 489d0a155..923c824ce 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -1171,19 +1171,13 @@ ProcessTryNotes(JSContext* cx, EnvironmentIter& ei, InterpreterRegs& regs) SettleOnTryNote(cx, tn, ei, regs); return FinallyContinuation; - case JSTRY_FOR_IN: - case JSTRY_ITERCLOSE: { + case JSTRY_FOR_IN: { /* This is similar to JSOP_ENDITER in the interpreter loop. */ DebugOnly pc = regs.fp()->script()->main() + tn->start + tn->length; - MOZ_ASSERT_IF(tn->kind == JSTRY_FOR_IN, JSOp(*pc) == JSOP_ENDITER); + MOZ_ASSERT(JSOp(*pc) == JSOP_ENDITER); Value* sp = regs.spForStackDepth(tn->stackDepth); RootedObject obj(cx, &sp[-1].toObject()); - bool ok; - if (tn->kind == JSTRY_FOR_IN) - ok = UnwindIteratorForException(cx, obj); - else - ok = IteratorCloseForException(cx, obj); - if (!ok) { + if (!UnwindIteratorForException(cx, obj)) { // We should only settle on the note only if // UnwindIteratorForException itself threw, as // onExceptionUnwind should be called anew with the new @@ -1195,6 +1189,33 @@ ProcessTryNotes(JSContext* cx, EnvironmentIter& ei, InterpreterRegs& regs) break; } + case JSTRY_ITERCLOSE: { + // The iterator object is at the top of the stack. + Value* sp = regs.spForStackDepth(tn->stackDepth); + RootedObject iterObject(cx, &sp[-1].toObject()); + if (!IteratorCloseForException(cx, iterObject)) { + SettleOnTryNote(cx, tn, ei, regs); + return ErrorReturnContinuation; + } + break; + } + + case JSTRY_DESTRUCTURING_ITERCLOSE: { + // Whether the destructuring iterator is done is at the top of the + // stack. The iterator object is second from the top. + MOZ_ASSERT(tn->stackDepth > 1); + Value* sp = regs.spForStackDepth(tn->stackDepth); + MOZ_ASSERT(sp[-1].isBoolean()); + if (sp[-1].isFalse()) { + RootedObject iterObject(cx, &sp[-2].toObject()); + if (!IteratorCloseForException(cx, iterObject)) { + SettleOnTryNote(cx, tn, ei, regs); + return ErrorReturnContinuation; + } + } + break; + } + case JSTRY_FOR_OF: case JSTRY_LOOP: break; -- cgit v1.2.3 From 1ea1ed151571a523d1c8016dcd314e12238cd785 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 24 Mar 2018 12:27:00 +0100 Subject: Bug 1147371: Implement calling IteratorClose and "return" on iterators in yield* Issue #74 --- js/src/frontend/BytecodeEmitter.cpp | 80 +++++++++----- js/src/jit/BaselineCompiler.cpp | 14 ++- js/src/jit/BaselineCompiler.h | 3 + js/src/js.msg | 1 + .../tests/ecma_6/Generators/delegating-yield-2.js | 20 ++-- .../ecma_6/Generators/yield-star-iterator-close.js | 123 +++++++++++++++++++++ js/src/tests/ecma_6/shell.js | 11 +- js/src/vm/Interpreter.cpp | 12 +- js/src/vm/Interpreter.h | 1 + js/src/vm/Opcodes.h | 12 +- 10 files changed, 232 insertions(+), 45 deletions(-) create mode 100644 js/src/tests/ecma_6/Generators/yield-star-iterator-close.js diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index e2c906850..98679eac6 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -4898,6 +4898,14 @@ BytecodeEmitter::emitIteratorClose(Maybe yieldStarTryStart, bool all return false; if (!ifReturnDone.emitIfElse()) // ITER OLDRESULT FTYPE FVALUE RESULT return false; + if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // ITER OLDRESULT FTYPE FVALUE VALUE + return false; + if (!emitPrepareIteratorResult()) // ITER OLDRESULT FTYPE FVALUE VALUE RESULT + return false; + if (!emit1(JSOP_SWAP)) // ITER OLDRESULT FTYPE FVALUE RESULT VALUE + return false; + if (!emitFinishIteratorResult(true)) // ITER OLDRESULT FTYPE FVALUE RESULT + return false; if (!emit1(JSOP_DUP)) // ITER OLDRESULT FTYPE FVALUE RESULT RESULT return false; if (!emit1(JSOP_SETRVAL)) // ITER OLDRESULT FTYPE FVALUE RESULT @@ -8059,61 +8067,77 @@ BytecodeEmitter::emitYieldStar(ParseNode* iter, ParseNode* gen) // Catch location. stackDepth = uint32_t(depth); // ITER RESULT - if (!emit1(JSOP_POP)) // ITER - return false; - // THROW? = 'throw' in ITER - if (!emit1(JSOP_EXCEPTION)) // ITER EXCEPTION + if (!emit1(JSOP_EXCEPTION)) // ITER RESULT EXCEPTION return false; - if (!emit1(JSOP_SWAP)) // EXCEPTION ITER + if (!emitDupAt(2)) // ITER RESULT EXCEPTION ITER return false; - if (!emit1(JSOP_DUP)) // EXCEPTION ITER ITER + if (!emit1(JSOP_DUP)) // ITER RESULT EXCEPTION ITER ITER return false; - if (!emitAtomOp(cx->names().throw_, JSOP_STRING)) // EXCEPTION ITER ITER "throw" + if (!emitAtomOp(cx->names().throw_, JSOP_CALLPROP)) // ITER RESULT EXCEPTION ITER THROW return false; - if (!emit1(JSOP_SWAP)) // EXCEPTION ITER "throw" ITER + if (!emit1(JSOP_DUP)) // ITER RESULT EXCEPTION ITER THROW THROW return false; - if (!emit1(JSOP_IN)) // EXCEPTION ITER THROW? + if (!emit1(JSOP_UNDEFINED)) // ITER RESULT EXCEPTION ITER THROW THROW UNDEFINED return false; - // if (THROW?) goto delegate - JumpList checkThrow; - if (!emitJump(JSOP_IFNE, &checkThrow)) // EXCEPTION ITER + if (!emit1(JSOP_EQ)) // ITER RESULT EXCEPTION ITER THROW ?EQL return false; - if (!emit1(JSOP_POP)) // EXCEPTION + + IfThenElseEmitter ifThrowMethodIsNotDefined(this); + if (!ifThrowMethodIsNotDefined.emitIf()) // ITER RESULT EXCEPTION ITER THROW return false; - if (!emit1(JSOP_THROW)) // throw EXCEPTION + if (!emitUint16Operand(JSOP_THROWMSG, JSMSG_ITERATOR_NO_THROW)) // throw return false; - - if (!emitJumpTargetAndPatch(checkThrow)) // delegate: + if (!ifThrowMethodIsNotDefined.emitEnd()) // ITER OLDRESULT EXCEPTION ITER THROW return false; - // RESULT = ITER.throw(EXCEPTION) // EXCEPTION ITER - stackDepth = uint32_t(depth); - if (!emit1(JSOP_DUP)) // EXCEPTION ITER ITER + // ES 14.4.13, YieldExpression : yield * AssignmentExpression, step 5.b.iii.4. + // RESULT = ITER.throw(EXCEPTION) // ITER OLDRESULT EXCEPTION ITER THROW + if (!emit1(JSOP_SWAP)) // ITER OLDRESULT EXCEPTION THROW ITER return false; - if (!emit1(JSOP_DUP)) // EXCEPTION ITER ITER ITER + if (!emit2(JSOP_PICK, 2)) // ITER OLDRESULT THROW ITER EXCEPTION return false; - if (!emitAtomOp(cx->names().throw_, JSOP_CALLPROP)) // EXCEPTION ITER ITER THROW + if (!emitCall(JSOP_CALL, 1, iter)) // ITER OLDRESULT RESULT return false; - if (!emit1(JSOP_SWAP)) // EXCEPTION ITER THROW ITER + checkTypeSet(JSOP_CALL); + if (!emitCheckIsObj(CheckIsObjectKind::IteratorThrow)) // ITER OLDRESULT RESULT return false; - if (!emit2(JSOP_PICK, 3)) // ITER THROW ITER EXCEPTION + if (!emit1(JSOP_SWAP)) // ITER RESULT OLDRESULT return false; - if (!emitCall(JSOP_CALL, 1, iter)) // ITER RESULT + if (!emit1(JSOP_POP)) // ITER RESULT return false; - checkTypeSet(JSOP_CALL); MOZ_ASSERT(this->stackDepth == depth); JumpList checkResult; + // Note that there is no GOSUB to the finally block here. If the iterator has a + // "throw" method, it does not perform IteratorClose per + // ES 14.4.13, YieldExpression : yield * AssignmentExpression, step 5.b.ii. if (!emitJump(JSOP_GOTO, &checkResult)) // goto checkResult return false; - // Catch epilogue. + // The finally block, IteratorClose logic. + + JumpTarget finallyStart{ 0 }; + if (!emitJumpTarget(&finallyStart)) + return false; + if (!emit1(JSOP_FINALLY)) // ITER RESULT FTYPE FVALUE + return false; + if (!emitDupAt(3)) // ITER RESULT FTYPE FVALUE ITER + return false; + if (!emitIteratorClose(Some(tryStart))) // ITER RESULT FTYPE FVALUE + return false; + if (!emit1(JSOP_RETSUB)) // ITER RESULT + return false; + + // Catch and finally epilogue. // This is a peace offering to ReconstructPCStack. See the note in EmitTry. if (!emit1(JSOP_NOP)) return false; - if (!tryNoteList.append(JSTRY_CATCH, depth, tryStart.offset + JSOP_TRY_LENGTH, tryEnd.offset)) + size_t tryStartOffset = tryStart.offset + JSOP_TRY_LENGTH; + if (!tryNoteList.append(JSTRY_CATCH, depth, tryStartOffset, tryEnd.offset)) + return false; + if (!tryNoteList.append(JSTRY_FINALLY, depth, tryStartOffset, finallyStart.offset)) return false; - // After the try/catch block: send the received value to the iterator. + // After the try-catch-finally block: send the received value to the iterator. if (!emitJumpTargetAndPatch(send)) // send: return false; diff --git a/js/src/jit/BaselineCompiler.cpp b/js/src/jit/BaselineCompiler.cpp index 2f0d41707..4524bae07 100644 --- a/js/src/jit/BaselineCompiler.cpp +++ b/js/src/jit/BaselineCompiler.cpp @@ -3975,7 +3975,7 @@ BaselineCompiler::emit_JSOP_MOREITER() } bool -BaselineCompiler::emit_JSOP_ISNOITER() +BaselineCompiler::emitIsMagicValue() { frame.syncStack(0); @@ -3993,6 +3993,12 @@ BaselineCompiler::emit_JSOP_ISNOITER() return true; } +bool +BaselineCompiler::emit_JSOP_ISNOITER() +{ + return emitIsMagicValue(); +} + bool BaselineCompiler::emit_JSOP_ENDITER() { @@ -4004,6 +4010,12 @@ BaselineCompiler::emit_JSOP_ENDITER() return emitOpIC(compiler.getStub(&stubSpace_)); } +bool +BaselineCompiler::emit_JSOP_ISGENCLOSING() +{ + return emitIsMagicValue(); +} + bool BaselineCompiler::emit_JSOP_GETRVAL() { diff --git a/js/src/jit/BaselineCompiler.h b/js/src/jit/BaselineCompiler.h index 60dac0966..18e56bcd4 100644 --- a/js/src/jit/BaselineCompiler.h +++ b/js/src/jit/BaselineCompiler.h @@ -203,6 +203,7 @@ namespace jit { _(JSOP_MOREITER) \ _(JSOP_ISNOITER) \ _(JSOP_ENDITER) \ + _(JSOP_ISGENCLOSING) \ _(JSOP_GENERATOR) \ _(JSOP_INITIALYIELD) \ _(JSOP_YIELD) \ @@ -342,6 +343,8 @@ class BaselineCompiler : public BaselineCompilerSpecific MOZ_MUST_USE bool emitThrowConstAssignment(); MOZ_MUST_USE bool emitUninitializedLexicalCheck(const ValueOperand& val); + MOZ_MUST_USE bool emitIsMagicValue(); + MOZ_MUST_USE bool addPCMappingEntry(bool addIndexEntry); MOZ_MUST_USE bool addYieldOffset(); diff --git a/js/src/js.msg b/js/src/js.msg index 30c0d3035..8d492f523 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -583,3 +583,4 @@ MSG_DEF(JSMSG_PROMISE_ERROR_IN_WRAPPED_REJECTION_REASON,0, JSEXN_INTERNALERR, "P // Iterator MSG_DEF(JSMSG_RETURN_NOT_CALLABLE, 0, JSEXN_TYPEERR, "property 'return' of iterator is not callable") +MSG_DEF(JSMSG_ITERATOR_NO_THROW, 0, JSEXN_TYPEERR, "iterator does not have a 'throw' method") diff --git a/js/src/tests/ecma_6/Generators/delegating-yield-2.js b/js/src/tests/ecma_6/Generators/delegating-yield-2.js index 918fb33e1..34cb3f4a9 100644 --- a/js/src/tests/ecma_6/Generators/delegating-yield-2.js +++ b/js/src/tests/ecma_6/Generators/delegating-yield-2.js @@ -25,8 +25,8 @@ assertThrowsValue(function () { outer.throw(42) }, 42); inner = g1(); outer = delegate(inner); assertIteratorNext(outer, 1); -inner.throw = function(e) { return e*2; }; -assertEq(84, outer.throw(42)); +inner.throw = function(e) { return { value: e*2 }; }; +assertEq(84, outer.throw(42).value); assertIteratorDone(outer, undefined); // Monkeypatching inner.next. @@ -41,7 +41,9 @@ outer = delegate(inner); assertIteratorNext(outer, 1); delete GeneratorObjectPrototype.throw; var outer_throw_42 = GeneratorObjectPrototype_throw.bind(outer, 42); -assertThrowsValue(outer_throw_42, 42); +// yield* protocol violation: no 'throw' method +assertThrowsInstanceOf(outer_throw_42, TypeError); +// Now done, so just throws. assertThrowsValue(outer_throw_42, 42); // Monkeypunch a different throw handler. @@ -49,11 +51,11 @@ inner = g2(); outer = delegate(inner); outer_throw_42 = GeneratorObjectPrototype_throw.bind(outer, 42); assertIteratorNext(outer, 1); -GeneratorObjectPrototype.throw = function(e) { return e*2; } -assertEq(84, outer_throw_42()); -assertEq(84, outer_throw_42()); +GeneratorObjectPrototype.throw = function(e) { return { value: e*2 }; } +assertEq(84, outer_throw_42().value); +assertEq(84, outer_throw_42().value); // This continues indefinitely. -assertEq(84, outer_throw_42()); +assertEq(84, outer_throw_42().value); assertIteratorDone(outer, undefined); // The same, but restoring the original pre-monkey throw. @@ -61,8 +63,8 @@ inner = g2(); outer = delegate(inner); outer_throw_42 = GeneratorObjectPrototype_throw.bind(outer, 42); assertIteratorNext(outer, 1); -assertEq(84, outer_throw_42()); -assertEq(84, outer_throw_42()); +assertEq(84, outer_throw_42().value); +assertEq(84, outer_throw_42().value); GeneratorObjectPrototype.throw = GeneratorObjectPrototype_throw; assertIteratorResult(outer_throw_42(), 42, false); assertIteratorDone(outer, undefined); diff --git a/js/src/tests/ecma_6/Generators/yield-star-iterator-close.js b/js/src/tests/ecma_6/Generators/yield-star-iterator-close.js new file mode 100644 index 000000000..ec62dd86d --- /dev/null +++ b/js/src/tests/ecma_6/Generators/yield-star-iterator-close.js @@ -0,0 +1,123 @@ +// Tests that the "return" method on iterators is called in yield* +// expressions. + +function test() { + var returnCalled = 0; + var returnCalledExpected = 0; + var nextCalled = 0; + var nextCalledExpected = 0; + var iterable = {}; + iterable[Symbol.iterator] = makeIterator({ + next: function() { + nextCalled++; + return { done: false }; + }, + ret: function() { + returnCalled++; + return { done: true, value: "iter.return" }; + } + }); + + function* y() { + yield* iterable; + } + + // G.p.throw on an iterator without "throw" calls IteratorClose. + var g1 = y(); + g1.next(); + assertThrowsValue(function() { + g1.throw("foo"); + }, "foo"); + assertEq(returnCalled, ++returnCalledExpected); + assertEq(nextCalled, ++nextCalledExpected); + g1.next(); + assertEq(nextCalled, nextCalledExpected); + + // G.p.return calls "return", and if the result.done is true, return the + // result. + var g2 = y(); + g2.next(); + var v2 = g2.return("test return"); + assertEq(v2.done, true); + assertEq(v2.value, "iter.return"); + assertEq(returnCalled, ++returnCalledExpected); + assertEq(nextCalled, ++nextCalledExpected); + g2.next(); + assertEq(nextCalled, nextCalledExpected); + + // G.p.return calls "return", and if the result.done is false, continue + // yielding. + iterable[Symbol.iterator] = makeIterator({ + next: function() { + nextCalled++; + return { done: false }; + }, + ret: function() { + returnCalled++; + return { done: false, value: "iter.return" }; + } + }); + var g3 = y(); + g3.next(); + var v3 = g3.return("test return"); + assertEq(v3.done, false); + assertEq(v3.value, "iter.return"); + assertEq(returnCalled, ++returnCalledExpected); + assertEq(nextCalled, ++nextCalledExpected); + g3.next(); + assertEq(nextCalled, ++nextCalledExpected); + + // G.p.return throwing does not re-call iter.return. + iterable[Symbol.iterator] = makeIterator({ + ret: function() { + returnCalled++; + throw "in iter.return"; + } + }); + var g4 = y(); + g4.next(); + assertThrowsValue(function() { + g4.return("in test"); + }, "in iter.return"); + assertEq(returnCalled, ++returnCalledExpected); + + // G.p.return expects iter.return to return an Object. + iterable[Symbol.iterator] = makeIterator({ + ret: function() { + returnCalled++; + return 42; + } + }); + var g5 = y(); + g5.next(); + assertThrowsInstanceOf(function() { + g5.return("foo"); + }, TypeError); + assertEq(returnCalled, ++returnCalledExpected); + + // IteratorClose expects iter.return to return an Object. + var g6 = y(); + g6.next(); + assertThrowsInstanceOf(function() { + g6.throw("foo"); + }, TypeError); + assertEq(returnCalled, ++returnCalledExpected); + + // G.p.return passes its argument to "return". + iterable[Symbol.iterator] = makeIterator({ + ret: function(x) { + assertEq(x, "in test"); + returnCalled++; + return { done: true }; + } + }); + var g7 = y(); + g7.next(); + g7.return("in test"); + assertEq(returnCalled, ++returnCalledExpected); +} + +test(); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/shell.js b/js/src/tests/ecma_6/shell.js index 1d067a282..4da9221d6 100644 --- a/js/src/tests/ecma_6/shell.js +++ b/js/src/tests/ecma_6/shell.js @@ -22,14 +22,17 @@ /** Make an iterator with a return method. */ global.makeIterator = function makeIterator(overrides) { var iterator = { - next: function() { + throw: function(e) { + throw e; + }, + next: function(x) { if (overrides && overrides.next) - return overrides.next(); + return overrides.next(x); return { done: false }; }, - return: function() { + return: function(x) { if (overrides && overrides.ret) - return overrides.ret(); + return overrides.ret(x); return { done: true }; } }; diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 923c824ce..9a8c6777f 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -1887,7 +1887,6 @@ CASE(EnableInterruptsPseudoOpcode) /* Various 1-byte no-ops. */ CASE(JSOP_NOP) CASE(JSOP_NOP_DESTRUCTURING) -CASE(JSOP_UNUSED187) CASE(JSOP_UNUSED192) CASE(JSOP_UNUSED209) CASE(JSOP_UNUSED210) @@ -2182,6 +2181,13 @@ CASE(JSOP_ENDITER) } END_CASE(JSOP_ENDITER) +CASE(JSOP_ISGENCLOSING) +{ + bool b = REGS.sp[-1].isMagic(JS_GENERATOR_CLOSING); + PUSH_BOOLEAN(b); +} +END_CASE(JSOP_ISGENCLOSING) + CASE(JSOP_DUP) { MOZ_ASSERT(REGS.stackDepth() >= 1); @@ -5074,6 +5080,10 @@ js::ThrowCheckIsObject(JSContext* cx, CheckIsObjectKind kind) JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ITER_METHOD_RETURNED_PRIMITIVE, "return"); break; + case CheckIsObjectKind::IteratorThrow: + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_ITER_METHOD_RETURNED_PRIMITIVE, "throw"); + break; case CheckIsObjectKind::GetIterator: JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_GET_ITER_RETURNED_PRIMITIVE); break; diff --git a/js/src/vm/Interpreter.h b/js/src/vm/Interpreter.h index 3a24240fc..38e23ec06 100644 --- a/js/src/vm/Interpreter.h +++ b/js/src/vm/Interpreter.h @@ -563,6 +563,7 @@ ReportRuntimeRedeclaration(JSContext* cx, HandlePropertyName name, const char* r enum class CheckIsObjectKind : uint8_t { IteratorNext, IteratorReturn, + IteratorThrow, GetIterator }; diff --git a/js/src/vm/Opcodes.h b/js/src/vm/Opcodes.h index b59b9388c..84f08c4d5 100644 --- a/js/src/vm/Opcodes.h +++ b/js/src/vm/Opcodes.h @@ -1916,8 +1916,16 @@ * Stack: => this */ \ macro(JSOP_GLOBALTHIS, 186,"globalthis", NULL, 1, 0, 1, JOF_BYTE) \ - macro(JSOP_UNUSED187, 187,"unused187", NULL, 1, 0, 0, JOF_BYTE) \ - \ + /* + * Pushes a boolean indicating whether the top of the stack is + * MagicValue(JS_GENERATOR_CLOSING). + * + * Category: Statements + * Type: For-In Statement + * Operands: + * Stack: val => val, res + */ \ + macro(JSOP_ISGENCLOSING, 187, "isgenclosing", NULL, 1, 1, 2, JOF_BYTE) \ /* * Pushes unsigned 24-bit int immediate integer operand onto the stack. * Category: Literals -- cgit v1.2.3 From fdedd57c60d35bed3e6cde12084b7abe08153ed3 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 24 Mar 2018 12:28:12 +0100 Subject: Bug 1147371: Implement JSOP_PICK and JSOP_UNPICK in the expression decompiler Issue #74 --- js/src/jsopcode.cpp | 28 ++++++++++++++++++++++++++++ js/src/jsopcode.h | 1 + js/src/jsopcodeinlines.h | 3 ++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/js/src/jsopcode.cpp b/js/src/jsopcode.cpp index 31bbfb471..eadbca4f8 100644 --- a/js/src/jsopcode.cpp +++ b/js/src/jsopcode.cpp @@ -460,6 +460,34 @@ BytecodeParser::simulateOp(JSOp op, uint32_t offset, uint32_t* offsetStack, uint offsetStack[stackDepth] = tmp; } break; + + case JSOP_PICK: { + jsbytecode* pc = script_->offsetToPC(offset); + unsigned n = GET_UINT8(pc); + MOZ_ASSERT(ndefs == n + 1); + if (offsetStack) { + uint32_t top = stackDepth + n; + uint32_t tmp = offsetStack[stackDepth]; + for (uint32_t i = stackDepth; i < top; i++) + offsetStack[i] = offsetStack[i + 1]; + offsetStack[top] = tmp; + } + break; + } + + case JSOP_UNPICK: { + jsbytecode* pc = script_->offsetToPC(offset); + unsigned n = GET_UINT8(pc); + MOZ_ASSERT(ndefs == n + 1); + if (offsetStack) { + uint32_t top = stackDepth + n; + uint32_t tmp = offsetStack[top]; + for (uint32_t i = top; i > stackDepth; i--) + offsetStack[i] = offsetStack[i - 1]; + offsetStack[stackDepth] = tmp; + } + break; + } } stackDepth += ndefs; return stackDepth; diff --git a/js/src/jsopcode.h b/js/src/jsopcode.h index 4f7859665..e56eebb2d 100644 --- a/js/src/jsopcode.h +++ b/js/src/jsopcode.h @@ -423,6 +423,7 @@ BytecodeFallsThrough(JSOp op) case JSOP_RETRVAL: case JSOP_FINALYIELDRVAL: case JSOP_THROW: + case JSOP_THROWMSG: case JSOP_TABLESWITCH: return false; case JSOP_GOSUB: diff --git a/js/src/jsopcodeinlines.h b/js/src/jsopcodeinlines.h index cf9c8357a..5b0ce0cf9 100644 --- a/js/src/jsopcodeinlines.h +++ b/js/src/jsopcodeinlines.h @@ -27,6 +27,7 @@ GetDefCount(JSScript* script, unsigned offset) case JSOP_AND: return 1; case JSOP_PICK: + case JSOP_UNPICK: /* * Pick pops and pushes how deep it looks in the stack + 1 * items. i.e. if the stack were |a b[2] c[1] d[0]|, pick 2 @@ -44,7 +45,7 @@ GetUseCount(JSScript* script, unsigned offset) { jsbytecode* pc = script->offsetToPC(offset); - if (JSOp(*pc) == JSOP_PICK) + if (JSOp(*pc) == JSOP_PICK || JSOp(*pc) == JSOP_UNPICK) return pc[1] + 1; if (CodeSpec[*pc].nuses == -1) return StackUses(script, pc); -- cgit v1.2.3 From 114eb8bf48ca0288f44705853239bdf198eeecdb Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 24 Mar 2018 12:29:12 +0100 Subject: Bug 1147371: Always decompile argument names in self-hosted code in the caller frame Issue #74 --- js/src/jsopcode.cpp | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/js/src/jsopcode.cpp b/js/src/jsopcode.cpp index eadbca4f8..6adb5401e 100644 --- a/js/src/jsopcode.cpp +++ b/js/src/jsopcode.cpp @@ -92,7 +92,8 @@ const char * const js::CodeName[] = { /************************************************************************/ -#define COUNTS_LEN 16 +static bool +DecompileArgumentFromStack(JSContext* cx, int formalIndex, char** res); size_t js::GetVariableBytecodeLength(jsbytecode* pc) @@ -1258,6 +1259,24 @@ ExpressionDecompiler::decompilePC(jsbytecode* pc) return write(loadAtom(pc)); case JSOP_GETARG: { unsigned slot = GET_ARGNO(pc); + + // For self-hosted scripts that are called from non-self-hosted code, + // decompiling the parameter name in the self-hosted script is + // unhelpful. Decompile the argument name instead. + if (script->selfHosted()) { + char* result; + if (!DecompileArgumentFromStack(cx, slot, &result)) + return false; + + // Note that decompiling the argument in the parent frame might + // not succeed. + if (result) { + bool ok = write(result); + js_free(result); + return ok; + } + } + JSAtom* atom = getArg(slot); if (!atom) return false; @@ -1621,12 +1640,17 @@ DecompileArgumentFromStack(JSContext* cx, int formalIndex, char** res) MOZ_ASSERT(frameIter.script()->selfHosted()); /* - * Get the second-to-top frame, the caller of the builtin that called the - * intrinsic. + * Get the second-to-top frame, the non-self-hosted caller of the builtin + * that called the intrinsic. */ ++frameIter; - if (frameIter.done() || !frameIter.hasScript() || frameIter.compartment() != cx->compartment()) + if (frameIter.done() || + !frameIter.hasScript() || + frameIter.script()->selfHosted() || + frameIter.compartment() != cx->compartment()) + { return true; + } RootedScript script(cx, frameIter.script()); jsbytecode* current = frameIter.pc(); -- cgit v1.2.3 From 8bb9649135c384a08b78295b9d07be32d50967d1 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 12:37:06 +0200 Subject: Bug 1331444 - Keep iterators alive in Ion in for-of loops for IteratorClose due to exceptions Issue #74 --- js/src/jit-test/tests/for-of/bug-1331444.js | 7 +++++++ js/src/jit/IonBuilder.cpp | 19 ++++++++++++++----- js/src/jit/IonBuilder.h | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 js/src/jit-test/tests/for-of/bug-1331444.js diff --git a/js/src/jit-test/tests/for-of/bug-1331444.js b/js/src/jit-test/tests/for-of/bug-1331444.js new file mode 100644 index 000000000..9770c584b --- /dev/null +++ b/js/src/jit-test/tests/for-of/bug-1331444.js @@ -0,0 +1,7 @@ +// |jit-test| error: ReferenceError + +symbols = [Symbol]; +for (comparator of[, ]) + for (a of symbols) + for (;;) + expect; diff --git a/js/src/jit/IonBuilder.cpp b/js/src/jit/IonBuilder.cpp index c4df415a4..4318db2b6 100644 --- a/js/src/jit/IonBuilder.cpp +++ b/js/src/jit/IonBuilder.cpp @@ -961,11 +961,16 @@ IonBuilder::processIterators() // Find phis that must directly hold an iterator live. Vector 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; + MDefinition* def = iterators_[i]; + if (def->isPhi()) { + if (!worklist.append(def->toPhi())) + return false; + } else { + for (MUseDefIterator iter(def); iter; iter++) { + if (iter.def()->isPhi()) { + if (!worklist.append(iter.def()->toPhi())) + return false; + } } } } @@ -1936,6 +1941,10 @@ IonBuilder::inspectOpcode(JSOp op) case JSOP_CALLITER: case JSOP_NEW: case JSOP_SUPERCALL: + if (op == JSOP_CALLITER) { + if (!outermostBuilder()->iterators_.append(current->peek(-1))) + return false; + } return jsop_call(GET_ARGC(pc), (JSOp)*pc == JSOP_NEW || (JSOp)*pc == JSOP_SUPERCALL); case JSOP_EVAL: diff --git a/js/src/jit/IonBuilder.h b/js/src/jit/IonBuilder.h index 0d1bdb1e3..c3cd9700a 100644 --- a/js/src/jit/IonBuilder.h +++ b/js/src/jit/IonBuilder.h @@ -1242,7 +1242,7 @@ class IonBuilder Vector loops_; Vector switches_; Vector labels_; - Vector iterators_; + Vector iterators_; Vector loopHeaders_; BaselineInspector* inspector; -- cgit v1.2.3 From 3df7c50fcccff433e4e24a0f1ce26859977948b4 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 12:37:44 +0200 Subject: Bug 1332155 - Skip non-try-related trynotes when asserting jump targets Issue #74 --- js/src/jsscript.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/src/jsscript.cpp b/js/src/jsscript.cpp index 929251d8b..e86ceab3d 100644 --- a/js/src/jsscript.cpp +++ b/js/src/jsscript.cpp @@ -2804,9 +2804,10 @@ JSScript::assertValidJumpTargets() const for (; tn < tnlimit; tn++) { jsbytecode* tryStart = mainEntry + tn->start; jsbytecode* tryPc = tryStart - 1; - if (JSOp(*tryPc) != JSOP_TRY) + if (tn->kind != JSTRY_CATCH && tn->kind != JSTRY_FINALLY) continue; + MOZ_ASSERT(JSOp(*tryPc) == JSOP_TRY); jsbytecode* tryTarget = tryStart + tn->length; MOZ_ASSERT(mainEntry <= tryTarget && tryTarget < end); MOZ_ASSERT(BytecodeIsJumpTarget(JSOp(*tryTarget))); -- cgit v1.2.3 From 34533e06a7b6f73bb9cf16058207e97bd91c832c Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 12:38:28 +0200 Subject: Bug 1332881 - Handle stack value in correct order when leaving loop and try-finally Issue #74 --- js/src/frontend/BytecodeEmitter.cpp | 6 +++ js/src/tests/ecma_6/Statements/for-inof-finally.js | 54 ++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 js/src/tests/ecma_6/Statements/for-inof-finally.js diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index 98679eac6..bf7797d37 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -2359,6 +2359,9 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta } case StatementKind::ForOfLoop: + if (!flushPops(bce_)) + return false; + // The iterator and the current value are on the stack. // if (emitIteratorClose) { @@ -2378,6 +2381,9 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta break; case StatementKind::ForInLoop: + if (!flushPops(bce_)) + return false; + // The iterator and the current value are on the stack. if (!bce_->emit1(JSOP_POP)) // ... ITER return false; diff --git a/js/src/tests/ecma_6/Statements/for-inof-finally.js b/js/src/tests/ecma_6/Statements/for-inof-finally.js new file mode 100644 index 000000000..187dcdaf5 --- /dev/null +++ b/js/src/tests/ecma_6/Statements/for-inof-finally.js @@ -0,0 +1,54 @@ +var BUGNUMBER = 1332881; +var summary = + "Leaving for-in and try should handle stack value in correct order"; + +print(BUGNUMBER + ": " + summary); + +var a = (function () { + for (var x in [0]) { + try {} finally { + return 11; + } + } +})(); +assertEq(a, 11); + +var b = (function () { + for (var x of [0]) { + try {} finally { + return 12; + } + } +})(); +assertEq(b, 12); + +var c = (function () { + for (var x in [0]) { + for (var y of [0]) { + try {} finally { + return 13; + } + } + } +})(); +assertEq(c, 13); + +var d = (function () { + for (var x in [0]) { + for (var y of [0]) { + try {} finally { + for (var z in [0]) { + for (var w of [0]) { + try {} finally { + return 14; + } + } + } + } + } + } +})(); +assertEq(d, 14); + +if (typeof reportCompare === "function") + reportCompare(true, true); -- cgit v1.2.3 From e641a2c53a857141042ca62f1fe0b63b55a130af Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 12:56:18 +0200 Subject: Bug 1334799 - Handle stack value in correct order when leaving for-of loop from finally block Issue #74 --- js/src/frontend/BytecodeEmitter.cpp | 6 ++-- js/src/tests/ecma_6/Statements/for-inof-finally.js | 32 +++++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index bf7797d37..76f1f75b5 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -2396,6 +2396,9 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta } } + if (!flushPops(bce_)) + return false; + if (target && target->is() && emitIteratorCloseAtTarget) { hasForOfLoopsWithIteratorClose = true; if (!target->as().finishIterCloseTryNote(bce_)) @@ -2417,9 +2420,6 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta return false; } - if (!flushPops(bce_)) - return false; - // See comment in ForOfLoopControl. if (hasForOfLoopsWithIteratorClose) { for (NestableControl* control = bce_->innermostNestableControl; diff --git a/js/src/tests/ecma_6/Statements/for-inof-finally.js b/js/src/tests/ecma_6/Statements/for-inof-finally.js index 187dcdaf5..e1f0c77e1 100644 --- a/js/src/tests/ecma_6/Statements/for-inof-finally.js +++ b/js/src/tests/ecma_6/Statements/for-inof-finally.js @@ -4,6 +4,24 @@ var summary = print(BUGNUMBER + ": " + summary); +var called = 0; +function reset() { + called = 0; +} +var obj = { + [Symbol.iterator]() { + return { + next() { + return { value: 10, done: false }; + }, + return() { + called++; + return {}; + } + }; + } +}; + var a = (function () { for (var x in [0]) { try {} finally { @@ -13,32 +31,37 @@ var a = (function () { })(); assertEq(a, 11); +reset(); var b = (function () { - for (var x of [0]) { + for (var x of obj) { try {} finally { return 12; } } })(); +assertEq(called, 1); assertEq(b, 12); +reset(); var c = (function () { for (var x in [0]) { - for (var y of [0]) { + for (var y of obj) { try {} finally { return 13; } } } })(); +assertEq(called, 1); assertEq(c, 13); +reset(); var d = (function () { for (var x in [0]) { - for (var y of [0]) { + for (var y of obj) { try {} finally { for (var z in [0]) { - for (var w of [0]) { + for (var w of obj) { try {} finally { return 14; } @@ -48,6 +71,7 @@ var d = (function () { } } })(); +assertEq(called, 2); assertEq(d, 14); if (typeof reportCompare === "function") -- cgit v1.2.3 From e2853b8a8fc05a7526d933a62828c26747b4580a Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 13:32:14 +0200 Subject: Bug 1322069 - Add TryEmitter Issue #74 --- js/src/frontend/BytecodeEmitter.cpp | 510 ++++++++++++++++++++++++------------ 1 file changed, 341 insertions(+), 169 deletions(-) diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index 76f1f75b5..b1b0055e2 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -1563,6 +1563,318 @@ BytecodeEmitter::TDZCheckCache::noteTDZCheck(BytecodeEmitter* bce, JSAtom* name, return true; } +class MOZ_STACK_CLASS TryEmitter +{ + public: + enum Kind { + TryCatch, + TryCatchFinally, + TryFinally + }; + enum ShouldUseRetVal { + UseRetVal, + DontUseRetVal + }; + enum ShouldUseControl { + UseControl, + DontUseControl, + }; + + private: + BytecodeEmitter* bce_; + Kind kind_; + ShouldUseRetVal retValKind_; + + // Track jumps-over-catches and gosubs-to-finally for later fixup. + // + // When a finally block is active, non-local jumps (including + // jumps-over-catches) result in a GOSUB being written into the bytecode + // stream and fixed-up later. + // + // If ShouldUseControl is DontUseControl, all that handling is skipped. + // DontUseControl is used by yield*, that matches to the following: + // * has only one catch block + // * has no catch guard + // * has JSOP_GOTO at the end of catch-block + // * has no non-local-jump + // * doesn't use finally block for normal completion of try-block and + // catch-block + Maybe controlInfo_; + + int depth_; + unsigned noteIndex_; + ptrdiff_t tryStart_; + JumpList catchAndFinallyJump_; + JumpTarget tryEnd_; + JumpTarget finallyStart_; + + enum State { + Start, + Try, + TryEnd, + Catch, + CatchEnd, + Finally, + FinallyEnd, + End + }; + State state_; + + bool hasCatch() const { + return kind_ == TryCatch || kind_ == TryCatchFinally; + } + bool hasFinally() const { + return kind_ == TryCatchFinally || kind_ == TryFinally; + } + + public: + TryEmitter(BytecodeEmitter* bce, Kind kind, ShouldUseRetVal retValKind = UseRetVal, + ShouldUseControl controlKind = UseControl) + : bce_(bce), + kind_(kind), + retValKind_(retValKind), + depth_(0), + noteIndex_(0), + tryStart_(0), + state_(Start) + { + if (controlKind == UseControl) + controlInfo_.emplace(bce_, hasFinally() ? StatementKind::Finally : StatementKind::Try); + finallyStart_.offset = 0; + } + + bool emitJumpOverCatchAndFinally() { + if (!bce_->emitJump(JSOP_GOTO, &catchAndFinallyJump_)) + return false; + return true; + } + + bool emitTry() { + MOZ_ASSERT(state_ == Start); + + // Since an exception can be thrown at any place inside the try block, + // we need to restore the stack and the scope chain before we transfer + // the control to the exception handler. + // + // For that we store in a try note associated with the catch or + // finally block the stack depth upon the try entry. The interpreter + // uses this depth to properly unwind the stack and the scope chain. + depth_ = bce_->stackDepth; + + // Record the try location, then emit the try block. + if (!bce_->newSrcNote(SRC_TRY, ¬eIndex_)) + return false; + if (!bce_->emit1(JSOP_TRY)) + return false; + tryStart_ = bce_->offset(); + + state_ = Try; + return true; + } + + private: + bool emitTryEnd() { + MOZ_ASSERT(state_ == Try); + MOZ_ASSERT(depth_ == bce_->stackDepth); + + // GOSUB to finally, if present. + if (hasFinally() && controlInfo_) { + if (!bce_->emitJump(JSOP_GOSUB, &controlInfo_->gosubs)) + return false; + } + + // Source note points to the jump at the end of the try block. + if (!bce_->setSrcNoteOffset(noteIndex_, 0, bce_->offset() - tryStart_ + JSOP_TRY_LENGTH)) + return false; + + // Emit jump over catch and/or finally. + if (!bce_->emitJump(JSOP_GOTO, &catchAndFinallyJump_)) + return false; + + if (!bce_->emitJumpTarget(&tryEnd_)) + return false; + + return true; + } + + public: + bool emitCatch() { + if (state_ == Try) { + if (!emitTryEnd()) + return false; + } else { + MOZ_ASSERT(state_ == Catch); + if (!emitCatchEnd(true)) + return false; + } + + MOZ_ASSERT(bce_->stackDepth == depth_); + + if (retValKind_ == UseRetVal) { + // Clear the frame's return value that might have been set by the + // try block: + // + // eval("try { 1; throw 2 } catch(e) {}"); // undefined, not 1 + if (!bce_->emit1(JSOP_UNDEFINED)) + return false; + if (!bce_->emit1(JSOP_SETRVAL)) + return false; + } + + state_ = Catch; + return true; + } + + private: + bool emitCatchEnd(bool hasNext) { + MOZ_ASSERT(state_ == Catch); + + if (!controlInfo_) + return true; + + // gosub , if required. + if (hasFinally()) { + if (!bce_->emitJump(JSOP_GOSUB, &controlInfo_->gosubs)) + return false; + MOZ_ASSERT(bce_->stackDepth == depth_); + } + + // Jump over the remaining catch blocks. This will get fixed + // up to jump to after catch/finally. + if (!bce_->emitJump(JSOP_GOTO, &catchAndFinallyJump_)) + return false; + + // If this catch block had a guard clause, patch the guard jump to + // come here. + if (controlInfo_->guardJump.offset != -1) { + if (!bce_->emitJumpTargetAndPatch(controlInfo_->guardJump)) + return false; + controlInfo_->guardJump.offset = -1; + + // If this catch block is the last one, rethrow, delegating + // execution of any finally block to the exception handler. + if (!hasNext) { + if (!bce_->emit1(JSOP_EXCEPTION)) + return false; + if (!bce_->emit1(JSOP_THROW)) + return false; + } + } + + return true; + } + + public: + bool emitFinally(Maybe finallyPos = Nothing()) { + MOZ_ASSERT(hasFinally()); + + if (state_ == Try) { + if (!emitTryEnd()) + return false; + } else { + MOZ_ASSERT(state_ == Catch); + if (!emitCatchEnd(false)) + return false; + } + + MOZ_ASSERT(bce_->stackDepth == depth_); + + if (!bce_->emitJumpTarget(&finallyStart_)) + return false; + + if (controlInfo_) { + // Fix up the gosubs that might have been emitted before non-local + // jumps to the finally code. + bce_->patchJumpsToTarget(controlInfo_->gosubs, finallyStart_); + + // Indicate that we're emitting a subroutine body. + controlInfo_->setEmittingSubroutine(); + } + if (finallyPos) { + if (!bce_->updateSourceCoordNotes(finallyPos.value())) + return false; + } + if (!bce_->emit1(JSOP_FINALLY)) + return false; + + if (retValKind_ == UseRetVal) { + if (!bce_->emit1(JSOP_GETRVAL)) + return false; + + // Clear the frame's return value to make break/continue return + // correct value even if there's no other statement before them: + // + // eval("x: try { 1 } finally { break x; }"); // undefined, not 1 + if (!bce_->emit1(JSOP_UNDEFINED)) + return false; + if (!bce_->emit1(JSOP_SETRVAL)) + return false; + } + + state_ = Finally; + return true; + } + + private: + bool emitFinallyEnd() { + MOZ_ASSERT(state_ == Finally); + + if (retValKind_ == UseRetVal) { + if (!bce_->emit1(JSOP_SETRVAL)) + return false; + } + + if (!bce_->emit1(JSOP_RETSUB)) + return false; + + bce_->hasTryFinally = true; + return true; + } + + public: + bool emitEnd() { + if (state_ == Catch) { + MOZ_ASSERT(!hasFinally()); + if (!emitCatchEnd(false)) + return false; + } else { + MOZ_ASSERT(state_ == Finally); + MOZ_ASSERT(hasFinally()); + if (!emitFinallyEnd()) + return false; + } + + MOZ_ASSERT(bce_->stackDepth == depth_); + + // ReconstructPCStack needs a NOP here to mark the end of the last + // catch block. + if (!bce_->emit1(JSOP_NOP)) + return false; + + // Fix up the end-of-try/catch jumps to come here. + if (!bce_->emitJumpTargetAndPatch(catchAndFinallyJump_)) + return false; + + // Add the try note last, to let post-order give us the right ordering + // (first to last for a given nesting level, inner to outer by level). + if (hasCatch()) { + if (!bce_->tryNoteList.append(JSTRY_CATCH, depth_, tryStart_, tryEnd_.offset)) + return false; + } + + // If we've got a finally, mark try+catch region with additional + // trynote to catch exceptions (re)thrown from a catch block or + // for the try{}finally{} case. + if (hasFinally()) { + if (!bce_->tryNoteList.append(JSTRY_FINALLY, depth_, tryStart_, finallyStart_.offset)) + return false; + } + + state_ = End; + return true; + } +}; + class MOZ_STACK_CLASS IfThenElseEmitter { BytecodeEmitter* bce_; @@ -6073,57 +6385,28 @@ BytecodeEmitter::emitCatch(ParseNode* pn) MOZ_NEVER_INLINE bool BytecodeEmitter::emitTry(ParseNode* pn) { - // Track jumps-over-catches and gosubs-to-finally for later fixup. - // - // When a finally block is active, non-local jumps (including - // jumps-over-catches) result in a GOSUB being written into the bytecode - // stream and fixed-up later. - // - TryFinallyControl controlInfo(this, pn->pn_kid3 ? StatementKind::Finally : StatementKind::Try); - - // Since an exception can be thrown at any place inside the try block, - // we need to restore the stack and the scope chain before we transfer - // the control to the exception handler. - // - // For that we store in a try note associated with the catch or - // finally block the stack depth upon the try entry. The interpreter - // uses this depth to properly unwind the stack and the scope chain. - // - int depth = stackDepth; - - // Record the try location, then emit the try block. - unsigned noteIndex; - if (!newSrcNote(SRC_TRY, ¬eIndex)) - return false; - if (!emit1(JSOP_TRY)) - return false; - - ptrdiff_t tryStart = offset(); - if (!emitTree(pn->pn_kid1)) - return false; - MOZ_ASSERT(depth == stackDepth); + ParseNode* catchList = pn->pn_kid2; + ParseNode* finallyNode = pn->pn_kid3; - // GOSUB to finally, if present. - if (pn->pn_kid3) { - if (!emitJump(JSOP_GOSUB, &controlInfo.gosubs)) - return false; + TryEmitter::Kind kind; + if (catchList) { + if (finallyNode) + kind = TryEmitter::TryCatchFinally; + else + kind = TryEmitter::TryCatch; + } else { + MOZ_ASSERT(finallyNode); + kind = TryEmitter::TryFinally; } + TryEmitter tryCatch(this, kind); - // Source note points to the jump at the end of the try block. - if (!setSrcNoteOffset(noteIndex, 0, offset() - tryStart + JSOP_TRY_LENGTH)) - return false; - - // Emit jump over catch and/or finally. - JumpList catchJump; - if (!emitJump(JSOP_GOTO, &catchJump)) + if (!tryCatch.emitTry()) return false; - JumpTarget tryEnd; - if (!emitJumpTarget(&tryEnd)) + if (!emitTree(pn->pn_kid1)) return false; // If this try has a catch block, emit it. - ParseNode* catchList = pn->pn_kid2; if (catchList) { MOZ_ASSERT(catchList->isKind(PNK_CATCHLIST)); @@ -6153,110 +6436,26 @@ BytecodeEmitter::emitTry(ParseNode* pn) // capturing exceptions thrown from catch{} blocks. // for (ParseNode* pn3 = catchList->pn_head; pn3; pn3 = pn3->pn_next) { - MOZ_ASSERT(this->stackDepth == depth); - - // Clear the frame's return value that might have been set by the - // try block: - // - // eval("try { 1; throw 2 } catch(e) {}"); // undefined, not 1 - if (!emit1(JSOP_UNDEFINED)) - return false; - if (!emit1(JSOP_SETRVAL)) + if (!tryCatch.emitCatch()) return false; // Emit the lexical scope and catch body. MOZ_ASSERT(pn3->isKind(PNK_LEXICALSCOPE)); if (!emitTree(pn3)) return false; - - // gosub , if required. - if (pn->pn_kid3) { - if (!emitJump(JSOP_GOSUB, &controlInfo.gosubs)) - return false; - MOZ_ASSERT(this->stackDepth == depth); - } - - // Jump over the remaining catch blocks. This will get fixed - // up to jump to after catch/finally. - if (!emitJump(JSOP_GOTO, &catchJump)) - return false; - - // If this catch block had a guard clause, patch the guard jump to - // come here. - if (controlInfo.guardJump.offset != -1) { - if (!emitJumpTargetAndPatch(controlInfo.guardJump)) - return false; - controlInfo.guardJump.offset = -1; - - // If this catch block is the last one, rethrow, delegating - // execution of any finally block to the exception handler. - if (!pn3->pn_next) { - if (!emit1(JSOP_EXCEPTION)) - return false; - if (!emit1(JSOP_THROW)) - return false; - } - } } } - MOZ_ASSERT(this->stackDepth == depth); - // Emit the finally handler, if there is one. - JumpTarget finallyStart{ 0 }; - if (pn->pn_kid3) { - if (!emitJumpTarget(&finallyStart)) - return false; - - // Fix up the gosubs that might have been emitted before non-local - // jumps to the finally code. - patchJumpsToTarget(controlInfo.gosubs, finallyStart); - - // Indicate that we're emitting a subroutine body. - controlInfo.setEmittingSubroutine(); - if (!updateSourceCoordNotes(pn->pn_kid3->pn_pos.begin)) - return false; - if (!emit1(JSOP_FINALLY)) - return false; - if (!emit1(JSOP_GETRVAL)) - return false; - - // Clear the frame's return value to make break/continue return - // correct value even if there's no other statement before them: - // - // eval("x: try { 1 } finally { break x; }"); // undefined, not 1 - if (!emit1(JSOP_UNDEFINED)) - return false; - if (!emit1(JSOP_SETRVAL)) + if (finallyNode) { + if (!tryCatch.emitFinally(Some(finallyNode->pn_pos.begin))) return false; - if (!emitTree(pn->pn_kid3)) - return false; - if (!emit1(JSOP_SETRVAL)) - return false; - if (!emit1(JSOP_RETSUB)) + if (!emitTree(finallyNode)) return false; - hasTryFinally = true; - MOZ_ASSERT(this->stackDepth == depth); } - // ReconstructPCStack needs a NOP here to mark the end of the last catch block. - if (!emit1(JSOP_NOP)) - return false; - - // Fix up the end-of-try/catch jumps to come here. - if (!emitJumpTargetAndPatch(catchJump)) - return false; - - // Add the try note last, to let post-order give us the right ordering - // (first to last for a given nesting level, inner to outer by level). - if (catchList && !tryNoteList.append(JSTRY_CATCH, depth, tryStart, tryEnd.offset)) - return false; - - // If we've got a finally, mark try+catch region with additional - // trynote to catch exceptions (re)thrown from a catch block or - // for the try{}finally{} case. - if (pn->pn_kid3 && !tryNoteList.append(JSTRY_FINALLY, depth, tryStart, finallyStart.offset)) + if (!tryCatch.emitEnd()) return false; return true; @@ -8040,17 +8239,15 @@ BytecodeEmitter::emitYieldStar(ParseNode* iter, ParseNode* gen) int depth = stackDepth; MOZ_ASSERT(depth >= 2); - JumpList send; - if (!emitJump(JSOP_GOTO, &send)) // goto send + TryEmitter tryCatch(this, TryEmitter::TryCatchFinally, TryEmitter::DontUseRetVal, + TryEmitter::DontUseControl); + if (!tryCatch.emitJumpOverCatchAndFinally()) // ITER RESULT return false; - // Try prologue. // ITER RESULT - unsigned noteIndex; - if (!newSrcNote(SRC_TRY, ¬eIndex)) - return false; JumpTarget tryStart{ offset() }; - if (!emit1(JSOP_TRY)) // tryStart: + if (!tryCatch.emitTry()) // ITER RESULT return false; + MOZ_ASSERT(this->stackDepth == depth); // Load the generator object. @@ -8061,17 +8258,9 @@ BytecodeEmitter::emitYieldStar(ParseNode* iter, ParseNode* gen) if (!emitYieldOp(JSOP_YIELD)) // ITER RECEIVED return false; - // Try epilogue. - if (!setSrcNoteOffset(noteIndex, 0, offset() - tryStart.offset)) - return false; - if (!emitJump(JSOP_GOTO, &send)) // goto send - return false; - - JumpTarget tryEnd; - if (!emitJumpTarget(&tryEnd)) // tryEnd: + if (!tryCatch.emitCatch()) // ITER RESULT return false; - // Catch location. stackDepth = uint32_t(depth); // ITER RESULT if (!emit1(JSOP_EXCEPTION)) // ITER RESULT EXCEPTION return false; @@ -8118,36 +8307,19 @@ BytecodeEmitter::emitYieldStar(ParseNode* iter, ParseNode* gen) if (!emitJump(JSOP_GOTO, &checkResult)) // goto checkResult return false; - // The finally block, IteratorClose logic. + // IteratorClose logic. + if (!tryCatch.emitFinally()) + return false; - JumpTarget finallyStart{ 0 }; - if (!emitJumpTarget(&finallyStart)) - return false; - if (!emit1(JSOP_FINALLY)) // ITER RESULT FTYPE FVALUE - return false; if (!emitDupAt(3)) // ITER RESULT FTYPE FVALUE ITER return false; if (!emitIteratorClose(Some(tryStart))) // ITER RESULT FTYPE FVALUE return false; - if (!emit1(JSOP_RETSUB)) // ITER RESULT - return false; - - // Catch and finally epilogue. - // This is a peace offering to ReconstructPCStack. See the note in EmitTry. - if (!emit1(JSOP_NOP)) - return false; - size_t tryStartOffset = tryStart.offset + JSOP_TRY_LENGTH; - if (!tryNoteList.append(JSTRY_CATCH, depth, tryStartOffset, tryEnd.offset)) - return false; - if (!tryNoteList.append(JSTRY_FINALLY, depth, tryStartOffset, finallyStart.offset)) + if (!tryCatch.emitEnd()) return false; // After the try-catch-finally block: send the received value to the iterator. - if (!emitJumpTargetAndPatch(send)) // send: - return false; - - // Send location. // result = iter.next(received) // ITER RECEIVED if (!emit1(JSOP_SWAP)) // RECEIVED ITER return false; -- cgit v1.2.3 From f3c542a6a55a90a0aa860b306fbefe329c3faaa4 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 13:34:31 +0200 Subject: Bug 1333946 - Make IonBuilder::processIterators transitive Issue #74 --- js/src/jit-test/tests/ion/bug1333946.js | 6 +++++ js/src/jit/IonBuilder.cpp | 45 +++++++++++++++------------------ 2 files changed, 27 insertions(+), 24 deletions(-) create mode 100644 js/src/jit-test/tests/ion/bug1333946.js diff --git a/js/src/jit-test/tests/ion/bug1333946.js b/js/src/jit-test/tests/ion/bug1333946.js new file mode 100644 index 000000000..9cb3fb629 --- /dev/null +++ b/js/src/jit-test/tests/ion/bug1333946.js @@ -0,0 +1,6 @@ +// |jit-test| exitstatus: 6; + +for (var x of [0]) { + timeout(0.001); + for (;;) {} +} diff --git a/js/src/jit/IonBuilder.cpp b/js/src/jit/IonBuilder.cpp index 4318db2b6..534a48a90 100644 --- a/js/src/jit/IonBuilder.cpp +++ b/js/src/jit/IonBuilder.cpp @@ -958,35 +958,32 @@ IonBuilder::build() bool IonBuilder::processIterators() { - // Find phis that must directly hold an iterator live. - Vector worklist; + // Find and mark phis that must transitively hold an iterator live. + + Vector worklist; + for (size_t i = 0; i < iterators_.length(); i++) { - MDefinition* def = iterators_[i]; - if (def->isPhi()) { - if (!worklist.append(def->toPhi())) - return false; - } else { - for (MUseDefIterator iter(def); iter; iter++) { - if (iter.def()->isPhi()) { - if (!worklist.append(iter.def()->toPhi())) - return false; - } - } - } + if (!worklist.append(iterators_[i])) + return false; + iterators_[i]->setInWorklist(); } - // 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)) + MDefinition* def = worklist.popCopy(); + def->setNotInWorklist(); + + if (def->isPhi()) { + MPhi* phi = def->toPhi(); + phi->setIterator(); + phi->setImplicitlyUsedUnchecked(); + } + + for (MUseDefIterator iter(def); iter; iter++) { + MDefinition* use = iter.def(); + if (!use->isInWorklist() && (!use->isPhi() || !use->toPhi()->isIterator())) { + if (!worklist.append(use)) return false; + use->setInWorklist(); } } } -- cgit v1.2.3 From c93787917dbf305aa622d2d288effff78fe81008 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 13:35:18 +0200 Subject: Bug 1335996 - Make test for bug 1333946 more reliable Issue #74 --- js/src/jit-test/tests/ion/bug1333946.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/js/src/jit-test/tests/ion/bug1333946.js b/js/src/jit-test/tests/ion/bug1333946.js index 9cb3fb629..1fa1b9c49 100644 --- a/js/src/jit-test/tests/ion/bug1333946.js +++ b/js/src/jit-test/tests/ion/bug1333946.js @@ -1,6 +1,8 @@ -// |jit-test| exitstatus: 6; +// |jit-test| error: 42; for (var x of [0]) { - timeout(0.001); - for (;;) {} + for (var i = 0; ; i++) { + if (i === 20000) + throw 42; + } } -- cgit v1.2.3 From 4ee42e38e0a490eb4880b4a260e3cbe07dd486d1 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 14:20:11 +0200 Subject: Bug 1338796 - Do not call iterator.return if iterator.throw is present in yield* Issue #74 --- js/src/frontend/BytecodeEmitter.cpp | 280 ++++++++++++--------- js/src/frontend/BytecodeEmitter.h | 4 +- .../ecma_6/Generators/yield-star-iterator-close.js | 38 ++- js/src/tests/ecma_6/shell.js | 7 +- 4 files changed, 197 insertions(+), 132 deletions(-) diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index b1b0055e2..4a60f1cf1 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -5141,7 +5141,7 @@ BytecodeEmitter::emitIteratorNext(ParseNode* pn, bool allowSelfHosted) } bool -BytecodeEmitter::emitIteratorClose(Maybe yieldStarTryStart, bool allowSelfHosted) +BytecodeEmitter::emitIteratorClose(bool allowSelfHosted) { MOZ_ASSERT(allowSelfHosted || emitterMode != BytecodeEmitter::SelfHosting, ".close() on iterators is prohibited in self-hosted code because it " @@ -5180,86 +5180,11 @@ BytecodeEmitter::emitIteratorClose(Maybe yieldStarTryStart, bool all // an Object. if (!emit1(JSOP_SWAP)) // ... RET ITER return false; - - // ES 14.4.13, yield * AssignmentExpression, step 5.c - // - // When emitting iterator.return() for yield* forced return, we need to - // pass the argument passed to Generator.prototype.return to the return - // method. - if (yieldStarTryStart) { - IfThenElseEmitter ifGeneratorClosing(this); - if (!emitDupAt(2)) // ... FTYPE FVALUE RET ITER FVALUE - return false; - if (!emit1(JSOP_ISGENCLOSING)) // ... FTYPE FVALUE RET ITER FVALUE CLOSING - return false; - if (!emit1(JSOP_SWAP)) // ... FTYPE FVALUE RET ITER CLOSING FVALUE - return false; - if (!emit1(JSOP_POP)) // ... FTYPE FVALUE RET ITER CLOSING - return false; - if (!ifGeneratorClosing.emitIfElse()) // ... FTYPE FVALUE RET ITER - return false; - - if (!emit1(JSOP_GETRVAL)) // ... FTYPE FVALUE RET ITER RVAL - return false; - if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // ... FTYPE FVALUE RET ITER VALUE - return false; - if (!emitCall(JSOP_CALL, 1)) // ... FTYPE FVALUE RESULT - return false; - checkTypeSet(JSOP_CALL); - if (!emitCheckIsObj(CheckIsObjectKind::IteratorReturn)) // ... FTYPE FVALUE RESULT - return false; - - IfThenElseEmitter ifReturnDone(this); - if (!emit1(JSOP_DUP)) // ITER OLDRESULT FTYPE FVALUE RESULT RESULT - return false; - if (!emitAtomOp(cx->names().done, JSOP_GETPROP)) // ITER OLDRESULT FTYPE FVALUE RESULT DONE - return false; - if (!ifReturnDone.emitIfElse()) // ITER OLDRESULT FTYPE FVALUE RESULT - return false; - if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // ITER OLDRESULT FTYPE FVALUE VALUE - return false; - if (!emitPrepareIteratorResult()) // ITER OLDRESULT FTYPE FVALUE VALUE RESULT - return false; - if (!emit1(JSOP_SWAP)) // ITER OLDRESULT FTYPE FVALUE RESULT VALUE - return false; - if (!emitFinishIteratorResult(true)) // ITER OLDRESULT FTYPE FVALUE RESULT - return false; - if (!emit1(JSOP_DUP)) // ITER OLDRESULT FTYPE FVALUE RESULT RESULT - return false; - if (!emit1(JSOP_SETRVAL)) // ITER OLDRESULT FTYPE FVALUE RESULT - return false; - if (!ifReturnDone.emitElse()) // ITER OLDRESULT FTYPE FVALUE RESULT - return false; - int32_t savedDepth = this->stackDepth; - if (!emit2(JSOP_UNPICK, 3)) // ITER RESULT OLDRESULT FTYPE FVALUE - return false; - if (!emitUint16Operand(JSOP_POPN, 3)) // ITER RESULT - return false; - JumpList beq; - JumpTarget breakTarget{ -1 }; - if (!emitBackwardJump(JSOP_GOTO, *yieldStarTryStart, &beq, &breakTarget)) // ITER RESULT - return false; - this->stackDepth = savedDepth; - if (!ifReturnDone.emitEnd()) - return false; - - if (!ifGeneratorClosing.emitElse()) // ... FTYPE FVALUE RET ITER - return false; - if (!emitCall(JSOP_CALL, 0)) // ... FTYPE FVALUE RESULT - return false; - checkTypeSet(JSOP_CALL); - if (!emitCheckIsObj(CheckIsObjectKind::IteratorReturn)) // ... FTYPE FVALUE RESULT - return false; - - if (!ifGeneratorClosing.emitEnd()) - return false; - } else { - if (!emitCall(JSOP_CALL, 0)) // ... RESULT - return false; - checkTypeSet(JSOP_CALL); - if (!emitCheckIsObj(CheckIsObjectKind::IteratorReturn)) // ... RESULT - return false; - } + if (!emitCall(JSOP_CALL, 0)) // ... RESULT + return false; + checkTypeSet(JSOP_CALL); + if (!emitCheckIsObj(CheckIsObjectKind::IteratorReturn)) // ... RESULT + return false; if (!ifReturnMethodIsDefined.emitElse()) return false; @@ -8227,93 +8152,202 @@ BytecodeEmitter::emitYieldStar(ParseNode* iter, ParseNode* gen) MOZ_ASSERT(sc->isFunctionBox()); MOZ_ASSERT(sc->asFunctionBox()->isStarGenerator()); - if (!emitTree(iter)) // ITERABLE + if (!emitTree(iter)) // ITERABLE return false; - if (!emitIterator()) // ITER + if (!emitIterator()) // ITER return false; // Initial send value is undefined. - if (!emit1(JSOP_UNDEFINED)) // ITER RECEIVED + if (!emit1(JSOP_UNDEFINED)) // ITER RECEIVED return false; - int depth = stackDepth; - MOZ_ASSERT(depth >= 2); + int32_t savedDepthTemp; + int32_t startDepth = stackDepth; + MOZ_ASSERT(startDepth >= 2); TryEmitter tryCatch(this, TryEmitter::TryCatchFinally, TryEmitter::DontUseRetVal, TryEmitter::DontUseControl); - if (!tryCatch.emitJumpOverCatchAndFinally()) // ITER RESULT + if (!tryCatch.emitJumpOverCatchAndFinally()) // ITER RESULT return false; JumpTarget tryStart{ offset() }; - if (!tryCatch.emitTry()) // ITER RESULT + if (!tryCatch.emitTry()) // ITER RESULT return false; - MOZ_ASSERT(this->stackDepth == depth); + MOZ_ASSERT(this->stackDepth == startDepth); // Load the generator object. - if (!emitTree(gen)) // ITER RESULT GENOBJ + if (!emitTree(gen)) // ITER RESULT GENOBJ return false; // Yield RESULT as-is, without re-boxing. - if (!emitYieldOp(JSOP_YIELD)) // ITER RECEIVED + if (!emitYieldOp(JSOP_YIELD)) // ITER RECEIVED return false; - if (!tryCatch.emitCatch()) // ITER RESULT + if (!tryCatch.emitCatch()) // ITER RESULT return false; - stackDepth = uint32_t(depth); // ITER RESULT - if (!emit1(JSOP_EXCEPTION)) // ITER RESULT EXCEPTION + stackDepth = startDepth; // ITER RESULT + if (!emit1(JSOP_EXCEPTION)) // ITER RESULT EXCEPTION return false; - if (!emitDupAt(2)) // ITER RESULT EXCEPTION ITER + if (!emitDupAt(2)) // ITER RESULT EXCEPTION ITER return false; - if (!emit1(JSOP_DUP)) // ITER RESULT EXCEPTION ITER ITER + if (!emit1(JSOP_DUP)) // ITER RESULT EXCEPTION ITER ITER return false; - if (!emitAtomOp(cx->names().throw_, JSOP_CALLPROP)) // ITER RESULT EXCEPTION ITER THROW + if (!emitAtomOp(cx->names().throw_, JSOP_CALLPROP)) // ITER RESULT EXCEPTION ITER THROW return false; - if (!emit1(JSOP_DUP)) // ITER RESULT EXCEPTION ITER THROW THROW + if (!emit1(JSOP_DUP)) // ITER RESULT EXCEPTION ITER THROW THROW return false; - if (!emit1(JSOP_UNDEFINED)) // ITER RESULT EXCEPTION ITER THROW THROW UNDEFINED + if (!emit1(JSOP_UNDEFINED)) // ITER RESULT EXCEPTION ITER THROW THROW UNDEFINED return false; - if (!emit1(JSOP_EQ)) // ITER RESULT EXCEPTION ITER THROW ?EQL + if (!emit1(JSOP_EQ)) // ITER RESULT EXCEPTION ITER THROW ?EQL return false; IfThenElseEmitter ifThrowMethodIsNotDefined(this); - if (!ifThrowMethodIsNotDefined.emitIf()) // ITER RESULT EXCEPTION ITER THROW + if (!ifThrowMethodIsNotDefined.emitIf()) // ITER RESULT EXCEPTION ITER THROW + return false; + savedDepthTemp = stackDepth; + if (!emit1(JSOP_POP)) // ITER RESULT EXCEPTION ITER + return false; + // ES 14.4.13, YieldExpression : yield * AssignmentExpression, step 5.b.iii.2 + // + // If the iterator does not have a "throw" method, it calls IteratorClose + // and then throws a TypeError. + if (!emitIteratorClose()) // ITER RESULT EXCEPTION return false; if (!emitUint16Operand(JSOP_THROWMSG, JSMSG_ITERATOR_NO_THROW)) // throw return false; - if (!ifThrowMethodIsNotDefined.emitEnd()) // ITER OLDRESULT EXCEPTION ITER THROW + stackDepth = savedDepthTemp; + if (!ifThrowMethodIsNotDefined.emitEnd()) // ITER OLDRESULT EXCEPTION ITER THROW return false; // ES 14.4.13, YieldExpression : yield * AssignmentExpression, step 5.b.iii.4. - // RESULT = ITER.throw(EXCEPTION) // ITER OLDRESULT EXCEPTION ITER THROW - if (!emit1(JSOP_SWAP)) // ITER OLDRESULT EXCEPTION THROW ITER + // RESULT = ITER.throw(EXCEPTION) // ITER OLDRESULT EXCEPTION ITER THROW + if (!emit1(JSOP_SWAP)) // ITER OLDRESULT EXCEPTION THROW ITER return false; - if (!emit2(JSOP_PICK, 2)) // ITER OLDRESULT THROW ITER EXCEPTION + if (!emit2(JSOP_PICK, 2)) // ITER OLDRESULT THROW ITER EXCEPTION return false; - if (!emitCall(JSOP_CALL, 1, iter)) // ITER OLDRESULT RESULT + if (!emitCall(JSOP_CALL, 1, iter)) // ITER OLDRESULT RESULT return false; checkTypeSet(JSOP_CALL); - if (!emitCheckIsObj(CheckIsObjectKind::IteratorThrow)) // ITER OLDRESULT RESULT + if (!emitCheckIsObj(CheckIsObjectKind::IteratorThrow)) // ITER OLDRESULT RESULT return false; - if (!emit1(JSOP_SWAP)) // ITER RESULT OLDRESULT + if (!emit1(JSOP_SWAP)) // ITER RESULT OLDRESULT return false; - if (!emit1(JSOP_POP)) // ITER RESULT + if (!emit1(JSOP_POP)) // ITER RESULT return false; - MOZ_ASSERT(this->stackDepth == depth); + MOZ_ASSERT(this->stackDepth == startDepth); JumpList checkResult; - // Note that there is no GOSUB to the finally block here. If the iterator has a - // "throw" method, it does not perform IteratorClose per // ES 14.4.13, YieldExpression : yield * AssignmentExpression, step 5.b.ii. - if (!emitJump(JSOP_GOTO, &checkResult)) // goto checkResult + // + // Note that there is no GOSUB to the finally block here. If the iterator has a + // "throw" method, it does not perform IteratorClose. + if (!emitJump(JSOP_GOTO, &checkResult)) // goto checkResult return false; - // IteratorClose logic. if (!tryCatch.emitFinally()) return false; - if (!emitDupAt(3)) // ITER RESULT FTYPE FVALUE ITER + // ES 14.4.13, yield * AssignmentExpression, step 5.c + // + // Call iterator.return() for receiving a "forced return" completion from + // the generator. + + IfThenElseEmitter ifGeneratorClosing(this); + if (!emit1(JSOP_ISGENCLOSING)) // ITER RESULT FTYPE FVALUE CLOSING + return false; + if (!ifGeneratorClosing.emitIf()) // ITER RESULT FTYPE FVALUE + return false; + + // Step ii. + // + // Get the "return" method. + if (!emitDupAt(3)) // ITER RESULT FTYPE FVALUE ITER + return false; + if (!emit1(JSOP_DUP)) // ITER RESULT FTYPE FVALUE ITER ITER + return false; + if (!emitAtomOp(cx->names().return_, JSOP_CALLPROP)) // ITER RESULT FTYPE FVALUE ITER RET + return false; + + // Step iii. + // + // Do nothing if "return" is undefined. + IfThenElseEmitter ifReturnMethodIsDefined(this); + if (!emit1(JSOP_DUP)) // ITER RESULT FTYPE FVALUE ITER RET RET + return false; + if (!emit1(JSOP_UNDEFINED)) // ITER RESULT FTYPE FVALUE ITER RET RET UNDEFINED + return false; + if (!emit1(JSOP_NE)) // ITER RESULT FTYPE FVALUE ITER RET ?NEQL + return false; + + // Step iv. + // + // Call "return" with the argument passed to Generator.prototype.return, + // which is currently in rval.value. + if (!ifReturnMethodIsDefined.emitIfElse()) // ITER OLDRESULT FTYPE FVALUE ITER RET + return false; + if (!emit1(JSOP_SWAP)) // ITER OLDRESULT FTYPE FVALUE RET ITER + return false; + if (!emit1(JSOP_GETRVAL)) // ITER OLDRESULT FTYPE FVALUE RET ITER RVAL + return false; + if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // ITER OLDRESULT FTYPE FVALUE RET ITER VALUE + return false; + if (!emitCall(JSOP_CALL, 1)) // ITER OLDRESULT FTYPE FVALUE RESULT + return false; + checkTypeSet(JSOP_CALL); + + // Step v. + if (!emitCheckIsObj(CheckIsObjectKind::IteratorReturn)) // ITER OLDRESULT FTYPE FVALUE RESULT + return false; + + // Steps vi-viii. + // + // Check if the returned object from iterator.return() is done. If not, + // continuing yielding. + IfThenElseEmitter ifReturnDone(this); + if (!emit1(JSOP_DUP)) // ITER OLDRESULT FTYPE FVALUE RESULT RESULT + return false; + if (!emitAtomOp(cx->names().done, JSOP_GETPROP)) // ITER OLDRESULT FTYPE FVALUE RESULT DONE + return false; + if (!ifReturnDone.emitIfElse()) // ITER OLDRESULT FTYPE FVALUE RESULT + return false; + if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // ITER OLDRESULT FTYPE FVALUE VALUE + return false; + if (!emitPrepareIteratorResult()) // ITER OLDRESULT FTYPE FVALUE VALUE RESULT + return false; + if (!emit1(JSOP_SWAP)) // ITER OLDRESULT FTYPE FVALUE RESULT VALUE + return false; + if (!emitFinishIteratorResult(true)) // ITER OLDRESULT FTYPE FVALUE RESULT + return false; + if (!emit1(JSOP_SETRVAL)) // ITER OLDRESULT FTYPE FVALUE + return false; + savedDepthTemp = this->stackDepth; + if (!ifReturnDone.emitElse()) // ITER OLDRESULT FTYPE FVALUE RESULT + return false; + if (!emit2(JSOP_UNPICK, 3)) // ITER RESULT OLDRESULT FTYPE FVALUE + return false; + if (!emitUint16Operand(JSOP_POPN, 3)) // ITER RESULT + return false; + { + // goto tryStart; + JumpList beq; + JumpTarget breakTarget{ -1 }; + if (!emitBackwardJump(JSOP_GOTO, tryStart, &beq, &breakTarget)) // ITER RESULT + return false; + } + this->stackDepth = savedDepthTemp; + if (!ifReturnDone.emitEnd()) + return false; + + if (!ifReturnMethodIsDefined.emitElse()) // ITER RESULT FTYPE FVALUE ITER RET + return false; + if (!emit1(JSOP_POP)) // ITER RESULT FTYPE FVALUE ITER return false; - if (!emitIteratorClose(Some(tryStart))) // ITER RESULT FTYPE FVALUE + if (!emit1(JSOP_POP)) // ITER RESULT FTYPE FVALUE + return false; + if (!ifReturnMethodIsDefined.emitEnd()) + return false; + + if (!ifGeneratorClosing.emitEnd()) return false; if (!tryCatch.emitEnd()) @@ -8338,7 +8372,7 @@ BytecodeEmitter::emitYieldStar(ParseNode* iter, ParseNode* gen) if (!emitCheckIsObj(CheckIsObjectKind::IteratorNext)) // ITER RESULT return false; checkTypeSet(JSOP_CALL); - MOZ_ASSERT(this->stackDepth == depth); + MOZ_ASSERT(this->stackDepth == startDepth); if (!emitJumpTargetAndPatch(checkResult)) // checkResult: return false; @@ -8349,10 +8383,12 @@ BytecodeEmitter::emitYieldStar(ParseNode* iter, ParseNode* gen) if (!emitAtomOp(cx->names().done, JSOP_GETPROP)) // ITER RESULT DONE return false; // if (!DONE) goto tryStart; - JumpList beq; - JumpTarget breakTarget{ -1 }; - if (!emitBackwardJump(JSOP_IFEQ, tryStart, &beq, &breakTarget)) // ITER RESULT - return false; + { + JumpList beq; + JumpTarget breakTarget{ -1 }; + if (!emitBackwardJump(JSOP_IFEQ, tryStart, &beq, &breakTarget)) // ITER RESULT + return false; + } // result.value if (!emit1(JSOP_SWAP)) // RESULT ITER @@ -8362,7 +8398,7 @@ BytecodeEmitter::emitYieldStar(ParseNode* iter, ParseNode* gen) if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // VALUE return false; - MOZ_ASSERT(this->stackDepth == depth - 1); + MOZ_ASSERT(this->stackDepth == startDepth - 1); return true; } diff --git a/js/src/frontend/BytecodeEmitter.h b/js/src/frontend/BytecodeEmitter.h index 78eb3510c..2ab6c6fef 100644 --- a/js/src/frontend/BytecodeEmitter.h +++ b/js/src/frontend/BytecodeEmitter.h @@ -679,9 +679,7 @@ struct MOZ_STACK_CLASS BytecodeEmitter // Pops iterator from the top of the stack. Pushes the result of |.next()| // onto the stack. MOZ_MUST_USE bool emitIteratorNext(ParseNode* pn, bool allowSelfHosted = false); - MOZ_MUST_USE bool emitIteratorClose( - mozilla::Maybe yieldStarTryStart = mozilla::Nothing(), - bool allowSelfHosted = false); + MOZ_MUST_USE bool emitIteratorClose(bool allowSelfHosted = false); template MOZ_MUST_USE bool wrapWithDestructuringIteratorCloseTryNote(int32_t iterDepth, diff --git a/js/src/tests/ecma_6/Generators/yield-star-iterator-close.js b/js/src/tests/ecma_6/Generators/yield-star-iterator-close.js index ec62dd86d..91ad31cb6 100644 --- a/js/src/tests/ecma_6/Generators/yield-star-iterator-close.js +++ b/js/src/tests/ecma_6/Generators/yield-star-iterator-close.js @@ -6,6 +6,8 @@ function test() { var returnCalledExpected = 0; var nextCalled = 0; var nextCalledExpected = 0; + var throwCalled = 0; + var throwCalledExpected = 0; var iterable = {}; iterable[Symbol.iterator] = makeIterator({ next: function() { @@ -25,9 +27,9 @@ function test() { // G.p.throw on an iterator without "throw" calls IteratorClose. var g1 = y(); g1.next(); - assertThrowsValue(function() { + assertThrowsInstanceOf(function() { g1.throw("foo"); - }, "foo"); + }, TypeError); assertEq(returnCalled, ++returnCalledExpected); assertEq(nextCalled, ++nextCalledExpected); g1.next(); @@ -98,9 +100,18 @@ function test() { // IteratorClose expects iter.return to return an Object. var g6 = y(); g6.next(); - assertThrowsInstanceOf(function() { + var exc; + try { g6.throw("foo"); - }, TypeError); + } catch (e) { + exc = e; + } finally { + assertEq(exc instanceof TypeError, true); + // The message test is here because instanceof TypeError doesn't + // distinguish the non-Object return TypeError and the + // throw-method-is-not-defined iterator protocol error. + assertEq(exc.toString().indexOf("non-object") > 0, true); + } assertEq(returnCalled, ++returnCalledExpected); // G.p.return passes its argument to "return". @@ -115,6 +126,25 @@ function test() { g7.next(); g7.return("in test"); assertEq(returnCalled, ++returnCalledExpected); + + // If a throw method is present, do not call "return". + iterable[Symbol.iterator] = makeIterator({ + throw: function(e) { + throwCalled++; + throw e; + }, + ret: function(x) { + returnCalled++; + return { done: true }; + } + }); + var g8 = y(); + g8.next(); + assertThrowsValue(function() { + g8.throw("foo"); + }, "foo"); + assertEq(throwCalled, ++throwCalledExpected); + assertEq(returnCalled, returnCalledExpected); } test(); diff --git a/js/src/tests/ecma_6/shell.js b/js/src/tests/ecma_6/shell.js index 4da9221d6..756da9f36 100644 --- a/js/src/tests/ecma_6/shell.js +++ b/js/src/tests/ecma_6/shell.js @@ -21,10 +21,11 @@ /** Make an iterator with a return method. */ global.makeIterator = function makeIterator(overrides) { + var throwMethod; + if (overrides && overrides.throw) + throwMethod = overrides.throw; var iterator = { - throw: function(e) { - throw e; - }, + throw: throwMethod, next: function(x) { if (overrides && overrides.next) return overrides.next(x); -- cgit v1.2.3 From 6056525ced07af6c6c4f48ea605a2f4589821fdf Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 14:47:39 +0200 Subject: Bug 1341339 - Check for duplicates in processIterators Issue #74 --- js/src/jit-test/tests/for-of/bug-1341339.js | 9 +++++++++ js/src/jit/IonBuilder.cpp | 9 ++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 js/src/jit-test/tests/for-of/bug-1341339.js diff --git a/js/src/jit-test/tests/for-of/bug-1341339.js b/js/src/jit-test/tests/for-of/bug-1341339.js new file mode 100644 index 000000000..1f88acdaf --- /dev/null +++ b/js/src/jit-test/tests/for-of/bug-1341339.js @@ -0,0 +1,9 @@ +let m = parseModule(` +function* values() {} +var iterator = values(); +for (var i=0; i < 10000; ++i) { + for (var x of iterator) {} +} +`); +m.declarationInstantiation(); +m.evaluation(); diff --git a/js/src/jit/IonBuilder.cpp b/js/src/jit/IonBuilder.cpp index 534a48a90..26bba0656 100644 --- a/js/src/jit/IonBuilder.cpp +++ b/js/src/jit/IonBuilder.cpp @@ -963,9 +963,12 @@ IonBuilder::processIterators() Vector worklist; for (size_t i = 0; i < iterators_.length(); i++) { - if (!worklist.append(iterators_[i])) - return false; - iterators_[i]->setInWorklist(); + MDefinition* iter = iterators_[i]; + if (!iter->isInWorklist()) { + if (!worklist.append(iter)) + return false; + iter->setInWorklist(); + } } while (!worklist.empty()) { -- cgit v1.2.3 From e7a220aae2dd6f92c57b92fdd08ff94e5826c035 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 14:48:34 +0200 Subject: Bug 1331585 - Allow falsy "done" values for IteratorClose due to exception during array destructuring Issue #74 --- js/src/jit/JitFrames.cpp | 12 ++++++------ .../tests/ecma_6/Destructuring/array-iterator-close.js | 16 ++++++++++++++++ js/src/vm/Interpreter.cpp | 5 +++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/js/src/jit/JitFrames.cpp b/js/src/jit/JitFrames.cpp index 30bbd0b9b..7c3f0d120 100644 --- a/js/src/jit/JitFrames.cpp +++ b/js/src/jit/JitFrames.cpp @@ -353,11 +353,11 @@ CloseLiveIteratorIon(JSContext* cx, const InlineFrameIterator& frame, JSTryNote* RootedObject iterObject(cx, &v.toObject()); if (isDestructuring) { - Value v = si.read(); - MOZ_ASSERT(v.isBoolean()); + RootedValue doneValue(cx, si.read()); + bool done = ToBoolean(doneValue); // Do not call IteratorClose if the destructuring iterator is already // done. - if (v.isTrue()) + if (done) return; } @@ -654,9 +654,9 @@ ProcessTryNotesBaseline(JSContext* cx, const JitFrameIterator& frame, Environmen uint8_t* framePointer; uint8_t* stackPointer; BaselineFrameAndStackPointersFromTryNote(tn, frame, &framePointer, &stackPointer); - Value doneValue(*(reinterpret_cast(stackPointer))); - MOZ_ASSERT(doneValue.isBoolean()); - if (doneValue.isFalse()) { + RootedValue doneValue(cx, *(reinterpret_cast(stackPointer))); + bool done = ToBoolean(doneValue); + if (!done) { Value iterValue(*(reinterpret_cast(stackPointer) + 1)); RootedObject iterObject(cx, &iterValue.toObject()); if (!IteratorCloseForException(cx, iterObject)) { diff --git a/js/src/tests/ecma_6/Destructuring/array-iterator-close.js b/js/src/tests/ecma_6/Destructuring/array-iterator-close.js index f7805540d..ed35135db 100644 --- a/js/src/tests/ecma_6/Destructuring/array-iterator-close.js +++ b/js/src/tests/ecma_6/Destructuring/array-iterator-close.js @@ -43,6 +43,22 @@ function test() { }, "in lhs"); assertEq(returnCalled, ++returnCalledExpected); + // throw in lhs ref calls IteratorClose with falsy "done". + iterable[Symbol.iterator] = makeIterator({ + next: function() { + // "done" is undefined. + return {}; + }, + ret: function() { + returnCalled++; + return {}; + } + }); + assertThrowsValue(function() { + 0, [...{}[throwlhs()]] = iterable; + }, "in lhs"); + assertEq(returnCalled, ++returnCalledExpected); + // throw in iter.next doesn't call IteratorClose iterable[Symbol.iterator] = makeIterator({ next: function() { diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 9a8c6777f..7f8ff8445 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -1205,8 +1205,9 @@ ProcessTryNotes(JSContext* cx, EnvironmentIter& ei, InterpreterRegs& regs) // stack. The iterator object is second from the top. MOZ_ASSERT(tn->stackDepth > 1); Value* sp = regs.spForStackDepth(tn->stackDepth); - MOZ_ASSERT(sp[-1].isBoolean()); - if (sp[-1].isFalse()) { + RootedValue doneValue(cx, sp[-1]); + bool done = ToBoolean(doneValue); + if (!done) { RootedObject iterObject(cx, &sp[-2].toObject()); if (!IteratorCloseForException(cx, iterObject)) { SettleOnTryNote(cx, tn, ei, regs); -- cgit v1.2.3 From 2818e8dedb4a9a6cf40688ad42bf2be05e2f26d2 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 14:49:32 +0200 Subject: Bug 1334314 - Fix debug mode OSR exception handling for IteratorClose trynotes Issue #74 --- js/src/jit-test/tests/ion/bug1334314.js | 16 +++++++++++++ js/src/jit/BaselineBailouts.cpp | 41 +++++++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 js/src/jit-test/tests/ion/bug1334314.js diff --git a/js/src/jit-test/tests/ion/bug1334314.js b/js/src/jit-test/tests/ion/bug1334314.js new file mode 100644 index 000000000..488fc9027 --- /dev/null +++ b/js/src/jit-test/tests/ion/bug1334314.js @@ -0,0 +1,16 @@ +// |jit-test| error: TypeError + +var g = newGlobal(); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function () { };"); + +function f() { + [[]] = []; +} +try { + f(); +} catch (e) {}; +try { + f(); +} catch (e) {}; +f(); diff --git a/js/src/jit/BaselineBailouts.cpp b/js/src/jit/BaselineBailouts.cpp index 8fc8a522d..161e1aa4c 100644 --- a/js/src/jit/BaselineBailouts.cpp +++ b/js/src/jit/BaselineBailouts.cpp @@ -487,7 +487,7 @@ GetNextNonLoopEntryPc(jsbytecode* pc) } static bool -HasLiveIteratorAtStackDepth(JSScript* script, jsbytecode* pc, uint32_t stackDepth) +HasLiveStackValueAtDepth(JSScript* script, jsbytecode* pc, uint32_t stackDepth) { if (!script->hasTrynotes()) return false; @@ -501,14 +501,37 @@ HasLiveIteratorAtStackDepth(JSScript* script, jsbytecode* pc, uint32_t stackDept if (pcOffset >= tn->start + tn->length) continue; - // For-in loops have only the iterator on stack. - if (tn->kind == JSTRY_FOR_IN && stackDepth == tn->stackDepth) - return true; + switch (tn->kind) { + case JSTRY_FOR_IN: + // For-in loops have only the iterator on stack. + if (stackDepth == tn->stackDepth) + return true; + break; + + case JSTRY_FOR_OF: + // For-of loops have both the iterator and the result object on + // stack. The iterator is below the result object. + if (stackDepth == tn->stackDepth - 1) + return true; + break; + + case JSTRY_ITERCLOSE: + // Code that need to call IteratorClose have the iterator on the + // stack. + if (stackDepth == tn->stackDepth) + return true; + break; - // For-of loops have both the iterator and the result object on - // stack. The iterator is below the result object. - if (tn->kind == JSTRY_FOR_OF && stackDepth == tn->stackDepth - 1) - return true; + case JSTRY_DESTRUCTURING_ITERCLOSE: + // Destructuring code that need to call IteratorClose have both + // the iterator and the "done" value on the stack. + if (stackDepth == tn->stackDepth || stackDepth == tn->stackDepth - 1) + return true; + break; + + default: + break; + } } return false; @@ -945,7 +968,7 @@ InitFromBailout(JSContext* cx, HandleScript caller, jsbytecode* callerPC, // iterators, however, so read them out. They will be closed by // HandleExceptionBaseline. MOZ_ASSERT(cx->compartment()->isDebuggee()); - if (iter.moreFrames() || HasLiveIteratorAtStackDepth(script, pc, i + 1)) { + if (iter.moreFrames() || HasLiveStackValueAtDepth(script, pc, i + 1)) { v = iter.read(); } else { iter.skip(); -- cgit v1.2.3 From b311e8fa718e2a32805c25847781cc8a6a0541bd Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 16:22:38 +0200 Subject: Bug 1342553, Bug 1343072, Bug 1344753 (details in the description) Bug 1342553 - Part 0.1: Use try-catch for IteratorClose in for-of Bug 1343072 - Update HasLiveStackValueAtDepth to follow the change in JSTRY_FOR_OF Bug 1344753 - Update for-of stack depth in ControlFlowGenerator::processWhileOrForInLoop Issue #74 --- js/src/frontend/BytecodeEmitter.cpp | 453 ++++++++++++++------- js/src/frontend/BytecodeEmitter.h | 6 +- .../auto-regress/for-of-iterator-close-debugger.js | 15 + js/src/jit/BaselineBailouts.cpp | 14 +- js/src/jit/JitFrames.cpp | 15 - js/src/jsapi.h | 9 + js/src/jsscript.h | 1 - js/src/shell/js.cpp | 2 - js/src/vm/Interpreter.cpp | 34 +- js/src/vm/Interpreter.h | 7 + js/src/vm/Opcodes.h | 11 +- 11 files changed, 378 insertions(+), 189 deletions(-) create mode 100644 js/src/jit-test/tests/auto-regress/for-of-iterator-close-debugger.js diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index 4a60f1cf1..4d6ff6305 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -244,9 +244,9 @@ class LoopControl : public BreakableControl loopDepth_ = enclosingLoop ? enclosingLoop->loopDepth_ + 1 : 1; int loopSlots; - if (loopKind == StatementKind::Spread) + if (loopKind == StatementKind::Spread || loopKind == StatementKind::ForOfLoop) loopSlots = 3; - else if (loopKind == StatementKind::ForInLoop || loopKind == StatementKind::ForOfLoop) + else if (loopKind == StatementKind::ForInLoop) loopSlots = 2; else loopSlots = 0; @@ -278,64 +278,6 @@ class LoopControl : public BreakableControl } }; -class ForOfLoopControl : public LoopControl -{ - // The stack depth of the iterator. - int32_t iterDepth_; - - // for-of loops, when throwing from non-iterator code (i.e. from the body - // or from evaluating the LHS of the loop condition), need to call - // IteratorClose. If IteratorClose itself throws, we must not re-call - // IteratorClose. Since non-local jumps like break and return call - // IteratorClose, whenever a non-local jump is emitted, we must terminate - // the current JSTRY_ITERCLOSE note to skip the non-local jump code, then - // start a new one. - // - // Visually, - // - // for (x of y) { - // ... instantiate ForOfLoopControl - // ... + <-- iterCloseTryStart_ points to right before - // ... assignment to loop variable - // ... ^ - // ... | - // if (...) v - // + call finishIterCloseTryNote before |break| - // above range is noted with JSTRY_ITERCLOSE - // - // break; <-- break and IteratorClose are not inside - // JSTRY_ITERCLOSE note - // - // call startNewIterCloseTryNote after |break| - // + <-- next iterCloseTryStart_ points here - // ... | - // ... ~ - // } - ptrdiff_t iterCloseTryStart_; - - public: - ForOfLoopControl(BytecodeEmitter* bce, int32_t iterDepth) - : LoopControl(bce, StatementKind::ForOfLoop), - iterDepth_(iterDepth), - iterCloseTryStart_(-1) - { - MOZ_ASSERT(bce->stackDepth >= iterDepth); - } - - MOZ_MUST_USE bool finishIterCloseTryNote(BytecodeEmitter* bce) { - ptrdiff_t end = bce->offset(); - MOZ_ASSERT(end >= iterCloseTryStart_); - if (end != iterCloseTryStart_) - return bce->tryNoteList.append(JSTRY_ITERCLOSE, iterDepth_, iterCloseTryStart_, end); - return true; - } - - void startNewIterCloseTryNote(BytecodeEmitter* bce) { - MOZ_ASSERT(bce->offset() > iterCloseTryStart_); - iterCloseTryStart_ = bce->offset(); - } -}; - class TryFinallyControl : public BytecodeEmitter::NestableControl { bool emittingSubroutine_; @@ -2025,6 +1967,144 @@ class MOZ_STACK_CLASS IfThenElseEmitter #endif }; +class ForOfLoopControl : public LoopControl +{ + // The stack depth of the iterator. + int32_t iterDepth_; + + // for-of loops, when throwing from non-iterator code (i.e. from the body + // or from evaluating the LHS of the loop condition), need to call + // IteratorClose. This is done by enclosing non-iterator code with + // try-catch and call IteratorClose in `catch` block. + // If IteratorClose itself throws, we must not re-call IteratorClose. Since + // non-local jumps like break and return call IteratorClose, whenever a + // non-local jump is emitted, we must tell catch block not to perform + // IteratorClose. + // + // for (x of y) { + // // Operations for iterator (IteratorNext etc) are outside of + // // try-block. + // try { + // ... + // if (...) { + // // Before non-local jump, clear iterator on the stack to tell + // // catch block not to perform IteratorClose. + // tmpIterator = iterator; + // iterator = undefined; + // IteratorClose(tmpIterator, { break }); + // break; + // } + // ... + // } catch (e) { + // // Just throw again when iterator is cleared by non-local jump. + // if (iterator === undefined) + // throw e; + // IteratorClose(iterator, { throw, e }); + // } + // } + Maybe tryCatch_; + + bool allowSelfHosted_; + + public: + ForOfLoopControl(BytecodeEmitter* bce, int32_t iterDepth, bool allowSelfHosted) + : LoopControl(bce, StatementKind::ForOfLoop), + iterDepth_(iterDepth), + allowSelfHosted_(allowSelfHosted) + { + } + + bool emitBeginCodeNeedingIteratorClose(BytecodeEmitter* bce) { + tryCatch_.emplace(bce, TryEmitter::TryCatch, TryEmitter::DontUseRetVal); + + if (!tryCatch_->emitTry()) + return false; + return true; + } + + bool emitEndCodeNeedingIteratorClose(BytecodeEmitter* bce) { + if (!tryCatch_->emitCatch()) // ITER ... + return false; + + if (!bce->emit1(JSOP_EXCEPTION)) // ITER ... EXCEPTION + return false; + unsigned slotFromTop = bce->stackDepth - iterDepth_; + if (!bce->emitDupAt(slotFromTop)) // ITER ... EXCEPTION ITER + return false; + + // If ITER is undefined, it means the exception is thrown by + // IteratorClose for non-local jump, and we should't perform + // IteratorClose again here. + if (!bce->emit1(JSOP_UNDEFINED)) // ITER ... EXCEPTION ITER UNDEF + return false; + if (!bce->emit1(JSOP_STRICTNE)) // ITER ... EXCEPTION NE + return false; + + IfThenElseEmitter ifIteratorIsNotClosed(bce); + if (!ifIteratorIsNotClosed.emitIf()) // ITER ... EXCEPTION + return false; + + MOZ_ASSERT(slotFromTop == unsigned(bce->stackDepth - iterDepth_)); + if (!bce->emitDupAt(slotFromTop)) // ITER ... EXCEPTION ITER + return false; + if (!emitIteratorClose(bce, CompletionKind::Throw)) // ITER ... EXCEPTION + return false; + + if (!ifIteratorIsNotClosed.emitEnd()) // ITER ... EXCEPTION + return false; + + if (!bce->emit1(JSOP_THROW)) // ITER ... + return false; + + if (!tryCatch_->emitEnd()) + return false; + + tryCatch_.reset(); + return true; + } + + bool emitIteratorClose(BytecodeEmitter* bce, + CompletionKind completionKind = CompletionKind::Normal) { + return bce->emitIteratorClose(completionKind, allowSelfHosted_); + } + + bool emitPrepareForNonLocalJump(BytecodeEmitter* bce, bool isTarget) { + // Pop unnecessary values from the stack. Effectively this means + // leaving try-catch block. However, the performing IteratorClose can + // reach the depth for try-catch, and effectively re-enter the + // try-catch block. + if (!bce->emit1(JSOP_POP)) // ITER RESULT + return false; + if (!bce->emit1(JSOP_POP)) // ITER + return false; + + // Clear ITER slot on the stack to tell catch block to avoid performing + // IteratorClose again. + if (!bce->emit1(JSOP_UNDEFINED)) // ITER UNDEF + return false; + if (!bce->emit1(JSOP_SWAP)) // UNDEF ITER + return false; + + if (!emitIteratorClose(bce)) // UNDEF + return false; + + if (isTarget) { + // At the level of the target block, there's bytecode after the + // loop that will pop the iterator and the value, so push + // undefineds to balance the stack. + if (!bce->emit1(JSOP_UNDEFINED)) // UNDEF UNDEF + return false; + if (!bce->emit1(JSOP_UNDEFINED)) // UNDEF UNDEF UNDEF + return false; + } else { + if (!bce->emit1(JSOP_POP)) // + return false; + } + + return true; + } +}; + BytecodeEmitter::BytecodeEmitter(BytecodeEmitter* parent, Parser* parser, SharedContext* sc, HandleScript script, Handle lazyScript, @@ -2383,6 +2463,12 @@ BytecodeEmitter::emitCheckIsObj(CheckIsObjectKind kind) return emit2(JSOP_CHECKISOBJ, uint8_t(kind)); } +bool +BytecodeEmitter::emitCheckIsCallable(CheckIsCallableKind kind) +{ + return emit2(JSOP_CHECKISCALLABLE, uint8_t(kind)); +} + static inline unsigned LengthOfSetLine(unsigned line) { @@ -2624,10 +2710,8 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta EmitterScope* es = bce_->innermostEmitterScope; int npops = 0; - bool hasForOfLoopsWithIteratorClose = false; - // IteratorClose is handled specially in the exception unwinder. For - // 'continue', 'break', and 'return' statements, emit IteratorClose + // For 'continue', 'break', and 'return' statements, emit IteratorClose // bytecode inline. 'continue' statements do not call IteratorClose for // the loop they are continuing. bool emitIteratorClose = kind_ == Continue || kind_ == Break || kind_ == Return; @@ -2664,31 +2748,22 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta } else { if (!flushPops(bce_)) return false; - if (!bce_->emitJump(JSOP_GOSUB, &finallyControl.gosubs)) + if (!bce_->emitJump(JSOP_GOSUB, &finallyControl.gosubs)) // ... return false; } break; } case StatementKind::ForOfLoop: - if (!flushPops(bce_)) - return false; - - // The iterator and the current value are on the stack. - // if (emitIteratorClose) { - hasForOfLoopsWithIteratorClose = true; - if (!control->as().finishIterCloseTryNote(bce_)) - return false; - if (!bce_->emit1(JSOP_POP)) // ... ITER + if (!flushPops(bce_)) return false; - if (!bce_->emitIteratorClose()) // ... + + ForOfLoopControl& loopinfo = control->as(); + if (!loopinfo.emitPrepareForNonLocalJump(bce_, /* isTarget = */ false)) // ... return false; } else { - if (!bce_->emit1(JSOP_POP)) // ... ITER - return false; - if (!bce_->emit1(JSOP_POP)) // ... - return false; + npops += 3; } break; @@ -2711,18 +2786,9 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta if (!flushPops(bce_)) return false; - if (target && target->is() && emitIteratorCloseAtTarget) { - hasForOfLoopsWithIteratorClose = true; - if (!target->as().finishIterCloseTryNote(bce_)) - return false; - - // The iterator and the current value are on the stack. At the level - // of the target block, there's bytecode after the loop that will pop - // the iterator and the value, so duplicate the iterator and call - // IteratorClose. - if (!bce_->emitDupAt(1)) // ... ITER RESULT ITER - return false; - if (!bce_->emitIteratorClose()) // ... ITER RESULT + if (target && emitIteratorCloseAtTarget && target->is()) { + ForOfLoopControl& loopinfo = target->as(); + if (!loopinfo.emitPrepareForNonLocalJump(bce_, /* isTarget = */ true)) // ... UNDEF UNDEF UNDEF return false; } @@ -2732,20 +2798,6 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta return false; } - // See comment in ForOfLoopControl. - if (hasForOfLoopsWithIteratorClose) { - for (NestableControl* control = bce_->innermostNestableControl; - control != target; - control = control->enclosing()) - { - if (control->is()) - control->as().startNewIterCloseTryNote(bce_); - } - - if (target && target->is() && emitIteratorCloseAtTarget) - target->as().startNewIterCloseTryNote(bce_); - } - return true; } @@ -5120,7 +5172,7 @@ BytecodeEmitter::emitSetOrInitializeDestructuring(ParseNode* target, Destructuri } bool -BytecodeEmitter::emitIteratorNext(ParseNode* pn, bool allowSelfHosted) +BytecodeEmitter::emitIteratorNext(ParseNode* pn, bool allowSelfHosted /* = false */) { MOZ_ASSERT(allowSelfHosted || emitterMode != BytecodeEmitter::SelfHosting, ".next() iteration is prohibited in self-hosted code because it " @@ -5141,7 +5193,8 @@ BytecodeEmitter::emitIteratorNext(ParseNode* pn, bool allowSelfHosted) } bool -BytecodeEmitter::emitIteratorClose(bool allowSelfHosted) +BytecodeEmitter::emitIteratorClose(CompletionKind completionKind /* = CompletionKind::Normal */, + bool allowSelfHosted /* = false */) { MOZ_ASSERT(allowSelfHosted || emitterMode != BytecodeEmitter::SelfHosting, ".close() on iterators is prohibited in self-hosted code because it " @@ -5174,17 +5227,86 @@ BytecodeEmitter::emitIteratorClose(bool allowSelfHosted) if (!ifReturnMethodIsDefined.emitIfElse()) return false; + if (completionKind == CompletionKind::Throw) { + // 7.4.6 IteratorClose ( iterator, completion ) + // ... + // 3. Let return be ? GetMethod(iterator, "return"). + // 4. If return is undefined, return Completion(completion). + // 5. Let innerResult be Call(return, iterator, « »). + // 6. If completion.[[Type]] is throw, return Completion(completion). + // 7. If innerResult.[[Type]] is throw, return + // Completion(innerResult). + // + // For CompletionKind::Normal case, JSOP_CALL for step 5 checks if RET + // is callable, and throws if not. Since step 6 doesn't match and + // error handling in step 3 and step 7 can be merged. + // + // For CompletionKind::Throw case, an error thrown by JSOP_CALL for + // step 5 is ignored by try-catch. So we should check if RET is + // callable here, outside of try-catch, and the throw immediately if + // not. + CheckIsCallableKind kind = CheckIsCallableKind::IteratorReturn; + if (!emitCheckIsCallable(kind)) // ... ITER RET + return false; + } + // Steps 5, 8. // // Call "return" if it is not undefined or null, and check that it returns // an Object. if (!emit1(JSOP_SWAP)) // ... RET ITER return false; - if (!emitCall(JSOP_CALL, 0)) // ... RESULT + + Maybe tryCatch; + + if (completionKind == CompletionKind::Throw) { + tryCatch.emplace(this, TryEmitter::TryCatch, TryEmitter::DontUseRetVal, + TryEmitter::DontUseControl); + + // Mutate stack to balance stack for try-catch. + if (!emit1(JSOP_UNDEFINED)) // ... RET ITER UNDEF + return false; + if (!tryCatch->emitTry()) // ... RET ITER UNDEF + return false; + if (!emitDupAt(2)) // ... RET ITER UNDEF RET + return false; + if (!emitDupAt(2)) // ... RET ITER UNDEF RET ITER + return false; + } + + if (!emitCall(JSOP_CALL, 0)) // ... ... RESULT return false; checkTypeSet(JSOP_CALL); - if (!emitCheckIsObj(CheckIsObjectKind::IteratorReturn)) // ... RESULT - return false; + + if (completionKind == CompletionKind::Throw) { + if (!emit1(JSOP_SWAP)) // ... RET ITER RESULT UNDEF + return false; + if (!emit1(JSOP_POP)) // ... RET ITER RESULT + return false; + + if (!tryCatch->emitCatch()) // ... RET ITER RESULT + return false; + + // Just ignore the exception thrown by call. + if (!emit1(JSOP_EXCEPTION)) // ... RET ITER RESULT EXC + return false; + if (!emit1(JSOP_POP)) // ... RET ITER RESULT + return false; + + if (!tryCatch->emitEnd()) // ... RET ITER RESULT + return false; + + // Restore stack. + if (!emit2(JSOP_UNPICK, 2)) // ... RESULT RET ITER + return false; + if (!emit1(JSOP_POP)) // ... RESULT RET + return false; + if (!emit1(JSOP_POP)) // ... RESULT + return false; + } else { + if (!emitCheckIsObj(CheckIsObjectKind::IteratorReturn)) // ... RESULT + return false; + } if (!ifReturnMethodIsDefined.emitElse()) return false; @@ -6749,8 +6871,19 @@ BytecodeEmitter::emitForOf(ParseNode* forOfLoop, EmitterScope* headLexicalEmitte MOZ_ASSERT(forOfHead->isKind(PNK_FOROF)); MOZ_ASSERT(forOfHead->isArity(PN_TERNARY)); - // Evaluate the expression being iterated. ParseNode* forHeadExpr = forOfHead->pn_kid3; + + // Certain builtins (e.g. Array.from) are implemented in self-hosting + // as for-of loops. + bool allowSelfHostedIter = false; + if (emitterMode == BytecodeEmitter::SelfHosting && + forHeadExpr->isKind(PNK_CALL) && + forHeadExpr->pn_head->name() == cx->names().allowContentIter) + { + allowSelfHostedIter = true; + } + + // Evaluate the expression being iterated. if (!emitTree(forHeadExpr)) // ITERABLE return false; if (!emitIterator()) // ITER @@ -6758,12 +6891,14 @@ BytecodeEmitter::emitForOf(ParseNode* forOfLoop, EmitterScope* headLexicalEmitte int32_t iterDepth = stackDepth; - // For-of loops have both the iterator and the value on the stack. Push - // undefined to balance the stack. + // For-of loops have both the iterator, the result, and the result.value + // on the stack. Push undefineds to balance the stack. if (!emit1(JSOP_UNDEFINED)) // ITER RESULT return false; + if (!emit1(JSOP_UNDEFINED)) // ITER RESULT UNDEF + return false; - ForOfLoopControl loopInfo(this, iterDepth); + ForOfLoopControl loopInfo(this, iterDepth, allowSelfHostedIter); // Annotate so IonMonkey can find the loop-closing jump. unsigned noteIndex; @@ -6771,11 +6906,11 @@ BytecodeEmitter::emitForOf(ParseNode* forOfLoop, EmitterScope* headLexicalEmitte return false; JumpList initialJump; - if (!emitJump(JSOP_GOTO, &initialJump)) // ITER RESULT + if (!emitJump(JSOP_GOTO, &initialJump)) // ITER RESULT UNDEF return false; JumpTarget top{ -1 }; - if (!emitLoopHead(nullptr, &top)) // ITER RESULT + if (!emitLoopHead(nullptr, &top)) // ITER RESULT UNDEF return false; // If the loop had an escaping lexical declaration, replace the current @@ -6792,7 +6927,7 @@ BytecodeEmitter::emitForOf(ParseNode* forOfLoop, EmitterScope* headLexicalEmitte MOZ_ASSERT(headLexicalEmitterScope->scope(this)->kind() == ScopeKind::Lexical); if (headLexicalEmitterScope->hasEnvironment()) { - if (!emit1(JSOP_RECREATELEXICALENV)) // ITER RESULT + if (!emit1(JSOP_RECREATELEXICALENV)) // ITER RESULT UNDEF return false; } @@ -6812,61 +6947,66 @@ BytecodeEmitter::emitForOf(ParseNode* forOfLoop, EmitterScope* headLexicalEmitte // // Note that ES 13.7.5.13, step 5.c says getting result.value does not // call IteratorClose, so start JSTRY_ITERCLOSE after the GETPROP. + if (!emit1(JSOP_POP)) // ITER RESULT + return false; if (!emit1(JSOP_DUP)) // ITER RESULT RESULT return false; if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // ITER RESULT VALUE return false; - loopInfo.startNewIterCloseTryNote(this); - - if (!emitInitializeForInOrOfTarget(forOfHead)) // ITER RESULT VALUE + if (!loopInfo.emitBeginCodeNeedingIteratorClose(this)) return false; - if (!emit1(JSOP_POP)) // ITER RESULT + if (!emitInitializeForInOrOfTarget(forOfHead)) // ITER RESULT VALUE return false; MOZ_ASSERT(stackDepth == loopDepth, "the stack must be balanced around the initializing " "operation"); + // Remove VALUE from the stack to release it. + if (!emit1(JSOP_POP)) // ITER RESULT + return false; + if (!emit1(JSOP_UNDEFINED)) // ITER RESULT UNDEF + return false; + // Perform the loop body. ParseNode* forBody = forOfLoop->pn_right; - if (!emitTree(forBody)) // ITER RESULT + if (!emitTree(forBody)) // ITER RESULT UNDEF return false; - if (!loopInfo.finishIterCloseTryNote(this)) + MOZ_ASSERT(stackDepth == loopDepth, + "the stack must be balanced around the for-of body"); + + if (!loopInfo.emitEndCodeNeedingIteratorClose(this)) return false; // Set offset for continues. loopInfo.continueTarget = { offset() }; - if (!emitLoopEntry(forHeadExpr, initialJump)) // ITER RESULT + if (!emitLoopEntry(forHeadExpr, initialJump)) // ITER RESULT UNDEF return false; - if (!emit1(JSOP_POP)) // ITER + if (!emit1(JSOP_SWAP)) // ITER UNDEF RESULT + return false; + if (!emit1(JSOP_POP)) // ITER UNDEF return false; - if (!emit1(JSOP_DUP)) // ITER ITER + if (!emitDupAt(1)) // ITER UNDEF ITER return false; - // Certain builtins (e.g. Array.from) are implemented in self-hosting - // as for-of loops. - bool allowSelfHostedIter = false; - if (emitterMode == BytecodeEmitter::SelfHosting && - forHeadExpr->isKind(PNK_CALL) && - forHeadExpr->pn_head->name() == cx->names().allowContentIter) - { - allowSelfHostedIter = true; - } + if (!emitIteratorNext(forOfHead, allowSelfHostedIter)) // ITER UNDEF RESULT + return false; - if (!emitIteratorNext(forOfHead, allowSelfHostedIter)) // ITER RESULT + if (!emit1(JSOP_SWAP)) // ITER RESULT UNDEF return false; - if (!emit1(JSOP_DUP)) // ITER RESULT RESULT + + if (!emitDupAt(1)) // ITER RESULT UNDEF RESULT return false; - if (!emitAtomOp(cx->names().done, JSOP_GETPROP)) // ITER RESULT DONE + if (!emitAtomOp(cx->names().done, JSOP_GETPROP)) // ITER RESULT UNDEF DONE return false; if (!emitBackwardJump(JSOP_IFEQ, top, &beq, &breakTarget)) - return false; // ITER RESULT + return false; // ITER RESULT UNDEF MOZ_ASSERT(this->stackDepth == loopDepth); } @@ -6881,7 +7021,7 @@ BytecodeEmitter::emitForOf(ParseNode* forOfLoop, EmitterScope* headLexicalEmitte if (!tryNoteList.append(JSTRY_FOR_OF, stackDepth, top.offset, breakTarget.offset)) return false; - return emitUint16Operand(JSOP_POPN, 2); // + return emitUint16Operand(JSOP_POPN, 3); // } bool @@ -7291,6 +7431,8 @@ BytecodeEmitter::emitComprehensionForOf(ParseNode* pn) // Push a dummy result so that we properly enter iteration midstream. if (!emit1(JSOP_UNDEFINED)) // ITER RESULT return false; + if (!emit1(JSOP_UNDEFINED)) // ITER RESULT VALUE + return false; // Enter the block before the loop body, after evaluating the obj. // Initialize let bindings with undefined when entering, as the name @@ -7328,42 +7470,59 @@ BytecodeEmitter::emitComprehensionForOf(ParseNode* pn) #endif // Emit code to assign result.value to the iteration variable. + if (!emit1(JSOP_POP)) // ITER RESULT + return false; if (!emit1(JSOP_DUP)) // ITER RESULT RESULT return false; if (!emitAtomOp(cx->names().value, JSOP_GETPROP)) // ITER RESULT VALUE return false; + + // Notice: Comprehension for-of doesn't perform IteratorClose, since it's + // not in the spec. + if (!emitAssignment(loopVariableName, JSOP_NOP, nullptr)) // ITER RESULT VALUE return false; + + // Remove VALUE from the stack to release it. if (!emit1(JSOP_POP)) // ITER RESULT return false; + if (!emit1(JSOP_UNDEFINED)) // ITER RESULT UNDEF + return false; // The stack should be balanced around the assignment opcode sequence. MOZ_ASSERT(this->stackDepth == loopDepth); // Emit code for the loop body. - if (!emitTree(forBody)) + if (!emitTree(forBody)) // ITER RESULT UNDEF return false; + // The stack should be balanced around the assignment opcode sequence. + MOZ_ASSERT(this->stackDepth == loopDepth); + // Set offset for continues. loopInfo.continueTarget = { offset() }; if (!emitLoopEntry(forHeadExpr, jmp)) return false; - if (!emit1(JSOP_POP)) // ITER + if (!emit1(JSOP_SWAP)) // ITER UNDEF RESULT return false; - if (!emit1(JSOP_DUP)) // ITER ITER + if (!emit1(JSOP_POP)) // ITER UNDEF return false; - if (!emitIteratorNext(forHead)) // ITER RESULT + if (!emitDupAt(1)) // ITER UNDEF ITER return false; - if (!emit1(JSOP_DUP)) // ITER RESULT RESULT + if (!emitIteratorNext(forHead)) // ITER UNDEF RESULT + return false; + if (!emit1(JSOP_SWAP)) // ITER RESULT UNDEF + return false; + if (!emitDupAt(1)) // ITER RESULT UNDEF RESULT return false; - if (!emitAtomOp(cx->names().done, JSOP_GETPROP)) // ITER RESULT DONE + if (!emitAtomOp(cx->names().done, JSOP_GETPROP)) // ITER RESULT UNDEF DONE return false; JumpList beq; JumpTarget breakTarget{ -1 }; - if (!emitBackwardJump(JSOP_IFEQ, top, &beq, &breakTarget)) // ITER RESULT + if (!emitBackwardJump(JSOP_IFEQ, top, &beq, &breakTarget)) // ITER RESULT UNDEF return false; MOZ_ASSERT(this->stackDepth == loopDepth); @@ -7385,7 +7544,7 @@ BytecodeEmitter::emitComprehensionForOf(ParseNode* pn) } // Pop the result and the iter. - return emitUint16Operand(JSOP_POPN, 2); // + return emitUint16Operand(JSOP_POPN, 3); // } bool diff --git a/js/src/frontend/BytecodeEmitter.h b/js/src/frontend/BytecodeEmitter.h index 2ab6c6fef..7ff40b462 100644 --- a/js/src/frontend/BytecodeEmitter.h +++ b/js/src/frontend/BytecodeEmitter.h @@ -473,6 +473,9 @@ struct MOZ_STACK_CLASS BytecodeEmitter // Helper to emit JSOP_CHECKISOBJ. MOZ_MUST_USE bool emitCheckIsObj(CheckIsObjectKind kind); + // Helper to emit JSOP_CHECKISCALLABLE. + MOZ_MUST_USE bool emitCheckIsCallable(CheckIsCallableKind kind); + // Emit a bytecode followed by an uint16 immediate operand stored in // big-endian order. MOZ_MUST_USE bool emitUint16Operand(JSOp op, uint32_t operand); @@ -679,7 +682,8 @@ struct MOZ_STACK_CLASS BytecodeEmitter // Pops iterator from the top of the stack. Pushes the result of |.next()| // onto the stack. MOZ_MUST_USE bool emitIteratorNext(ParseNode* pn, bool allowSelfHosted = false); - MOZ_MUST_USE bool emitIteratorClose(bool allowSelfHosted = false); + MOZ_MUST_USE bool emitIteratorClose(CompletionKind completionKind = CompletionKind::Normal, + bool allowSelfHosted = false); template MOZ_MUST_USE bool wrapWithDestructuringIteratorCloseTryNote(int32_t iterDepth, diff --git a/js/src/jit-test/tests/auto-regress/for-of-iterator-close-debugger.js b/js/src/jit-test/tests/auto-regress/for-of-iterator-close-debugger.js new file mode 100644 index 000000000..a4d0bf654 --- /dev/null +++ b/js/src/jit-test/tests/auto-regress/for-of-iterator-close-debugger.js @@ -0,0 +1,15 @@ +// |jit-test| error:ReferenceError + +// for-of should close iterator even if the exception is once caught by the +// debugger. + +var g = newGlobal(); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function () { };"); +// jsfunfuzz-generated +for (var x of []) {}; +for (var l of [0]) { + for (var y = 0; y < 1; y++) { + g2; + } +} diff --git a/js/src/jit/BaselineBailouts.cpp b/js/src/jit/BaselineBailouts.cpp index 161e1aa4c..3ab722b3d 100644 --- a/js/src/jit/BaselineBailouts.cpp +++ b/js/src/jit/BaselineBailouts.cpp @@ -509,16 +509,10 @@ HasLiveStackValueAtDepth(JSScript* script, jsbytecode* pc, uint32_t stackDepth) break; case JSTRY_FOR_OF: - // For-of loops have both the iterator and the result object on - // stack. The iterator is below the result object. - if (stackDepth == tn->stackDepth - 1) - return true; - break; - - case JSTRY_ITERCLOSE: - // Code that need to call IteratorClose have the iterator on the - // stack. - if (stackDepth == tn->stackDepth) + // For-of loops have the iterator, the result object, and the value + // of the result object on stack. The iterator is below the result + // object and the value. + if (stackDepth == tn->stackDepth - 2) return true; break; diff --git a/js/src/jit/JitFrames.cpp b/js/src/jit/JitFrames.cpp index 7c3f0d120..a70356ad4 100644 --- a/js/src/jit/JitFrames.cpp +++ b/js/src/jit/JitFrames.cpp @@ -331,7 +331,6 @@ static void CloseLiveIteratorIon(JSContext* cx, const InlineFrameIterator& frame, JSTryNote* tn) { MOZ_ASSERT(tn->kind == JSTRY_FOR_IN || - tn->kind == JSTRY_ITERCLOSE || tn->kind == JSTRY_DESTRUCTURING_ITERCLOSE); bool isDestructuring = tn->kind == JSTRY_DESTRUCTURING_ITERCLOSE; @@ -442,7 +441,6 @@ HandleExceptionIon(JSContext* cx, const InlineFrameIterator& frame, ResumeFromEx switch (tn->kind) { case JSTRY_FOR_IN: - case JSTRY_ITERCLOSE: case JSTRY_DESTRUCTURING_ITERCLOSE: MOZ_ASSERT_IF(tn->kind == JSTRY_FOR_IN, JSOp(*(script->main() + tn->start + tn->length)) == JSOP_ENDITER); @@ -637,19 +635,6 @@ ProcessTryNotesBaseline(JSContext* cx, const JitFrameIterator& frame, Environmen break; } - case JSTRY_ITERCLOSE: { - uint8_t* framePointer; - uint8_t* stackPointer; - BaselineFrameAndStackPointersFromTryNote(tn, frame, &framePointer, &stackPointer); - Value iterValue(*reinterpret_cast(stackPointer)); - RootedObject iterObject(cx, &iterValue.toObject()); - if (!IteratorCloseForException(cx, iterObject)) { - SettleOnTryNote(cx, tn, frame, ei, rfe, pc); - return false; - } - break; - } - case JSTRY_DESTRUCTURING_ITERCLOSE: { uint8_t* framePointer; uint8_t* stackPointer; diff --git a/js/src/jsapi.h b/js/src/jsapi.h index cbef0f8fb..5523dfa1d 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -6622,5 +6622,14 @@ SetGetPerformanceGroupsCallback(JSContext*, GetGroupsCallback, void*); } /* namespace js */ +namespace js { + +enum class CompletionKind { + Normal, + Return, + Throw +}; + +} /* namespace js */ #endif /* jsapi_h */ diff --git a/js/src/jsscript.h b/js/src/jsscript.h index 8bba3ec39..9cb7c538f 100644 --- a/js/src/jsscript.h +++ b/js/src/jsscript.h @@ -85,7 +85,6 @@ enum JSTryNoteKind { JSTRY_FOR_IN, JSTRY_FOR_OF, JSTRY_LOOP, - JSTRY_ITERCLOSE, JSTRY_DESTRUCTURING_ITERCLOSE }; diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 6fe01de22..5d6508d59 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -2599,8 +2599,6 @@ TryNoteName(JSTryNoteKind kind) return "for-of"; case JSTRY_LOOP: return "loop"; - case JSTRY_ITERCLOSE: - return "iterclose"; case JSTRY_DESTRUCTURING_ITERCLOSE: return "dstr-iterclose"; } diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 7f8ff8445..eb3000e07 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -1189,17 +1189,6 @@ ProcessTryNotes(JSContext* cx, EnvironmentIter& ei, InterpreterRegs& regs) break; } - case JSTRY_ITERCLOSE: { - // The iterator object is at the top of the stack. - Value* sp = regs.spForStackDepth(tn->stackDepth); - RootedObject iterObject(cx, &sp[-1].toObject()); - if (!IteratorCloseForException(cx, iterObject)) { - SettleOnTryNote(cx, tn, ei, regs); - return ErrorReturnContinuation; - } - break; - } - case JSTRY_DESTRUCTURING_ITERCLOSE: { // Whether the destructuring iterator is done is at the top of the // stack. The iterator object is second from the top. @@ -1892,7 +1881,6 @@ CASE(JSOP_UNUSED192) CASE(JSOP_UNUSED209) CASE(JSOP_UNUSED210) CASE(JSOP_UNUSED211) -CASE(JSOP_UNUSED219) CASE(JSOP_UNUSED220) CASE(JSOP_UNUSED221) CASE(JSOP_UNUSED222) @@ -2637,6 +2625,15 @@ CASE(JSOP_CHECKISOBJ) } END_CASE(JSOP_CHECKISOBJ) +CASE(JSOP_CHECKISCALLABLE) +{ + if (!IsCallable(REGS.sp[-1])) { + MOZ_ALWAYS_FALSE(ThrowCheckIsCallable(cx, CheckIsCallableKind(GET_UINT8(REGS.pc)))); + goto error; + } +} +END_CASE(JSOP_CHECKISCALLABLE) + CASE(JSOP_CHECKTHIS) { if (REGS.sp[-1].isMagic(JS_UNINITIALIZED_LEXICAL)) { @@ -5094,6 +5091,19 @@ js::ThrowCheckIsObject(JSContext* cx, CheckIsObjectKind kind) return false; } +bool +js::ThrowCheckIsCallable(JSContext* cx, CheckIsCallableKind kind) +{ + switch (kind) { + case CheckIsCallableKind::IteratorReturn: + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_RETURN_NOT_CALLABLE); + break; + default: + MOZ_CRASH("Unknown kind"); + } + return false; +} + bool js::ThrowUninitializedThis(JSContext* cx, AbstractFramePtr frame) { diff --git a/js/src/vm/Interpreter.h b/js/src/vm/Interpreter.h index 38e23ec06..330dbef5f 100644 --- a/js/src/vm/Interpreter.h +++ b/js/src/vm/Interpreter.h @@ -570,6 +570,13 @@ enum class CheckIsObjectKind : uint8_t { bool ThrowCheckIsObject(JSContext* cx, CheckIsObjectKind kind); +enum class CheckIsCallableKind : uint8_t { + IteratorReturn +}; + +bool +ThrowCheckIsCallable(JSContext* cx, CheckIsCallableKind kind); + bool ThrowUninitializedThis(JSContext* cx, AbstractFramePtr frame); diff --git a/js/src/vm/Opcodes.h b/js/src/vm/Opcodes.h index 84f08c4d5..3848445ff 100644 --- a/js/src/vm/Opcodes.h +++ b/js/src/vm/Opcodes.h @@ -2201,7 +2201,16 @@ */ \ macro(JSOP_HOLE, 218, "hole", NULL, 1, 0, 1, JOF_BYTE) \ \ - macro(JSOP_UNUSED219, 219,"unused219", NULL, 1, 0, 0, JOF_BYTE) \ + /* + * Checks that the top value on the stack is callable, and throws a + * TypeError if not. The operand 'kind' is used only to generate an + * appropriate error message. + * Category: Statements + * Type: Function + * Operands: uint8_t kind + * Stack: result => result, callable + */ \ + macro(JSOP_CHECKISCALLABLE, 219, "checkiscallable", NULL, 2, 1, 1, JOF_UINT8) \ macro(JSOP_UNUSED220, 220,"unused220", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED221, 221,"unused221", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED222, 222,"unused222", NULL, 1, 0, 0, JOF_BYTE) \ -- cgit v1.2.3 From 727c27a30d10a811d5a3fe04e2407cd7b3993b5e Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 17:00:47 +0200 Subject: Bug 1342553 - Part 0.2: Support JSOP_CHECKISCALLABLE in JIT Issue #74 --- js/src/jit/BaselineCompiler.cpp | 20 +++++++ js/src/jit/BaselineCompiler.h | 1 + js/src/jit/CodeGenerator.cpp | 109 ++++++++++++++++++++++-------------- js/src/jit/CodeGenerator.h | 7 +++ js/src/jit/IonBuilder.cpp | 12 ++++ js/src/jit/IonBuilder.h | 1 + js/src/jit/Lowering.cpp | 13 +++++ js/src/jit/Lowering.h | 1 + js/src/jit/MIR.h | 32 ++++++++++- js/src/jit/MOpcodes.h | 1 + js/src/jit/VMFunctions.cpp | 9 +++ js/src/jit/VMFunctions.h | 4 ++ js/src/jit/shared/LIR-shared.h | 21 +++++++ js/src/jit/shared/LOpcodes-shared.h | 1 + 14 files changed, 189 insertions(+), 43 deletions(-) diff --git a/js/src/jit/BaselineCompiler.cpp b/js/src/jit/BaselineCompiler.cpp index 4524bae07..07d8e629d 100644 --- a/js/src/jit/BaselineCompiler.cpp +++ b/js/src/jit/BaselineCompiler.cpp @@ -1381,6 +1381,26 @@ BaselineCompiler::emit_JSOP_CHECKISOBJ() return true; } +typedef bool (*CheckIsCallableFn)(JSContext*, HandleValue, CheckIsCallableKind); +static const VMFunction CheckIsCallableInfo = + FunctionInfo(CheckIsCallable, "CheckIsCallable"); + +bool +BaselineCompiler::emit_JSOP_CHECKISCALLABLE() +{ + frame.syncStack(0); + masm.loadValue(frame.addressOfStackValue(frame.peek(-1)), R0); + + prepareVMCall(); + + pushArg(Imm32(GET_UINT8(pc))); + pushArg(R0); + if (!callVM(CheckIsCallableInfo)) + return false; + + return true; +} + typedef bool (*ThrowUninitializedThisFn)(JSContext*, BaselineFrame* frame); static const VMFunction ThrowUninitializedThisInfo = FunctionInfo(BaselineThrowUninitializedThis, diff --git a/js/src/jit/BaselineCompiler.h b/js/src/jit/BaselineCompiler.h index 18e56bcd4..0bacf6f18 100644 --- a/js/src/jit/BaselineCompiler.h +++ b/js/src/jit/BaselineCompiler.h @@ -218,6 +218,7 @@ namespace jit { _(JSOP_FUNCTIONTHIS) \ _(JSOP_GLOBALTHIS) \ _(JSOP_CHECKISOBJ) \ + _(JSOP_CHECKISCALLABLE) \ _(JSOP_CHECKTHIS) \ _(JSOP_CHECKRETURN) \ _(JSOP_NEWTARGET) \ diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp index 3b5ec6baa..ccdc5fbfa 100644 --- a/js/src/jit/CodeGenerator.cpp +++ b/js/src/jit/CodeGenerator.cpp @@ -11326,25 +11326,35 @@ class OutOfLineIsCallable : public OutOfLineCodeBase } }; +template void -CodeGenerator::visitIsCallable(LIsCallable* ins) +CodeGenerator::emitIsCallableOrConstructor(Register object, Register output, Label* failure) { - Register object = ToRegister(ins->object()); - Register output = ToRegister(ins->output()); - - OutOfLineIsCallable* ool = new(alloc()) OutOfLineIsCallable(ins); - addOutOfLineCode(ool, ins->mir()); - Label notFunction, hasCOps, done; masm.loadObjClass(object, output); - // Just skim proxies off. Their notion of isCallable() is more complicated. - masm.branchTestClassIsProxy(true, output, ool->entry()); + // Just skim proxies off. Their notion of isCallable()/isConstructor() is + // more complicated. + masm.branchTestClassIsProxy(true, output, failure); // An object is callable iff: // is() || (getClass()->cOps && getClass()->cOps->call). + // An object is constructor iff: + // ((is() && as().isConstructor) || + // (getClass()->cOps && getClass()->cOps->construct)). masm.branchPtr(Assembler::NotEqual, output, ImmPtr(&JSFunction::class_), ¬Function); - masm.move32(Imm32(1), output); + if (mode == Callable) { + masm.move32(Imm32(1), output); + } else { + Label notConstructor; + masm.load16ZeroExtend(Address(object, JSFunction::offsetOfFlags()), output); + masm.and32(Imm32(JSFunction::CONSTRUCTOR), output); + masm.branchTest32(Assembler::Zero, output, output, ¬Constructor); + masm.move32(Imm32(1), output); + masm.jump(&done); + masm.bind(¬Constructor); + masm.move32(Imm32(0), output); + } masm.jump(&done); masm.bind(¬Function); @@ -11355,10 +11365,26 @@ CodeGenerator::visitIsCallable(LIsCallable* ins) masm.bind(&hasCOps); masm.loadPtr(Address(output, offsetof(js::Class, cOps)), output); - masm.cmpPtrSet(Assembler::NonZero, Address(output, offsetof(js::ClassOps, call)), + size_t opsOffset = mode == Callable + ? offsetof(js::ClassOps, call) + : offsetof(js::ClassOps, construct); + masm.cmpPtrSet(Assembler::NonZero, Address(output, opsOffset), ImmPtr(nullptr), output); masm.bind(&done); +} + +void +CodeGenerator::visitIsCallable(LIsCallable* ins) +{ + Register object = ToRegister(ins->object()); + Register output = ToRegister(ins->output()); + + OutOfLineIsCallable* ool = new(alloc()) OutOfLineIsCallable(ins); + addOutOfLineCode(ool, ins->mir()); + + emitIsCallableOrConstructor(object, output, ool->entry()); + masm.bind(ool->rejoin()); } @@ -11378,6 +11404,36 @@ CodeGenerator::visitOutOfLineIsCallable(OutOfLineIsCallable* ool) masm.jump(ool->rejoin()); } +typedef bool (*CheckIsCallableFn)(JSContext*, HandleValue, CheckIsCallableKind); +static const VMFunction CheckIsCallableInfo = + FunctionInfo(CheckIsCallable, "CheckIsCallable"); + +void +CodeGenerator::visitCheckIsCallable(LCheckIsCallable* ins) +{ + ValueOperand checkValue = ToValue(ins, LCheckIsCallable::CheckValue); + Register temp = ToRegister(ins->temp()); + + // OOL code is used in the following 2 cases: + // * checkValue is not callable + // * checkValue is proxy and it's unknown whether it's callable or not + // CheckIsCallable checks if passed value is callable, regardless of the + // cases above. IsCallable operation is not observable and checking it + // again doesn't matter. + OutOfLineCode* ool = oolCallVM(CheckIsCallableInfo, ins, + ArgList(checkValue, Imm32(ins->mir()->checkKind())), + StoreNothing()); + + masm.branchTestObject(Assembler::NotEqual, checkValue, ool->entry()); + + Register object = masm.extractObject(checkValue, temp); + emitIsCallableOrConstructor(object, temp, ool->entry()); + + masm.branchTest32(Assembler::Zero, temp, temp, ool->entry()); + + masm.bind(ool->rejoin()); +} + class OutOfLineIsConstructor : public OutOfLineCodeBase { LIsConstructor* ins_; @@ -11404,37 +11460,8 @@ CodeGenerator::visitIsConstructor(LIsConstructor* ins) OutOfLineIsConstructor* ool = new(alloc()) OutOfLineIsConstructor(ins); addOutOfLineCode(ool, ins->mir()); - Label notFunction, notConstructor, hasCOps, done; - masm.loadObjClass(object, output); - - // Just skim proxies off. Their notion of isConstructor() is more complicated. - masm.branchTestClassIsProxy(true, output, ool->entry()); + emitIsCallableOrConstructor(object, output, ool->entry()); - // An object is constructor iff - // ((is() && as().isConstructor) || - // (getClass()->cOps && getClass()->cOps->construct)). - masm.branchPtr(Assembler::NotEqual, output, ImmPtr(&JSFunction::class_), ¬Function); - masm.load16ZeroExtend(Address(object, JSFunction::offsetOfFlags()), output); - masm.and32(Imm32(JSFunction::CONSTRUCTOR), output); - masm.branchTest32(Assembler::Zero, output, output, ¬Constructor); - masm.move32(Imm32(1), output); - masm.jump(&done); - masm.bind(¬Constructor); - masm.move32(Imm32(0), output); - masm.jump(&done); - - masm.bind(¬Function); - masm.branchPtr(Assembler::NonZero, Address(output, offsetof(js::Class, cOps)), - ImmPtr(nullptr), &hasCOps); - masm.move32(Imm32(0), output); - masm.jump(&done); - - masm.bind(&hasCOps); - masm.loadPtr(Address(output, offsetof(js::Class, cOps)), output); - masm.cmpPtrSet(Assembler::NonZero, Address(output, offsetof(js::ClassOps, construct)), - ImmPtr(nullptr), output); - - masm.bind(&done); masm.bind(ool->rejoin()); } diff --git a/js/src/jit/CodeGenerator.h b/js/src/jit/CodeGenerator.h index b69e919a3..d3126651b 100644 --- a/js/src/jit/CodeGenerator.h +++ b/js/src/jit/CodeGenerator.h @@ -364,6 +364,12 @@ class CodeGenerator final : public CodeGeneratorSpecific void visitCallDOMNative(LCallDOMNative* lir); void visitCallGetIntrinsicValue(LCallGetIntrinsicValue* lir); void visitCallBindVar(LCallBindVar* lir); + enum CallableOrConstructor { + Callable, + Constructor + }; + template + void emitIsCallableOrConstructor(Register object, Register output, Label* failure); void visitIsCallable(LIsCallable* lir); void visitOutOfLineIsCallable(OutOfLineIsCallable* ool); void visitIsConstructor(LIsConstructor* lir); @@ -384,6 +390,7 @@ class CodeGenerator final : public CodeGeneratorSpecific void visitArrowNewTarget(LArrowNewTarget* ins); void visitCheckReturn(LCheckReturn* ins); void visitCheckIsObj(LCheckIsObj* ins); + void visitCheckIsCallable(LCheckIsCallable* ins); void visitCheckObjCoercible(LCheckObjCoercible* ins); void visitDebugCheckSelfHosted(LDebugCheckSelfHosted* ins); void visitNaNToZero(LNaNToZero* ins); diff --git a/js/src/jit/IonBuilder.cpp b/js/src/jit/IonBuilder.cpp index 26bba0656..ed09fb504 100644 --- a/js/src/jit/IonBuilder.cpp +++ b/js/src/jit/IonBuilder.cpp @@ -2183,6 +2183,9 @@ IonBuilder::inspectOpcode(JSOp op) case JSOP_CHECKISOBJ: return jsop_checkisobj(GET_UINT8(pc)); + case JSOP_CHECKISCALLABLE: + return jsop_checkiscallable(GET_UINT8(pc)); + case JSOP_CHECKOBJCOERCIBLE: return jsop_checkobjcoercible(); @@ -10899,6 +10902,15 @@ IonBuilder::jsop_checkisobj(uint8_t kind) return true; } +bool +IonBuilder::jsop_checkiscallable(uint8_t kind) +{ + MCheckIsCallable* check = MCheckIsCallable::New(alloc(), current->pop(), kind); + current->add(check); + current->push(check); + return true; +} + bool IonBuilder::jsop_checkobjcoercible() { diff --git a/js/src/jit/IonBuilder.h b/js/src/jit/IonBuilder.h index c3cd9700a..35ad120f7 100644 --- a/js/src/jit/IonBuilder.h +++ b/js/src/jit/IonBuilder.h @@ -783,6 +783,7 @@ class IonBuilder MOZ_MUST_USE bool jsop_debugger(); MOZ_MUST_USE bool jsop_newtarget(); MOZ_MUST_USE bool jsop_checkisobj(uint8_t kind); + MOZ_MUST_USE bool jsop_checkiscallable(uint8_t kind); MOZ_MUST_USE bool jsop_checkobjcoercible(); MOZ_MUST_USE bool jsop_pushcallobj(); diff --git a/js/src/jit/Lowering.cpp b/js/src/jit/Lowering.cpp index a21a529be..7f28a9020 100644 --- a/js/src/jit/Lowering.cpp +++ b/js/src/jit/Lowering.cpp @@ -4688,6 +4688,19 @@ LIRGenerator::visitCheckIsObj(MCheckIsObj* ins) assignSafepoint(lir, ins); } +void +LIRGenerator::visitCheckIsCallable(MCheckIsCallable* ins) +{ + MDefinition* checkVal = ins->checkValue(); + MOZ_ASSERT(checkVal->type() == MIRType::Value); + + LCheckIsCallable* lir = new(alloc()) LCheckIsCallable(useBox(checkVal), + temp()); + redefine(ins, checkVal); + add(lir, ins); + assignSafepoint(lir, ins); +} + void LIRGenerator::visitCheckObjCoercible(MCheckObjCoercible* ins) { diff --git a/js/src/jit/Lowering.h b/js/src/jit/Lowering.h index 4062c0960..b2805cb7a 100644 --- a/js/src/jit/Lowering.h +++ b/js/src/jit/Lowering.h @@ -329,6 +329,7 @@ class LIRGenerator : public LIRGeneratorSpecific void visitGuardSharedTypedArray(MGuardSharedTypedArray* ins); void visitCheckReturn(MCheckReturn* ins); void visitCheckIsObj(MCheckIsObj* ins); + void visitCheckIsCallable(MCheckIsCallable* ins); void visitCheckObjCoercible(MCheckObjCoercible* ins); void visitDebugCheckSelfHosted(MDebugCheckSelfHosted* ins); }; diff --git a/js/src/jit/MIR.h b/js/src/jit/MIR.h index 3caa7e357..2de91e2df 100644 --- a/js/src/jit/MIR.h +++ b/js/src/jit/MIR.h @@ -13455,8 +13455,9 @@ class MCheckIsObj { uint8_t checkKind_; - explicit MCheckIsObj(MDefinition* toCheck, uint8_t checkKind) - : MUnaryInstruction(toCheck), checkKind_(checkKind) + MCheckIsObj(MDefinition* toCheck, uint8_t checkKind) + : MUnaryInstruction(toCheck), + checkKind_(checkKind) { setResultType(MIRType::Value); setResultTypeSet(toCheck->resultTypeSet()); @@ -13475,6 +13476,33 @@ class MCheckIsObj } }; +class MCheckIsCallable + : public MUnaryInstruction, + public BoxInputsPolicy::Data +{ + uint8_t checkKind_; + + MCheckIsCallable(MDefinition* toCheck, uint8_t checkKind) + : MUnaryInstruction(toCheck), + checkKind_(checkKind) + { + setResultType(MIRType::Value); + setResultTypeSet(toCheck->resultTypeSet()); + setGuard(); + } + + public: + INSTRUCTION_HEADER(CheckIsCallable) + TRIVIAL_NEW_WRAPPERS + NAMED_OPERANDS((0, checkValue)) + + uint8_t checkKind() const { return checkKind_; } + + AliasSet getAliasSet() const override { + return AliasSet::None(); + } +}; + class MCheckObjCoercible : public MUnaryInstruction, public BoxInputsPolicy::Data diff --git a/js/src/jit/MOpcodes.h b/js/src/jit/MOpcodes.h index 1a6911247..bb2ab8190 100644 --- a/js/src/jit/MOpcodes.h +++ b/js/src/jit/MOpcodes.h @@ -285,6 +285,7 @@ namespace jit { _(ArrowNewTarget) \ _(CheckReturn) \ _(CheckIsObj) \ + _(CheckIsCallable) \ _(CheckObjCoercible) \ _(DebugCheckSelfHosted) \ _(AsmJSNeg) \ diff --git a/js/src/jit/VMFunctions.cpp b/js/src/jit/VMFunctions.cpp index 4edbc3c83..77b9e3647 100644 --- a/js/src/jit/VMFunctions.cpp +++ b/js/src/jit/VMFunctions.cpp @@ -1349,5 +1349,14 @@ BaselineGetFunctionThis(JSContext* cx, BaselineFrame* frame, MutableHandleValue return GetFunctionThis(cx, frame, res); } +bool +CheckIsCallable(JSContext* cx, HandleValue v, CheckIsCallableKind kind) +{ + if (!IsCallable(v)) + return ThrowCheckIsCallable(cx, kind); + + return true; +} + } // namespace jit } // namespace js diff --git a/js/src/jit/VMFunctions.h b/js/src/jit/VMFunctions.h index f754d58c7..572f05373 100644 --- a/js/src/jit/VMFunctions.h +++ b/js/src/jit/VMFunctions.h @@ -13,6 +13,7 @@ #include "jit/CompileInfo.h" #include "jit/JitFrames.h" +#include "vm/Interpreter.h" namespace js { @@ -802,6 +803,9 @@ ThrowObjectCoercible(JSContext* cx, HandleValue v); MOZ_MUST_USE bool BaselineGetFunctionThis(JSContext* cx, BaselineFrame* frame, MutableHandleValue res); +MOZ_MUST_USE bool +CheckIsCallable(JSContext* cx, HandleValue v, CheckIsCallableKind kind); + } // namespace jit } // namespace js diff --git a/js/src/jit/shared/LIR-shared.h b/js/src/jit/shared/LIR-shared.h index f8e0ce9cc..9dcb527c5 100644 --- a/js/src/jit/shared/LIR-shared.h +++ b/js/src/jit/shared/LIR-shared.h @@ -8893,6 +8893,27 @@ class LCheckIsObj : public LInstructionHelper } }; +class LCheckIsCallable : public LInstructionHelper +{ + public: + LIR_HEADER(CheckIsCallable) + + static const size_t CheckValue = 0; + + LCheckIsCallable(const LBoxAllocation& value, const LDefinition& temp) { + setBoxOperand(CheckValue, value); + setTemp(0, temp); + } + + const LDefinition* temp() { + return getTemp(0); + } + + MCheckIsCallable* mir() const { + return mir_->toCheckIsCallable(); + } +}; + class LCheckObjCoercible : public LCallInstructionHelper { public: diff --git a/js/src/jit/shared/LOpcodes-shared.h b/js/src/jit/shared/LOpcodes-shared.h index e57751437..3eea1b449 100644 --- a/js/src/jit/shared/LOpcodes-shared.h +++ b/js/src/jit/shared/LOpcodes-shared.h @@ -402,6 +402,7 @@ _(ArrowNewTarget) \ _(CheckReturn) \ _(CheckIsObj) \ + _(CheckIsCallable) \ _(CheckObjCoercible) \ _(DebugCheckSelfHosted) \ _(AsmJSLoadHeap) \ -- cgit v1.2.3 From 05441d12b6bbc9dde268914fcfd374db61b83462 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 18:21:15 +0200 Subject: Bug 1346862 - Fix IteratorClose due to non-local jumps being catchable by try statements inside for-of Issue #74 --- js/src/frontend/BytecodeEmitter.cpp | 20 +++++------- js/src/frontend/BytecodeEmitter.h | 2 -- js/src/jit/JitFrames.cpp | 31 ++++++++++++++++++- js/src/jsscript.h | 1 + js/src/shell/js.cpp | 6 ++-- .../Statements/for-of-iterator-close-throw.js | 35 +++++++++++++++++++++ js/src/vm/Interpreter.cpp | 36 ++++++++++++++++++++++ 7 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 js/src/tests/ecma_6/Statements/for-of-iterator-close-throw.js diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index 4d6ff6305..b2e48d7ea 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -2065,7 +2065,11 @@ class ForOfLoopControl : public LoopControl bool emitIteratorClose(BytecodeEmitter* bce, CompletionKind completionKind = CompletionKind::Normal) { - return bce->emitIteratorClose(completionKind, allowSelfHosted_); + ptrdiff_t start = bce->offset(); + if (!bce->emitIteratorClose(completionKind, allowSelfHosted_)) + return false; + ptrdiff_t end = bce->offset(); + return bce->tryNoteList.append(JSTRY_FOR_OF_ITERCLOSE, 0, start, end); } bool emitPrepareForNonLocalJump(BytecodeEmitter* bce, bool isTarget) { @@ -2614,17 +2618,6 @@ BytecodeEmitter::emitUint32Operand(JSOp op, uint32_t operand) return true; } -bool -BytecodeEmitter::flushPops(int* npops) -{ - MOZ_ASSERT(*npops != 0); - if (!emitUint16Operand(JSOP_POPN, *npops)) - return false; - - *npops = 0; - return true; -} - namespace { class NonLocalExitControl @@ -2718,8 +2711,9 @@ NonLocalExitControl::prepareForNonLocalJump(BytecodeEmitter::NestableControl* ta bool emitIteratorCloseAtTarget = emitIteratorClose && kind_ != Continue; auto flushPops = [&npops](BytecodeEmitter* bce) { - if (npops && !bce->flushPops(&npops)) + if (npops && !bce->emitUint16Operand(JSOP_POPN, npops)) return false; + npops = 0; return true; }; diff --git a/js/src/frontend/BytecodeEmitter.h b/js/src/frontend/BytecodeEmitter.h index 7ff40b462..7ac9e540b 100644 --- a/js/src/frontend/BytecodeEmitter.h +++ b/js/src/frontend/BytecodeEmitter.h @@ -452,8 +452,6 @@ struct MOZ_STACK_CLASS BytecodeEmitter JSOp strictifySetNameOp(JSOp op); - MOZ_MUST_USE bool flushPops(int* npops); - MOZ_MUST_USE bool emitCheck(ptrdiff_t delta, ptrdiff_t* offset); // Emit one bytecode. diff --git a/js/src/jit/JitFrames.cpp b/js/src/jit/JitFrames.cpp index a70356ad4..966d952d3 100644 --- a/js/src/jit/JitFrames.cpp +++ b/js/src/jit/JitFrames.cpp @@ -436,6 +436,8 @@ HandleExceptionIon(JSContext* cx, const InlineFrameIterator& frame, ResumeFromEx if (!script->hasTrynotes()) return; + bool inForOfIterClose = false; + for (TryNoteIterIon tni(cx, frame); !tni.done(); ++tni) { JSTryNote* tn = *tni; @@ -447,12 +449,23 @@ HandleExceptionIon(JSContext* cx, const InlineFrameIterator& frame, ResumeFromEx CloseLiveIteratorIon(cx, frame, tn); break; + case JSTRY_FOR_OF_ITERCLOSE: + inForOfIterClose = true; + break; + case JSTRY_FOR_OF: + inForOfIterClose = false; + break; + case JSTRY_LOOP: break; case JSTRY_CATCH: if (cx->isExceptionPending()) { + // See corresponding comment in ProcessTryNotes. + if (inForOfIterClose) + break; + // Ion can compile try-catch, but bailing out to catch // exceptions is slow. Reset the warm-up counter so that if we // catch many exceptions we won't Ion-compile the script. @@ -583,6 +596,7 @@ ProcessTryNotesBaseline(JSContext* cx, const JitFrameIterator& frame, Environmen ResumeFromException* rfe, jsbytecode** pc) { RootedScript script(cx, frame.baselineFrame()->script()); + bool inForOfIterClose = false; for (TryNoteIterBaseline tni(cx, frame.baselineFrame(), *pc); !tni.done(); ++tni) { JSTryNote* tn = *tni; @@ -593,7 +607,11 @@ ProcessTryNotesBaseline(JSContext* cx, const JitFrameIterator& frame, Environmen // If we're closing a legacy generator, we have to skip catch // blocks. if (cx->isClosingGenerator()) - continue; + break; + + // See corresponding comment in ProcessTryNotes. + if (inForOfIterClose) + break; SettleOnTryNote(cx, tn, frame, ei, rfe, pc); @@ -609,6 +627,10 @@ ProcessTryNotesBaseline(JSContext* cx, const JitFrameIterator& frame, Environmen } case JSTRY_FINALLY: { + // See corresponding comment in ProcessTryNotes. + if (inForOfIterClose) + break; + SettleOnTryNote(cx, tn, frame, ei, rfe, pc); rfe->kind = ResumeFromException::RESUME_FINALLY; rfe->target = script->baselineScript()->nativeCodeForPC(script, *pc); @@ -652,7 +674,14 @@ ProcessTryNotesBaseline(JSContext* cx, const JitFrameIterator& frame, Environmen break; } + case JSTRY_FOR_OF_ITERCLOSE: + inForOfIterClose = true; + break; + case JSTRY_FOR_OF: + inForOfIterClose = false; + break; + case JSTRY_LOOP: break; diff --git a/js/src/jsscript.h b/js/src/jsscript.h index 9cb7c538f..87da79901 100644 --- a/js/src/jsscript.h +++ b/js/src/jsscript.h @@ -85,6 +85,7 @@ enum JSTryNoteKind { JSTRY_FOR_IN, JSTRY_FOR_OF, JSTRY_LOOP, + JSTRY_FOR_OF_ITERCLOSE, JSTRY_DESTRUCTURING_ITERCLOSE }; diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 5d6508d59..b53914942 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -2599,6 +2599,8 @@ TryNoteName(JSTryNoteKind kind) return "for-of"; case JSTRY_LOOP: return "loop"; + case JSTRY_FOR_OF_ITERCLOSE: + return "for-of-iterclose"; case JSTRY_DESTRUCTURING_ITERCLOSE: return "dstr-iterclose"; } @@ -2612,14 +2614,14 @@ TryNotes(JSContext* cx, HandleScript script, Sprinter* sp) if (!script->hasTrynotes()) return true; - if (sp->put("\nException table:\nkind stack start end\n") < 0) + if (sp->put("\nException table:\nkind stack start end\n") < 0) return false; JSTryNote* tn = script->trynotes()->vector; JSTryNote* tnlimit = tn + script->trynotes()->length; do { uint32_t startOff = script->pcToOffset(script->main()) + tn->start; - if (!sp->jsprintf(" %-14s %6u %8u %8u\n", + if (!sp->jsprintf(" %-16s %6u %8u %8u\n", TryNoteName(static_cast(tn->kind)), tn->stackDepth, startOff, startOff + tn->length)) { diff --git a/js/src/tests/ecma_6/Statements/for-of-iterator-close-throw.js b/js/src/tests/ecma_6/Statements/for-of-iterator-close-throw.js new file mode 100644 index 000000000..1974e416b --- /dev/null +++ b/js/src/tests/ecma_6/Statements/for-of-iterator-close-throw.js @@ -0,0 +1,35 @@ +function test() { + var returnCalled = 0; + var returnCalledExpected = 0; + var catchEntered = 0; + var finallyEntered = 0; + var finallyEnteredExpected = 0; + var iterable = {}; + iterable[Symbol.iterator] = makeIterator({ + ret: function() { + returnCalled++; + throw 42; + } + }); + + // inner try cannot catch IteratorClose throwing + assertThrowsValue(function() { + for (var x of iterable) { + try { + return; + } catch (e) { + catchEntered++; + } finally { + finallyEntered++; + } + } + }, 42); + assertEq(returnCalled, ++returnCalledExpected); + assertEq(catchEntered, 0); + assertEq(finallyEntered, ++finallyEnteredExpected); +} + +test(); + +if (typeof reportCompare === "function") + reportCompare(0, 0); diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index eb3000e07..d20e5284d 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -1156,6 +1156,7 @@ enum HandleErrorContinuation static HandleErrorContinuation ProcessTryNotes(JSContext* cx, EnvironmentIter& ei, InterpreterRegs& regs) { + bool inForOfIterClose = false; for (TryNoteIterInterpreter tni(cx, regs); !tni.done(); ++tni) { JSTryNote* tn = *tni; @@ -1164,10 +1165,38 @@ ProcessTryNotes(JSContext* cx, EnvironmentIter& ei, InterpreterRegs& regs) /* Catch cannot intercept the closing of a generator. */ if (cx->isClosingGenerator()) break; + + // If IteratorClose due to abnormal completion threw inside a + // for-of loop, it is not catchable by try statements inside of + // the for-of loop. + // + // This is handled by this weirdness in the exception handler + // instead of in bytecode because it is hard to do so in bytecode: + // + // 1. IteratorClose emitted due to abnormal completion (break, + // throw, return) are emitted inline, at the source location of + // the break, throw, or return statement. For example: + // + // for (x of iter) { + // try { return; } catch (e) { } + // } + // + // From the try-note nesting's perspective, the IteratorClose + // resulting from |return| is covered by the inner try, when it + // should not be. + // + // 2. Try-catch notes cannot be disjoint. That is, we can't have + // multiple notes with disjoint pc ranges jumping to the same + // catch block. + if (inForOfIterClose) + break; SettleOnTryNote(cx, tn, ei, regs); return CatchContinuation; case JSTRY_FINALLY: + // See note above. + if (inForOfIterClose) + break; SettleOnTryNote(cx, tn, ei, regs); return FinallyContinuation; @@ -1206,7 +1235,14 @@ ProcessTryNotes(JSContext* cx, EnvironmentIter& ei, InterpreterRegs& regs) break; } + case JSTRY_FOR_OF_ITERCLOSE: + inForOfIterClose = true; + break; + case JSTRY_FOR_OF: + inForOfIterClose = false; + break; + case JSTRY_LOOP: break; -- cgit v1.2.3 From aafdd314442c903815f6fdf6072b001c25ae85c5 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 18:24:06 +0200 Subject: Bug 1357075 - Pad a nop to unwind to the scope just before a destructuring iterator close trynote Issue #74 --- js/src/frontend/BytecodeEmitter.cpp | 8 ++++++++ js/src/jit-test/tests/parser/bug-1357075.js | 10 ++++++++++ js/src/jit/BaselineCompiler.cpp | 6 ++++++ js/src/jit/BaselineCompiler.h | 7 ++++--- js/src/jit/IonBuilder.cpp | 1 + js/src/vm/Interpreter.cpp | 5 ++++- js/src/vm/Opcodes.h | 11 ++++++++++- 7 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 js/src/jit-test/tests/parser/bug-1357075.js diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index b2e48d7ea..4d3b60c2f 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -5318,6 +5318,14 @@ BytecodeEmitter::wrapWithDestructuringIteratorCloseTryNote(int32_t iterDepth, In { MOZ_ASSERT(this->stackDepth >= iterDepth); + // Pad a nop at the beginning of the bytecode covered by the trynote so + // that when unwinding environments, we may unwind to the scope + // corresponding to the pc *before* the start, in case the first bytecode + // emitted by |emitter| is the start of an inner scope. See comment above + // UnwindEnvironmentToTryPc. + if (!emit1(JSOP_TRY_DESTRUCTURING_ITERCLOSE)) + return false; + ptrdiff_t start = offset(); if (!emitter(this)) return false; diff --git a/js/src/jit-test/tests/parser/bug-1357075.js b/js/src/jit-test/tests/parser/bug-1357075.js new file mode 100644 index 000000000..47482e372 --- /dev/null +++ b/js/src/jit-test/tests/parser/bug-1357075.js @@ -0,0 +1,10 @@ +// |jit-test| error: TypeError + +var iterable = {}; +var iterator = { + return: 1 +}; +iterable[Symbol.iterator] = function() { + return iterator; +}; +for ([ class get {} ().iterator ] of [iterable]) {} diff --git a/js/src/jit/BaselineCompiler.cpp b/js/src/jit/BaselineCompiler.cpp index 07d8e629d..3fa5a80ed 100644 --- a/js/src/jit/BaselineCompiler.cpp +++ b/js/src/jit/BaselineCompiler.cpp @@ -1062,6 +1062,12 @@ BaselineCompiler::emit_JSOP_NOP_DESTRUCTURING() return true; } +bool +BaselineCompiler::emit_JSOP_TRY_DESTRUCTURING_ITERCLOSE() +{ + return true; +} + bool BaselineCompiler::emit_JSOP_LABEL() { diff --git a/js/src/jit/BaselineCompiler.h b/js/src/jit/BaselineCompiler.h index 0bacf6f18..6b5bf009e 100644 --- a/js/src/jit/BaselineCompiler.h +++ b/js/src/jit/BaselineCompiler.h @@ -226,7 +226,7 @@ namespace jit { _(JSOP_SPREADSUPERCALL) \ _(JSOP_THROWSETCONST) \ _(JSOP_THROWSETALIASEDCONST) \ - _(JSOP_THROWSETCALLEE) \ + _(JSOP_THROWSETCALLEE) \ _(JSOP_INITHIDDENPROP_GETTER) \ _(JSOP_INITHIDDENPROP_SETTER) \ _(JSOP_INITHIDDENELEM) \ @@ -234,8 +234,9 @@ namespace jit { _(JSOP_INITHIDDENELEM_SETTER) \ _(JSOP_CHECKOBJCOERCIBLE) \ _(JSOP_DEBUGCHECKSELFHOSTED) \ - _(JSOP_JUMPTARGET) \ - _(JSOP_IS_CONSTRUCTING) + _(JSOP_JUMPTARGET) \ + _(JSOP_IS_CONSTRUCTING) \ + _(JSOP_TRY_DESTRUCTURING_ITERCLOSE) class BaselineCompiler : public BaselineCompilerSpecific { diff --git a/js/src/jit/IonBuilder.cpp b/js/src/jit/IonBuilder.cpp index ed09fb504..54d05cac4 100644 --- a/js/src/jit/IonBuilder.cpp +++ b/js/src/jit/IonBuilder.cpp @@ -1678,6 +1678,7 @@ IonBuilder::inspectOpcode(JSOp op) switch (op) { case JSOP_NOP: case JSOP_NOP_DESTRUCTURING: + case JSOP_TRY_DESTRUCTURING_ITERCLOSE: case JSOP_LINENO: case JSOP_LOOPENTRY: case JSOP_JUMPTARGET: diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index d20e5284d..b747e4d7a 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -1080,6 +1080,9 @@ js::UnwindEnvironmentToTryPc(JSScript* script, JSTryNote* tn) if (tn->kind == JSTRY_CATCH || tn->kind == JSTRY_FINALLY) { pc -= JSOP_TRY_LENGTH; MOZ_ASSERT(*pc == JSOP_TRY); + } else if (tn->kind == JSTRY_DESTRUCTURING_ITERCLOSE) { + pc -= JSOP_TRY_DESTRUCTURING_ITERCLOSE_LENGTH; + MOZ_ASSERT(*pc == JSOP_TRY_DESTRUCTURING_ITERCLOSE); } return pc; } @@ -1917,7 +1920,7 @@ CASE(JSOP_UNUSED192) CASE(JSOP_UNUSED209) CASE(JSOP_UNUSED210) CASE(JSOP_UNUSED211) -CASE(JSOP_UNUSED220) +CASE(JSOP_TRY_DESTRUCTURING_ITERCLOSE) CASE(JSOP_UNUSED221) CASE(JSOP_UNUSED222) CASE(JSOP_UNUSED223) diff --git a/js/src/vm/Opcodes.h b/js/src/vm/Opcodes.h index 3848445ff..4b044c8d8 100644 --- a/js/src/vm/Opcodes.h +++ b/js/src/vm/Opcodes.h @@ -2211,7 +2211,16 @@ * Stack: result => result, callable */ \ macro(JSOP_CHECKISCALLABLE, 219, "checkiscallable", NULL, 2, 1, 1, JOF_UINT8) \ - macro(JSOP_UNUSED220, 220,"unused220", NULL, 1, 0, 0, JOF_BYTE) \ + \ + /* + * No-op used by the exception unwinder to determine the correct + * environment to unwind to when performing IteratorClose due to + * destructuring. + * Category: Other + * Operands: + * Stack: => + */ \ + macro(JSOP_TRY_DESTRUCTURING_ITERCLOSE, 220, "try-destructuring-iterclose", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED221, 221,"unused221", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED222, 222,"unused222", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED223, 223,"unused223", NULL, 1, 0, 0, JOF_BYTE) \ -- cgit v1.2.3 From 70c8cf8db71880c1ab1f8fee4787a19316960dac Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sun, 25 Mar 2018 19:06:08 +0200 Subject: Bug 1360839 - Call IteratorClose due to abrupt completion from yield Issue #74 --- js/src/frontend/BytecodeEmitter.cpp | 70 +++++++++++++++++++--- js/src/frontend/BytecodeEmitter.h | 4 +- .../ecma_6/Generators/yield-iterator-close.js | 58 ++++++++++++++++++ 3 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 js/src/tests/ecma_6/Generators/yield-iterator-close.js diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index 4d3b60c2f..c7c615ccf 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -1534,13 +1534,19 @@ class MOZ_STACK_CLASS TryEmitter // stream and fixed-up later. // // If ShouldUseControl is DontUseControl, all that handling is skipped. - // DontUseControl is used by yield*, that matches to the following: - // * has only one catch block - // * has no catch guard - // * has JSOP_GOTO at the end of catch-block - // * has no non-local-jump - // * doesn't use finally block for normal completion of try-block and + // DontUseControl is used by yield* and the internal try-catch around + // IteratorClose. These internal uses must: + // * have only one catch block + // * have no catch guard + // * have JSOP_GOTO at the end of catch-block + // * have no non-local-jump + // * don't use finally block for normal completion of try-block and // catch-block + // + // Additionally, a finally block may be emitted when ShouldUseControl is + // DontUseControl, even if the kind is not TryCatchFinally or TryFinally, + // because GOSUBs are not emitted. This internal use shares the + // requirements as above. Maybe controlInfo_; int depth_; @@ -1708,7 +1714,17 @@ class MOZ_STACK_CLASS TryEmitter public: bool emitFinally(Maybe finallyPos = Nothing()) { - MOZ_ASSERT(hasFinally()); + // If we are using controlInfo_ (i.e., emitting a syntactic try + // blocks), we must have specified up front if there will be a finally + // close. For internal try blocks, like those emitted for yield* and + // IteratorClose inside for-of loops, we can emitFinally even without + // specifying up front, since the internal try blocks emit no GOSUBs. + if (!controlInfo_) { + if (kind_ == TryCatch) + kind_ = TryCatchFinally; + } else { + MOZ_ASSERT(hasFinally()); + } if (state_ == Try) { if (!emitTryEnd()) @@ -2004,21 +2020,31 @@ class ForOfLoopControl : public LoopControl // } Maybe tryCatch_; + // Used to track if any yields were emitted between calls to to + // emitBeginCodeNeedingIteratorClose and emitEndCodeNeedingIteratorClose. + uint32_t numYieldsAtBeginCodeNeedingIterClose_; + bool allowSelfHosted_; public: ForOfLoopControl(BytecodeEmitter* bce, int32_t iterDepth, bool allowSelfHosted) : LoopControl(bce, StatementKind::ForOfLoop), iterDepth_(iterDepth), + numYieldsAtBeginCodeNeedingIterClose_(UINT32_MAX), allowSelfHosted_(allowSelfHosted) { } bool emitBeginCodeNeedingIteratorClose(BytecodeEmitter* bce) { - tryCatch_.emplace(bce, TryEmitter::TryCatch, TryEmitter::DontUseRetVal); + tryCatch_.emplace(bce, TryEmitter::TryCatch, TryEmitter::DontUseRetVal, + TryEmitter::DontUseControl); if (!tryCatch_->emitTry()) return false; + + MOZ_ASSERT(numYieldsAtBeginCodeNeedingIterClose_ == UINT32_MAX); + numYieldsAtBeginCodeNeedingIterClose_ = bce->yieldOffsetList.numYields; + return true; } @@ -2056,10 +2082,33 @@ class ForOfLoopControl : public LoopControl if (!bce->emit1(JSOP_THROW)) // ITER ... return false; + // If any yields were emitted, then this for-of loop is inside a star + // generator and must handle the case of Generator.return. Like in + // yield*, it is handled with a finally block. + uint32_t numYieldsEmitted = bce->yieldOffsetList.numYields; + if (numYieldsEmitted > numYieldsAtBeginCodeNeedingIterClose_) { + if (!tryCatch_->emitFinally()) + return false; + + IfThenElseEmitter ifGeneratorClosing(bce); + if (!bce->emit1(JSOP_ISGENCLOSING)) // ITER ... FTYPE FVALUE CLOSING + return false; + if (!ifGeneratorClosing.emitIf()) // ITER ... FTYPE FVALUE + return false; + if (!bce->emitDupAt(slotFromTop + 1)) // ITER ... FTYPE FVALUE ITER + return false; + if (!emitIteratorClose(bce, CompletionKind::Normal)) // ITER ... FTYPE FVALUE + return false; + if (!ifGeneratorClosing.emitEnd()) // ITER ... FTYPE FVALUE + return false; + } + if (!tryCatch_->emitEnd()) return false; tryCatch_.reset(); + numYieldsAtBeginCodeNeedingIterClose_ = UINT32_MAX; + return true; } @@ -4748,6 +4797,11 @@ BytecodeEmitter::emitYieldOp(JSOp op) return false; } + if (op == JSOP_YIELD) + yieldOffsetList.numYields++; + else + yieldOffsetList.numAwaits++; + SET_UINT24(code(off), yieldIndex); if (!yieldOffsetList.append(offset())) diff --git a/js/src/frontend/BytecodeEmitter.h b/js/src/frontend/BytecodeEmitter.h index 7ac9e540b..04307c8c1 100644 --- a/js/src/frontend/BytecodeEmitter.h +++ b/js/src/frontend/BytecodeEmitter.h @@ -100,7 +100,9 @@ struct CGScopeNoteList { struct CGYieldOffsetList { Vector list; - explicit CGYieldOffsetList(ExclusiveContext* cx) : list(cx) {} + uint32_t numYields; + uint32_t numAwaits; + explicit CGYieldOffsetList(ExclusiveContext* cx) : list(cx), numYields(0), numAwaits(0) {} MOZ_MUST_USE bool append(uint32_t offset) { return list.append(offset); } size_t length() const { return list.length(); } diff --git a/js/src/tests/ecma_6/Generators/yield-iterator-close.js b/js/src/tests/ecma_6/Generators/yield-iterator-close.js new file mode 100644 index 000000000..970ad494d --- /dev/null +++ b/js/src/tests/ecma_6/Generators/yield-iterator-close.js @@ -0,0 +1,58 @@ +// Test that IteratorClose is called when a Generator is abruptly completed by +// Generator.return. + +var returnCalled = 0; +function* wrapNoThrow() { + let iter = { + [Symbol.iterator]() { + return this; + }, + next() { + return { value: 10, done: false }; + }, + return() { + returnCalled++; + return {}; + } + }; + for (const i of iter) { + yield i; + } +} + +// Breaking calls Generator.return, which causes the yield above to resume with +// an abrupt completion of kind "return", which then calls +// iter.return. +for (const i of wrapNoThrow()) { + break; +} +assertEq(returnCalled, 1); + +function* wrapThrow() { + let iter = { + [Symbol.iterator]() { + return this; + }, + next() { + return { value: 10, done: false }; + }, + return() { + throw 42; + } + }; + for (const i of iter) { + yield i; + } +} + +// Breaking calls Generator.return, which, like above, calls iter.return. If +// iter.return throws, since the yield is resuming with an abrupt completion of +// kind "return", the newly thrown value takes precedence over returning. +assertThrowsValue(() => { + for (const i of wrapThrow()) { + break; + } +}, 42); + +if (typeof reportCompare === "function") + reportCompare(0, 0); -- cgit v1.2.3