diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index ed0bce688cb..e51723b9e95 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -5047,19 +5047,30 @@ BytecodeEmitter::emitNormalFor(ParseNode *pn, ptrdiff_t top) ParseNode *forBody = pn->pn_right; /* C-style for (init; cond; update) ... loop. */ - JSOp op = JSOP_POP; - ParseNode *pn3 = forHead->pn_kid1; - if (!pn3) { - // No initializer, but emit a nop so that there's somewhere to put the + bool forLoopRequiresFreshening = false; + JSOp op; + ParseNode *init = forHead->pn_kid1; + if (!init) { + // If there's no init, emit a nop so that there's somewhere to put the // SRC_FOR annotation that IonBuilder will look for. op = JSOP_NOP; + } else if (init->isKind(PNK_FRESHENBLOCK)) { + // Also emit a nop, as above. + op = JSOP_NOP; + + // The loop's init declaration was hoisted into an enclosing lexical + // scope node. Note that the block scope must be freshened each + // iteration. + forLoopRequiresFreshening = true; } else { emittingForInit = true; - if (!updateSourceCoordNotes(pn3->pn_pos.begin)) + if (!updateSourceCoordNotes(init->pn_pos.begin)) return false; - if (!emitTree(pn3)) + if (!emitTree(init)) return false; emittingForInit = false; + + op = JSOP_POP; } /* @@ -5099,19 +5110,38 @@ BytecodeEmitter::emitNormalFor(ParseNode *pn, ptrdiff_t top) /* Set the second note offset so we can find the update part. */ ptrdiff_t tmp2 = offset(); - /* Set loop and enclosing "update" offsets, for continue. */ + // Set loop and enclosing "update" offsets, for continue. Note that we + // continue to immediately *before* the block-freshening: continuing must + // refresh the block. StmtInfoBCE *stmt = &stmtInfo; do { stmt->update = offset(); } while ((stmt = stmt->down) != nullptr && stmt->type == STMT_LABEL); + // Freshen the block on the scope chain to expose distinct bindings for each loop + // iteration. + if (forLoopRequiresFreshening) { + // The scope chain only includes an actual block *if* the scope object + // is captured and therefore requires cloning. Get the static block + // object from the parent let-block statement (which *must* be the + // let-statement for the guarding condition to have held) and freshen + // if the block object needs cloning. + StmtInfoBCE *parent = stmtInfo.down; + MOZ_ASSERT(parent->type == STMT_BLOCK); + MOZ_ASSERT(parent->isBlockScope); + + if (parent->staticScope->as().needsClone()) { + if (!emit1(JSOP_FRESHENBLOCKSCOPE)) + return false; + } + } + /* Check for update code to do before the condition (if any). */ - pn3 = forHead->pn_kid3; - if (pn3) { - if (!updateSourceCoordNotes(pn3->pn_pos.begin)) + if (ParseNode *update = forHead->pn_kid3) { + if (!updateSourceCoordNotes(update->pn_pos.begin)) return false; op = JSOP_POP; - if (!emitTree(pn3)) + if (!emitTree(update)) return false; /* Always emit the POP or NOP to help IonBuilder. */ diff --git a/js/src/frontend/FoldConstants.cpp b/js/src/frontend/FoldConstants.cpp index 85083635ad8..8958a8a09f6 100644 --- a/js/src/frontend/FoldConstants.cpp +++ b/js/src/frontend/FoldConstants.cpp @@ -415,6 +415,7 @@ ContainsHoistedDeclaration(ExclusiveContext *cx, ParseNode *node, bool *result) case PNK_FORIN: case PNK_FOROF: case PNK_FORHEAD: + case PNK_FRESHENBLOCK: case PNK_CLASSMETHOD: case PNK_CLASSMETHODLIST: case PNK_CLASSNAMES: diff --git a/js/src/frontend/FullParseHandler.h b/js/src/frontend/FullParseHandler.h index ee31f6589e1..043d8d5d69c 100644 --- a/js/src/frontend/FullParseHandler.h +++ b/js/src/frontend/FullParseHandler.h @@ -499,6 +499,10 @@ class FullParseHandler return new_(kind, JSOP_NOP, pn1, pn2, pn3, pos); } + ParseNode *newFreshenBlock(const TokenPos &pos) { + return new_(PNK_FRESHENBLOCK, pos); + } + ParseNode *newSwitchStatement(uint32_t begin, ParseNode *discriminant, ParseNode *caseList) { TokenPos pos(begin, caseList->pn_pos.end); return new_(PNK_SWITCH, JSOP_NOP, pos, discriminant, caseList); diff --git a/js/src/frontend/ParseNode.cpp b/js/src/frontend/ParseNode.cpp index 4eb140d7d3d..75dc62276ea 100644 --- a/js/src/frontend/ParseNode.cpp +++ b/js/src/frontend/ParseNode.cpp @@ -208,6 +208,7 @@ PushNodeChildren(ParseNode *pn, NodeStack *stack) case PNK_DEBUGGER: case PNK_EXPORT_BATCH_SPEC: case PNK_OBJECT_PROPERTY_NAME: + case PNK_FRESHENBLOCK: MOZ_ASSERT(pn->isArity(PN_NULLARY)); MOZ_ASSERT(!pn->isUsed(), "handle non-trivial cases separately"); MOZ_ASSERT(!pn->isDefn(), "handle non-trivial cases separately"); diff --git a/js/src/frontend/ParseNode.h b/js/src/frontend/ParseNode.h index c62297daa04..deae0020e77 100644 --- a/js/src/frontend/ParseNode.h +++ b/js/src/frontend/ParseNode.h @@ -146,6 +146,7 @@ class UpvarCookie F(FORIN) \ F(FOROF) \ F(FORHEAD) \ + F(FRESHENBLOCK) \ F(ARGSBODY) \ F(SPREAD) \ F(MUTATEPROTO) \ diff --git a/js/src/frontend/Parser.cpp b/js/src/frontend/Parser.cpp index 971c1854d41..6e70e8a8aa5 100644 --- a/js/src/frontend/Parser.cpp +++ b/js/src/frontend/Parser.cpp @@ -4709,11 +4709,50 @@ Parser::forStatement() MOZ_ASSERT_IF(isForDecl, pn1->isArity(PN_LIST)); MOZ_ASSERT(!!blockObj == (isForDecl && pn1->isOp(JSOP_NOP))); - // The form 'for (let ; ; ) ' generates an - // implicit block even if stmt is not a BlockStatement. - // If the loop has that exact form, then: - // - forLetImpliedBlock is the node for the implicit block scope. - // - forLetDecl is the node for the decl 'let '. + // All forms of for-loop (for(;;), for-in, for-of) generate an implicit + // block to store any lexical variables declared by the loop-head. We + // implement this by desugaring such loops. These: + // + // for (let/const ; ; ) + // for (let in ) + // for (let of ) + // + // transform into almost these desugarings: + // + // let () { for (; ; ) } + // let () { for ( in ) } + // let () { for ( of ) } + // + // This desugaring is not *quite* correct. Assignments in the head of a + // let-block are evaluated *outside* the scope of the variables declared by + // the let-block-head. But ES6 mandates that they be evaluated in the same + // scope, triggering used-before-initialization temporal dead zone errors + // as necessary. Bug 1069480 will fix this. + // + // Additionally, ES6 mandates that *each iteration* of a for-loop create a + // fresh binding of loop variables. For example: + // + // var funcs = []; + // for (let i = 0; i < 2; i++) + // funcs.push(function() { return i; }); + // assertEq(funcs[0](), 0); + // assertEq(funcs[1](), 1); + // + // These semantics are implemented by "freshening" the implicit block -- + // changing the scope chain to a fresh clone of the instantaneous block + // object -- each iteration, just before evaluating the "update" in + // for(;;) loops. (We don't implement this freshening for for-in/of loops, + // but soon: bug 449811.) No freshening occurs in for (const ...;;) as + // there's no point (you can't reassign consts), and moreover the spec + // requires it (which fact isn't exposed in-language but can be observed + // through the Debugger API). + // + // If the for-loop head includes a lexical declaration, then we create an + // implicit block scope, and: + // + // * forLetImpliedBlock is the node for the implicit block scope. + // * forLetDecl is the node for the decl 'let/const '. + // // Otherwise both are null. ParseNode *forLetImpliedBlock = nullptr; ParseNode *forLetDecl = nullptr; @@ -4885,23 +4924,45 @@ Parser::forStatement() headKind = PNK_FORHEAD; if (blockObj) { - /* - * Desugar 'for (let A; B; C) D' into 'let (A) { for (; B; C) D }' - * to induce the correct scoping for A. Ensure here that the previously - * unchecked assignment mandate for const declarations holds. - */ + // Ensure here that the previously-unchecked assignment mandate for + // const declarations holds. if (!checkForHeadConstInitializers(pn1)) { report(ParseError, false, nullptr, JSMSG_BAD_CONST_DECL); return null(); } + // Desugar + // + // for (let INIT; TEST; UPDATE) STMT + // + // into + // + // let (INIT) { for (; TEST; UPDATE) STMT } + // + // to provide a block scope for INIT. forLetImpliedBlock = pushLetScope(blockObj, &letStmt); if (!forLetImpliedBlock) return null(); letStmt.isForLetBlock = true; forLetDecl = pn1; - pn1 = nullptr; + + // The above transformation isn't enough to implement |INIT| + // scoping, because each loop iteration must see separate bindings + // of |INIT|. We handle this by replacing the block on the scope + // chain with a new block, copying the old one's contents, each + // iteration. We supply a special PNK_FRESHENBLOCK node as the + // |let INIT| node for |for(let INIT;;)| loop heads to distinguish + // such nodes from *actual*, non-desugared use of the above syntax. + // (We don't do this for PNK_CONST nodes because the spec says no + // freshening happens -- observable with the Debugger API.) + if (pn1->isKind(PNK_CONST)) { + pn1 = nullptr; + } else { + pn1 = handler.newFreshenBlock(pn1->pn_pos); + if (!pn1) + return null(); + } } /* Parse the loop condition or null into pn2. */ diff --git a/js/src/jit-test/tests/basic/testBug763950.js b/js/src/jit-test/tests/basic/testBug763950.js index 246a7a155ea..6f685217fdc 100644 --- a/js/src/jit-test/tests/basic/testBug763950.js +++ b/js/src/jit-test/tests/basic/testBug763950.js @@ -2,5 +2,5 @@ var x; for (let j = 0; j < 1; j = j + 1) x = function() { return j; }; - assertEq(x(), 1); + assertEq(x(), 0); })(); diff --git a/js/src/jit/BaselineCompiler.cpp b/js/src/jit/BaselineCompiler.cpp index 2cc417ae9bb..5fd488fd35c 100644 --- a/js/src/jit/BaselineCompiler.cpp +++ b/js/src/jit/BaselineCompiler.cpp @@ -3011,7 +3011,6 @@ static const VMFunction PopBlockScopeInfo = FunctionInfo(jit::P bool BaselineCompiler::emit_JSOP_POPBLOCKSCOPE() { - // Call a stub to pop the block from the block chain. prepareVMCall(); masm.loadBaselineFramePtr(BaselineFrameReg, R0.scratchReg()); @@ -3020,6 +3019,21 @@ BaselineCompiler::emit_JSOP_POPBLOCKSCOPE() return callVM(PopBlockScopeInfo); } +typedef bool (*FreshenBlockScopeFn)(JSContext *, BaselineFrame *); +static const VMFunction FreshenBlockScopeInfo = + FunctionInfo(jit::FreshenBlockScope); + +bool +BaselineCompiler::emit_JSOP_FRESHENBLOCKSCOPE() +{ + prepareVMCall(); + + masm.loadBaselineFramePtr(BaselineFrameReg, R0.scratchReg()); + pushArg(R0.scratchReg()); + + return callVM(FreshenBlockScopeInfo); +} + typedef bool (*DebugLeaveBlockFn)(JSContext *, BaselineFrame *, jsbytecode *); static const VMFunction DebugLeaveBlockInfo = FunctionInfo(jit::DebugLeaveBlock); diff --git a/js/src/jit/BaselineCompiler.h b/js/src/jit/BaselineCompiler.h index 6abf948359e..da4ff491138 100644 --- a/js/src/jit/BaselineCompiler.h +++ b/js/src/jit/BaselineCompiler.h @@ -172,6 +172,7 @@ namespace jit { _(JSOP_RETSUB) \ _(JSOP_PUSHBLOCKSCOPE) \ _(JSOP_POPBLOCKSCOPE) \ + _(JSOP_FRESHENBLOCKSCOPE) \ _(JSOP_DEBUGLEAVEBLOCK) \ _(JSOP_EXCEPTION) \ _(JSOP_DEBUGGER) \ diff --git a/js/src/jit/BaselineFrame-inl.h b/js/src/jit/BaselineFrame-inl.h index cb46a24ec12..4cb42f4f41d 100644 --- a/js/src/jit/BaselineFrame-inl.h +++ b/js/src/jit/BaselineFrame-inl.h @@ -45,6 +45,13 @@ BaselineFrame::popWith(JSContext *cx) popOffScopeChain(); } +inline void +BaselineFrame::replaceInnermostScope(ScopeObject &scope) +{ + MOZ_ASSERT(scope.enclosingScope() == scopeChain_->as().enclosingScope()); + scopeChain_ = &scope; +} + inline bool BaselineFrame::pushBlock(JSContext *cx, Handle block) { @@ -66,6 +73,18 @@ BaselineFrame::popBlock(JSContext *cx) popOffScopeChain(); } +inline bool +BaselineFrame::freshenBlock(JSContext *cx) +{ + Rooted current(cx, &scopeChain_->as()); + ClonedBlockObject *clone = ClonedBlockObject::clone(cx, current); + if (!clone) + return false; + + replaceInnermostScope(*clone); + return true; +} + inline CallObject & BaselineFrame::callObj() const { diff --git a/js/src/jit/BaselineFrame.h b/js/src/jit/BaselineFrame.h index 76f71247224..9fd31238246 100644 --- a/js/src/jit/BaselineFrame.h +++ b/js/src/jit/BaselineFrame.h @@ -133,6 +133,7 @@ class BaselineFrame inline void pushOnScopeChain(ScopeObject &scope); inline void popOffScopeChain(); + inline void replaceInnermostScope(ScopeObject &scope); inline void popWith(JSContext *cx); @@ -249,6 +250,7 @@ class BaselineFrame inline bool pushBlock(JSContext *cx, Handle block); inline void popBlock(JSContext *cx); + inline bool freshenBlock(JSContext *cx); bool strictEvalPrologue(JSContext *cx); bool heavyweightFunPrologue(JSContext *cx); diff --git a/js/src/jit/IonBuilder.cpp b/js/src/jit/IonBuilder.cpp index 960f9374fdf..4d9de2ae55e 100644 --- a/js/src/jit/IonBuilder.cpp +++ b/js/src/jit/IonBuilder.cpp @@ -2005,6 +2005,17 @@ IonBuilder::inspectOpcode(JSOp op) // Just fall through to the unsupported bytecode case. break; +#ifdef DEBUG + case JSOP_PUSHBLOCKSCOPE: + case JSOP_FRESHENBLOCKSCOPE: + case JSOP_POPBLOCKSCOPE: + // These opcodes are currently unhandled by Ion, but in principle + // there's no reason they couldn't be. Whenever this happens, OSR will + // have to consider that JSOP_FRESHENBLOCK mutates the scope chain -- + // right now it caches the scope chain in MBasicBlock::scopeChain(). + // That stale value will have to be updated when JSOP_FRESHENBLOCK is + // encountered. +#endif default: break; } @@ -3150,6 +3161,7 @@ IonBuilder::forLoop(JSOp op, jssrcnote *sn) // body: // ; [body] // [increment:] + // [FRESHENBLOCKSCOPE, if needed by a cloned block] // ; [increment] // [cond:] // LOOPENTRY @@ -3157,6 +3169,10 @@ IonBuilder::forLoop(JSOp op, jssrcnote *sn) // // If there is a condition (condpc != ifne), this acts similar to a while // loop otherwise, it acts like a do-while loop. + // + // Note that currently Ion does not compile pushblockscope/popblockscope as + // necessary prerequisites to freshenblockscope. So the code below doesn't + // and needn't consider the implications of freshenblockscope. jsbytecode *bodyStart = pc; jsbytecode *bodyEnd = updatepc; jsbytecode *loopEntry = condpc; diff --git a/js/src/jit/VMFunctions.cpp b/js/src/jit/VMFunctions.cpp index c40553ed078..038049e85db 100644 --- a/js/src/jit/VMFunctions.cpp +++ b/js/src/jit/VMFunctions.cpp @@ -994,6 +994,12 @@ PopBlockScope(JSContext *cx, BaselineFrame *frame) return true; } +bool +FreshenBlockScope(JSContext *cx, BaselineFrame *frame) +{ + return frame->freshenBlock(cx); +} + bool DebugLeaveBlock(JSContext *cx, BaselineFrame *frame, jsbytecode *pc) { diff --git a/js/src/jit/VMFunctions.h b/js/src/jit/VMFunctions.h index e954dc0c365..20003530494 100644 --- a/js/src/jit/VMFunctions.h +++ b/js/src/jit/VMFunctions.h @@ -728,6 +728,7 @@ bool LeaveWith(JSContext *cx, BaselineFrame *frame); bool PushBlockScope(JSContext *cx, BaselineFrame *frame, Handle block); bool PopBlockScope(JSContext *cx, BaselineFrame *frame); +bool FreshenBlockScope(JSContext *cx, BaselineFrame *frame); bool DebugLeaveBlock(JSContext *cx, BaselineFrame *frame, jsbytecode *pc); bool InitBaselineFrameForOsr(BaselineFrame *frame, InterpreterFrame *interpFrame, diff --git a/js/src/jsobj.h b/js/src/jsobj.h index cecee582dee..1a7cea12353 100644 --- a/js/src/jsobj.h +++ b/js/src/jsobj.h @@ -147,7 +147,7 @@ class JSObject : public js::gc::Cell } /* - * Whether this is the only object which has its specified gbroup. This + * Whether this is the only object which has its specified group. This * object will have its group constructed lazily as needed by analysis. */ bool isSingleton() const { @@ -180,7 +180,7 @@ class JSObject : public js::gc::Cell js::HandleObjectGroup group); // Set the shape of an object. This pointer is valid for native objects and - // some non-native objects. After creating an object, tobjects for which + // some non-native objects. After creating an object, the objects for which // the shape pointer is invalid need to overwrite this pointer before a GC // can occur. inline void setInitialShapeMaybeNonNative(js::Shape *shape); diff --git a/js/src/jsreflect.cpp b/js/src/jsreflect.cpp index d232f5b3885..b6038185641 100644 --- a/js/src/jsreflect.cpp +++ b/js/src/jsreflect.cpp @@ -2560,7 +2560,10 @@ ASTSerializer::statement(ParseNode *pn, MutableHandleValue dst) RootedValue init(cx), test(cx), update(cx); - return forInit(head->pn_kid1, &init) && + return forInit(head->pn_kid1 && !head->pn_kid1->isKind(PNK_FRESHENBLOCK) + ? head->pn_kid1 + : nullptr, + &init) && optExpression(head->pn_kid2, &test) && optExpression(head->pn_kid3, &update) && builder.forStatement(init, test, update, stmt, &pn->pn_pos, dst); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/for-loop.js b/js/src/tests/ecma_6/LexicalEnvironment/for-loop.js new file mode 100644 index 00000000000..db36323488a --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/for-loop.js @@ -0,0 +1,128 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ + +var gTestfile = "for-loop.js"; +//----------------------------------------------------------------------------- +var BUGNUMBER = 985733; +var summary = + "ES6 for-loop semantics for for(;;) loops whose heads contain lexical " + "declarations"; + +print(BUGNUMBER + ": " + summary); + +/************** + * BEGIN TEST * + **************/ + +function isError(code, type) +{ + try + { + Function(code); + throw new Error("didn't throw"); + } + catch (e) + { + assertEq(e instanceof type, true, + "unexpected error for `" + code + "`: got " + e); + } +} + +function isOK(code) +{ + Function(code); +} + +isError("for (const x; ; ) ;", SyntaxError); +isError("for (const x = 5, y; ; ) ;", SyntaxError); +isError("for (const [z]; ; ) ;", SyntaxError); +//isError("for (const [z, z]; ; ) ;", SyntaxError); +//isError("for (const [z, z] = [0, 1]; ; ) ;", SyntaxError); + +isOK("for (let x; ; ) ;"); +isOK("for (let x = 5, y; ; ) ;"); + +// I'm fairly sure this is supposed to work: the negative-lookahead rules in +// IterationStatement ensure that |for (let| *always* is a loop header starting +// with a lexical declaration. But I'm not 100% certain, so these tests might +// need to be fixed when we implement the negative-lookahead restrictions. +isOK("for (let [z] = [3]; ; ) ;"); +isError("for (let [z, z]; ; ) ;", SyntaxError); // because missing initializer + +// This is wrong! Per 13.2.1.1, "It is a Syntax Error if the BoundNames of +// BindingList contains any duplicate entries." But we don't implement this +// yet, so it becomes a TypeError at runtime. +isError("for (let [z, z] = [0, 1]; ; ) ;", TypeError); + +// A for-loop with lexical declarations, with a mixture of bindings that are and +// aren't aliased. (The mixture stress-tests any code that incorrectly assumes +// all bindings are aliased.) +var funcs = []; +for (let [i, j, k] = [0, 1, 2]; i < 10; i++) + funcs.push(() => i); + +assertEq(funcs[0](), 0); +assertEq(funcs[1](), 1); +assertEq(funcs[2](), 2); +assertEq(funcs[3](), 3); +assertEq(funcs[4](), 4); +assertEq(funcs[5](), 5); +assertEq(funcs[6](), 6); +assertEq(funcs[7](), 7); +assertEq(funcs[8](), 8); +assertEq(funcs[9](), 9); + +var outer = "OUTER V IGNORE"; +var save; +for (let outer = (save = function() { return outer; }); ; ) + break; +assertEq(save(), "OUTER V IGNORE", + "this is actually a bug: fix for(;;) loops to evaluate init RHSes " + + "in the block scope containing all the LHS bindings!"); + + + +var funcs = []; +function t(i, name, expect) +{ + assertEq(funcs[i].name, name); + assertEq(funcs[i](), expect); +} + +if (save() !== "OUTER V IGNORE") +{ + var v = "OUTER V IGNORE"; + var i = 0; + for (let v = (funcs.push(function init() { return v; }), + 0); + v = (funcs.push(function test() { return v; }), + v + 1); + v = (funcs.push(function incr() { return v; }), + v + 1)) + { + v = (funcs.push(function body() { return v; }), + v + 1); + i++; + if (i >= 3) + break; + } + t(0, "init", 2); + t(1, "test", 2); + t(2, "body", 2); + t(3, "incr", 5); + t(4, "test", 5); + t(5, "body", 5); + t(6, "incr", 8); + t(7, "test", 8); + t(8, "body", 8); + assertEq(funcs.length, 9); +} + +/******************************************************************************/ + +if (typeof reportCompare === "function") + reportCompare(true, true); + +print("Tests complete"); diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index c0c7b928b78..0ad1ef419fb 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -1681,7 +1681,6 @@ CASE(JSOP_UNUSED189) CASE(JSOP_UNUSED190) CASE(JSOP_UNUSED191) CASE(JSOP_UNUSED192) -CASE(JSOP_UNUSED196) CASE(JSOP_UNUSED209) CASE(JSOP_UNUSED210) CASE(JSOP_UNUSED211) @@ -3440,6 +3439,13 @@ CASE(JSOP_DEBUGLEAVEBLOCK) } END_CASE(JSOP_DEBUGLEAVEBLOCK) +CASE(JSOP_FRESHENBLOCKSCOPE) +{ + if (!REGS.fp()->freshenBlock(cx)) + goto error; +} +END_CASE(JSOP_FRESHENBLOCKSCOPE) + CASE(JSOP_GENERATOR) { MOZ_ASSERT(!cx->isExceptionPending()); diff --git a/js/src/vm/Opcodes.h b/js/src/vm/Opcodes.h index 94e707a1292..c88d85caf54 100644 --- a/js/src/vm/Opcodes.h +++ b/js/src/vm/Opcodes.h @@ -1649,8 +1649,6 @@ */ \ macro(JSOP_GETXPROP, 195,"getxprop", NULL, 5, 1, 1, JOF_ATOM|JOF_PROP|JOF_TYPESET) \ \ - macro(JSOP_UNUSED196, 196,"unused196", NULL, 1, 0, 0, JOF_BYTE) \ - \ /* * Pops the top stack value as 'val' and pushes 'typeof val'. Note that * this opcode isn't used when, in the original source code, 'val' is a @@ -1661,9 +1659,21 @@ * Operands: * Stack: val => (typeof val) */ \ - macro(JSOP_TYPEOFEXPR, 197,"typeofexpr", NULL, 1, 1, 1, JOF_BYTE|JOF_DETECTING) \ + macro(JSOP_TYPEOFEXPR, 196,"typeofexpr", NULL, 1, 1, 1, JOF_BYTE|JOF_DETECTING) \ \ /* Block-local scope support. */ \ + /* + * Replaces the current block on the scope chain with a fresh block + * that copies all the bindings in the bock. This operation implements the + * behavior of inducing a fresh block scope for every iteration of a + * for(let ...; ...; ...) loop, if any declarations induced by such a loop + * are captured within the loop. + * Category: Variables and Scopes + * Type: Block-local Scope + * Operands: + * Stack: => + */ \ + macro(JSOP_FRESHENBLOCKSCOPE,197,"freshenblockscope", NULL, 1, 0, 0, JOF_BYTE) \ /* * Pushes block onto the scope chain. * Category: Variables and Scopes diff --git a/js/src/vm/ScopeObject.cpp b/js/src/vm/ScopeObject.cpp index 089d20caeb4..6ef12041809 100644 --- a/js/src/vm/ScopeObject.cpp +++ b/js/src/vm/ScopeObject.cpp @@ -660,6 +660,35 @@ ClonedBlockObject::copyUnaliasedValues(AbstractFramePtr frame) } } +/* static */ ClonedBlockObject * +ClonedBlockObject::clone(ExclusiveContext *cx, Handle block) +{ + RootedObject enclosing(cx, &block->enclosingScope()); + + MOZ_ASSERT(block->getClass() == &BlockObject::class_); + + RootedObjectGroup cloneGroup(cx, block->group()); + RootedShape cloneShape(cx, block->lastProperty()); + + JSObject *obj = JSObject::create(cx, FINALIZE_KIND, gc::TenuredHeap, cloneShape, cloneGroup); + if (!obj) + return nullptr; + + ClonedBlockObject © = obj->as(); + + MOZ_ASSERT(!copy.inDictionaryMode()); + MOZ_ASSERT(copy.isDelegate()); + MOZ_ASSERT(block->slotSpan() == copy.slotSpan()); + MOZ_ASSERT(copy.slotSpan() >= copy.numVariables() + RESERVED_SLOTS); + + copy.setReservedSlot(SCOPE_CHAIN_SLOT, block->getReservedSlot(SCOPE_CHAIN_SLOT)); + + for (uint32_t i = 0, count = copy.numVariables(); i < count; i++) + copy.setVar(i, block->var(i, DONT_CHECK_ALIASING), DONT_CHECK_ALIASING); + + return © +} + StaticBlockObject * StaticBlockObject::create(ExclusiveContext *cx) { diff --git a/js/src/vm/ScopeObject.h b/js/src/vm/ScopeObject.h index f4866b4f60c..dae35a207b5 100644 --- a/js/src/vm/ScopeObject.h +++ b/js/src/vm/ScopeObject.h @@ -669,6 +669,12 @@ class ClonedBlockObject : public BlockObject /* Copy in all the unaliased formals and locals. */ void copyUnaliasedValues(AbstractFramePtr frame); + + /* + * Create a new ClonedBlockObject with the same enclosing scope and + * variable values as this. + */ + static ClonedBlockObject *clone(ExclusiveContext *cx, Handle block); }; // Internal scope object used by JSOP_BINDNAME upon encountering an diff --git a/js/src/vm/Stack-inl.h b/js/src/vm/Stack-inl.h index 97e86820b97..8d496882bc2 100644 --- a/js/src/vm/Stack-inl.h +++ b/js/src/vm/Stack-inl.h @@ -207,6 +207,14 @@ InterpreterFrame::popOffScopeChain() scopeChain_ = &scopeChain_->as().enclosingScope(); } +inline void +InterpreterFrame::replaceInnermostScope(ScopeObject &scope) +{ + MOZ_ASSERT(flags_ & HAS_SCOPECHAIN); + MOZ_ASSERT(scope.enclosingScope() == scopeChain_->as().enclosingScope()); + scopeChain_ = &scope; +} + bool InterpreterFrame::hasCallObj() const { @@ -799,6 +807,14 @@ AbstractFramePtr::thisValue() const return asRematerializedFrame()->thisValue(); } +inline bool +AbstractFramePtr::freshenBlock(JSContext *cx) const +{ + if (isInterpreterFrame()) + return asInterpreterFrame()->freshenBlock(cx); + return asBaselineFrame()->freshenBlock(cx); +} + inline void AbstractFramePtr::popBlock(JSContext *cx) const { diff --git a/js/src/vm/Stack.cpp b/js/src/vm/Stack.cpp index 8f7fb194d96..8e93daeeac4 100644 --- a/js/src/vm/Stack.cpp +++ b/js/src/vm/Stack.cpp @@ -293,6 +293,19 @@ InterpreterFrame::pushBlock(JSContext *cx, StaticBlockObject &block) return true; } +bool +InterpreterFrame::freshenBlock(JSContext *cx) +{ + MOZ_ASSERT(flags_ & HAS_SCOPECHAIN); + Rooted block(cx, &scopeChain_->as()); + ClonedBlockObject *fresh = ClonedBlockObject::clone(cx, block); + if (!fresh) + return false; + + replaceInnermostScope(*fresh); + return true; +} + void InterpreterFrame::popBlock(JSContext *cx) { diff --git a/js/src/vm/Stack.h b/js/src/vm/Stack.h index 88ba5f12f1c..072adefd255 100644 --- a/js/src/vm/Stack.h +++ b/js/src/vm/Stack.h @@ -28,6 +28,7 @@ class AsmJSModule; class InterpreterRegs; class CallObject; class ScopeObject; +class ClonedBlockObject; class ScriptFrameIter; class SPSProfiler; class InterpreterFrame; @@ -234,6 +235,8 @@ class AbstractFramePtr bool hasPushedSPSFrame() const; + inline bool freshenBlock(JSContext *cx) const; + inline void popBlock(JSContext *cx) const; inline void popWith(JSContext *cx) const; @@ -587,14 +590,18 @@ class InterpreterFrame inline void pushOnScopeChain(ScopeObject &scope); inline void popOffScopeChain(); + inline void replaceInnermostScope(ScopeObject &scope); /* * For blocks with aliased locals, these interfaces push and pop entries on - * the scope chain. + * the scope chain. The "freshen" operation replaces the current block + * with a fresh copy of it, to implement semantics providing distinct + * bindings per iteration of a for-loop. */ bool pushBlock(JSContext *cx, StaticBlockObject &block); void popBlock(JSContext *cx); + bool freshenBlock(JSContext *cx); /* * With