Bug Bug 692274, part 4 - Rewrite parsing, emitting and decompiling of let to fix scoping properly (r=jorendorff)
authorLuke Wagner <luke@mozilla.com>
Fri, 07 Oct 2011 12:02:50 -0700
changeset 85701 38344f96b3e3763be2d1d4d919e3615ac2fa640d
parent 85700 9272bb82eebac5e30a16af20e4443ee30fc26c33
child 85702 6707b24155987811ad405d6158f19ad0f2910b45
push idunknown
push userunknown
push dateunknown
reviewersjorendorff
bugs692274
milestone12.0a1
Bug Bug 692274, part 4 - Rewrite parsing, emitting and decompiling of let to fix scoping properly (r=jorendorff)
js/public/Vector.h
js/src/frontend/BytecodeEmitter.cpp
js/src/frontend/BytecodeEmitter.h
js/src/frontend/ParseNode.h
js/src/frontend/Parser.cpp
js/src/frontend/Parser.h
js/src/jit-test/tests/basic/bug657975.js
js/src/jit-test/tests/basic/testBug666292.js
js/src/jit-test/tests/basic/testBug683470.js
js/src/jit-test/tests/basic/testBug692274-1.js
js/src/jit-test/tests/basic/testBug692274-2.js
js/src/jit-test/tests/basic/testBug692274-3.js
js/src/jit-test/tests/basic/testBug692274-4.js
js/src/jit-test/tests/basic/testBug703857.js
js/src/jit-test/tests/basic/testBug709633.js
js/src/jit-test/tests/basic/testLet.js
js/src/jsanalyze.cpp
js/src/jsanalyze.h
js/src/jsinfer.cpp
js/src/jsinterp.cpp
js/src/jsobj.cpp
js/src/jsobj.h
js/src/jsopcode.cpp
js/src/jsopcode.h
js/src/jsopcode.tbl
js/src/jsreflect.cpp
js/src/jsxdrapi.h
js/src/methodjit/Compiler.cpp
js/src/methodjit/LoopState.cpp
js/src/methodjit/StubCalls.cpp
js/src/tests/js1_8/regress/regress-465567-01.js
js/src/tests/js1_8_5/extensions/reflect-parse.js
js/src/tests/js1_8_5/extensions/regress-672804-1.js
js/src/tests/js1_8_5/extensions/regress-672804-3.js
--- a/js/public/Vector.h
+++ b/js/public/Vector.h
@@ -375,16 +375,33 @@ class Vector : private AllocPolicy
         return *(end() - 1);
     }
 
     const T &back() const {
         JS_ASSERT(!entered && !empty());
         return *(end() - 1);
     }
 
+    class Range {
+        friend class Vector;
+        T *cur, *end;
+        Range(T *cur, T *end) : cur(cur), end(end) {}
+      public:
+        Range() {}
+        bool empty() const { return cur == end; }
+        size_t remain() const { return end - cur; }
+        T &front() const { return *cur; }
+        void popFront() { JS_ASSERT(!empty()); ++cur; }
+        T popCopyFront() { JS_ASSERT(!empty()); return *cur++; }
+    };
+
+    Range all() {
+        return Range(begin(), end());
+    }
+
     /* mutators */
 
     /* If reserve(length() + N) succeeds, the N next appends are guaranteed to succeed. */
     bool reserve(size_t capacity);
 
     /*
      * Destroy elements in the range [end() - incr, end()). Does not deallocate
      * or unreserve storage for those elements.
--- a/js/src/frontend/BytecodeEmitter.cpp
+++ b/js/src/frontend/BytecodeEmitter.cpp
@@ -185,64 +185,63 @@ EmitCheck(JSContext *cx, BytecodeEmitter
         JS_ASSERT(newlength >= size_t(offset + delta));
         bce->current->base = newbase;
         bce->current->limit = newbase + newlength;
         bce->current->next = newbase + offset;
     }
     return offset;
 }
 
+static JSObject *
+CurrentBlock(BytecodeEmitter *bce)
+{
+    JS_ASSERT(bce->topStmt->type == STMT_BLOCK || bce->topStmt->type == STMT_SWITCH);
+    JS_ASSERT(bce->topStmt->blockObj->isStaticBlock());
+    return bce->topStmt->blockObj;
+}
+
 static void
 UpdateDepth(JSContext *cx, BytecodeEmitter *bce, ptrdiff_t target)
 {
-    jsbytecode *pc;
-    JSOp op;
-    const JSCodeSpec *cs;
-    uintN nuses;
-    intN ndefs;
-
-    pc = bce->code(target);
-    op = (JSOp) *pc;
-    cs = &js_CodeSpec[op];
-    if ((cs->format & JOF_TMPSLOT_MASK)) {
+    jsbytecode *pc = bce->code(target);
+    JSOp op = (JSOp) *pc;
+    const JSCodeSpec *cs = &js_CodeSpec[op];
+
+
+    if (cs->format & JOF_TMPSLOT_MASK) {
+        /*
+         * An opcode may temporarily consume stack space during execution.
+         * Account for this in maxStackDepth separately from uses/defs here.
+         */
         uintN depth = (uintN) bce->stackDepth +
                       ((cs->format & JOF_TMPSLOT_MASK) >> JOF_TMPSLOT_SHIFT);
         if (depth > bce->maxStackDepth)
             bce->maxStackDepth = depth;
     }
 
-    nuses = js_GetStackUses(cs, op, pc);
+    /*
+     * Specially handle any case that would call js_GetIndexFromBytecode since
+     * it requires a well-formed script. This allows us to safely pass NULL as
+     * the 'script' parameter.
+     */
+    intN nuses, ndefs;
+    if (op == JSOP_ENTERBLOCK) {
+        nuses = 0;
+        ndefs = OBJ_BLOCK_COUNT(cx, CurrentBlock(bce));
+    } else if (op == JSOP_ENTERLET0) {
+        nuses = ndefs = OBJ_BLOCK_COUNT(cx, CurrentBlock(bce));
+    } else if (op == JSOP_ENTERLET1) {
+        nuses = ndefs = OBJ_BLOCK_COUNT(cx, CurrentBlock(bce)) + 1;
+    } else {
+        nuses = StackUses(NULL, pc);
+        ndefs = StackDefs(NULL, pc);
+    }
+
     bce->stackDepth -= nuses;
     JS_ASSERT(bce->stackDepth >= 0);
-    if (bce->stackDepth < 0) {
-        char numBuf[12];
-        TokenStream *ts;
-
-        JS_snprintf(numBuf, sizeof numBuf, "%d", target);
-        ts = &bce->parser->tokenStream;
-        JS_ReportErrorFlagsAndNumber(cx, JSREPORT_WARNING,
-                                     js_GetErrorMessage, NULL,
-                                     JSMSG_STACK_UNDERFLOW,
-                                     ts->getFilename() ? ts->getFilename() : "stdin",
-                                     numBuf);
-    }
-    ndefs = cs->ndefs;
-    if (ndefs < 0) {
-        JSObject *blockObj;
-
-        /* We just executed IndexParsedObject */
-        JS_ASSERT(op == JSOP_ENTERBLOCK);
-        JS_ASSERT(nuses == 0);
-        blockObj = bce->objectList.lastbox->object;
-        JS_ASSERT(blockObj->isStaticBlock());
-        JS_ASSERT(blockObj->getSlot(JSSLOT_BLOCK_DEPTH).isUndefined());
-
-        OBJ_SET_BLOCK_DEPTH(cx, blockObj, bce->stackDepth);
-        ndefs = OBJ_BLOCK_COUNT(cx, blockObj);
-    }
     bce->stackDepth += ndefs;
     if ((uintN)bce->stackDepth > bce->maxStackDepth)
         bce->maxStackDepth = bce->stackDepth;
 }
 
 static inline void
 UpdateDecomposeLength(BytecodeEmitter *bce, uintN start)
 {
@@ -1491,16 +1490,26 @@ FlushPops(JSContext *cx, BytecodeEmitter
     JS_ASSERT(*npops != 0);
     if (NewSrcNote(cx, bce, SRC_HIDDEN) < 0)
         return JS_FALSE;
     EMIT_UINT16_IMM_OP(JSOP_POPN, *npops);
     *npops = 0;
     return JS_TRUE;
 }
 
+static bool
+PopIterator(JSContext *cx, BytecodeEmitter *bce)
+{
+    if (NewSrcNote(cx, bce, SRC_HIDDEN) < 0)
+        return false;
+    if (Emit1(cx, bce, JSOP_ENDITER) < 0)
+        return false;
+    return true;
+}
+
 /*
  * Emit additional bytecode(s) for non-local jumps.
  */
 static JSBool
 EmitNonLocalJumpFixup(JSContext *cx, BytecodeEmitter *bce, StmtInfo *toStmt)
 {
     /*
      * The non-local jump fixup we emit will unbalance bce->stackDepth, because
@@ -1528,44 +1537,58 @@ EmitNonLocalJumpFixup(JSContext *cx, Byt
             FLUSH_POPS();
             if (NewSrcNote(cx, bce, SRC_HIDDEN) < 0)
                 return JS_FALSE;
             if (Emit1(cx, bce, JSOP_LEAVEWITH) < 0)
                 return JS_FALSE;
             break;
 
           case STMT_FOR_IN_LOOP:
-            /*
-             * The iterator and the object being iterated need to be popped.
-             */
             FLUSH_POPS();
-            if (NewSrcNote(cx, bce, SRC_HIDDEN) < 0)
-                return JS_FALSE;
-            if (Emit1(cx, bce, JSOP_ENDITER) < 0)
+            if (!PopIterator(cx, bce))
                 return JS_FALSE;
             break;
 
           case STMT_SUBROUTINE:
             /*
              * There's a [exception or hole, retsub pc-index] pair on the
              * stack that we need to pop.
              */
             npops += 2;
             break;
 
           default:;
         }
 
         if (stmt->flags & SIF_SCOPE) {
-            /* There is a Block object with locals on the stack to pop. */
             FLUSH_POPS();
-            if (NewSrcNote(cx, bce, SRC_HIDDEN) < 0)
-                return JS_FALSE;
-            uintN i = OBJ_BLOCK_COUNT(cx, stmt->blockObj);
-            EMIT_UINT16_IMM_OP(JSOP_LEAVEBLOCK, i);
+            uintN blockObjCount = OBJ_BLOCK_COUNT(cx, stmt->blockObj);
+            if (stmt->flags & SIF_FOR_BLOCK) {
+                /*
+                 * For a for-let-in statement, pushing/popping the block is
+                 * interleaved with JSOP_(END)ITER. Just handle both together
+                 * here and skip over the enclosing STMT_FOR_IN_LOOP.
+                 */
+                JS_ASSERT(stmt->down->type == STMT_FOR_IN_LOOP);
+                stmt = stmt->down;
+                if (stmt == toStmt)
+                    break;
+                if (Emit1(cx, bce, JSOP_LEAVEFORLETIN) < 0)
+                    return JS_FALSE;
+                if (!PopIterator(cx, bce))
+                    return JS_FALSE;
+                if (NewSrcNote(cx, bce, SRC_HIDDEN) < 0)
+                    return JS_FALSE;
+                EMIT_UINT16_IMM_OP(JSOP_POPN, blockObjCount);
+            } else {
+                /* There is a Block object with locals on the stack to pop. */
+                if (NewSrcNote(cx, bce, SRC_HIDDEN) < 0)
+                    return JS_FALSE;
+                EMIT_UINT16_IMM_OP(JSOP_LEAVEBLOCK, blockObjCount);
+            }
         }
     }
 
     FLUSH_POPS();
     bce->stackDepth = depth;
     return JS_TRUE;
 
 #undef FLUSH_POPS
@@ -1923,41 +1946,48 @@ AdjustBlockSlot(JSContext *cx, BytecodeE
                                      JSMSG_TOO_MANY_LOCALS);
             slot = -1;
         }
     }
     return slot;
 }
 
 static bool
-EmitEnterBlock(JSContext *cx, ParseNode *pn, BytecodeEmitter *bce)
+EmitEnterBlock(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn, JSOp op)
 {
     JS_ASSERT(pn->isKind(PNK_LEXICALSCOPE));
-    if (!EmitObjectOp(cx, pn->pn_objbox, JSOP_ENTERBLOCK, bce))
+    if (!EmitObjectOp(cx, pn->pn_objbox, op, bce))
         return false;
 
     JSObject *blockObj = pn->pn_objbox->object;
-    jsint depth = AdjustBlockSlot(cx, bce, OBJ_BLOCK_DEPTH(cx, blockObj));
-    if (depth < 0)
+    JS_ASSERT(blockObj->isStaticBlock());
+    JS_ASSERT(blockObj->getSlot(JSSLOT_BLOCK_DEPTH).isUndefined());
+
+    int depth = bce->stackDepth -
+                (OBJ_BLOCK_COUNT(cx, blockObj) + ((op == JSOP_ENTERLET1) ? 1 : 0));
+    JS_ASSERT(depth >= 0);
+    OBJ_SET_BLOCK_DEPTH(cx, blockObj, depth);
+    int depthPlusFixed = AdjustBlockSlot(cx, bce, depth);
+    if (depthPlusFixed < 0)
         return false;
 
     uintN base = JSSLOT_FREE(&BlockClass);
     for (uintN slot = base, limit = base + OBJ_BLOCK_COUNT(cx, blockObj); slot < limit; slot++) {
         const Value &v = blockObj->getSlot(slot);
 
         /* Beware the empty destructuring dummy. */
         if (v.isUndefined()) {
             JS_ASSERT(slot + 1 <= limit);
             continue;
         }
 
         Definition *dn = (Definition *) v.toPrivate();
         JS_ASSERT(dn->isDefn());
-        JS_ASSERT(uintN(dn->frameSlot() + depth) < JS_BIT(16));
-        dn->pn_cookie.set(dn->pn_cookie.level(), uint16_t(dn->frameSlot() + depth));
+        JS_ASSERT(uintN(dn->frameSlot() + depthPlusFixed) < JS_BIT(16));
+        dn->pn_cookie.set(dn->pn_cookie.level(), uint16_t(dn->frameSlot() + depthPlusFixed));
 #ifdef DEBUG
         for (ParseNode *pnu = dn->dn_uses; pnu; pnu = pnu->pn_link) {
             JS_ASSERT(pnu->pn_lexdef == dn);
             JS_ASSERT(!(pnu->pn_dflags & PND_BOUND));
             JS_ASSERT(pnu->pn_cookie.isFree());
         }
 #endif
 
@@ -3173,63 +3203,52 @@ EmitSwitch(JSContext *cx, BytecodeEmitte
     ptrdiff_t top, off, defaultOffset;
     ParseNode *pn2, *pn3, *pn4;
     uint32_t caseCount, tableLength;
     ParseNode **table;
     int32_t i, low, high;
     intN noteIndex;
     size_t switchSize, tableSize;
     jsbytecode *pc, *savepc;
-#if JS_HAS_BLOCK_SCOPE
-    int count;
-#endif
     StmtInfo stmtInfo;
 
     /* Try for most optimal, fall back if not dense ints, and per ECMAv2. */
     switchOp = JSOP_TABLESWITCH;
     ok = JS_TRUE;
     hasDefault = constPropagated = JS_FALSE;
     defaultOffset = -1;
 
-    /*
-     * If the switch contains let variables scoped by its body, model the
-     * resulting block on the stack first, before emitting the discriminant's
-     * bytecode (in case the discriminant contains a stack-model dependency
-     * such as a let expression).
-     */
     pn2 = pn->pn_right;
 #if JS_HAS_BLOCK_SCOPE
+    /*
+     * If there are hoisted let declarations, their stack slots go under the
+     * discriminant's value so push their slots now and enter the block later.
+     */
+    uint32_t blockCount = 0;
     if (pn2->isKind(PNK_LEXICALSCOPE)) {
-        /*
-         * Push the body's block scope before discriminant code-gen to reflect
-         * the order of slots on the stack. The block's locals must lie under
-         * the discriminant on the stack so that case-dispatch bytecodes can
-         * find the discriminant on top of stack.
-         */
-        count = OBJ_BLOCK_COUNT(cx, pn2->pn_objbox->object);
+        blockCount = OBJ_BLOCK_COUNT(cx, pn2->pn_objbox->object);
+        for (uint32_t i = 0; i < blockCount; ++i) {
+            if (Emit1(cx, bce, JSOP_UNDEFINED) < 0)
+                return JS_FALSE;
+        }
+    }
+#endif
+
+    /* Push the discriminant. */
+    if (!EmitTree(cx, bce, pn->pn_left))
+        return JS_FALSE;
+
+#if JS_HAS_BLOCK_SCOPE
+    if (pn2->isKind(PNK_LEXICALSCOPE)) {
         PushBlockScope(bce, &stmtInfo, pn2->pn_objbox->object, -1);
         stmtInfo.type = STMT_SWITCH;
-
-        /* Emit JSOP_ENTERBLOCK before code to evaluate the discriminant. */
-        if (!EmitEnterBlock(cx, pn2, bce))
+        if (!EmitEnterBlock(cx, bce, pn2, JSOP_ENTERLET1))
             return JS_FALSE;
     }
-#ifdef __GNUC__
-    else {
-        count = 0;
-    }
 #endif
-#endif
-
-    /*
-     * Emit code for the discriminant first (or nearly first, in the case of a
-     * switch whose body is a block scope).
-     */
-    if (!EmitTree(cx, bce, pn->pn_left))
-        return JS_FALSE;
 
     /* Switch bytecodes run from here till end of final case. */
     top = bce->offset();
 #if !JS_HAS_BLOCK_SCOPE
     PushStatement(bce, &stmtInfo, STMT_SWITCH, top);
 #else
     if (pn2->isKind(PNK_STATEMENTLIST)) {
         PushStatement(bce, &stmtInfo, STMT_SWITCH, top);
@@ -3681,17 +3700,17 @@ EmitSwitch(JSContext *cx, BytecodeEmitte
 out:
     if (table)
         cx->free_(table);
     if (ok) {
         ok = PopStatementBCE(cx, bce);
 
 #if JS_HAS_BLOCK_SCOPE
         if (ok && pn->pn_right->isKind(PNK_LEXICALSCOPE))
-            EMIT_UINT16_IMM_OP(JSOP_LEAVEBLOCK, count);
+            EMIT_UINT16_IMM_OP(JSOP_LEAVEBLOCK, blockCount);
 #endif
     }
     return ok;
 
 bad:
     ok = JS_FALSE;
     goto out;
 }
@@ -3767,16 +3786,31 @@ MaybeEmitVarDecl(JSContext *cx, Bytecode
             return false;
     }
 
     if (result)
         *result = atomIndex;
     return true;
 }
 
+/*
+ * This enum tells EmitVariables and the destructuring functions how emit the
+ * given Parser::variables parse tree. In the base case, DefineVars, the caller
+ * only wants variables to be defined in the prologue (if necessary). For
+ * PushInitialValues, variable initializer expressions are evaluated and left
+ * on the stack. For InitializeVars, the initializer expressions values are
+ * assigned (to local variables) and popped.
+ */
+enum VarEmitOption
+{
+    DefineVars        = 0,
+    PushInitialValues = 1,
+    InitializeVars    = 2
+};
+
 #if JS_HAS_DESTRUCTURING
 
 typedef JSBool
 (*DestructuringDeclEmitter)(JSContext *cx, BytecodeEmitter *bce, JSOp prologOp, ParseNode *pn);
 
 static JSBool
 EmitDestructuringDecl(JSContext *cx, BytecodeEmitter *bce, JSOp prologOp, ParseNode *pn)
 {
@@ -3812,33 +3846,66 @@ EmitDestructuringDecls(JSContext *cx, By
             if (!emitter(cx, bce, prologOp, pn3))
                 return JS_FALSE;
         }
     }
     return JS_TRUE;
 }
 
 static JSBool
-EmitDestructuringOpsHelper(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn);
-
+EmitDestructuringOpsHelper(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn,
+                           VarEmitOption emitOption);
+
+/*
+ * EmitDestructuringLHS assumes the to-be-destructured value has been pushed on
+ * the stack and emits code to destructure a single lhs expression (either a
+ * name or a compound []/{} expression).
+ *
+ * If emitOption is InitializeVars, the to-be-destructured value is assigned to
+ * locals and ultimately the initial slot is popped (-1 total depth change).
+ *
+ * If emitOption is PushInitialValues, the to-be-destructured value is replaced
+ * with the initial values of the N (where 0 <= N) variables assigned in the
+ * lhs expression. (Same post-condition as EmitDestructuringOpsHelper)
+ */
 static JSBool
-EmitDestructuringLHS(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
-{
+EmitDestructuringLHS(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn, VarEmitOption emitOption)
+{
+    JS_ASSERT(emitOption != DefineVars);
+
     /*
      * Now emit the lvalue opcode sequence.  If the lvalue is a nested
      * destructuring initialiser-form, call ourselves to handle it, then
      * pop the matched value.  Otherwise emit an lvalue bytecode sequence
      * ending with a JSOP_ENUMELEM or equivalent op.
      */
     if (pn->isKind(PNK_RB) || pn->isKind(PNK_RC)) {
-        if (!EmitDestructuringOpsHelper(cx, bce, pn))
+        if (!EmitDestructuringOpsHelper(cx, bce, pn, emitOption))
             return JS_FALSE;
-        if (Emit1(cx, bce, JSOP_POP) < 0)
-            return JS_FALSE;
+        if (emitOption == InitializeVars) {
+            /*
+             * Per its post-condition, EmitDestructuringOpsHelper has left the
+             * to-be-destructured value on top of the stack.
+             */
+            if (Emit1(cx, bce, JSOP_POP) < 0)
+                return JS_FALSE;
+        }
     } else {
+        if (emitOption == PushInitialValues) {
+            /*
+             * The lhs is a simple name so the to-be-destructured value is
+             * its initial value and there is nothing to do.
+             */
+            JS_ASSERT(pn->getOp() == JSOP_SETLOCAL);
+            JS_ASSERT(pn->pn_dflags & PND_BOUND);
+            return JS_TRUE;
+        }
+
+        /* All paths below must pop after assigning to the lhs. */
+
         if (pn->isKind(PNK_NAME)) {
             if (!BindNameToSlot(cx, bce, pn))
                 return JS_FALSE;
             if (pn->isConst() && !pn->isInitialized())
                 return Emit1(cx, bce, JSOP_POP) >= 0;
         }
 
         switch (pn->getOp()) {
@@ -3893,39 +3960,48 @@ EmitDestructuringLHS(JSContext *cx, Byte
         }
     }
 
     return JS_TRUE;
 }
 
 /*
  * Recursive helper for EmitDestructuringOps.
+ * EmitDestructuringOpsHelper assumes the to-be-destructured value has been
+ * pushed on the stack and emits code to destructure each part of a [] or {}
+ * lhs expression.
  *
- * Given a value to destructure on the stack, walk over an object or array
- * initialiser at pn, emitting bytecodes to match property values and store
- * them in the lvalues identified by the matched property names.
+ * If emitOption is InitializeVars, the initial to-be-destructured value is
+ * left untouched on the stack and the overall depth is not changed.
+ *
+ * If emitOption is PushInitialValues, the to-be-destructured value is replaced
+ * with the initial values of the N (where 0 <= N) variables assigned in the
+ * lhs expression. (Same post-condition as EmitDestructuringLHS)
  */
 static JSBool
-EmitDestructuringOpsHelper(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
-{
+EmitDestructuringOpsHelper(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn,
+                           VarEmitOption emitOption)
+{
+    JS_ASSERT(emitOption != DefineVars);
+
     jsuint index;
     ParseNode *pn2, *pn3;
     JSBool doElemOp;
 
 #ifdef DEBUG
     intN stackDepth = bce->stackDepth;
     JS_ASSERT(stackDepth != 0);
     JS_ASSERT(pn->isArity(PN_LIST));
     JS_ASSERT(pn->isKind(PNK_RB) || pn->isKind(PNK_RC));
 #endif
 
     if (pn->pn_count == 0) {
         /* Emit a DUP;POP sequence for the decompiler. */
-        return Emit1(cx, bce, JSOP_DUP) >= 0 &&
-               Emit1(cx, bce, JSOP_POP) >= 0;
+        if (Emit1(cx, bce, JSOP_DUP) < 0 || Emit1(cx, bce, JSOP_POP) < 0)
+            return JS_FALSE;
     }
 
     index = 0;
     for (pn2 = pn->pn_head; pn2; pn2 = pn2->pn_next) {
         /*
          * Duplicate the value being destructured to use as a reference base.
          * If dup is not the first one, annotate it for the decompiler.
          */
@@ -3971,34 +4047,67 @@ EmitDestructuringOpsHelper(JSContext *cx
         if (doElemOp) {
             /*
              * Ok, get the value of the matching property name.  This leaves
              * that value on top of the value being destructured, so the stack
              * is one deeper than when we started.
              */
             if (!EmitElemOpBase(cx, bce, JSOP_GETELEM))
                 return JS_FALSE;
-            JS_ASSERT(bce->stackDepth == stackDepth + 1);
+            JS_ASSERT(bce->stackDepth >= stackDepth + 1);
         }
 
         /* Nullary comma node makes a hole in the array destructurer. */
         if (pn3->isKind(PNK_COMMA) && pn3->isArity(PN_NULLARY)) {
             JS_ASSERT(pn->isKind(PNK_RB));
             JS_ASSERT(pn2 == pn3);
             if (Emit1(cx, bce, JSOP_POP) < 0)
                 return JS_FALSE;
         } else {
-            if (!EmitDestructuringLHS(cx, bce, pn3))
+            intN depthBefore = bce->stackDepth;
+            if (!EmitDestructuringLHS(cx, bce, pn3, emitOption))
                 return JS_FALSE;
-        }
-
-        JS_ASSERT(bce->stackDepth == stackDepth);
+
+            if (emitOption == PushInitialValues) {
+                /*
+                 * After '[x,y]' in 'let ([[x,y], z] = o)', the stack is
+                 *   | to-be-decompiled-value | x | y |
+                 * The goal is:
+                 *   | x | y | z |
+                 * so emit a pick to produce the intermediate state
+                 *   | x | y | to-be-decompiled-value |
+                 * before destructuring z. This gives the loop invariant that
+                 * the to-be-compiled-value is always on top of the stack.
+                 */
+                JS_ASSERT((bce->stackDepth - bce->stackDepth) >= -1);
+                uintN pickDistance = (uintN)((bce->stackDepth + 1) - depthBefore);
+                if (pickDistance > 0) {
+                    if (pickDistance > jsbytecode(-1)) {
+                        ReportCompileErrorNumber(cx, bce->tokenStream(), pn3, JSREPORT_ERROR,
+                                                 JSMSG_TOO_MANY_LOCALS);
+                        return JS_FALSE;
+                    }
+                    if (Emit2(cx, bce, JSOP_PICK, (jsbytecode)pickDistance) < 0)
+                        return false;
+                }
+            }
+        }
+
         ++index;
     }
 
+    if (emitOption == PushInitialValues) {
+        /*
+         * Per the above loop invariant, to-be-decompiled-value is at the top
+         * of the stack. To achieve the post-condition, pop it.
+         */
+        if (Emit1(cx, bce, JSOP_POP) < 0)
+            return JS_FALSE;
+    }
+
     return JS_TRUE;
 }
 
 static ptrdiff_t
 OpToDeclType(JSOp op)
 {
     switch (op) {
       case JSOP_NOP:
@@ -4007,33 +4116,106 @@ OpToDeclType(JSOp op)
         return SRC_DECL_CONST;
       case JSOP_DEFVAR:
         return SRC_DECL_VAR;
       default:
         return SRC_DECL_NONE;
     }
 }
 
+/*
+ * This utility accumulates a set of SRC_DESTRUCTLET notes which need to be
+ * backpatched with the offset from JSOP_DUP to JSOP_LET0.
+ *
+ * Also record whether the let head was a group assignment ([x,y] = [a,b])
+ * (which implies no SRC_DESTRUCTLET notes).
+ */
+class LetNotes
+{
+    struct Pair {
+        ptrdiff_t dup;
+        uintN index;
+        Pair(ptrdiff_t dup, uintN index) : dup(dup), index(index) {}
+    };
+    Vector<Pair> notes;
+    bool groupAssign;
+    DebugOnly<bool> updateCalled;
+
+  public:
+    LetNotes(JSContext *cx) : notes(cx), groupAssign(false), updateCalled(false) {}
+
+    ~LetNotes() {
+        JS_ASSERT_IF(!notes.allocPolicy().context()->isExceptionPending(), updateCalled);
+    }
+
+    void setGroupAssign() {
+        JS_ASSERT(notes.empty());
+        groupAssign = true;
+    }
+
+    bool isGroupAssign() const {
+        return groupAssign;
+    }
+
+    bool append(JSContext *cx, BytecodeEmitter *bce, ptrdiff_t dup, uintN index) {
+        JS_ASSERT(!groupAssign);
+        JS_ASSERT(SN_TYPE(bce->notes() + index) == SRC_DESTRUCTLET);
+        if (!notes.append(Pair(dup, index)))
+            return false;
+
+        /*
+         * Pessimistically inflate each srcnote. That way, there is no danger
+         * of inflation during update() (which would invalidate all indices).
+         */
+        if (!SetSrcNoteOffset(cx, bce, index, 0, SN_MAX_OFFSET))
+            return false;
+        JS_ASSERT(bce->notes()[index + 1] & SN_3BYTE_OFFSET_FLAG);
+        return true;
+    }
+
+    /* This should be called exactly once, right before JSOP_ENTERLET0. */
+    bool update(JSContext *cx, BytecodeEmitter *bce, ptrdiff_t offset) {
+        JS_ASSERT(!updateCalled);
+        for (size_t i = 0; i < notes.length(); ++i) {
+            JS_ASSERT(offset > notes[i].dup);
+            JS_ASSERT(*bce->code(notes[i].dup) == JSOP_DUP);
+            JS_ASSERT(bce->notes()[notes[i].index + 1] & SN_3BYTE_OFFSET_FLAG);
+            if (!SetSrcNoteOffset(cx, bce, notes[i].index, 0, offset - notes[i].dup))
+                return false;
+        }
+        updateCalled = true;
+        return true;
+    }
+};
+
 static JSBool
-EmitDestructuringOps(JSContext *cx, BytecodeEmitter *bce, JSOp prologOp, ParseNode *pn)
+EmitDestructuringOps(JSContext *cx, BytecodeEmitter *bce, ptrdiff_t declType, ParseNode *pn,
+                     LetNotes *letNotes = NULL)
 {
     /*
      * If we're called from a variable declaration, help the decompiler by
      * annotating the first JSOP_DUP that EmitDestructuringOpsHelper emits.
      * If the destructuring initialiser is empty, our helper will emit a
      * JSOP_DUP followed by a JSOP_POP for the decompiler.
      */
-    if (NewSrcNote2(cx, bce, SRC_DESTRUCT, OpToDeclType(prologOp)) < 0)
-        return JS_FALSE;
+    if (letNotes) {
+        ptrdiff_t index = NewSrcNote2(cx, bce, SRC_DESTRUCTLET, 0);
+        if (index < 0 || !letNotes->append(cx, bce, bce->offset(), (uintN)index))
+            return JS_FALSE;
+    } else {
+        if (NewSrcNote2(cx, bce, SRC_DESTRUCT, declType) < 0)
+            return JS_FALSE;
+    }
 
     /*
      * Call our recursive helper to emit the destructuring assignments and
      * related stack manipulations.
      */
-    return EmitDestructuringOpsHelper(cx, bce, pn);
+    VarEmitOption emitOption = letNotes ? PushInitialValues : InitializeVars;
+    return EmitDestructuringOpsHelper(cx, bce, pn, emitOption);
 }
 
 static JSBool
 EmitGroupAssignment(JSContext *cx, BytecodeEmitter *bce, JSOp prologOp,
                     ParseNode *lhs, ParseNode *rhs)
 {
     jsuint depth, limit, i, nslots;
     ParseNode *pn;
@@ -4064,17 +4246,17 @@ EmitGroupAssignment(JSContext *cx, Bytec
         if (slot < 0)
             return JS_FALSE;
         EMIT_UINT16_IMM_OP(JSOP_GETLOCAL, slot);
 
         if (pn->isKind(PNK_COMMA) && pn->isArity(PN_NULLARY)) {
             if (Emit1(cx, bce, JSOP_POP) < 0)
                 return JS_FALSE;
         } else {
-            if (!EmitDestructuringLHS(cx, bce, pn))
+            if (!EmitDestructuringLHS(cx, bce, pn, InitializeVars))
                 return JS_FALSE;
         }
     }
 
     nslots = limit - depth;
     EMIT_UINT16_IMM_OP(JSOP_POPN, nslots);
     bce->stackDepth = (uintN) depth;
     return JS_TRUE;
@@ -4100,80 +4282,101 @@ MaybeEmitGroupAssignment(JSContext *cx, 
         lhs->pn_count <= rhs->pn_count) {
         if (!EmitGroupAssignment(cx, bce, prologOp, lhs, rhs))
             return JS_FALSE;
         *pop = JSOP_NOP;
     }
     return JS_TRUE;
 }
 
+/*
+ * Like MaybeEmitGroupAssignment, but for 'let ([x,y] = [a,b]) ...'.
+ *
+ * Instead of issuing a sequence |dup|eval-rhs|set-lhs|pop| (which doesn't work
+ * since the bound vars don't yet have slots), just eval/push each rhs element
+ * just like what EmitLet would do for 'let (x = a, y = b) ...'. While shorter,
+ * simpler and more efficient than MaybeEmitGroupAssignment, it is harder to
+ * decompile so we restrict the ourselves to cases where the lhs and rhs are in
+ * 1:1 correspondence and lhs elements are simple names.
+ */
+static bool
+MaybeEmitLetGroupDecl(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn,
+                      LetNotes *letNotes, JSOp *pop)
+{
+    JS_ASSERT(pn->isKind(PNK_ASSIGN));
+    JS_ASSERT(pn->isOp(JSOP_NOP));
+    JS_ASSERT(*pop == JSOP_POP || *pop == JSOP_POPV);
+
+    ParseNode *lhs = pn->pn_left;
+    ParseNode *rhs = pn->pn_right;
+    if (lhs->isKind(PNK_RB) && rhs->isKind(PNK_RB) &&
+        !(rhs->pn_xflags & PNX_HOLEY) &&
+        !(lhs->pn_xflags & PNX_HOLEY) &&
+        lhs->pn_count == rhs->pn_count)
+    {
+        for (ParseNode *l = lhs->pn_head; l; l = l->pn_next) {
+            if (l->getOp() != JSOP_SETLOCAL)
+                return true;
+        }
+
+        for (ParseNode *r = rhs->pn_head; r; r = r->pn_next) {
+            if (!EmitTree(cx, bce, r))
+                return false;
+        }
+
+        letNotes->setGroupAssign();
+        *pop = JSOP_NOP;
+    }
+    return true;
+}
+
 #endif /* JS_HAS_DESTRUCTURING */
 
 static JSBool
-EmitVariables(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn, JSBool inLetHead,
-              ptrdiff_t *headNoteIndex)
-{
-    bool forInVar, first;
-    ptrdiff_t off, noteIndex, tmp;
-    ParseNode *pn2, *pn3, *next;
-    JSOp op;
-    jsatomid atomIndex;
-    uintN oldflags;
-
-    /* Default in case of JS_HAS_BLOCK_SCOPE early return, below. */
-    *headNoteIndex = -1;
-
-    /*
-     * Let blocks and expressions have a parenthesized head in which the new
-     * scope is not yet open. Initializer evaluation uses the parent node's
-     * lexical scope. If popScope is true below, then we hide the top lexical
-     * block from any calls to BindNameToSlot hiding in pn2->pn_expr so that
-     * it won't find any names in the new let block.
-     *
-     * The same goes for let declarations in the head of any kind of for loop.
-     * Unlike a let declaration 'let x = i' within a block, where x is hoisted
-     * to the start of the block, a 'for (let x = i...) ...' loop evaluates i
-     * in the containing scope, and puts x in the loop body's scope.
-     */
-    DebugOnly<bool> let = (pn->isOp(JSOP_NOP));
-    forInVar = (pn->pn_xflags & PNX_FORINVAR) != 0;
-
-    off = noteIndex = -1;
-    for (pn2 = pn->pn_head; ; pn2 = next) {
-        first = pn2 == pn->pn_head;
+EmitVariables(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn, VarEmitOption emitOption,
+              LetNotes *letNotes = NULL)
+{
+    JS_ASSERT(pn->isArity(PN_LIST));
+    JS_ASSERT(!!letNotes == (emitOption == PushInitialValues));
+
+    ptrdiff_t off = -1, noteIndex = -1;
+    ParseNode *next;
+    for (ParseNode *pn2 = pn->pn_head; ; pn2 = next) {
+        bool first = pn2 == pn->pn_head;
         next = pn2->pn_next;
 
+        ParseNode *pn3;
         if (!pn2->isKind(PNK_NAME)) {
 #if JS_HAS_DESTRUCTURING
             if (pn2->isKind(PNK_RB) || pn2->isKind(PNK_RC)) {
                 /*
                  * Emit variable binding ops, but not destructuring ops.  The
                  * parser (see Parser::variables) has ensured that our caller
                  * will be the PNK_FOR/PNK_FORIN case in EmitTree, and that
                  * case will emit the destructuring code only after emitting an
                  * enumerating opcode and a branch that tests whether the
                  * enumeration ended.
                  */
-                JS_ASSERT(forInVar);
+                JS_ASSERT(emitOption == DefineVars);
                 JS_ASSERT(pn->pn_count == 1);
                 if (!EmitDestructuringDecls(cx, bce, pn->getOp(), pn2))
                     return JS_FALSE;
                 break;
             }
 #endif
 
             /*
              * A destructuring initialiser assignment preceded by var will
              * never occur to the left of 'in' in a for-in loop.  As with 'for
              * (var x = i in o)...', this will cause the entire 'var [a, b] =
              * i' to be hoisted out of the loop.
              */
             JS_ASSERT(pn2->isKind(PNK_ASSIGN));
             JS_ASSERT(pn2->isOp(JSOP_NOP));
-            JS_ASSERT(!forInVar);
+            JS_ASSERT(emitOption != DefineVars);
 
             /*
              * To allow the front end to rewrite var f = x; as f = x; when a
              * function f(){} precedes the var, detect simple name assignment
              * here and initialize the name.
              */
 #if !JS_HAS_DESTRUCTURING
             JS_ASSERT(pn2->pn_left->isKind(PNK_NAME));
@@ -4182,52 +4385,66 @@ EmitVariables(JSContext *cx, BytecodeEmi
 #endif
             {
                 pn3 = pn2->pn_right;
                 pn2 = pn2->pn_left;
                 goto do_name;
             }
 
 #if JS_HAS_DESTRUCTURING
+            ptrdiff_t stackDepthBefore = bce->stackDepth;
+            JSOp op = JSOP_POP;
             if (pn->pn_count == 1) {
                 /*
                  * If this is the only destructuring assignment in the list,
                  * try to optimize to a group assignment.  If we're in a let
                  * head, pass JSOP_POP rather than the pseudo-prolog JSOP_NOP
                  * in pn->pn_op, to suppress a second (and misplaced) 'let'.
                  */
                 JS_ASSERT(noteIndex < 0 && !pn2->pn_next);
-                op = JSOP_POP;
-                if (!MaybeEmitGroupAssignment(cx, bce,
-                                              inLetHead ? JSOP_POP : pn->getOp(),
-                                              pn2, &op)) {
-                    return JS_FALSE;
-                }
-                if (op == JSOP_NOP) {
-                    pn->pn_xflags = (pn->pn_xflags & ~PNX_POPVAR) | PNX_GROUPINIT;
-                    break;
+                if (letNotes) {
+                    if (!MaybeEmitLetGroupDecl(cx, bce, pn2, letNotes, &op))
+                        return JS_FALSE;
+                } else {
+                    if (!MaybeEmitGroupAssignment(cx, bce, pn->getOp(), pn2, &op))
+                        return JS_FALSE;
                 }
             }
-
-            pn3 = pn2->pn_left;
-            if (!EmitDestructuringDecls(cx, bce, pn->getOp(), pn3))
-                return JS_FALSE;
-
-            if (!EmitTree(cx, bce, pn2->pn_right))
-                return JS_FALSE;
-
-            /*
-             * Veto pn->pn_op if inLetHead to avoid emitting a SRC_DESTRUCT
-             * that's redundant with respect to the SRC_DECL/SRC_DECL_LET that
-             * we will emit at the bottom of this function.
-             */
-            if (!EmitDestructuringOps(cx, bce,
-                                      inLetHead ? JSOP_POP : pn->getOp(),
-                                      pn3)) {
-                return JS_FALSE;
+            if (op == JSOP_NOP) {
+                pn->pn_xflags = (pn->pn_xflags & ~PNX_POPVAR) | PNX_GROUPINIT;
+            } else {
+                pn3 = pn2->pn_left;
+                if (!EmitDestructuringDecls(cx, bce, pn->getOp(), pn3))
+                    return JS_FALSE;
+
+                if (!EmitTree(cx, bce, pn2->pn_right))
+                    return JS_FALSE;
+
+                /* Only the first list element should print 'let' or 'var'. */
+                ptrdiff_t declType = pn2 == pn->pn_head
+                                     ? OpToDeclType(pn->getOp())
+                                     : SRC_DECL_NONE;
+
+                if (!EmitDestructuringOps(cx, bce, declType, pn3, letNotes))
+                    return JS_FALSE;
+            }
+            ptrdiff_t stackDepthAfter = bce->stackDepth;
+
+            /* Give let ([] = x) a slot (see CheckDestructuring). */
+            JS_ASSERT(stackDepthBefore <= stackDepthAfter);
+            if (letNotes && stackDepthBefore == stackDepthAfter) {
+                if (Emit1(cx, bce, JSOP_UNDEFINED) < 0)
+                    return JS_FALSE;
+            }
+
+            /* If we are not initializing, nothing to pop. */
+            if (emitOption != InitializeVars) {
+                if (next)
+                    continue;
+                break;
             }
             goto emit_note_pop;
 #endif
         }
 
         /*
          * Load initializer early to share code above that jumps to do_name.
          * NB: if this var redeclares an existing binding, then pn2 is linked
@@ -4235,114 +4452,103 @@ EmitVariables(JSContext *cx, BytecodeEmi
          * pn_lexdef.
          */
         pn3 = pn2->maybeExpr();
 
      do_name:
         if (!BindNameToSlot(cx, bce, pn2))
             return JS_FALSE;
 
+        JSOp op;
+        jsatomid atomIndex;
+
         op = pn2->getOp();
         if (op == JSOP_ARGUMENTS) {
             /* JSOP_ARGUMENTS => no initializer */
-            JS_ASSERT(!pn3 && !let);
+            JS_ASSERT(!pn3 && !letNotes);
             pn3 = NULL;
-#ifdef __GNUC__
-            atomIndex = 0;            /* quell GCC overwarning */
-#endif
+            atomIndex = 0;
         } else {
             JS_ASSERT(op != JSOP_CALLEE);
-            JS_ASSERT(!pn2->pn_cookie.isFree() || !let);
+            JS_ASSERT(!pn2->pn_cookie.isFree() || !pn->isOp(JSOP_NOP));
             if (!MaybeEmitVarDecl(cx, bce, pn->getOp(), pn2, &atomIndex))
                 return JS_FALSE;
 
             if (pn3) {
-                JS_ASSERT(!forInVar);
-                if (op == JSOP_SETNAME) {
-                    JS_ASSERT(!let);
+                JS_ASSERT(emitOption != DefineVars);
+                JS_ASSERT_IF(emitOption == PushInitialValues, op == JSOP_SETLOCAL);
+                if (op == JSOP_SETNAME)
                     EMIT_INDEX_OP(JSOP_BINDNAME, atomIndex);
-                } else if (op == JSOP_SETGNAME) {
-                    JS_ASSERT(!let);
+                else if (op == JSOP_SETGNAME)
                     EMIT_INDEX_OP(JSOP_BINDGNAME, atomIndex);
-                }
                 if (pn->isOp(JSOP_DEFCONST) &&
                     !DefineCompileTimeConstant(cx, bce, pn2->pn_atom, pn3))
                 {
                     return JS_FALSE;
                 }
 
-                oldflags = bce->flags;
+                uintN oldflags = bce->flags;
                 bce->flags &= ~TCF_IN_FOR_INIT;
                 if (!EmitTree(cx, bce, pn3))
                     return JS_FALSE;
                 bce->flags |= oldflags & TCF_IN_FOR_INIT;
+            } else if (letNotes) {
+                /* JSOP_ENTERLETx expects at least 1 slot to have been pushed. */
+                if (Emit1(cx, bce, JSOP_UNDEFINED) < 0)
+                    return JS_FALSE;
             }
         }
 
-        /*
-         * The parser rewrites 'for (var x = i in o)' to hoist 'var x = i' --
-         * likewise 'for (let x = i in o)' becomes 'i; for (let x in o)' using
-         * a PNK_SEQ node to make the two statements appear as one. Therefore
-         * if this declaration is part of a for-in loop head, we do not need to
-         * emit op or any source note. Our caller, the PNK_FOR/PNK_IN case in
-         * EmitTree, will annotate appropriately.
-         */
+        /* If we are not initializing, nothing to pop. */
+        if (emitOption != InitializeVars) {
+            if (next)
+                continue;
+            break;
+        }
+
         JS_ASSERT_IF(pn2->isDefn(), pn3 == pn2->pn_expr);
-        if (forInVar) {
-            JS_ASSERT(pn->pn_count == 1);
-            JS_ASSERT(!pn3);
-            break;
-        }
-
-        if (first &&
-            !inLetHead &&
-            NewSrcNote2(cx, bce, SRC_DECL,
-                        (pn->isOp(JSOP_DEFCONST))
-                        ? SRC_DECL_CONST
-                        : (pn->isOp(JSOP_DEFVAR))
-                        ? SRC_DECL_VAR
-                        : SRC_DECL_LET) < 0)
-        {
+        if (first && NewSrcNote2(cx, bce, SRC_DECL,
+                                 (pn->isOp(JSOP_DEFCONST))
+                                 ? SRC_DECL_CONST
+                                 : (pn->isOp(JSOP_DEFVAR))
+                                 ? SRC_DECL_VAR
+                                 : SRC_DECL_LET) < 0) {
             return JS_FALSE;
         }
         if (op == JSOP_ARGUMENTS) {
             if (Emit1(cx, bce, op) < 0)
                 return JS_FALSE;
         } else if (!pn2->pn_cookie.isFree()) {
             EMIT_UINT16_IMM_OP(op, atomIndex);
         } else {
             EMIT_INDEX_OP(op, atomIndex);
         }
 
 #if JS_HAS_DESTRUCTURING
     emit_note_pop:
 #endif
-        tmp = bce->offset();
+        ptrdiff_t tmp = bce->offset();
         if (noteIndex >= 0) {
             if (!SetSrcNoteOffset(cx, bce, (uintN)noteIndex, 0, tmp-off))
                 return JS_FALSE;
         }
         if (!next)
             break;
         off = tmp;
         noteIndex = NewSrcNote2(cx, bce, SRC_PCDELTA, 0);
         if (noteIndex < 0 || Emit1(cx, bce, JSOP_POP) < 0)
             return JS_FALSE;
     }
 
-    /* If this is a let head, emit and return a srcnote on the pop. */
-    if (inLetHead) {
-        *headNoteIndex = NewSrcNote(cx, bce, SRC_DECL);
-        if (*headNoteIndex < 0)
+    if (pn->pn_xflags & PNX_POPVAR) {
+        if (Emit1(cx, bce, JSOP_POP) < 0)
             return JS_FALSE;
-        if (!(pn->pn_xflags & PNX_POPVAR))
-            return Emit1(cx, bce, JSOP_NOP) >= 0;
-    }
-
-    return !(pn->pn_xflags & PNX_POPVAR) || Emit1(cx, bce, JSOP_POP) >= 0;
+    }
+
+    return JS_TRUE;
 }
 
 static bool
 EmitAssignment(JSContext *cx, BytecodeEmitter *bce, ParseNode *lhs, JSOp op, ParseNode *rhs)
 {
     ptrdiff_t top = bce->offset();
 
     /*
@@ -4513,34 +4719,34 @@ EmitAssignment(JSContext *cx, BytecodeEm
       case PNK_LB:
       case PNK_LP:
         if (Emit1(cx, bce, JSOP_SETELEM) < 0)
             return false;
         break;
 #if JS_HAS_DESTRUCTURING
       case PNK_RB:
       case PNK_RC:
-        if (!EmitDestructuringOps(cx, bce, JSOP_SETNAME, lhs))
+        if (!EmitDestructuringOps(cx, bce, SRC_DECL_NONE, lhs))
             return false;
         break;
 #endif
 #if JS_HAS_XML_SUPPORT
       case PNK_XMLUNARY:
         JS_ASSERT(!bce->inStrictMode());
         if (Emit1(cx, bce, JSOP_SETXMLNAME) < 0)
             return false;
         break;
 #endif
       default:
         JS_ASSERT(0);
     }
     return true;
 }
 
-#if defined DEBUG_brendan || defined DEBUG_mrbkap
+#ifdef DEBUG
 static JSBool
 GettableNoteForNextOp(BytecodeEmitter *bce)
 {
     ptrdiff_t offset, target;
     jssrcnote *sn, *end;
 
     offset = 0;
     target = bce->offset();
@@ -4731,17 +4937,17 @@ EmitCatch(JSContext *cx, BytecodeEmitter
     if (pn->pn_kid2 && Emit1(cx, bce, JSOP_DUP) < 0)
         return false;
 
     ParseNode *pn2 = pn->pn_kid1;
     switch (pn2->getKind()) {
 #if JS_HAS_DESTRUCTURING
       case PNK_RB:
       case PNK_RC:
-        if (!EmitDestructuringOps(cx, bce, JSOP_NOP, pn2))
+        if (!EmitDestructuringOps(cx, bce, SRC_DECL_NONE, pn2))
             return false;
         if (Emit1(cx, bce, JSOP_POP) < 0)
             return false;
         break;
 #endif
 
       case PNK_NAME:
         /* Inline and specialize BindNameToSlot for pn2. */
@@ -5101,53 +5307,119 @@ EmitIf(JSContext *cx, BytecodeEmitter *b
     } else {
         /* No else part, fixup the branch-if-false to come here. */
         CHECK_AND_SET_JUMP_OFFSET_AT(cx, bce, beq);
     }
     return PopStatementBCE(cx, bce);
 }
 
 #if JS_HAS_BLOCK_SCOPE
+/*
+ * pnLet represents one of:
+ *
+ *   let-expression:   (let (x = y) EXPR)
+ *   let-statement:    let (x = y) { ... }
+ *
+ * For a let-expression 'let (x = a, [y,z] = b) e', EmitLet produces:
+ *
+ *  bytecode          stackDepth  srcnotes
+ *  evaluate a        +1
+ *  evaluate b        +1
+ *  dup               +1          SRC_DESTRUCTLET + offset to enterlet0
+ *  destructure y
+ *  pick 1
+ *  dup               +1          SRC_DESTRUCTLET + offset to enterlet0
+ *  pick
+ *  destructure z
+ *  pick 1
+ *  pop               -1
+ *  enterlet0                     SRC_DECL + offset to leaveblockexpr
+ *  evaluate e        +1
+ *  leaveblockexpr    -3          SRC_PCBASE + offset to evaluate a
+ *
+ * Note that, since enterlet0 simply changes fp->blockChain and does not
+ * otherwise touch the stack, evaluation of the let-var initializers must leave
+ * the initial value in the let-var's future slot.
+ *
+ * The SRC_DESTRUCTLET distinguish JSOP_DUP as the beginning of a destructuring
+ * let initialization and the offset allows the decompiler to find the block
+ * object from which to find let var names. These forward offsets require
+ * backpatching, which is handled by LetNotes.
+ *
+ * The SRC_DECL offset allows recursive decompilation of 'e'.
+ *
+ * The SRC_PCBASE allows js_DecompileValueGenerator to walk backwards from
+ * JSOP_LEAVEBLOCKEXPR to the beginning of the let and is only needed for
+ * let-expressions.
+ */
 static bool
-EmitLet(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
-{
-    /*
-     * pn represents one of these syntactic constructs:
-     *   let-expression:                        (let (x = y) EXPR)
-     *   let-statement:                         let (x = y) { ... }
-     *   let-declaration in statement context:  let x = y;
-     *   let-declaration in for-loop head:      for (let ...) ...
-     *
-     * Let-expressions and let-statements are represented as binary nodes
-     * with their variable declarations on the left and the body on the
-     * right.
-     */
-    ParseNode *pn2;
-    if (pn->isArity(PN_BINARY)) {
-        pn2 = pn->pn_right;
-        pn = pn->pn_left;
-    } else {
-        pn2 = NULL;
-    }
-
-    /* Non-null pn2 means that pn is the variable list from a let head. */
-    JS_ASSERT(pn->isArity(PN_LIST));
-    ptrdiff_t noteIndex;
-    if (!EmitVariables(cx, bce, pn, pn2 != NULL, &noteIndex))
-        return false;
-    ptrdiff_t tmp = bce->offset();
-
-    /* Thus non-null pn2 is the body of the let block or expression. */
-    if (pn2 && !EmitTree(cx, bce, pn2))
-        return false;
-
-    if (noteIndex >= 0 && !SetSrcNoteOffset(cx, bce, (uintN)noteIndex, 0, bce->offset() - tmp))
-        return false;
-
-    return true;
+EmitLet(JSContext *cx, BytecodeEmitter *bce, ParseNode *pnLet)
+{
+    JS_ASSERT(pnLet->isArity(PN_BINARY));
+    ParseNode *varList = pnLet->pn_left;
+    JS_ASSERT(varList->isArity(PN_LIST));
+    ParseNode *letBody = pnLet->pn_right;
+    JS_ASSERT(letBody->isLet() && letBody->isKind(PNK_LEXICALSCOPE));
+    JSObject *blockObj = letBody->pn_objbox->object;
+    JS_ASSERT(blockObj->isStaticBlock());
+
+    ptrdiff_t letHeadOffset = bce->offset();
+    intN letHeadDepth = bce->stackDepth;
+
+    LetNotes letNotes(cx);
+    if (!EmitVariables(cx, bce, varList, PushInitialValues, &letNotes))
+        return false;
+
+    /* Push storage for hoisted let decls (e.g. 'let (x) { let y }'). */
+    uint32_t alreadyPushed = uintN(bce->stackDepth - letHeadDepth);
+    uint32_t blockObjCount = OBJ_BLOCK_COUNT(cx, blockObj);
+    for (uint32_t i = alreadyPushed; i < blockObjCount; ++i) {
+        /* Tell the decompiler not to print the decl in the let head. */
+        if (NewSrcNote(cx, bce, SRC_CONTINUE) < 0)
+            return false;
+        if (Emit1(cx, bce, JSOP_UNDEFINED) < 0)
+            return false;
+    }
+
+    StmtInfo stmtInfo;
+    PushBlockScope(bce, &stmtInfo, blockObj, bce->offset());
+
+    if (!letNotes.update(cx, bce, bce->offset()))
+        return false;
+
+    ptrdiff_t declNote = NewSrcNote(cx, bce, SRC_DECL);
+    if (declNote < 0)
+        return false;
+
+    ptrdiff_t bodyBegin = bce->offset();
+    if (!EmitEnterBlock(cx, bce, letBody, JSOP_ENTERLET0))
+        return false;
+
+    if (!EmitTree(cx, bce, letBody->pn_expr))
+        return false;
+
+    JSOp leaveOp = letBody->getOp();
+    if (leaveOp == JSOP_LEAVEBLOCKEXPR) {
+        if (NewSrcNote2(cx, bce, SRC_PCBASE, bce->offset() - letHeadOffset) < 0)
+            return false;
+    }
+
+    JS_ASSERT(leaveOp == JSOP_LEAVEBLOCK || leaveOp == JSOP_LEAVEBLOCKEXPR);
+    EMIT_UINT16_IMM_OP(leaveOp, OBJ_BLOCK_COUNT(cx, blockObj));
+
+    ptrdiff_t bodyEnd = bce->offset();
+    JS_ASSERT(bodyEnd > bodyBegin);
+
+    if (!PopStatementBCE(cx, bce))
+        return false;
+
+    ptrdiff_t o = PackLetData((bodyEnd - bodyBegin) -
+                              (JSOP_ENTERLET0_LENGTH + JSOP_LEAVEBLOCK_LENGTH),
+                              letNotes.isGroupAssign());
+    return SetSrcNoteOffset(cx, bce, declNote, 0, o);
 }
 #endif
 
 #if JS_HAS_XML_SUPPORT
 static bool
 EmitXMLTag(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
 {
     JS_ASSERT(!bce->inStrictMode());
@@ -5219,66 +5491,62 @@ EmitXMLProcessingInstruction(JSContext *
         return false;
     return true;
 }
 #endif
 
 static bool
 EmitLexicalScope(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
 {
+    JS_ASSERT(pn->isKind(PNK_LEXICALSCOPE));
+    JS_ASSERT(pn->getOp() == JSOP_LEAVEBLOCK);
+
     StmtInfo stmtInfo;
-    StmtInfo *stmt;
     ObjectBox *objbox = pn->pn_objbox;
-    PushBlockScope(bce, &stmtInfo, objbox->object, bce->offset());
+    JSObject *blockObj = objbox->object;
+    JS_ASSERT(blockObj->isStaticBlock());
+    PushBlockScope(bce, &stmtInfo, blockObj, bce->offset());
 
     /*
-     * If this lexical scope is not for a catch block, let block or let
-     * expression, or any kind of for loop (where the scope starts in the
-     * head after the first part if for (;;), else in the body if for-in);
-     * and if our container is top-level but not a function body, or else
-     * a block statement; then emit a SRC_BRACE note.  All other container
-     * statements get braces by default from the decompiler.
+     * For compound statements (i.e. { stmt-list }), the decompiler does not
+     * emit curlies by default. However, if this stmt-list contains a let
+     * declaration, this is semantically invalid so we need to add a srcnote to
+     * enterblock to tell the decompiler to add curlies. This condition
+     * shouldn't be so complicated; try to find a simpler condition.
      */
     ptrdiff_t noteIndex = -1;
-    ParseNodeKind kind = pn->expr()->getKind();
-    if (kind != PNK_CATCH && kind != PNK_LET && kind != PNK_FOR &&
-        (!(stmt = stmtInfo.down)
-         ? !bce->inFunction()
-         : stmt->type == STMT_BLOCK))
+    if (pn->expr()->getKind() != PNK_FOR &&
+        pn->expr()->getKind() != PNK_CATCH &&
+        (stmtInfo.down
+         ? stmtInfo.down->type == STMT_BLOCK &&
+           (!stmtInfo.down->down || stmtInfo.down->down->type != STMT_FOR_IN_LOOP)
+         : !bce->inFunction()))
     {
-#if defined DEBUG_brendan || defined DEBUG_mrbkap
         /* There must be no source note already output for the next op. */
         JS_ASSERT(bce->noteCount() == 0 ||
                   bce->lastNoteOffset() != bce->offset() ||
                   !GettableNoteForNextOp(bce));
-#endif
         noteIndex = NewSrcNote2(cx, bce, SRC_BRACE, 0);
         if (noteIndex < 0)
             return false;
     }
 
-    ptrdiff_t top = bce->offset();
-    if (!EmitEnterBlock(cx, pn, bce))
+    ptrdiff_t bodyBegin = bce->offset();
+    if (!EmitEnterBlock(cx, bce, pn, JSOP_ENTERBLOCK))
         return false;
 
     if (!EmitTree(cx, bce, pn->pn_expr))
         return false;
 
-    JSOp op = pn->getOp();
-    if (op == JSOP_LEAVEBLOCKEXPR) {
-        if (NewSrcNote2(cx, bce, SRC_PCBASE, bce->offset() - top) < 0)
-            return false;
-    } else {
-        if (noteIndex >= 0 && !SetSrcNoteOffset(cx, bce, (uintN)noteIndex, 0, bce->offset() - top))
-            return false;
-    }
-
-    /* Emit the JSOP_LEAVEBLOCK or JSOP_LEAVEBLOCKEXPR opcode. */
-    uintN count = OBJ_BLOCK_COUNT(cx, objbox->object);
-    EMIT_UINT16_IMM_OP(op, count);
+    if (noteIndex >= 0) {
+        if (!SetSrcNoteOffset(cx, bce, (uintN)noteIndex, 0, bce->offset() - bodyBegin))
+            return false;
+    }
+
+    EMIT_UINT16_IMM_OP(JSOP_LEAVEBLOCK, OBJ_BLOCK_COUNT(cx, blockObj));
 
     return PopStatementBCE(cx, bce);
 }
 
 static bool
 EmitWith(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
 {
     StmtInfo stmtInfo;
@@ -5333,27 +5601,58 @@ static bool
 EmitForIn(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn, ptrdiff_t top)
 {
     StmtInfo stmtInfo;
     PushStatement(bce, &stmtInfo, STMT_FOR_IN_LOOP, top);
 
     ParseNode *forHead = pn->pn_left;
     ParseNode *forBody = pn->pn_right;
 
+    ParseNode *pn1 = forHead->pn_kid1;
+    bool letDecl = pn1 && pn1->isKind(PNK_LEXICALSCOPE);
+    JS_ASSERT_IF(letDecl, pn1->isLet());
+
+    JSObject *blockObj = letDecl ? pn1->pn_objbox->object : NULL;
+    uint32_t blockObjCount = blockObj ? OBJ_BLOCK_COUNT(cx, blockObj) : 0;
+
+    if (letDecl) {
+        /*
+         * The let's slot(s) will be under the iterator, but the block must not
+         * be entered (i.e. fp->blockChain set) until after evaluating the rhs.
+         * Thus, push to reserve space and enterblock after. The same argument
+         * applies when leaving the loop. Thus, a for-let-in loop looks like:
+         *
+         *   push x N
+         *   eval rhs
+         *   iter
+         *   enterlet1
+         *   goto
+         *     ... loop body
+         *   ifne
+         *   leaveforinlet
+         *   enditer
+         *   popn(N)
+         */
+        for (uint32_t i = 0; i < blockObjCount; ++i) {
+            if (Emit1(cx, bce, JSOP_UNDEFINED) < 0)
+                return false;
+        }
+    }
+
     /*
      * If the left part is 'var x', emit code to define x if necessary
-     * using a prolog opcode, but do not emit a pop. If the left part
-     * was originally 'var x = i', the parser will have rewritten it;
-     * see Parser::forStatement. 'for (let x = i in o)' is mercifully
-     * banned.
+     * using a prolog opcode, but do not emit a pop. If the left part was
+     * originally 'var x = i', the parser will have rewritten it; see
+     * Parser::forStatement. 'for (let x = i in o)' is mercifully banned.
      */
-    if (ParseNode *decl = forHead->pn_kid1) {
+    if (pn1) {
+        ParseNode *decl = letDecl ? pn1->pn_expr : pn1;
         JS_ASSERT(decl->isKind(PNK_VAR) || decl->isKind(PNK_LET));
         bce->flags |= TCF_IN_FOR_INIT;
-        if (!EmitTree(cx, bce, decl))
+        if (!EmitVariables(cx, bce, decl, DefineVars))
             return false;
         bce->flags &= ~TCF_IN_FOR_INIT;
     }
 
     /* Compile the object expression to the right of 'in'. */
     if (!EmitTree(cx, bce, forHead->pn_kid3))
         return JS_FALSE;
 
@@ -5361,16 +5660,25 @@ EmitForIn(JSContext *cx, BytecodeEmitter
      * Emit a bytecode to convert top of stack value to the iterator
      * object depending on the loop variant (for-in, for-each-in, or
      * destructuring for-in).
      */
     JS_ASSERT(pn->isOp(JSOP_ITER));
     if (Emit2(cx, bce, JSOP_ITER, (uint8_t) pn->pn_iflags) < 0)
         return false;
 
+    /* Enter the block before the loop body, after evaluating the obj. */
+    StmtInfo letStmt;
+    if (letDecl) {
+        PushBlockScope(bce, &letStmt, blockObj, bce->offset());
+        letStmt.flags |= SIF_FOR_BLOCK;
+        if (!EmitEnterBlock(cx, bce, pn1, JSOP_ENTERLET1))
+            return false;
+    }
+
     /* Annotate so the decompiler can find the loop-closing jump. */
     intN noteIndex = NewSrcNote(cx, bce, SRC_FOR_IN);
     if (noteIndex < 0)
         return false;
 
     /*
      * Jump down to the loop condition to minimize overhead assuming at
      * least one iteration, as the other loop forms do.
@@ -5391,16 +5699,17 @@ EmitForIn(JSContext *cx, BytecodeEmitter
     /*
      * Emit code to get the next enumeration value and assign it to the
      * left hand side. The JSOP_POP after this assignment is annotated
      * so that the decompiler can distinguish 'for (x in y)' from
      * 'for (var x in y)'.
      */
     if (!EmitAssignment(cx, bce, forHead->pn_kid2, JSOP_NOP, NULL))
         return false;
+
     ptrdiff_t tmp2 = bce->offset();
     if (forHead->pn_kid1 && NewSrcNote2(cx, bce, SRC_DECL,
                                         (forHead->pn_kid1->isOp(JSOP_DEFVAR))
                                         ? SRC_DECL_VAR
                                         : SRC_DECL_LET) < 0) {
         return false;
     }
     if (Emit1(cx, bce, JSOP_POP) < 0)
@@ -5431,24 +5740,40 @@ EmitForIn(JSContext *cx, BytecodeEmitter
 
     /* Set the first srcnote offset so we can find the start of the loop body. */
     if (!SetSrcNoteOffset(cx, bce, (uintN)noteIndex, 0, tmp2 - jmp))
         return false;
     /* Set the second srcnote offset so we can find the closing jump. */
     if (!SetSrcNoteOffset(cx, bce, (uintN)noteIndex, 1, beq - jmp))
         return false;
 
-    /* Now fixup all breaks and continues (before the JSOP_ENDITER). */
+    /* Fixup breaks and continues before JSOP_ITER (and JSOP_LEAVEFORINLET). */
     if (!PopStatementBCE(cx, bce))
         return false;
 
+    if (letDecl) {
+        if (!PopStatementBCE(cx, bce))
+            return false;
+        if (Emit1(cx, bce, JSOP_LEAVEFORLETIN) < 0)
+            return false;
+    }
+
     if (!NewTryNote(cx, bce, JSTRY_ITER, bce->stackDepth, top, bce->offset()))
         return false;
-
-    return Emit1(cx, bce, JSOP_ENDITER) >= 0;
+    if (Emit1(cx, bce, JSOP_ENDITER) < 0)
+        return false;
+
+    if (letDecl) {
+        /* Tell the decompiler to pop but not to print. */
+        if (NewSrcNote(cx, bce, SRC_CONTINUE) < 0)
+            return false;
+        EMIT_UINT16_IMM_OP(JSOP_POPN, blockObjCount);
+    }
+
+    return true;
 }
 
 static bool
 EmitNormalFor(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn, ptrdiff_t top)
 {
     StmtInfo stmtInfo;
     PushStatement(bce, &stmtInfo, STMT_FOR_LOOP, top);
 
@@ -5475,17 +5800,17 @@ EmitNormalFor(JSContext *cx, BytecodeEmi
                 return false;
             if (pn3->isKind(PNK_VAR) || pn3->isKind(PNK_CONST) || pn3->isKind(PNK_LET)) {
                 /*
                  * Check whether a destructuring-initialized var decl
                  * was optimized to a group assignment.  If so, we do
                  * not need to emit a pop below, so switch to a nop,
                  * just for the decompiler.
                  */
-                JS_ASSERT(pn3->isArity(PN_LIST));
+                JS_ASSERT(pn3->isArity(PN_LIST) || pn3->isArity(PN_BINARY));
                 if (pn3->pn_xflags & PNX_GROUPINIT)
                     op = JSOP_NOP;
             }
         }
         bce->flags &= ~TCF_IN_FOR_INIT;
     }
 
     /*
@@ -6711,28 +7036,23 @@ EmitUnary(JSContext *cx, BytecodeEmitter
 
     bce->flags |= oldflags & TCF_IN_FOR_INIT;
     return Emit1(cx, bce, op) >= 0;
 }
 
 JSBool
 frontend::EmitTree(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
 {
-    ptrdiff_t top, off, tmp, jmp;
-    ParseNode *pn2;
-    JSAtom *atom;
-    ptrdiff_t noteIndex;
-    JSOp op;
+    JS_CHECK_RECURSION(cx, return JS_FALSE);
+
     EmitLevelManager elm(bce);
-    jsint sharpnum = -1;
-
-    JS_CHECK_RECURSION(cx, return JS_FALSE);
 
     JSBool ok = true;
-    pn->pn_offset = top = bce->offset();
+    ptrdiff_t top = bce->offset();
+    pn->pn_offset = top;
 
     /* Emit notes to tell the current bytecode's source line number. */
     UPDATE_LINE_NUMBER_NOTES(cx, bce, pn->pn_pos.begin.lineno);
 
     switch (pn->getKind()) {
       case PNK_FUNCTION:
         ok = EmitFunc(cx, bce, pn);
         break;
@@ -6801,17 +7121,17 @@ frontend::EmitTree(JSContext *cx, Byteco
 
       case PNK_CATCH:
         if (!EmitCatch(cx, bce, pn))
             return false;
         break;
 
       case PNK_VAR:
       case PNK_CONST:
-        if (!EmitVariables(cx, bce, pn, JS_FALSE, &noteIndex))
+        if (!EmitVariables(cx, bce, pn, InitializeVars))
             return JS_FALSE;
         break;
 
       case PNK_RETURN:
         ok = EmitReturn(cx, bce, pn);
         break;
 
 #if JS_HAS_GENERATORS
@@ -6853,40 +7173,42 @@ frontend::EmitTree(JSContext *cx, Byteco
         ok = EmitStatement(cx, bce, pn);
         break;
 
       case PNK_COLON:
         ok = EmitLabel(cx, bce, pn);
         break;
 
       case PNK_COMMA:
+      {
         /*
          * Emit SRC_PCDELTA notes on each JSOP_POP between comma operands.
          * These notes help the decompiler bracket the bytecodes generated
          * from each sub-expression that follows a comma.
          */
-        off = noteIndex = -1;
-        for (pn2 = pn->pn_head; ; pn2 = pn2->pn_next) {
+        ptrdiff_t off = -1, noteIndex = -1;
+        for (ParseNode *pn2 = pn->pn_head; ; pn2 = pn2->pn_next) {
             if (!EmitTree(cx, bce, pn2))
                 return JS_FALSE;
-            tmp = bce->offset();
+            ptrdiff_t tmp = bce->offset();
             if (noteIndex >= 0) {
                 if (!SetSrcNoteOffset(cx, bce, (uintN)noteIndex, 0, tmp-off))
                     return JS_FALSE;
             }
             if (!pn2->pn_next)
                 break;
             off = tmp;
             noteIndex = NewSrcNote2(cx, bce, SRC_PCDELTA, 0);
             if (noteIndex < 0 ||
                 Emit1(cx, bce, JSOP_POP) < 0) {
                 return JS_FALSE;
             }
         }
         break;
+      }
 
       case PNK_ASSIGN:
       case PNK_ADDASSIGN:
       case PNK_SUBASSIGN:
       case PNK_BITORASSIGN:
       case PNK_BITXORASSIGN:
       case PNK_BITANDASSIGN:
       case PNK_LSHASSIGN:
@@ -6926,20 +7248,20 @@ frontend::EmitTree(JSContext *cx, Byteco
       case PNK_LSH:
       case PNK_RSH:
       case PNK_URSH:
       case PNK_STAR:
       case PNK_DIV:
       case PNK_MOD:
         if (pn->isArity(PN_LIST)) {
             /* Left-associative operator chain: avoid too much recursion. */
-            pn2 = pn->pn_head;
+            ParseNode *pn2 = pn->pn_head;
             if (!EmitTree(cx, bce, pn2))
                 return JS_FALSE;
-            op = pn->getOp();
+            JSOp op = pn->getOp();
             while ((pn2 = pn2->pn_next) != NULL) {
                 if (!EmitTree(cx, bce, pn2))
                     return JS_FALSE;
                 if (Emit1(cx, bce, op) < 0)
                     return JS_FALSE;
             }
         } else {
 #if JS_HAS_XML_SUPPORT
@@ -7018,32 +7340,34 @@ frontend::EmitTree(JSContext *cx, Byteco
         break;
 
       case PNK_DELETE:
         ok = EmitDelete(cx, bce, pn);
         break;
 
 #if JS_HAS_XML_SUPPORT
       case PNK_FILTER:
+      {
         JS_ASSERT(!bce->inStrictMode());
 
         if (!EmitTree(cx, bce, pn->pn_left))
             return JS_FALSE;
-        jmp = EmitJump(cx, bce, JSOP_FILTER, 0);
+        ptrdiff_t jmp = EmitJump(cx, bce, JSOP_FILTER, 0);
         if (jmp < 0)
             return JS_FALSE;
         top = EmitTraceOp(cx, bce, pn->pn_right);
         if (top < 0)
             return JS_FALSE;
         if (!EmitTree(cx, bce, pn->pn_right))
             return JS_FALSE;
         CHECK_AND_SET_JUMP_OFFSET_AT(cx, bce, jmp);
         if (EmitJump(cx, bce, JSOP_ENDFILTER, top - bce->offset()) < 0)
             return JS_FALSE;
         break;
+      }
 #endif
 
       case PNK_DOT:
         /*
          * Pop a stack operand, convert it to object, get a property named by
          * this bytecode's immediate-indexed atom operand, and push its value
          * (not a reference to it).
          */
@@ -7071,18 +7395,19 @@ frontend::EmitTree(JSContext *cx, Byteco
         break;
 
       case PNK_LEXICALSCOPE:
         ok = EmitLexicalScope(cx, bce, pn);
         break;
 
 #if JS_HAS_BLOCK_SCOPE
       case PNK_LET:
-        if (!EmitLet(cx, bce, pn))
-            return false;
+        ok = pn->isArity(PN_BINARY)
+             ? EmitLet(cx, bce, pn)
+             : EmitVariables(cx, bce, pn, InitializeVars);
         break;
 #endif /* JS_HAS_BLOCK_SCOPE */
 #if JS_HAS_GENERATORS
       case PNK_ARRAYPUSH: {
         jsint slot;
 
         /*
          * The array object's stack index is in bce->arrayCompDepth. See below
@@ -7107,18 +7432,19 @@ frontend::EmitTree(JSContext *cx, Byteco
         break;
 
       case PNK_RC:
         ok = EmitObject(cx, bce, pn, -1);
         break;
 
 #if JS_HAS_SHARP_VARS
       case PNK_DEFSHARP:
+      {
         JS_ASSERT(bce->hasSharps());
-        sharpnum = pn->pn_num;
+        int sharpnum = pn->pn_num;
         pn = pn->pn_kid;
         if (pn->isKind(PNK_RB)) {
             ok = EmitArray(cx, bce, pn, sharpnum);
             break;
         }
 # if JS_HAS_GENERATORS
         if (pn->isKind(PNK_ARRAYCOMP)) {
             ok = EmitArray(cx, bce, pn, sharpnum);
@@ -7129,16 +7455,17 @@ frontend::EmitTree(JSContext *cx, Byteco
             ok = EmitObject(cx, bce, pn, sharpnum);
             break;
         }
 
         if (!EmitTree(cx, bce, pn))
             return JS_FALSE;
         EMIT_UINT16PAIR_IMM_OP(JSOP_DEFSHARP, bce->sharpSlotBase, (jsatomid) sharpnum);
         break;
+      }
 
       case PNK_USESHARP:
         JS_ASSERT(bce->hasSharps());
         EMIT_UINT16PAIR_IMM_OP(JSOP_USESHARP, bce->sharpSlotBase, (jsatomid) pn->pn_num);
         break;
 #endif /* JS_HAS_SHARP_VARS */
 
       case PNK_NAME:
@@ -7204,29 +7531,29 @@ frontend::EmitTree(JSContext *cx, Byteco
           case PNK_XMLPTAGC:
           case PNK_XMLSTAGO:
             break;
           default:
             if (Emit1(cx, bce, JSOP_STARTXML) < 0)
                 return JS_FALSE;
         }
 
-        for (pn2 = pn->pn_head; pn2; pn2 = pn2->pn_next) {
+        for (ParseNode *pn2 = pn->pn_head; pn2; pn2 = pn2->pn_next) {
             if (pn2->isKind(PNK_XMLCURLYEXPR) && Emit1(cx, bce, JSOP_STARTXMLEXPR) < 0)
                 return JS_FALSE;
             if (!EmitTree(cx, bce, pn2))
                 return JS_FALSE;
             if (pn2 != pn->pn_head && Emit1(cx, bce, JSOP_ADD) < 0)
                 return JS_FALSE;
         }
 
         if (pn->pn_xflags & PNX_XMLROOT) {
             if (pn->pn_count == 0) {
                 JS_ASSERT(pn->isKind(PNK_XMLLIST));
-                atom = cx->runtime->atomState.emptyAtom;
+                JSAtom *atom = cx->runtime->atomState.emptyAtom;
                 jsatomid index;
                 if (!bce->makeAtomIndex(atom, &index))
                     return JS_FALSE;
                 EMIT_INDEX_OP(JSOP_STRING, index);
             }
             if (Emit1(cx, bce, pn->getOp()) < 0)
                 return JS_FALSE;
         }
@@ -7243,17 +7570,17 @@ frontend::EmitTree(JSContext *cx, Byteco
             return false;
         break;
 
       case PNK_XMLNAME:
         JS_ASSERT(!bce->inStrictMode());
 
         if (pn->isArity(PN_LIST)) {
             JS_ASSERT(pn->pn_count != 0);
-            for (pn2 = pn->pn_head; pn2; pn2 = pn2->pn_next) {
+            for (ParseNode *pn2 = pn->pn_head; pn2; pn2 = pn2->pn_next) {
                 if (pn2->isKind(PNK_XMLCURLYEXPR) && Emit1(cx, bce, JSOP_STARTXMLEXPR) < 0)
                     return JS_FALSE;
                 if (!EmitTree(cx, bce, pn2))
                     return JS_FALSE;
                 if (pn2 != pn->pn_head && Emit1(cx, bce, JSOP_ADD) < 0)
                     return JS_FALSE;
             }
         } else {
@@ -7442,32 +7769,36 @@ frontend::AddToSrcNoteDelta(JSContext *c
 }
 
 static JSBool
 SetSrcNoteOffset(JSContext *cx, BytecodeEmitter *bce, uintN index, uintN which, ptrdiff_t offset)
 {
     jssrcnote *sn;
     ptrdiff_t diff;
 
-    if ((jsuword)offset >= (jsuword)((ptrdiff_t)SN_3BYTE_OFFSET_FLAG << 16)) {
+    if (size_t(offset) > SN_MAX_OFFSET) {
         ReportStatementTooLarge(cx, bce);
         return JS_FALSE;
     }
 
     /* Find the offset numbered which (i.e., skip exactly which offsets). */
     sn = &bce->notes()[index];
     JS_ASSERT(SN_TYPE(sn) != SRC_XDELTA);
     JS_ASSERT((intN) which < js_SrcNoteSpec[SN_TYPE(sn)].arity);
     for (sn++; which; sn++, which--) {
         if (*sn & SN_3BYTE_OFFSET_FLAG)
             sn += 2;
     }
 
-    /* See if the new offset requires three bytes. */
-    if (offset > (ptrdiff_t)SN_3BYTE_OFFSET_MASK) {
+    /*
+     * See if the new offset requires three bytes either by being too big or if
+     * the offset has already been inflated (in which case, we need to stay big
+     * to not break the srcnote encoding if this isn't the last srcnote).
+     */
+    if (offset > (ptrdiff_t)SN_3BYTE_OFFSET_MASK || (*sn & SN_3BYTE_OFFSET_FLAG)) {
         /* Maybe this offset was already set to a three-byte value. */
         if (!(*sn & SN_3BYTE_OFFSET_FLAG)) {
             /* Losing, need to insert another two bytes for this offset. */
             index = sn - bce->notes();
 
             /*
              * Test to see if the source note array must grow to accommodate
              * either the first or second byte of additional storage required
--- a/js/src/frontend/BytecodeEmitter.h
+++ b/js/src/frontend/BytecodeEmitter.h
@@ -937,26 +937,29 @@ enum SrcNoteType {
     SRC_IF_ELSE     = 2,        /* JSOP_IFEQ bytecode is from an if-then-else */
     SRC_FOR_IN      = 2,        /* JSOP_GOTO to for-in loop condition from
                                    before loop (same arity as SRC_IF_ELSE) */
     SRC_FOR         = 3,        /* JSOP_NOP or JSOP_POP in for(;;) loop head */
     SRC_WHILE       = 4,        /* JSOP_GOTO to for or while loop condition
                                    from before loop, else JSOP_NOP at top of
                                    do-while loop */
     SRC_CONTINUE    = 5,        /* JSOP_GOTO is a continue, not a break;
-                                   also used on JSOP_ENDINIT if extra comma
-                                   at end of array literal: [1,2,,];
-                                   JSOP_DUP continuing destructuring pattern */
+                                   JSOP_ENDINIT needs extra comma at end of
+                                   array literal: [1,2,,];
+                                   JSOP_DUP continuing destructuring pattern;
+                                   JSOP_POP at end of for-in */
     SRC_DECL        = 6,        /* type of a declaration (var, const, let*) */
     SRC_DESTRUCT    = 6,        /* JSOP_DUP starting a destructuring assignment
                                    operation, with SRC_DECL_* offset operand */
     SRC_PCDELTA     = 7,        /* distance forward from comma-operator to
                                    next POP, or from CONDSWITCH to first CASE
                                    opcode, etc. -- always a forward delta */
     SRC_GROUPASSIGN = 7,        /* SRC_DESTRUCT variant for [a, b] = [c, d] */
+    SRC_DESTRUCTLET = 7,        /* JSOP_DUP starting a destructuring let
+                                   operation, with offset to JSOP_ENTERLET0 */
     SRC_ASSIGNOP    = 8,        /* += or another assign-op follows */
     SRC_COND        = 9,        /* JSOP_IFEQ is from conditional ?: operator */
     SRC_BRACE       = 10,       /* mandatory brace, for scope or to avoid
                                    dangling else */
     SRC_HIDDEN      = 11,       /* opcode shouldn't be decompiled */
     SRC_PCBASE      = 12,       /* distance back from annotated getprop or
                                    setprop op to left-most obj.prop.subprop
                                    bytecode -- always a backward delta */
@@ -1025,16 +1028,18 @@ enum SrcNoteType {
 /*
  * Offset fields follow certain notes and are frequency-encoded: an offset in
  * [0,0x7f] consumes one byte, an offset in [0x80,0x7fffff] takes three, and
  * the high bit of the first byte is set.
  */
 #define SN_3BYTE_OFFSET_FLAG    0x80
 #define SN_3BYTE_OFFSET_MASK    0x7f
 
+#define SN_MAX_OFFSET ((size_t)((ptrdiff_t)SN_3BYTE_OFFSET_FLAG << 16) - 1)
+
 #define SN_LENGTH(sn)           ((js_SrcNoteSpec[SN_TYPE(sn)].arity == 0) ? 1 \
                                  : js_SrcNoteLength(sn))
 #define SN_NEXT(sn)             ((sn) + SN_LENGTH(sn))
 
 /* A source note array is terminated by an all-zero element. */
 #define SN_MAKE_TERMINATOR(sn)  (*(sn) = SRC_NULL)
 #define SN_IS_TERMINATOR(sn)    (*(sn) == SRC_NULL)
 
@@ -1097,16 +1102,38 @@ BytecodeEmitter::countFinalSourceNotes()
                     : SN_DELTA_MASK - (*sn & SN_DELTA_MASK);
         }
         if (diff > 0)
             cnt += JS_HOWMANY(diff, SN_XDELTA_MASK);
     }
     return cnt;
 }
 
+/*
+ * To avoid offending js_SrcNoteSpec[SRC_DECL].arity, pack the two data needed
+ * to decompile let into one ptrdiff_t:
+ *   offset: offset to the LEAVEBLOCK(EXPR) op (not including ENTER/LEAVE)
+ *   groupAssign: whether this was an optimized group assign ([x,y] = [a,b])
+ */
+inline ptrdiff_t PackLetData(size_t offset, bool groupAssign)
+{
+    JS_ASSERT(offset <= (size_t(-1) >> 1));
+    return ptrdiff_t(offset << 1) | ptrdiff_t(groupAssign);
+}
+
+inline size_t LetDataToOffset(ptrdiff_t w)
+{
+    return size_t(w) >> 1;
+}
+
+inline bool LetDataToGroupAssign(ptrdiff_t w)
+{
+    return size_t(w) & 1;
+}
+
 } /* namespace js */
 
 struct JSSrcNoteSpec {
     const char      *name;      /* name for disassembly/debugging output */
     int8_t          arity;      /* number of offset operands */
     uint8_t         offsetBias; /* bias of offset(s) from annotated pc */
     int8_t          isSpanDep;  /* 1 or -1 if offsets could span extended ops,
                                    0 otherwise; sign tells span direction */
--- a/js/src/frontend/ParseNode.h
+++ b/js/src/frontend/ParseNode.h
@@ -840,16 +840,21 @@ struct ParseNode {
         JSString *str = pn_atom;
         return (pn_pos.begin.lineno == pn_pos.end.lineno &&
                 pn_pos.begin.index + str->length() + 2 == pn_pos.end.index);
     }
 
     /* Return true if this node appears in a Directive Prologue. */
     bool isDirectivePrologueMember() const { return pn_prologue; }
 
+#ifdef JS_HAS_DESTRUCTURING
+    /* Return true if this represents a hole in an array literal. */
+    bool isArrayHole() const { return isKind(PNK_COMMA) && isArity(PN_NULLARY); }
+#endif
+
 #ifdef JS_HAS_GENERATOR_EXPRS
     /*
      * True if this node is a desugared generator expression.
      */
     bool isGeneratorExpr() const {
         if (getKind() == PNK_LP) {
             ParseNode *callee = this->pn_head;
             if (callee->getKind() == PNK_FUNCTION) {
--- a/js/src/frontend/Parser.cpp
+++ b/js/src/frontend/Parser.cpp
@@ -832,29 +832,51 @@ js::DefineArg(ParseNode *pn, JSAtom *ato
  * helper function signature in order to share code among destructuring and
  * simple variable declaration parsers.  In the destructuring case, the binder
  * function is called indirectly from the variable declaration parser by way
  * of CheckDestructuring and its friends.
  */
 typedef JSBool
 (*Binder)(JSContext *cx, BindData *data, JSAtom *atom, TreeContext *tc);
 
+static JSBool
+BindLet(JSContext *cx, BindData *data, JSAtom *atom, TreeContext *tc);
+
+static JSBool
+BindVarOrConst(JSContext *cx, BindData *data, JSAtom *atom, TreeContext *tc);
+
 struct BindData {
     BindData() : fresh(true) {}
 
     ParseNode       *pn;        /* name node for definition processing and
                                    error source coordinates */
     JSOp            op;         /* prolog bytecode or nop */
     Binder          binder;     /* binder, discriminates u */
     union {
         struct {
+            VarContext varContext;
+            JSObject *blockObj;
             uintN   overflow;
         } let;
     };
     bool fresh;
+
+    void initLet(VarContext varContext, JSObject *blockObj, uintN overflow) {
+        this->pn = NULL;
+        this->op = JSOP_NOP;
+        this->binder = BindLet;
+        this->let.varContext = varContext;
+        this->let.blockObj = blockObj;
+        this->let.overflow = overflow;
+    }
+
+    void initVarOrConst(JSOp op) {
+        this->op = op;
+        this->binder = BindVarOrConst;
+    }
 };
 
 static bool
 BindLocalVariable(JSContext *cx, TreeContext *tc, ParseNode *pn, BindingKind kind)
 {
     JS_ASSERT(kind == VARIABLE || kind == CONSTANT);
 
     /* 'arguments' can be bound as a local only via a destructuring formal parameter. */
@@ -1872,118 +1894,129 @@ MatchLabel(JSContext *cx, TokenStream *t
         (void) ts->getToken();
         *label = ts->currentToken().name();
     } else {
         *label = NULL;
     }
     return true;
 }
 
+static bool
+ReportRedeclaration(JSContext *cx, TreeContext *tc, ParseNode *pn, bool isConst, JSAtom *atom)
+{
+    JSAutoByteString name;
+    if (js_AtomToPrintableString(cx, atom, &name)) {
+        ReportCompileErrorNumber(cx, TS(tc->parser), pn,
+                                 JSREPORT_ERROR, JSMSG_REDECLARED_VAR,
+                                 isConst ? "const" : "variable",
+                                 name.ptr());
+    }
+    return false;
+}
+
 /*
  * Define a let-variable in a block, let-expression, or comprehension scope. tc
  * must already be in such a scope.
  *
  * Throw a SyntaxError if 'atom' is an invalid name. Otherwise create a
  * property for the new variable on the block object, tc->blockChain;
  * populate data->pn->pn_{op,cookie,defn,dflags}; and stash a pointer to
  * data->pn in a slot of the block object.
  */
 static JSBool
 BindLet(JSContext *cx, BindData *data, JSAtom *atom, TreeContext *tc)
 {
-    ParseNode *pn;
-    JSObject *blockObj;
-    jsint n;
-
-    /*
-     * Body-level 'let' is the same as 'var' currently -- this may change in a
-     * successor standard to ES5 that specifies 'let'.
-     */
-    JS_ASSERT(!tc->atBodyLevel());
-
-    pn = data->pn;
+    ParseNode *pn = data->pn;
     if (!CheckStrictBinding(cx, tc, atom->asPropertyName(), pn))
         return false;
 
-    blockObj = tc->blockChain;
-    Definition *dn = tc->decls.lookupFirst(atom);
-    if (dn && dn->pn_blockid == tc->blockid()) {
-        JSAutoByteString name;
-        if (js_AtomToPrintableString(cx, atom, &name)) {
-            ReportCompileErrorNumber(cx, TS(tc->parser), pn,
-                                     JSREPORT_ERROR, JSMSG_REDECLARED_VAR,
-                                     dn->isConst() ? "const" : "variable",
-                                     name.ptr());
-        }
-        return false;
-    }
-
-    n = OBJ_BLOCK_COUNT(cx, blockObj);
-    if (n == JS_BIT(16)) {
+    JSObject *blockObj = data->let.blockObj;
+    uintN blockCount = OBJ_BLOCK_COUNT(cx, blockObj);
+    if (blockCount == JS_BIT(16)) {
         ReportCompileErrorNumber(cx, TS(tc->parser), pn,
                                  JSREPORT_ERROR, data->let.overflow);
         return false;
     }
 
     /*
-     * Pass push = true to Define so it pushes an ale ahead of any outer scope.
-     * This is balanced by PopStatement, defined immediately below.
+     * For bindings that are hoisted to the beginning of the block/function,
+     * Define() right now. For the rest, delay Define() until PushLetScope.
      */
-    if (!Define(pn, atom, tc, true))
-        return false;
+    if (data->let.varContext == HoistVars) {
+        JS_ASSERT(!tc->atBodyLevel());
+        Definition *dn = tc->decls.lookupFirst(atom);
+        if (dn && dn->pn_blockid == tc->blockid())
+            return ReportRedeclaration(cx, tc, pn, dn->isConst(), atom);
+        if (!Define(pn, atom, tc, true))
+            return false;
+    }
 
     /*
      * Assign block-local index to pn->pn_cookie right away, encoding it as an
      * upvar cookie whose skip tells the current static level. The emitter will
      * adjust the node's slot based on its stack depth model -- and, for global
      * and eval code, js::frontend::CompileScript will adjust the slot
      * again to include script->nfixed.
      */
     pn->setOp(JSOP_GETLOCAL);
-    pn->pn_cookie.set(tc->staticLevel, uint16_t(n));
+    pn->pn_cookie.set(tc->staticLevel, uint16_t(blockCount));
     pn->pn_dflags |= PND_LET | PND_BOUND;
 
     /*
      * Define the let binding's property before storing pn in the the binding's
-     * slot indexed by n off the class-reserved slot base.
+     * slot indexed by blockCount off the class-reserved slot base.
      */
-    const Shape *shape = blockObj->defineBlockVariable(cx, ATOM_TO_JSID(atom), n);
-    if (!shape)
+    bool redeclared;
+    jsid id = ATOM_TO_JSID(atom);
+    const Shape *shape = blockObj->defineBlockVariable(cx, id, blockCount, &redeclared);
+    if (!shape) {
+        if (redeclared)
+            ReportRedeclaration(cx, tc, pn, false, atom);
         return false;
+    }
 
     /*
-     * Store pn temporarily in what would be shape-mapped slots in a cloned
-     * block object (once the prototype's final population is known, after all
-     * 'let' bindings for this block have been parsed). We free these slots in
-     * BytecodeEmitter.cpp:EmitEnterBlock so they don't tie up unused space
-     * in the so-called "static" prototype Block.
+     * Store pn temporarily in the shape-mapped slots in the static block
+     * object. This value is clobbered in EmitEnterBlock.
      */
     blockObj->setSlot(shape->slot(), PrivateValue(pn));
     return true;
 }
 
+template <class Op>
+static inline bool
+ForEachLetDef(TreeContext *tc, JSObject *blockObj, Op op)
+{
+    for (Shape::Range r = blockObj->lastProperty()->all(); !r.empty(); r.popFront()) {
+        const Shape &shape = r.front();
+
+        /* Beware the destructuring dummy slots. */
+        if (JSID_IS_INT(shape.propid()))
+            continue;
+
+        if (!op(tc, blockObj, shape, JSID_TO_ATOM(shape.propid())))
+            return false;
+    }
+    return true;
+}
+
+struct RemoveDecl {
+    bool operator()(TreeContext *tc, JSObject *, const Shape &, JSAtom *atom) {
+        tc->decls.remove(atom);
+        return true;
+    }
+};
+
 static void
 PopStatement(TreeContext *tc)
 {
-    StmtInfo *stmt = tc->topStmt;
-
-    if (stmt->flags & SIF_SCOPE) {
-        JSObject *obj = stmt->blockObj;
-        JS_ASSERT(!obj->isClonedBlock());
-
-        for (Shape::Range r = obj->lastProperty()->all(); !r.empty(); r.popFront()) {
-            JSAtom *atom = JSID_TO_ATOM(r.front().propid());
-
-            /* Beware the empty destructuring dummy. */
-            if (atom == tc->parser->context->runtime->atomState.emptyAtom)
-                continue;
-            tc->decls.remove(atom);
-        }
-
+    if (tc->topStmt->flags & SIF_SCOPE) {
+        JSObject *obj = tc->topStmt->blockObj;
         JS_ASSERT(!obj->inDictionaryMode());
+        ForEachLetDef(tc, obj, RemoveDecl());
     }
     PopStatementTC(tc);
 }
 
 static inline bool
 OuterLet(TreeContext *tc, StmtInfo *stmt, JSAtom *atom)
 {
     while (stmt->downScope) {
@@ -2531,34 +2564,41 @@ BindDestructuringLHS(JSContext *cx, Pars
  * CheckDestructuring, we require the pattern's property value
  * positions to be simple names, and define them as appropriate to the
  * context.  For these calls, |data| points to the right sort of
  * BindData.
  *
  * See also UndominateInitializers, immediately below. If you change
  * either of these functions, you might have to change the other to
  * match.
+ *
+ * The 'toplevel' is a private detail of the recursive strategy used by
+ * CheckDestructuring and callers should use the default value.
  */
 static bool
-CheckDestructuring(JSContext *cx, BindData *data, ParseNode *left, TreeContext *tc)
+CheckDestructuring(JSContext *cx, BindData *data, ParseNode *left, TreeContext *tc,
+                   bool toplevel = true)
 {
     bool ok;
 
     if (left->isKind(PNK_ARRAYCOMP)) {
         ReportCompileErrorNumber(cx, TS(tc->parser), left, JSREPORT_ERROR,
                                  JSMSG_ARRAY_COMP_LEFTSIDE);
         return false;
     }
 
+    JSObject *blockObj = data && data->binder == BindLet ? data->let.blockObj : NULL;
+    uint32_t blockCountBefore = blockObj ? OBJ_BLOCK_COUNT(cx, blockObj) : 0;
+
     if (left->isKind(PNK_RB)) {
         for (ParseNode *pn = left->pn_head; pn; pn = pn->pn_next) {
             /* Nullary comma is an elision; binary comma is an expression.*/
-            if (!pn->isKind(PNK_COMMA) || !pn->isArity(PN_NULLARY)) {
+            if (!pn->isArrayHole()) {
                 if (pn->isKind(PNK_RB) || pn->isKind(PNK_RC)) {
-                    ok = CheckDestructuring(cx, data, pn, tc);
+                    ok = CheckDestructuring(cx, data, pn, tc, false);
                 } else {
                     if (data) {
                         if (!pn->isKind(PNK_NAME)) {
                             ReportCompileErrorNumber(cx, TS(tc->parser), pn, JSREPORT_ERROR,
                                                      JSMSG_NO_VARIABLE_NAME);
                             return false;
                         }
                         ok = BindDestructuringVar(cx, data, pn, tc);
@@ -2572,17 +2612,17 @@ CheckDestructuring(JSContext *cx, BindDa
         }
     } else {
         JS_ASSERT(left->isKind(PNK_RC));
         for (ParseNode *pair = left->pn_head; pair; pair = pair->pn_next) {
             JS_ASSERT(pair->isKind(PNK_COLON));
             ParseNode *pn = pair->pn_right;
 
             if (pn->isKind(PNK_RB) || pn->isKind(PNK_RC)) {
-                ok = CheckDestructuring(cx, data, pn, tc);
+                ok = CheckDestructuring(cx, data, pn, tc, false);
             } else if (data) {
                 if (!pn->isKind(PNK_NAME)) {
                     ReportCompileErrorNumber(cx, TS(tc->parser), pn, JSREPORT_ERROR,
                                              JSMSG_NO_VARIABLE_NAME);
                     return false;
                 }
                 ok = BindDestructuringVar(cx, data, pn, tc);
             } else {
@@ -2598,33 +2638,38 @@ CheckDestructuring(JSContext *cx, BindDa
      * that any operation that introduces a new scope (like a "let" or "with"
      * block) increases the stack depth. This way, it is possible to restore
      * the scope chain based on stack depth of the handler alone. "let" with
      * an empty destructuring pattern like in
      *
      *   let [] = 1;
      *
      * would violate this assumption as the there would be no let locals to
-     * store on the stack. To satisfy it we add an empty property to such
-     * blocks so that OBJ_BLOCK_COUNT(cx, blockObj), which gives the number of
-     * slots, would be always positive.
+     * store on the stack.
+     *
+     * Furthermore, the decompiler needs an abstract stack location to store
+     * the decompilation of each let block/expr initializer. E.g., given:
      *
-     * Note that we add such a property even if the block has locals due to
-     * later let declarations in it. We optimize for code simplicity here,
-     * not the fastest runtime performance with empty [] or {}.
+     *   let (x = 1, [[]] = b, y = 3, {a:[]} = c) { ... }
+     *
+     * four slots are needed.
+     *
+     * To satisfy both constraints, we push a dummy slot (and add a
+     * corresponding dummy property to the block object) for each initializer
+     * that doesn't introduce at least one binding.
      */
-    if (data &&
-        data->binder == BindLet &&
-        OBJ_BLOCK_COUNT(cx, tc->blockChain) == 0 &&
-        !DefineNativeProperty(cx, tc->blockChain,
-                              ATOM_TO_JSID(cx->runtime->atomState.emptyAtom),
-                              UndefinedValue(), NULL, NULL,
-                              JSPROP_ENUMERATE | JSPROP_PERMANENT,
-                              Shape::HAS_SHORTID, 0)) {
-        return false;
+    if (toplevel && blockObj && blockCountBefore == OBJ_BLOCK_COUNT(cx, blockObj)) {
+        if (!DefineNativeProperty(cx, blockObj,
+                                  INT_TO_JSID(blockCountBefore),
+                                  UndefinedValue(), NULL, NULL,
+                                  JSPROP_ENUMERATE | JSPROP_PERMANENT,
+                                  Shape::HAS_SHORTID, blockCountBefore)) {
+            return false;
+        }
+        JS_ASSERT(OBJ_BLOCK_COUNT(cx, blockObj) == blockCountBefore + 1);
     }
 
     return true;
 }
 
 /*
  * Extend the pn_pos.end source coordinate of each name in a destructuring
  * binding such as
@@ -2759,71 +2804,116 @@ Parser::returnOrYield(bool useAssignExpr
                          JSMSG_ANON_NO_RETURN_VALUE)) {
         return NULL;
     }
 
     return pn;
 }
 
 static ParseNode *
-PushLexicalScope(JSContext *cx, TokenStream *ts, TreeContext *tc, StmtInfo *stmt)
+PushLexicalScope(JSContext *cx, TreeContext *tc, JSObject *obj, StmtInfo *stmt)
 {
     ParseNode *pn = LexicalScopeNode::create(PNK_LEXICALSCOPE, tc);
     if (!pn)
         return NULL;
 
-    JSObject *obj = js_NewBlockObject(cx);
-    if (!obj)
-        return NULL;
-
     ObjectBox *blockbox = tc->parser->newObjectBox(obj);
     if (!blockbox)
         return NULL;
 
     PushBlockScope(tc, stmt, obj, -1);
     pn->setOp(JSOP_LEAVEBLOCK);
     pn->pn_objbox = blockbox;
     pn->pn_cookie.makeFree();
     pn->pn_dflags = 0;
     if (!GenerateBlockId(tc, stmt->blockid))
         return NULL;
     pn->pn_blockid = stmt->blockid;
     return pn;
 }
 
+static ParseNode *
+PushLexicalScope(JSContext *cx, TreeContext *tc, StmtInfo *stmt)
+{
+    JSObject *obj = js_NewBlockObject(cx);
+    if (!obj)
+        return NULL;
+
+    return PushLexicalScope(cx, tc, obj, stmt);
+}
+
 #if JS_HAS_BLOCK_SCOPE
 
+struct AddDecl
+{
+    uint32_t blockid;
+
+    AddDecl(uint32_t blockid) : blockid(blockid) {}
+
+    bool operator()(TreeContext *tc, JSObject *blockObj, const Shape &shape, JSAtom *atom)
+    {
+        ParseNode *def = (ParseNode *) blockObj->getSlot(shape.slot()).toPrivate();
+        def->pn_blockid = blockid;
+        return Define(def, atom, tc, true);
+    }
+};
+
+static ParseNode *
+PushLetScope(JSContext *cx, TreeContext *tc, JSObject *blockObj, StmtInfo *stmt)
+{
+    ParseNode *pn = PushLexicalScope(cx, tc, blockObj, stmt);
+    if (!pn)
+        return NULL;
+
+    /* Tell codegen to emit JSOP_ENTERLETx (not JSOP_ENTERBLOCK). */
+    pn->pn_dflags |= PND_LET;
+
+    /* Populate the new scope with decls found in the head with updated blockid. */
+    if (!ForEachLetDef(tc, blockObj, AddDecl(stmt->blockid)))
+        return NULL;
+
+    return pn;
+}
+
+/*
+ * Parse a let block statement or let expression (determined by 'letContext').
+ * In both cases, bindings are not hoisted to the top of the enclosing block
+ * and thus must be carefully injected between variables() and the let body.
+ */
 ParseNode *
-Parser::letBlock(JSBool statement)
+Parser::letBlock(LetContext letContext)
 {
     JS_ASSERT(tokenStream.currentToken().type == TOK_LET);
 
-    /* Create the let binary node. */
     ParseNode *pnlet = BinaryNode::create(PNK_LET, tc);
     if (!pnlet)
         return NULL;
 
+    JSObject *blockObj = js_NewBlockObject(context);
+    if (!blockObj)
+        return NULL;
+
     MUST_MATCH_TOKEN(TOK_LP, JSMSG_PAREN_BEFORE_LET);
 
-    /* This is a let block or expression of the form: let (a, b, c) .... */
-    StmtInfo stmtInfo;
-    ParseNode *pnblock = PushLexicalScope(context, &tokenStream, tc, &stmtInfo);
-    if (!pnblock)
+    ParseNode *vars = variables(PNK_LET, blockObj, DontHoistVars);
+    if (!vars)
         return NULL;
-    ParseNode *pn = pnblock;
-    pn->pn_expr = pnlet;
-
-    pnlet->pn_left = variables(PNK_LP, true);
-    if (!pnlet->pn_left)
-        return NULL;
-    pnlet->pn_left->pn_xflags = PNX_POPVAR;
 
     MUST_MATCH_TOKEN(TOK_RP, JSMSG_PAREN_AFTER_LET);
 
-    if (statement && !tokenStream.matchToken(TOK_LC, TSF_OPERAND)) {
+    StmtInfo stmtInfo;
+    ParseNode *block = PushLetScope(context, tc, blockObj, &stmtInfo);
+    if (!block)
+        return NULL;
+
+    pnlet->pn_left = vars;
+    pnlet->pn_right = block;
+
+    ParseNode *ret;
+    if (letContext == LetStatement && !tokenStream.matchToken(TOK_LC, TSF_OPERAND)) {
         /*
          * Strict mode eliminates a grammar ambiguity with unparenthesized
          * LetExpressions in an ExpressionStatement. If followed immediately
          * by an arguments list, it's ambiguous whether the let expression
          * is the callee or the call is inside the let expression body.
          *
          * See bug 569464.
          */
@@ -2832,92 +2922,101 @@ Parser::letBlock(JSBool statement)
             return NULL;
         }
 
         /*
          * If this is really an expression in let statement guise, then we
          * need to wrap the TOK_LET node in a TOK_SEMI node so that we pop
          * the return value of the expression.
          */
-        pn = UnaryNode::create(PNK_SEMI, tc);
-        if (!pn)
+        ParseNode *semi = UnaryNode::create(PNK_SEMI, tc);
+        if (!semi)
             return NULL;
-        pn->pn_num = -1;
-        pn->pn_kid = pnblock;
-
-        statement = JS_FALSE;
+
+        semi->pn_num = -1;
+        semi->pn_kid = pnlet;
+
+        letContext = LetExpresion;
+        ret = semi;
+    } else {
+        ret = pnlet;
     }
 
-    if (statement) {
-        pnlet->pn_right = statements();
-        if (!pnlet->pn_right)
+    if (letContext == LetStatement) {
+        JS_ASSERT(block->getOp() == JSOP_LEAVEBLOCK);
+        block->pn_expr = statements();
+        if (!block->pn_expr)
             return NULL;
         MUST_MATCH_TOKEN(TOK_RC, JSMSG_CURLY_AFTER_LET);
     } else {
-        /*
-         * Change pnblock's opcode to the variant that propagates the last
-         * result down after popping the block, and clear statement.
-         */
-        pnblock->setOp(JSOP_LEAVEBLOCKEXPR);
-        pnlet->pn_right = assignExpr();
-        if (!pnlet->pn_right)
+        JS_ASSERT(letContext == LetExpresion);
+        block->setOp(JSOP_LEAVEBLOCKEXPR);
+        block->pn_expr = assignExpr();
+        if (!block->pn_expr)
             return NULL;
     }
 
     PopStatement(tc);
-    return pn;
+    return ret;
 }
 
 #endif /* JS_HAS_BLOCK_SCOPE */
 
 static bool
 PushBlocklikeStatement(StmtInfo *stmt, StmtType type, TreeContext *tc)
 {
     PushStatement(tc, stmt, type, -1);
     return GenerateBlockId(tc, stmt->blockid);
 }
 
 static ParseNode *
-NewBindingNode(JSAtom *atom, TreeContext *tc, bool let = false)
+NewBindingNode(JSAtom *atom, TreeContext *tc, JSObject *blockObj = NULL,
+               VarContext varContext = HoistVars)
 {
-    ParseNode *pn;
-    AtomDefnPtr removal;
-
-    if ((pn = tc->decls.lookupFirst(atom))) {
-        JS_ASSERT(!pn->isPlaceholder());
-    } else {
-        removal = tc->lexdeps->lookup(atom);
-        pn = removal ? removal.value() : NULL;
-        JS_ASSERT_IF(pn, pn->isPlaceholder());
-    }
-
-    if (pn) {
-        JS_ASSERT(pn->isDefn());
-
-        /*
-         * A let binding at top level becomes a var before we get here, so if
-         * pn and tc have the same blockid then that id must not be the bodyid.
-         * If pn is a forward placeholder definition from the same or a higher
-         * block then we claim it.
-         */
-        JS_ASSERT_IF(let && pn->pn_blockid == tc->blockid(),
-                     pn->pn_blockid != tc->bodyid);
-
-        if (pn->isPlaceholder() && pn->pn_blockid >= (let ? tc->blockid() : tc->bodyid)) {
-            if (let)
+    /*
+     * If this name is being injected into an existing block/function, see if
+     * it has already been declared or if it resolves an outstanding lexdep.
+     * Otherwise, this is a let block/expr that introduces a new scope and thus
+     * shadows existing decls and doesn't resolve existing lexdeps. Duplicate
+     * names are caught by BindLet.
+     */
+    if (!blockObj || varContext == HoistVars) {
+        ParseNode *pn = tc->decls.lookupFirst(atom);
+        AtomDefnPtr removal;
+        if (pn) {
+            JS_ASSERT(!pn->isPlaceholder());
+        } else {
+            removal = tc->lexdeps->lookup(atom);
+            pn = removal ? removal.value() : NULL;
+            JS_ASSERT_IF(pn, pn->isPlaceholder());
+        }
+
+        if (pn) {
+            JS_ASSERT(pn->isDefn());
+
+            /*
+             * A let binding at top level becomes a var before we get here, so if
+             * pn and tc have the same blockid then that id must not be the bodyid.
+             * If pn is a forward placeholder definition from the same or a higher
+             * block then we claim it.
+             */
+            JS_ASSERT_IF(blockObj && pn->pn_blockid == tc->blockid(),
+                         pn->pn_blockid != tc->bodyid);
+
+            if (pn->isPlaceholder() && pn->pn_blockid >= tc->blockid()) {
                 pn->pn_blockid = tc->blockid();
-
-            tc->lexdeps->remove(removal);
-            return pn;
+                tc->lexdeps->remove(removal);
+                return pn;
+            }
         }
     }
 
     /* Make a new node for this declarator name (or destructuring pattern). */
     JS_ASSERT(tc->parser->tokenStream.currentToken().type == TOK_NAME);
-    pn = NameNode::create(PNK_NAME, atom, tc);
+    ParseNode *pn = NameNode::create(PNK_NAME, atom, tc);
     if (!pn)
         return NULL;
 
     if (atom == tc->parser->context->runtime->atomState.argumentsAtom)
         tc->countArgumentsUse(pn);
 
     return pn;
 }
@@ -3038,50 +3137,44 @@ Parser::switchStatement()
     return pn;
 }
 
 ParseNode *
 Parser::forStatement()
 {
     JS_ASSERT(tokenStream.isCurrentTokenType(TOK_FOR));
 
-    ParseNode *pnseq = NULL;
-#if JS_HAS_BLOCK_SCOPE
-    ParseNode *pnlet = NULL;
-    StmtInfo blockInfo;
-#endif
-
     /* A FOR node is binary, left is loop control and right is the body. */
     ParseNode *pn = BinaryNode::create(PNK_FOR, tc);
     if (!pn)
         return NULL;
-    StmtInfo stmtInfo;
-    PushStatement(tc, &stmtInfo, STMT_FOR_LOOP, -1);
+
+    StmtInfo forStmt;
+    PushStatement(tc, &forStmt, STMT_FOR_LOOP, -1);
 
     pn->setOp(JSOP_ITER);
     pn->pn_iflags = 0;
     if (tokenStream.matchToken(TOK_NAME)) {
         if (tokenStream.currentToken().name() == context->runtime->atomState.eachAtom)
             pn->pn_iflags = JSITER_FOREACH;
         else
             tokenStream.ungetToken();
     }
 
     MUST_MATCH_TOKEN(TOK_LP, JSMSG_PAREN_AFTER_FOR);
 
-#ifdef JS_HAS_BLOCK_SCOPE
-    bool let = false;
-#endif
-
     /*
      * True if we have 'for (var/let/const ...)', except in the oddball case
      * where 'let' begins a let-expression in 'for (let (...) ...)'.
      */
     bool forDecl = false;
 
+    /* Non-null when forDecl is true for a 'for (let ...)' statement. */
+    JSObject *blockObj = NULL;
+
     /* Set to 'x' in 'for (x ;... ;...)' or 'for (x in ...)'. */
     ParseNode *pn1;
 
     {
         TokenKind tt = tokenStream.peekToken(TSF_OPERAND);
         if (tt == TOK_SEMI) {
             if (pn->pn_iflags & JSITER_FOREACH) {
                 reportErrorNumber(pn, JSREPORT_ERROR, JSMSG_BAD_FOR_EACH_LOOP);
@@ -3102,61 +3195,68 @@ Parser::forStatement()
              * A side effect of this restriction is that (unparenthesized)
              * expressions involving an 'in' operator are illegal in the init
              * clause of an ordinary for loop.
              */
             tc->flags |= TCF_IN_FOR_INIT;
             if (tt == TOK_VAR || tt == TOK_CONST) {
                 forDecl = true;
                 tokenStream.consumeKnownToken(tt);
-                pn1 = variables(tt == TOK_VAR ? PNK_VAR : PNK_CONST, false);
+                pn1 = variables(tt == TOK_VAR ? PNK_VAR : PNK_CONST);
             }
 #if JS_HAS_BLOCK_SCOPE
             else if (tt == TOK_LET) {
-                let = true;
                 (void) tokenStream.getToken();
                 if (tokenStream.peekToken() == TOK_LP) {
-                    pn1 = letBlock(JS_FALSE);
+                    pn1 = letBlock(LetExpresion);
                 } else {
                     forDecl = true;
-                    pnlet = PushLexicalScope(context, &tokenStream, tc, &blockInfo);
-                    if (!pnlet)
+                    blockObj = js_NewBlockObject(context);
+                    if (!blockObj)
                         return NULL;
-                    blockInfo.flags |= SIF_FOR_BLOCK;
-                    pn1 = variables(PNK_LET, false);
+                    pn1 = variables(PNK_LET, blockObj, DontHoistVars);
                 }
             }
 #endif
             else {
                 pn1 = expr();
             }
             tc->flags &= ~TCF_IN_FOR_INIT;
             if (!pn1)
                 return NULL;
         }
     }
 
+    JS_ASSERT_IF(forDecl, pn1->isArity(PN_LIST));
+    JS_ASSERT(!!blockObj == (forDecl && pn1->isOp(JSOP_NOP)));
+
+    const TokenPos pos = tokenStream.currentToken().pos;
+
+    /* If non-null, the parent that should be returned instead of forHead. */
+    ParseNode *forParent = NULL;
+
     /*
      * We can be sure that it's a for/in loop if there's still an 'in'
      * keyword here, even if JavaScript recognizes 'in' as an operator,
      * as we've excluded 'in' from being parsed in RelExpr by setting
      * the TCF_IN_FOR_INIT flag in our TreeContext.
      */
-    TokenPos pos = tokenStream.currentToken().pos;
-    ParseNode *pn2, *pn3, *pn4;
+    ParseNode *forHead;     /* initialized by both branches. */
+    StmtInfo letStmt;       /* used if blockObj != NULL. */
+    ParseNode *pn2, *pn3;   /* forHead->pn_kid1 and pn_kid2. */
     if (pn1 && tokenStream.matchToken(TOK_IN)) {
         /*
          * Parse the rest of the for/in head.
          *
          * Here pn1 is everything to the left of 'in'. At the end of this block,
          * pn1 is a decl or NULL, pn2 is the assignment target that receives the
          * enumeration value each iteration, and pn3 is the rhs of 'in'.
          */
         pn->pn_iflags |= JSITER_ENUMERATE;
-        stmtInfo.type = STMT_FOR_IN_LOOP;
+        forStmt.type = STMT_FOR_IN_LOOP;
 
         /* Check that the left side of the 'in' is valid. */
         if (forDecl
             ? (pn1->pn_count > 1 || pn1->isOp(JSOP_DEFCONST)
 #if JS_HAS_DESTRUCTURING
                || (versionNumber() == JSVERSION_1_7 &&
                    pn->isOp(JSOP_ITER) &&
                    !(pn->pn_iflags & JSITER_FOREACH) &&
@@ -3209,67 +3309,92 @@ Parser::forStatement()
                 /*
                  * Declaration with initializer.
                  *
                  * Rewrite 'for (<decl> x = i in o)' where <decl> is 'var' or
                  * 'const' to hoist the initializer or the entire decl out of
                  * the loop head.
                  */
 #if JS_HAS_BLOCK_SCOPE
-                if (let) {
+                if (blockObj) {
                     reportErrorNumber(pn2, JSREPORT_ERROR, JSMSG_INVALID_FOR_IN_INIT);
                     return NULL;
                 }
 #endif /* JS_HAS_BLOCK_SCOPE */
 
-                pnseq = ListNode::create(PNK_SEQ, tc);
+                ParseNode *pnseq = ListNode::create(PNK_SEQ, tc);
                 if (!pnseq)
                     return NULL;
-                pnseq->pn_pos.begin = pn->pn_pos.begin;
 
                 dflag = PND_INITIALIZED;
 
                 /*
                  * All of 'var x = i' is hoisted above 'for (x in o)',
                  * so clear PNX_FORINVAR.
                  *
                  * Request JSOP_POP here since the var is for a simple
                  * name (it is not a destructuring binding's left-hand
                  * side) and it has an initializer.
                  */
                 pn1->pn_xflags &= ~PNX_FORINVAR;
                 pn1->pn_xflags |= PNX_POPVAR;
                 pnseq->initList(pn1);
+                pn1 = NULL;
 
 #if JS_HAS_DESTRUCTURING
                 if (pn2->isKind(PNK_ASSIGN)) {
                     pn2 = pn2->pn_left;
                     JS_ASSERT(pn2->isKind(PNK_RB) || pn2->isKind(PNK_RC) ||
                               pn2->isKind(PNK_NAME));
                 }
 #endif
-                pn1 = NULL;
+                pnseq->append(pn);
+                forParent = pnseq;
             }
-
-            /*
-             * pn2 is part of a declaration. Make a copy that can be passed to
-             * EmitAssignment.
-             */
-            pn2 = CloneLeftHandSide(pn2, tc);
-            if (!pn2)
-                return NULL;
         } else {
             /* Not a declaration. */
+            JS_ASSERT(!blockObj);
             pn2 = pn1;
             pn1 = NULL;
 
             if (!setAssignmentLhsOps(pn2, JSOP_NOP))
                 return NULL;
         }
 
+        pn3 = expr();
+        if (!pn3)
+            return NULL;
+
+        if (blockObj) {
+            /*
+             * Now that the pn3 has been parsed, push the let scope. To hold
+             * the blockObj for the emitter, wrap the TOK_LEXICALSCOPE node
+             * created by PushLetScope around the for's initializer. This also
+             * serves to indicate the let-decl to the emitter.
+             */
+            ParseNode *block = PushLetScope(context, tc, blockObj, &letStmt);
+            if (!block)
+                return NULL;
+            letStmt.flags |= SIF_FOR_BLOCK;
+            block->pn_expr = pn1;
+            pn1 = block;
+        }
+
+        if (forDecl) {
+            /*
+             * pn2 is part of a declaration. Make a copy that can be passed to
+             * EmitAssignment. Take care to do this after PushLetScope has
+             * Define's the new binding since this pn2->isDefn() which tells
+             * CloneLeftHandSide to make the new pn2 a use.
+             */
+            pn2 = CloneLeftHandSide(pn2, tc);
+            if (!pn2)
+                return NULL;
+        }
+
         switch (pn2->getKind()) {
           case PNK_NAME:
             /* Beware 'for (arguments in ...)' with or without a 'var'. */
             NoteLValue(context, pn2, tc, dflag);
             break;
 
 #if JS_HAS_DESTRUCTURING
           case PNK_ASSIGN:
@@ -3288,102 +3413,99 @@ Parser::forStatement()
                     pn->pn_iflags |= JSITER_FOREACH | JSITER_KEYVALUE;
             }
             break;
 #endif
 
           default:;
         }
 
-        /*
-         * Parse the object expression as the right operand of 'in', first
-         * removing the top statement from the statement-stack if this is a
-         * 'for (let x in y)' loop.
-         */
-#if JS_HAS_BLOCK_SCOPE
-        StmtInfo *save = tc->topStmt;
-        if (let)
-            tc->topStmt = save->down;
-#endif
-        pn3 = expr();
-        if (!pn3)
-            return NULL;
-#if JS_HAS_BLOCK_SCOPE
-        if (let)
-            tc->topStmt = save;
-#endif
-
-        pn4 = TernaryNode::create(PNK_FORIN, tc);
-        if (!pn4)
+        forHead = TernaryNode::create(PNK_FORIN, tc);
+        if (!forHead)
             return NULL;
     } else {
+        if (blockObj) {
+            /*
+             * Desugar 'for (let A; B; C) D' into 'let (A) { for (; B; C) D }'
+             * to induce the correct scoping for A.
+             */
+            ParseNode *block = PushLetScope(context, tc, blockObj, &letStmt);
+            if (!block)
+                return NULL;
+            letStmt.flags |= SIF_FOR_BLOCK;
+
+            ParseNode *let = new_<BinaryNode>(PNK_LET, JSOP_NOP, pos, pn1, block);
+            if (!let)
+                return NULL;
+
+            pn1 = NULL;
+            block->pn_expr = pn;
+            forParent = let;
+        }
+
         if (pn->pn_iflags & JSITER_FOREACH) {
             reportErrorNumber(pn, JSREPORT_ERROR, JSMSG_BAD_FOR_EACH_LOOP);
             return NULL;
         }
         pn->setOp(JSOP_NOP);
 
         /* Parse the loop condition or null into pn2. */
         MUST_MATCH_TOKEN(TOK_SEMI, JSMSG_SEMI_AFTER_FOR_INIT);
-        TokenKind tt = tokenStream.peekToken(TSF_OPERAND);
-        if (tt == TOK_SEMI) {
+        if (tokenStream.peekToken(TSF_OPERAND) == TOK_SEMI) {
             pn2 = NULL;
         } else {
             pn2 = expr();
             if (!pn2)
                 return NULL;
         }
 
         /* Parse the update expression or null into pn3. */
         MUST_MATCH_TOKEN(TOK_SEMI, JSMSG_SEMI_AFTER_FOR_COND);
-        tt = tokenStream.peekToken(TSF_OPERAND);
-        if (tt == TOK_RP) {
+        if (tokenStream.peekToken(TSF_OPERAND) == TOK_RP) {
             pn3 = NULL;
         } else {
             pn3 = expr();
             if (!pn3)
                 return NULL;
         }
 
-        pn4 = TernaryNode::create(PNK_FORHEAD, tc);
-        if (!pn4)
+        forHead = TernaryNode::create(PNK_FORHEAD, tc);
+        if (!forHead)
             return NULL;
     }
-    pn4->pn_pos = pos;
-    pn4->setOp(JSOP_NOP);
-    pn4->pn_kid1 = pn1;
-    pn4->pn_kid2 = pn2;
-    pn4->pn_kid3 = pn3;
-    pn->pn_left = pn4;
+
+    forHead->pn_pos = pos;
+    forHead->setOp(JSOP_NOP);
+    forHead->pn_kid1 = pn1;
+    forHead->pn_kid2 = pn2;
+    forHead->pn_kid3 = pn3;
+    pn->pn_left = forHead;
 
     MUST_MATCH_TOKEN(TOK_RP, JSMSG_PAREN_AFTER_FOR_CTRL);
 
-    /* Parse the loop body into pn->pn_right. */
-    pn2 = statement();
-    if (!pn2)
+    /* Parse the loop body. */
+    ParseNode *body = statement();
+    if (!body)
         return NULL;
-    pn->pn_right = pn2;
 
     /* Record the absolute line number for source note emission. */
-    pn->pn_pos.end = pn2->pn_pos.end;
+    pn->pn_pos.end = body->pn_pos.end;
+    pn->pn_right = body;
+
+    if (forParent) {
+        forParent->pn_pos.begin = pn->pn_pos.begin;
+        forParent->pn_pos.end = pn->pn_pos.end;
+    }
 
 #if JS_HAS_BLOCK_SCOPE
-    if (pnlet) {
+    if (blockObj)
         PopStatement(tc);
-        pnlet->pn_expr = pn;
-        pn = pnlet;
-    }
 #endif
-    if (pnseq) {
-        pnseq->pn_pos.end = pn->pn_pos.end;
-        pnseq->append(pn);
-        pn = pnseq;
-    }
     PopStatement(tc);
-    return pn;
+    return forParent ? forParent : pn;
 }
 
 ParseNode *
 Parser::tryStatement()
 {
     JS_ASSERT(tokenStream.isCurrentTokenType(TOK_TRY));
 
     /*
@@ -3437,17 +3559,17 @@ Parser::tryStatement()
                 reportErrorNumber(NULL, JSREPORT_ERROR, JSMSG_CATCH_AFTER_GENERAL);
                 return NULL;
             }
 
             /*
              * Create a lexical scope node around the whole catch clause,
              * including the head.
              */
-            pnblock = PushLexicalScope(context, &tokenStream, tc, &stmtInfo);
+            pnblock = PushLexicalScope(context, tc, &stmtInfo);
             if (!pnblock)
                 return NULL;
             stmtInfo.type = STMT_CATCH;
 
             /*
              * Legal catch forms are:
              *   catch (lhs)
              *   catch (lhs if <boolean_expression>)
@@ -3460,37 +3582,35 @@ Parser::tryStatement()
             pnblock->pn_expr = pn2;
             MUST_MATCH_TOKEN(TOK_LP, JSMSG_PAREN_BEFORE_CATCH);
 
             /*
              * Contrary to ECMA Ed. 3, the catch variable is lexically
              * scoped, not a property of a new Object instance.  This is
              * an intentional change that anticipates ECMA Ed. 4.
              */
-            data.pn = NULL;
-            data.op = JSOP_NOP;
-            data.binder = BindLet;
-            data.let.overflow = JSMSG_TOO_MANY_CATCH_VARS;
+            data.initLet(HoistVars, tc->blockChain, JSMSG_TOO_MANY_CATCH_VARS);
+            JS_ASSERT(data.let.blockObj && data.let.blockObj == pnblock->pn_objbox->object);
 
             tt = tokenStream.getToken();
             ParseNode *pn3;
             switch (tt) {
 #if JS_HAS_DESTRUCTURING
               case TOK_LB:
               case TOK_LC:
                 pn3 = destructuringExpr(&data, tt);
                 if (!pn3)
                     return NULL;
                 break;
 #endif
 
               case TOK_NAME:
               {
                 JSAtom *label = tokenStream.currentToken().name();
-                pn3 = NewBindingNode(label, tc, true);
+                pn3 = NewBindingNode(label, tc);
                 if (!pn3)
                     return NULL;
                 data.pn = pn3;
                 if (!data.binder(context, &data, label, tc))
                     return NULL;
                 break;
               }
 
@@ -3606,22 +3726,26 @@ Parser::withStatement()
 #if JS_HAS_BLOCK_SCOPE
 ParseNode *
 Parser::letStatement()
 {
     ParseNode *pn;
     do {
         /* Check for a let statement or let expression. */
         if (tokenStream.peekToken() == TOK_LP) {
-            pn = letBlock(JS_TRUE);
-            if (!pn || pn->isOp(JSOP_LEAVEBLOCK))
+            pn = letBlock(LetStatement);
+            if (!pn)
+                return NULL;
+
+            JS_ASSERT(pn->isKind(PNK_LET) || pn->isKind(PNK_SEMI));
+            if (pn->isKind(PNK_LET) && pn->pn_expr->getOp() == JSOP_LEAVEBLOCK)
                 return pn;
 
             /* Let expressions require automatic semicolon insertion. */
-            JS_ASSERT(pn->isKind(PNK_SEMI) || pn->isOp(JSOP_LEAVEBLOCKEXPR));
+            JS_ASSERT(pn->isKind(PNK_SEMI) || pn->isOp(JSOP_NOP));
             break;
         }
 
         /*
          * This is a let declaration. We must be directly under a block per the
          * proposed ES4 specs, but not an implicit block created due to
          * 'for (let ...)'. If we pass this error test, make the enclosing
          * StmtInfo be our scope. Further let declarations in this block will
@@ -3641,17 +3765,17 @@ Parser::letStatement()
         if (stmt && (stmt->flags & SIF_SCOPE)) {
             JS_ASSERT(tc->blockChain == stmt->blockObj);
         } else {
             if (!stmt || (stmt->flags & SIF_BODY_BLOCK)) {
                 /*
                  * ES4 specifies that let at top level and at body-block scope
                  * does not shadow var, so convert back to var.
                  */
-                pn = variables(PNK_VAR, false);
+                pn = variables(PNK_VAR);
                 if (!pn)
                     return NULL;
                 pn->pn_xflags |= PNX_POPVAR;
                 break;
             }
 
             /*
              * Some obvious assertions here, but they may help clarify the
@@ -3702,17 +3826,17 @@ Parser::letStatement()
             pn1->setOp(JSOP_LEAVEBLOCK);
             pn1->pn_pos = tc->blockNode->pn_pos;
             pn1->pn_objbox = blockbox;
             pn1->pn_expr = tc->blockNode;
             pn1->pn_blockid = tc->blockNode->pn_blockid;
             tc->blockNode = pn1;
         }
 
-        pn = variables(PNK_LET, false);
+        pn = variables(PNK_LET, tc->blockChain, HoistVars);
         if (!pn)
             return NULL;
         pn->pn_xflags = PNX_POPVAR;
     } while (0);
 
     /* Check termination of this primitive statement. */
     return MatchOrInsertSemicolon(context, &tokenStream) ? pn : NULL;
 }
@@ -4034,26 +4158,26 @@ Parser::statement()
         }
         break;
       }
 
       case TOK_WITH:
         return withStatement();
 
       case TOK_VAR:
-        pn = variables(PNK_VAR, false);
+        pn = variables(PNK_VAR);
         if (!pn)
             return NULL;
 
         /* Tell js_EmitTree to generate a final POP. */
         pn->pn_xflags |= PNX_POPVAR;
         break;
 
       case TOK_CONST:
-        pn = variables(PNK_CONST, false);
+        pn = variables(PNK_CONST);
         if (!pn)
             return NULL;
 
         /* Tell js_EmitTree to generate a final POP. */
         pn->pn_xflags |= PNX_POPVAR;
         break;
 
 #if JS_HAS_BLOCK_SCOPE
@@ -4147,65 +4271,50 @@ Parser::statement()
       default:
         return expressionStatement();
     }
 
     /* Check termination of this primitive statement. */
     return MatchOrInsertSemicolon(context, &tokenStream) ? pn : NULL;
 }
 
+/*
+ * The 'blockObj' parameter is non-null when parsing the 'vars' in a let
+ * expression, block statement, non-top-level let declaration in statement
+ * context, and the let-initializer of a for-statement.
+ */
 ParseNode *
-Parser::variables(ParseNodeKind kind, bool inLetHead)
+Parser::variables(ParseNodeKind kind, JSObject *blockObj, VarContext varContext)
 {
     /*
      * The four options here are:
      * - PNK_VAR:   We're parsing var declarations.
      * - PNK_CONST: We're parsing const declarations.
      * - PNK_LET:   We are parsing a let declaration.
      * - PNK_LP:    We are parsing the head of a let block.
      */
     JS_ASSERT(kind == PNK_VAR || kind == PNK_CONST || kind == PNK_LET || kind == PNK_LP);
 
-    bool let = (kind == PNK_LET || kind == PNK_LP);
-
-#if JS_HAS_BLOCK_SCOPE
-    bool popScope = (inLetHead || (let && (tc->flags & TCF_IN_FOR_INIT)));
-    StmtInfo *save = tc->topStmt, *saveScope = tc->topScopeStmt;
-#endif
-
-    /* Make sure that statement set up the tree context correctly. */
-    StmtInfo *scopeStmt = tc->topScopeStmt;
-    if (let) {
-        while (scopeStmt && !(scopeStmt->flags & SIF_SCOPE)) {
-            JS_ASSERT(!STMT_MAYBE_SCOPE(scopeStmt));
-            scopeStmt = scopeStmt->downScope;
-        }
-        JS_ASSERT(scopeStmt);
-    }
-
-    BindData data;
-    data.op = let ? JSOP_NOP : kind == PNK_VAR ? JSOP_DEFVAR : JSOP_DEFCONST;
     ParseNode *pn = ListNode::create(kind, tc);
     if (!pn)
         return NULL;
-    pn->setOp(data.op);
+
+    pn->setOp(blockObj ? JSOP_NOP : kind == PNK_VAR ? JSOP_DEFVAR : JSOP_DEFCONST);
     pn->makeEmpty();
 
     /*
      * SpiderMonkey const is really "write once per initialization evaluation"
      * var, whereas let is block scoped. ES-Harmony wants block-scoped const so
      * this code will change soon.
      */
-    if (let) {
-        JS_ASSERT(tc->blockChain == scopeStmt->blockObj);
-        data.binder = BindLet;
-        data.let.overflow = JSMSG_TOO_MANY_LOCALS;
-    } else {
-        data.binder = BindVarOrConst;
-    }
+    BindData data;
+    if (blockObj)
+        data.initLet(varContext, blockObj, JSMSG_TOO_MANY_LOCALS);
+    else
+        data.initVarOrConst(pn->getOp());
 
     ParseNode *pn2;
     do {
         TokenKind tt = tokenStream.getToken();
 #if JS_HAS_DESTRUCTURING
         if (tt == TOK_LB || tt == TOK_LC) {
             tc->flags |= TCF_DECL_DESTRUCTURING;
             pn2 = primaryExpr(tt, JS_FALSE);
@@ -4218,30 +4327,17 @@ Parser::variables(ParseNodeKind kind, bo
             if ((tc->flags & TCF_IN_FOR_INIT) && tokenStream.peekToken() == TOK_IN) {
                 pn->append(pn2);
                 continue;
             }
 
             MUST_MATCH_TOKEN(TOK_ASSIGN, JSMSG_BAD_DESTRUCT_DECL);
             JS_ASSERT(tokenStream.currentToken().t_op == JSOP_NOP);
 
-#if JS_HAS_BLOCK_SCOPE
-            if (popScope) {
-                tc->topStmt = save->down;
-                tc->topScopeStmt = saveScope->downScope;
-            }
-#endif
             ParseNode *init = assignExpr();
-#if JS_HAS_BLOCK_SCOPE
-            if (popScope) {
-                tc->topStmt = save;
-                tc->topScopeStmt = saveScope;
-            }
-#endif
-
             if (!init)
                 return NULL;
             UndominateInitializers(pn2, init->pn_pos.end, tc);
 
             pn2 = ParseNode::newBinaryOrAppend(PNK_ASSIGN, JSOP_NOP, pn2, init, tc);
             if (!pn2)
                 return NULL;
             pn->append(pn2);
@@ -4251,42 +4347,30 @@ Parser::variables(ParseNodeKind kind, bo
 
         if (tt != TOK_NAME) {
             if (tt != TOK_ERROR)
                 reportErrorNumber(NULL, JSREPORT_ERROR, JSMSG_NO_VARIABLE_NAME);
             return NULL;
         }
 
         PropertyName *name = tokenStream.currentToken().name();
-        pn2 = NewBindingNode(name, tc, let);
+        pn2 = NewBindingNode(name, tc, blockObj, varContext);
         if (!pn2)
             return NULL;
         if (data.op == JSOP_DEFCONST)
             pn2->pn_dflags |= PND_CONST;
         data.pn = pn2;
         if (!data.binder(context, &data, name, tc))
             return NULL;
         pn->append(pn2);
 
         if (tokenStream.matchToken(TOK_ASSIGN)) {
             JS_ASSERT(tokenStream.currentToken().t_op == JSOP_NOP);
 
-#if JS_HAS_BLOCK_SCOPE
-            if (popScope) {
-                tc->topStmt = save->down;
-                tc->topScopeStmt = saveScope->downScope;
-            }
-#endif
             ParseNode *init = assignExpr();
-#if JS_HAS_BLOCK_SCOPE
-            if (popScope) {
-                tc->topStmt = save;
-                tc->topScopeStmt = saveScope;
-            }
-#endif
             if (!init)
                 return NULL;
 
             if (pn2->isUsed()) {
                 pn2 = MakeAssignment(pn2, init, tc);
                 if (!pn2)
                     return NULL;
             } else {
@@ -4305,17 +4389,17 @@ Parser::variables(ParseNodeKind kind, bo
 
             NoteLValue(context, pn2, tc, data.fresh ? PND_INITIALIZED : PND_ASSIGNED);
 
             /* The declarator's position must include the initializer. */
             pn2->pn_pos.end = init->pn_pos.end;
 
             if (tc->inFunction() && name == context->runtime->atomState.argumentsAtom) {
                 tc->noteArgumentsUse(pn2);
-                if (!let)
+                if (!blockObj)
                     tc->flags |= TCF_FUN_HEAVYWEIGHT;
             }
         }
     } while (tokenStream.matchToken(TOK_COMMA));
 
     pn->pn_pos.end = pn->last()->pn_pos.end;
     return pn;
 }
@@ -5249,17 +5333,17 @@ Parser::comprehensionTail(ParseNode *kid
     JS_ASSERT(tokenStream.currentToken().type == TOK_FOR);
 
     if (kind == PNK_SEMI) {
         /*
          * Generator expression desugars to an immediately applied lambda that
          * yields the next value from a for-in loop (possibly nested, and with
          * optional if guard). Make pn be the TOK_LC body node.
          */
-        pn = PushLexicalScope(context, &tokenStream, tc, &stmtInfo);
+        pn = PushLexicalScope(context, tc, &stmtInfo);
         if (!pn)
             return NULL;
         adjust = pn->pn_blockid - blockid;
     } else {
         JS_ASSERT(kind == PNK_ARRAYPUSH);
 
         /*
          * Make a parse-node and literal object representing the block scope of
@@ -5269,37 +5353,35 @@ Parser::comprehensionTail(ParseNode *kid
          * here, by calling PushLexicalScope.
          *
          * In the case of a comprehension expression that has nested blocks
          * (e.g., let expressions), we will allocate a higher blockid but then
          * slide all blocks "to the right" to make room for the comprehension's
          * block scope.
          */
         adjust = tc->blockid();
-        pn = PushLexicalScope(context, &tokenStream, tc, &stmtInfo);
+        pn = PushLexicalScope(context, tc, &stmtInfo);
         if (!pn)
             return NULL;
 
         JS_ASSERT(blockid <= pn->pn_blockid);
         JS_ASSERT(blockid < tc->blockidGen);
         JS_ASSERT(tc->bodyid < blockid);
         pn->pn_blockid = stmtInfo.blockid = blockid;
         JS_ASSERT(adjust < blockid);
         adjust = blockid - adjust;
     }
 
     pnp = &pn->pn_expr;
 
     CompExprTransplanter transplanter(kid, tc, kind == PNK_SEMI, adjust);
     transplanter.transplant(kid);
 
-    data.pn = NULL;
-    data.op = JSOP_NOP;
-    data.binder = BindLet;
-    data.let.overflow = JSMSG_ARRAY_INIT_TOO_BIG;
+    JS_ASSERT(tc->blockChain && tc->blockChain == pn->pn_objbox->object);
+    data.initLet(HoistVars, tc->blockChain, JSMSG_ARRAY_INIT_TOO_BIG);
 
     do {
         /*
          * FOR node is binary, left is loop control and right is body.  Use
          * index to count each block-local let-variable on the left-hand side
          * of the IN.
          */
         pn2 = BinaryNode::create(PNK_FOR, tc);
@@ -5337,17 +5419,17 @@ Parser::comprehensionTail(ParseNode *kid
 
             /*
              * Create a name node with pn_op JSOP_NAME.  We can't set pn_op to
              * JSOP_GETLOCAL here, because we don't yet know the block's depth
              * in the operand stack frame.  The code generator computes that,
              * and it tries to bind all names to slots, so we must let it do
              * the deed.
              */
-            pn3 = NewBindingNode(name, tc, true);
+            pn3 = NewBindingNode(name, tc);
             if (!pn3)
                 return NULL;
             break;
 
           default:
             reportErrorNumber(NULL, JSREPORT_ERROR, JSMSG_NO_VARIABLE_NAME);
 
           case TOK_ERROR:
@@ -6490,50 +6572,16 @@ Parser::parseXMLText(JSObject *chain, bo
     }
     tokenStream.setXMLOnlyMode(false);
 
     return pn;
 }
 
 #endif /* JS_HAS_XMLSUPPORT */
 
-#if JS_HAS_BLOCK_SCOPE
-/*
- * Check whether blockid is an active scoping statement in tc. This code is
- * necessary to qualify tc->decls.lookup() hits in primaryExpr's TOK_NAME case
- * (below) where the hits come from Scheme-ish let bindings in for loop heads
- * and let blocks and expressions (not let declarations).
- *
- * Unlike let declarations ("let as the new var"), which is a kind of letrec
- * due to hoisting, let in a for loop head, let block, or let expression acts
- * like Scheme's let: initializers are evaluated without the new let bindings
- * being in scope.
- *
- * Name binding analysis is eager with fixups, rather than multi-pass, and let
- * bindings push on the front of the tc->decls AtomDecls (either the singular
- * list or on a hash chain -- see JSAtomMultiList::add*) in order to shadow
- * outer scope bindings of the same name.
- *
- * This simplifies binding lookup code at the price of a linear search here,
- * but only if code uses let (var predominates), and even then this function's
- * loop iterates more than once only in crazy cases.
- */
-static inline bool
-BlockIdInScope(uintN blockid, TreeContext *tc)
-{
-    if (blockid > tc->blockid())
-        return false;
-    for (StmtInfo *stmt = tc->topScopeStmt; stmt; stmt = stmt->downScope) {
-        if (stmt->blockid == blockid)
-            return true;
-    }
-    return false;
-}
-#endif
-
 static ParseNode *
 PrimaryExprNode(ParseNodeKind kind, JSOp op, TreeContext *tc)
 {
     ParseNode *pn = NullaryNode::create(kind, tc);
     if (!pn)
         return NULL;
     pn->setOp(op);
     return pn;
@@ -6886,17 +6934,17 @@ Parser::primaryExpr(TokenKind tt, JSBool
 
       end_obj_init:
         pn->pn_pos.end = tokenStream.currentToken().pos.end;
         return pn;
       }
 
 #if JS_HAS_BLOCK_SCOPE
       case TOK_LET:
-        pn = letBlock(JS_FALSE);
+        pn = letBlock(LetExpresion);
         if (!pn)
             return NULL;
         break;
 #endif
 
 #if JS_HAS_SHARP_VARS
       case TOK_DEFSHARP:
         pn = UnaryNode::create(PNK_DEFSHARP, tc);
@@ -7043,36 +7091,18 @@ Parser::primaryExpr(TokenKind tt, JSBool
             if (!tc->inFunction() &&
                 pn->pn_atom == context->runtime->atomState.argumentsAtom) {
                 tc->countArgumentsUse(pn);
             }
 
             StmtInfo *stmt = LexicalLookup(tc, pn->pn_atom, NULL);
 
             MultiDeclRange mdl = tc->decls.lookupMulti(pn->pn_atom);
+
             Definition *dn;
-
-            if (!mdl.empty()) {
-                dn = mdl.front();
-#if JS_HAS_BLOCK_SCOPE
-                /*
-                 * Skip out-of-scope let bindings along an ALE list or hash
-                 * chain. These can happen due to |let (x = x) x| block and
-                 * expression bindings, where the x on the right of = comes
-                 * from an outer scope. See bug 496532.
-                 */
-                while (dn->isLet() && !BlockIdInScope(dn->pn_blockid, tc)) {
-                    mdl.popFront();
-                    if (mdl.empty())
-                        break;
-                    dn = mdl.front();
-                }
-#endif
-            }
-
             if (!mdl.empty()) {
                 dn = mdl.front();
             } else {
                 AtomDefnAddPtr p = tc->lexdeps->lookupForAdd(pn->pn_atom);
                 if (p) {
                     dn = p.value();
                 } else {
                     /*
--- a/js/src/frontend/Parser.h
+++ b/js/src/frontend/Parser.h
@@ -56,16 +56,18 @@
 
 #define NUM_TEMP_FREELISTS      6U      /* 32 to 2048 byte size classes (32 bit) */
 
 typedef struct BindData BindData;
 
 namespace js {
 
 enum FunctionSyntaxKind { Expression, Statement };
+enum LetContext { LetExpresion, LetStatement };
+enum VarContext { HoistVars, DontHoistVars };
 
 struct Parser : private AutoGCRooter
 {
     JSContext           *const context; /* FIXME Bug 551291: use AutoGCRooter::context? */
     void                *tempFreeList[NUM_TEMP_FREELISTS];
     TokenStream         tokenStream;
     void                *tempPoolMark;  /* initial JSContext.tempLifoAlloc mark */
     JSPrincipals        *principals;    /* principals associated with source */
@@ -191,17 +193,17 @@ struct Parser : private AutoGCRooter
     ParseNode *switchStatement();
     ParseNode *forStatement();
     ParseNode *tryStatement();
     ParseNode *withStatement();
 #if JS_HAS_BLOCK_SCOPE
     ParseNode *letStatement();
 #endif
     ParseNode *expressionStatement();
-    ParseNode *variables(ParseNodeKind kind, bool inLetHead);
+    ParseNode *variables(ParseNodeKind kind, JSObject *blockObj = NULL, VarContext varContext = HoistVars);
     ParseNode *expr();
     ParseNode *assignExpr();
     ParseNode *condExpr1();
     ParseNode *orExpr1();
     ParseNode *andExpr1i();
     ParseNode *andExpr1n();
     ParseNode *bitOrExpr1i();
     ParseNode *bitOrExpr1n();
@@ -235,17 +237,17 @@ struct Parser : private AutoGCRooter
     ParseNode *unaryOpExpr(ParseNodeKind kind, JSOp op);
 
     ParseNode *condition();
     ParseNode *comprehensionTail(ParseNode *kid, uintN blockid, bool isGenexp,
                                  ParseNodeKind kind = PNK_SEMI, JSOp op = JSOP_NOP);
     ParseNode *generatorExpr(ParseNode *kid);
     JSBool argumentList(ParseNode *listNode);
     ParseNode *bracketedExpr();
-    ParseNode *letBlock(JSBool statement);
+    ParseNode *letBlock(LetContext letContext);
     ParseNode *returnOrYield(bool useAssignExpr);
     ParseNode *destructuringExpr(BindData *data, TokenKind tt);
 
 #if JS_HAS_XML_SUPPORT
     ParseNode *endBracketedExpr();
 
     ParseNode *propertySelector();
     ParseNode *qualifiedSuffix(ParseNode *pn);
--- a/js/src/jit-test/tests/basic/bug657975.js
+++ b/js/src/jit-test/tests/basic/bug657975.js
@@ -55,17 +55,17 @@ for (a in f8())
   (function() {})()
 
 // bug 659043
 f9 = (function() {
   for (let a = 0; a < 0; ++a) {
     for each(let w in []) {}
   }
 })
-trap(f9, 22, undefined);
+trap(f9, 23, undefined);
 for (b in f9())
   (function() {})()
 
 // bug 659233
 f10 = (function() {
     while (h) {
         continue
     }
--- a/js/src/jit-test/tests/basic/testBug666292.js
+++ b/js/src/jit-test/tests/basic/testBug666292.js
@@ -1,14 +1,14 @@
 // |jit-test| debug
 
 function f(){
   this.zzz.zzz;
   for(let d in []);
 }
-trap(f, 18, '')
+trap(f, 16, '')
 try {
     f()
 } catch(e) {
     caught = true;
     assertEq(""+e, "TypeError: this.zzz is undefined");
 }
 assertEq(caught, true);
--- a/js/src/jit-test/tests/basic/testBug683470.js
+++ b/js/src/jit-test/tests/basic/testBug683470.js
@@ -6,10 +6,10 @@ f = (function() {
     Object.defineProperty(this, "x", ({}));
   }
   for each(let d in [0, 0]) {
     try {
       b(d);
     } catch (e) {}
   }
 })
-trap(f, 39, undefined);
+trap(f, 40, undefined);
 f()
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/testBug692274-1.js
@@ -0,0 +1,6 @@
+// |jit-test| debug; error: TypeError
+function f() {
+    ""(this.z)
+}
+trap(f, 0, '')
+f()
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/testBug692274-2.js
@@ -0,0 +1,7 @@
+function f() {
+    var ss = [new f("abc"), new String("foobar"), new String("quux")];
+    for (let a6 = this ;; ) {}
+}
+try {
+    f();
+} catch (e) {}
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/testBug692274-3.js
@@ -0,0 +1,16 @@
+var x = -false;
+switch(x) {
+    case 11: 
+    let y = 42;
+}
+switch(x) {
+    case 11: 
+    let y = 42;
+    let z = 'ponies';
+}
+switch(x) {
+    case 11: 
+    let y = 42;
+    let z = 'ponies';
+    let a = false;
+}
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/testBug692274-4.js
@@ -0,0 +1,4 @@
+// |jit-test| error: TypeError
+var obj = {};
+let ([] = print) 3; 
+let ( i = "a" ) new i [ obj[i] ];
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/testBug703857.js
@@ -0,0 +1,12 @@
+Function.prototype.X = 42;
+function ownProperties() {
+    var props = {};
+    var r = function () {};
+    for (var a in r) {
+        let (a = function() { for (var r=0;r<6;++r) ++a; }) {
+            a();
+        }
+        props[a] = true;
+    }
+}
+ownProperties();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/testBug709633.js
@@ -0,0 +1,9 @@
+test();
+function test() {
+  var f;
+  f = function() { (let(x) {y: z}) }
+    let (f = function() {
+        for (var t=0;t<6;++t) ++f;
+    }) { f(); } //  {  }       
+  actual = f + '';
+}
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/testLet.js
@@ -0,0 +1,342 @@
+function test(str, arg, result)
+{
+    arg = arg || 'ponies';
+    result = result || 'ponies';
+
+    var fun = new Function('x', str);
+
+    var got = fun.toSource().replace(/\n/g,'');
+    var expect = '(function anonymous(x) {' + str + '})';
+    if (got !== expect) {
+        print("GOT:    " + got);
+        print("EXPECT: " + expect);
+        assertEq(got, expect);
+    }
+
+    Reflect.parse(got);
+
+    var got = fun(arg);
+    var expect = result;
+    if (got !== expect) {
+        print("GOT:" + got);
+        print("EXPECT: " + expect);
+        assertEq(got, expect);
+    }
+}
+
+function isError(str)
+{
+    var caught = false;
+    try {
+        new Function(str);
+    } catch(e) {
+        assertEq(String(e).indexOf('TypeError') == 0 || String(e).indexOf('SyntaxError') == 0, true);
+        caught = true;
+    }
+    assertEq(caught, true);
+}
+
+// let expr
+test('return let (y) x;');
+test('return let (x) "" + x;', 'unicorns', 'undefined');
+test('return let (y = x) (y++, "" + y);', 'unicorns', 'NaN');
+test('return let (y = 1) (y = x, y);');
+test('return let ([] = x) x;');
+test('return let (x = {a: x}) x.a;');
+test('return let ({a: x} = {a: x}) x;');
+test('return let ([x] = {0: x}) x;');
+test('return let ({0: x} = [x]) x;');
+test('return let ({0: []} = []) x;');
+test('return let ([, ] = x) x;');
+test('return let ([, , , , ] = x) x;');
+test('return let ([[]] = x) x;');
+test('return let ([[[[[[[[[[[[[]]]]]]]]]]]]] = x) x;');
+test('return let ([[], []] = x) x;');
+test('return let ([[[[]]], [], , [], [[]]] = x) x;');
+test('return let ({x: []} = x) x;');
+test('return let ({x: [], y: {x: []}} = x) "ponies";', {y:{}});
+test('return let ({x: []} = x, [{x: []}] = x) "ponies";');
+test('return let (x = x) x;');
+test('return let (x = eval("x")) x;');
+test('return let (x = (let (x = x + 1) x) + 1) x;', 1, 3);
+test('return let (x = (let (x = eval("x") + 1) eval("x")) + 1) eval("x");', 1, 3);
+test('return let (x = x + 1, y = x) y;');
+test('return let (x = x + 1, [] = x, [[, , ]] = x, y = x) y;');
+test('return let ([{a: x}] = x, [, {b: y}] = x) let (x = x + 1, y = y + 2) x + y;', [{a:"p"},{b:"p"}], "p1p2");
+test('return let ([] = []) x;');
+test('return let ([] = [x]) x;');
+test('return let ([x] = [x]) x;');
+test('return let ([[a, [b, c]]] = [[x, []]]) a;');
+test('return let ([x, y] = [x, x + 1]) x + y;', 1, 3);
+test('return let ([x, y, z] = [x, x + 1, x + 2]) x + y + z;', 1, 6);
+test('return let ([[x]] = [[x]]) x;');
+test('return let ([x, y] = [x, x + 1]) x;');
+test('return let ([x, [y, z]] = [x, x + 1]) x;');
+test('return let ([{x: [x]}, {y1: y, z1: z}] = [x, x + 1]) x;',{x:['ponies']});
+test('return let (x = (3, x)) x;');
+test('return let (x = x + "s") x;', 'ponie');
+test('return let ([x] = (3, [x])) x;');
+test('return let ([] = [[]] = {}) x;');
+test('return let (y = x) function () {return eval("y");}();');
+test('return eval("let (y = x) y");');
+test('return let (y = x) (eval("var y = 2"), y);', 'ponies', 2);
+test('"use strict";return let (y = x) (eval("var y = 2"), y);');
+test('this.y = x;return let (y = 1) this.eval("y");');
+test('try {let (x = x) eval("throw x");} catch (e) {return e;}');
+test('try {return let (x = eval("throw x")) x;} catch (e) {return e;}');
+isError('let (x = 1, x = 2) x');
+isError('let ([x, y] = a, {a:x} = b) x');
+isError('let ([x, y, x] = a) x');
+isError('let ([x, [y, [x]]] = a) x');
+isError('let (x = function() { return x}) x()return x;');
+isError('(let (x = function() { return x}) x())return x;');
+
+// let block
+test('let (y) {return x;}');
+test('let (y = x) {y++;return "" + y;}', 'unicorns', 'NaN');
+test('let (y = 1) {y = x;return y;}');
+test('let (x) {return "" + x;}', 'unicorns', 'undefined');
+test('let ([] = x) {return x;}');
+test('let (x) {}return x;');
+test('let (x = {a: x}) {return x.a;}');
+test('let ({a: x} = {a: x}) {return x;}');
+test('let ([x] = {0: x}) {return x;}');
+test('let ({0: x} = [x]) {return x;}');
+test('let ({0: []} = []) {return x;}');
+test('let ([, ] = x) {return x;}');
+test('let ([, , , , ] = x) {return x;}');
+test('let ([[]] = x) {return x;}');
+test('let ([[[[[[[[[[[[[]]]]]]]]]]]]] = x) {return x;}');
+test('let ([[], []] = x) {return x;}');
+test('let ([[[[]]], [], , [], [[]]] = x) {return x;}');
+test('let ({x: []} = x) {return x;}');
+test('let ({x: [], y: {x: []}} = x) {return "ponies";}', {y:{}});
+test('let ({x: []} = x, [{x: []}] = x) {return "ponies";}');
+test('let (x = x) {return x;}');
+test('let (x = eval("x")) {return x;}');
+test('let (x = (let (x = x + 1) x) + 1) {return x;}', 1, 3);
+test('let (x = (let (x = eval("x") + 1) eval("x")) + 1) {return eval("x");}', 1, 3);
+test('let (x = x + 1, y = x) {return y;}');
+test('let (x = x + 1, [] = x, [[, , ]] = x, y = x) {return y;}');
+test('let ([{a: x}] = x, [, {b: y}] = x) {let (x = x + 1, y = y + 2) {return x + y;}}', [{a:"p"},{b:"p"}], "p1p2");
+test('let ([] = []) {return x;}');
+test('let ([] = [x]) {return x;}');
+test('let ([x] = [x]) {return x;}');
+test('let ([[a, [b, c]]] = [[x, []]]) {return a;}');
+test('let ([x, y] = [x, x + 1]) {return x + y;}', 1, 3);
+test('let ([x, y, z] = [x, x + 1, x + 2]) {return x + y + z;}', 1, 6);
+test('let ([[x]] = [[x]]) {return x;}');
+test('let ([x, y] = [x, x + 1]) {return x;}');
+test('let ([x, [y, z]] = [x, x + 1]) {return x;}');
+test('let ([{x: [x]}, {y1: y, z1: z}] = [x, x + 1]) {return x;}',{x:['ponies']});
+test('let (y = x[1]) {let (x = x[0]) {try {let (y = "unicorns") {throw y;}} catch (e) {return x + y;}}}', ['pon','ies']);
+test('let (x = x) {try {let (x = "unicorns") eval("throw x");} catch (e) {return x;}}');
+test('let ([] = [[]] = {}) {return x;}');
+test('let (y = x) {return function () {return eval("y");}();}');
+test('return eval("let (y = x) {y;}");');
+test('let (y = x) {eval("var y = 2");return y;}', 'ponies', 2);
+test('"use strict";let (y = x) {eval("var y = 2");return y;}');
+test('this.y = x;let (y = 1) {return this.eval("y");}');
+isError('let (x = 1, x = 2) {x}');
+isError('let ([x, y] = a, {a:x} = b) {x}');
+isError('let ([x, y, x] = a) {x}');
+isError('let ([x, [y, [x]]] = a) {x}');
+
+// var declarations
+test('var y;return x;');
+test('var y = x;return x;');
+test('var [] = x;return x;');
+test('var [, ] = x;return x;');
+test('var [, , , , ] = x;return x;');
+test('var [[]] = x;return x;');
+test('var [[[[[[[[[[[[[]]]]]]]]]]]]] = x;return x;');
+test('var [[], []] = x;return x;');
+test('var [[[[]]], [], , [], [[]]] = x;return x;');
+test('var {x: []} = x;return x;');
+test('var {x: [], y: {x: []}} = x;return "ponies";', {y:{}});
+test('var {x: []} = x, [{x: []}] = x;return "ponies";');
+test('var x = x;return x;');
+test('var y = y;return "" + y;', 'unicorns', 'undefined');
+test('var x = eval("x");return x;');
+test('var x = (let (x = x + 1) x) + 1;return x;', 1, 3);
+test('var x = (let (x = eval("x") + 1) eval("x")) + 1;return eval("x");', 1, 3);
+test('var X = x + 1, y = x;return y;');
+test('var X = x + 1, [] = X, [[, , ]] = X, y = x;return y;');
+test('var [{a: X}] = x, [, {b: y}] = x;var X = X + 1, y = y + 2;return X + y;', [{a:"p"},{b:"p"}], "p1p2");
+test('var [x] = [x];return x;');
+test('var [[a, [b, c]]] = [[x, []]];return a;');
+test('var [y] = [x];return y;');
+test('var [x, y] = [x, x + 1];return x + y;', 1, 3);
+test('var [x, y, z] = [x, x + 1, x + 2];return x + y + z;', 1, 6);
+test('var [[x]] = [[x]];return x;');
+test('var [x, y] = [x, x + 1];return x;');
+test('var [x, [y, z]] = [x, x + 1];return x;');
+test('var [{x: [x]}, {y1: y, z1: z}] = [x, x + 1];return x;',{x:['ponies']});
+test('var [] = [[]] = {};return x;');
+test('if (x) {var y = x;return x;}');
+test('if (x) {y = x;var y = y;return y;}');
+test('if (x) {var z = y;var [y] = x;z += y;}return z;', ['-'], 'undefined-');
+
+// let declaration in context
+test('if (x) {let y;return x;}');
+test('if (x) {let x;return "" + x;}', 'unicorns', 'undefined');
+test('if (x) {let y = x;return x;}');
+test('if (x) {y = x;let y = y;return y;}');
+test('if (x) {var z = y;let [y] = x;z += y;}return z;', ['-'], 'undefined-');
+test('if (x) {let y = x;return x;}');
+test('if (x) {let [] = x;return x;}');
+test('if (x) {let [, ] = x;return x;}');
+test('if (x) {let [, , , , ] = x;return x;}');
+test('if (x) {let [[]] = x;return x;}');
+test('if (x) {let [[[[[[[[[[[[[]]]]]]]]]]]]] = x;return x;}');
+test('if (x) {let [[], []] = x;return x;}');
+test('if (x) {let [[[[]]], [], , [], [[]]] = x;return x;}');
+test('if (x) {let {x: []} = x;return x;}');
+test('if (x) {let {x: [], y: {x: []}} = x;return "ponies";}', {y:{}});
+test('if (x) {let {x: []} = x, [{x: []}] = x;return "ponies";}');
+test('if (x) {let x = x;return "" + x;}', 'unicorns', 'undefined');
+test('if (x) {let y = y;return "" + y;}', 'unicorns', 'undefined');
+test('if (x) {let x = eval("x");return "" + x;}', 'unicorns', 'undefined');
+test('if (x) {let y = (let (x = x + 1) x) + 1;return y;}', 1, 3);
+test('if (x) {let y = (let (x = eval("x") + 1) eval("x")) + 1;return eval("y");}', 1, 3);
+test('if (x) {let X = x + 1, y = x;return y;}');
+test('if (x) {let X = x + 1, [] = X, [[, , ]] = X, y = x;return y;}');
+test('if (x) {let [{a: X}] = x, [, {b: Y}] = x;var XX = X + 1, YY = Y + 2;return XX + YY;}', [{a:"p"},{b:"p"}], "p1p2");
+test('if (x) {let [[a, [b, c]]] = [[x, []]];return a;}');
+test('if (x) {let [X] = [x];return X;}');
+test('if (x) {let [y] = [x];return y;}');
+test('if (x) {let [X, y] = [x, x + 1];return X + y;}', 1, 3);
+test('if (x) {let [X, y, z] = [x, x + 1, x + 2];return X + y + z;}', 1, 6);
+test('if (x) {let [[X]] = [[x]];return X;}');
+test('if (x) {let [X, y] = [x, x + 1];return X;}');
+test('if (x) {let [X, [y, z]] = [x, x + 1];return X;}');
+test('if (x) {let [{x: [X]}, {y1: y, z1: z}] = [x, x + 1];return X;}',{x:['ponies']});
+test('if (x) {let y = x;try {let x = 1;throw 2;} catch (e) {return y;}}');
+test('if (x) {let [] = [[]] = {};return x;}');
+test('let (y, [] = x) {}try {let a = b(), b;} catch (e) {return x;}');
+test('try {let x = 1;throw 2;} catch (e) {return x;}');
+test('let (y = x) {let x;return y;}');
+test('let (y = x) {let x = y;return x;}');
+test('let ([y, z] = x) {let a = x, b = y;return a;}');
+test('let ([y, z] = x, a = x, [] = x) {let b = x, c = y;return a;}');
+test('function f() {return unicorns;}try {let (x = 1) {let a, b;f();}} catch (e) {return x;}');
+test('function f() {return unicorns;}try {let (x = 1) {let a, b;}f();} catch (e) {return x;}');
+test('x.foo;{let y = x;return y;}');
+test('x.foo;if (x) {x.bar;let y = x;return y;}');
+test('if (x) {let y = x;return function () {return eval("y");}();}');
+test('return eval("let y = x; y");');
+test('if (x) {let y = x;eval("var y = 2");return y;}', 'ponies', 2);
+test('"use strict";if (x) {let y = x;eval("var y = 2");return y;}');
+test('"use strict";if (x) {let y = x;eval("let y = 2");return y;}');
+test('"use strict";if (x) {let y = 1;return eval("let y = x;y;");}');
+test('this.y = x;if (x) {let y = 1;return this.eval("y");}');
+isError('if (x) {let (x = 1, x = 2) {x}}');
+isError('if (x) {let ([x, y] = a, {a:x} = b) {x}}');
+isError('if (x) {let ([x, y, x] = a) {x}}');
+isError('if (x) {let ([x, [y, [x]]] = a) {x}}');
+isError('let ([x, y] = x) {let x;}');
+
+// for(;;)
+test('for (;;) {return x;}');
+test('for (let y = 1;;) {return x;}');
+test('for (let y = 1;; ++y) {return x;}');
+test('for (let y = 1; ++y;) {return x;}');
+test('for (let (x = 1) x; x != 1; ++x) {return x;}');
+test('for (let [, {a: [], b: []}] = x, [] = x; x;) {return x;}');
+test('for (let x = 1, [y, z] = x, a = x; z < 4; ++z) {return x + y;}', [2,3], 3);
+test('for (let (x = 1, [{a: b, c: d}] = [{a: 1, c: 2}]) x; x != 1; ++x) {return x;}');
+test('for (let [[a, [b, c]]] = [[x, []]];;) {return a;}');
+test('var sum = 0;for (let y = x; y < 4; ++y) {sum += y;}return sum;', 1, 6);
+test('var sum = 0;for (let x = x, y = 10; x < 4; ++x) {sum += x;}return sum;', 1, 6);
+test('var sum = 0;for (let x = x; x < 4; ++x) {sum += x;}return x;', 1, 1);
+test('var sum = 0;for (let x = eval("x"); x < 4; ++x) {sum += x;}return sum;', 1, 6);
+test('var sum = 0;for (let x = x; eval("x") < 4; ++x) {sum += eval("x");}return sum;', 1, 6);
+test('var sum = 0;for (let x = eval("x"); eval("x") < 4; ++x) {sum += eval("x");}return sum;', 1, 6);
+test('for (var y = 1;;) {return x;}');
+test('for (var y = 1;; ++y) {return x;}');
+test('for (var y = 1; ++y;) {return x;}');
+test('for (var [, {a: [], b: []}] = x, [] = x; x;) {return x;}');
+test('for (var X = 1, [y, z] = x, a = x; z < 4; ++z) {return X + y;}', [2,3], 3);
+test('var sum = 0;for (var y = x; y < 4; ++y) {sum += y;}return sum;', 1, 6);
+test('var sum = 0;for (var X = x, y = 10; X < 4; ++X) {sum += X;}return sum;', 1, 6);
+test('var sum = 0;for (var X = x; X < 4; ++X) {sum += X;}return x;', 1, 1);
+test('var sum = 0;for (var X = eval("x"); X < 4; ++X) {sum += X;}return sum;', 1, 6);
+test('var sum = 0;for (var X = x; eval("X") < 4; ++X) {sum += eval("X");}return sum;', 1, 6);
+test('var sum = 0;for (var X = eval("x"); eval("X") < 4; ++X) {sum += eval("X");}return sum;', 1, 6);
+test('try {for (let x = eval("throw x");;) {}} catch (e) {return e;}');
+test('try {for (let x = x + "s"; eval("throw x");) {}} catch (e) {return e;}', 'ponie');
+test('for (let y = x;;) {let x;return y;}');
+test('for (let y = x;;) {let y;return x;}');
+test('for (let y;;) {let y;return x;}');
+test('for (let a = x;;) {let c = x, d = x;return c;}');
+test('for (let [a, b] = x;;) {let c = x, d = x;return c;}');
+test('for (let [] = [[]] = {};;) {return x;}');
+isError('for (let x = 1, x = 2;;) {}');
+isError('for (let [x, y] = a, {a:x} = b;;) {}');
+isError('for (let [x, y, x] = a;;) {}');
+isError('for (let [x, [y, [x]]] = a;;) {}');
+
+// for(in)
+test('for (let i in x) {return x;}');
+test('for (let i in x) {let y;return x;}');
+test('for each (let [a, b] in x) {let y;return x;}');
+test('for (let i in x) {let (i = x) {return i;}}');
+test('for (let i in x) {let i = x;return i;}');
+test('for each (let [x, y] in x) {return x + y;}', [['ponies', '']]);
+test('for each (let [{0: x, 1: y}, z] in x) {return x + y + z;}', [[['po','nies'], '']]);
+test('var s = "";for (let a in x) {for (let b in x) {s += a + b;}}return s;', [1,2], '00011011');
+test('var res = "";for (let i in x) {res += x[i];}return res;');
+test('var res = "";for (var i in x) {res += x[i];}return res;');
+test('for each (let {x: y, y: x} in [{x: x, y: x}]) {return y;}');
+test('for (let x in eval("x")) {return x;}', {ponies:true});
+test('for (let x in x) {return eval("x");}', {ponies:true});
+test('for (let x in eval("x")) {return eval("x");}', {ponies:true});
+test('for ((let (x = {y: true}) x).y in eval("x")) {return eval("x");}');
+test('for (let i in x) {break;}return x;');
+test('for (let i in x) {break;}return eval("x");');
+test('for (let x in x) {break;}return x;');
+test('for (let x in x) {break;}return eval("x");');
+test('a:for (let i in x) {for (let j in x) {break a;}}return x;');
+test('a:for (let i in x) {for (let j in x) {break a;}}return eval("x");');
+test('var j;for (let i in x) {j = i;break;}return j;', {ponies:true});
+test('try {for (let x in eval("throw x")) {}} catch (e) {return e;}');
+test('try {for each (let x in x) {eval("throw x");}} catch (e) {return e;}', ['ponies']);
+isError('for (let [x, x] in o) {}');
+isError('for (let [x, y, x] in o) {}');
+isError('for (let [x, [y, [x]]] in o) {}');
+
+// genexps
+test('return (i for (i in x)).next();', {ponies:true});
+test('return (eval("i") for (i in x)).next();', {ponies:true});
+test('return (eval("i") for (i in eval("x"))).next();', {ponies:true});
+test('try {return (eval("throw i") for (i in x)).next();} catch (e) {return e;}', {ponies:true});
+
+// array comprehension
+test('return [i for (i in x)][0];', {ponies:true});
+test('return [eval("i") for (i in x)][0];', {ponies:true});
+test('return [eval("i") for (i in eval("x"))][0];', {ponies:true});
+test('try {return [eval("throw i") for (i in x)][0];} catch (e) {return e;}', {ponies:true});
+
+// don't forget about switch craziness
+test('var y = 3;switch (function () {return eval("y");}()) {case 3:let y;return x;default:;}');
+test('switch (x) {case 3:let y;return 3;case 4:let z;return 4;default:return x;}');
+test('switch (x) {case 3:let x;break;default:if (x === undefined) {return "ponies";}}');
+test('switch (x) {case 3:default:let y;let (y = x) {return y;}}');
+isError('switch (x) {case 3:let y;return 3;case 4:let y;return 4;default:;}');
+
+// test weird cases where the decompiler changes tokens
+function testWeird(str, printedAs, arg, result)
+{
+    var fun = new Function('x', str);
+
+    // this is lame and doesn't normalize whitespace so if an assert fails
+    // here, see if its just whitespace and fix the caller
+    assertEq(fun.toSource(), '(function anonymous(x) {' + printedAs + '})');
+
+    test(printedAs, arg, result);
+}
+
+testWeird('let y = x;return x;', 'var y = x;return x;');
+testWeird('let y = 1, y = x;return y;', 'var y = 1, y = x;return y;');
+testWeird('return let ({x:x, y:y} = x) x + y', 'return let ({x, y} = x) x + y;', {x:'pon', y:'ies'});
+testWeird('let ({x:x, y:y} = x) {return x + y;}', 'let ({x, y} = x) {return x + y;}', {x:'pon', y:'ies'});
--- a/js/src/jsanalyze.cpp
+++ b/js/src/jsanalyze.cpp
@@ -114,21 +114,21 @@ ScriptAnalysis::checkAliasedName(JSConte
      * current script, and mark that local/arg as escaping. We don't need to
      * worry about marking locals/arguments in scripts this is nested in, as
      * the escaping name will be caught by the parser and the nested local/arg
      * will be marked as closed.
      */
 
     JSAtom *atom;
     if (JSOp(*pc) == JSOP_DEFFUN) {
-        JSFunction *fun = script->getFunction(js_GetIndexFromBytecode(cx, script, pc, 0));
+        JSFunction *fun = script->getFunction(js_GetIndexFromBytecode(script, pc, 0));
         atom = fun->atom;
     } else {
         JS_ASSERT(JOF_TYPE(js_CodeSpec[*pc].format) == JOF_ATOM);
-        atom = script->getAtom(js_GetIndexFromBytecode(cx, script, pc, 0));
+        atom = script->getAtom(js_GetIndexFromBytecode(script, pc, 0));
     }
 
     uintN index;
     BindingKind kind = script->bindings.lookup(cx, atom, &index);
 
     if (kind == ARGUMENT)
         escapedSlots[ArgSlot(index)] = true;
     else if (kind == VARIABLE)
@@ -384,16 +384,18 @@ ScriptAnalysis::analyzeBytecode(JSContex
             isInlineable = canTrackVars = false;
             break;
 
           case JSOP_ENTERWITH:
             addsScopeObjects_ = true;
             isInlineable = canTrackVars = false;
             break;
 
+          case JSOP_ENTERLET0:
+          case JSOP_ENTERLET1:
           case JSOP_ENTERBLOCK:
           case JSOP_LEAVEBLOCK:
             addsScopeObjects_ = true;
             isInlineable = false;
             break;
 
           case JSOP_THIS:
             usesThisValue_ = true;
--- a/js/src/jsanalyze.h
+++ b/js/src/jsanalyze.h
@@ -198,19 +198,16 @@ class Bytecode
 };
 
 static inline unsigned
 GetDefCount(JSScript *script, unsigned offset)
 {
     JS_ASSERT(offset < script->length);
     jsbytecode *pc = script->code + offset;
 
-    if (js_CodeSpec[*pc].ndefs == -1)
-        return js_GetEnterBlockStackDefs(NULL, script, pc);
-
     /*
      * Add an extra pushed value for OR/AND opcodes, so that they are included
      * in the pushed array of stack values for type inference.
      */
     switch (JSOp(*pc)) {
       case JSOP_OR:
       case JSOP_ORX:
       case JSOP_AND:
@@ -222,30 +219,30 @@ GetDefCount(JSScript *script, unsigned o
         /*
          * 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
          * would pop b, c, and d to rearrange the stack to |a c[0]
          * d[1] b[2]|.
          */
         return (pc[1] + 1);
       default:
-        return js_CodeSpec[*pc].ndefs;
+        return StackDefs(script, pc);
     }
 }
 
 static inline unsigned
 GetUseCount(JSScript *script, unsigned offset)
 {
     JS_ASSERT(offset < script->length);
     jsbytecode *pc = script->code + offset;
 
     if (JSOp(*pc) == JSOP_PICK)
         return (pc[1] + 1);
     if (js_CodeSpec[*pc].nuses == -1)
-        return js_GetVariableStackUses(JSOp(*pc), pc);
+        return StackUses(script, pc);
     return js_CodeSpec[*pc].nuses;
 }
 
 /*
  * For opcodes which assign to a local variable or argument, track an extra def
  * during SSA analysis for the value's use chain and assigned type.
  */
 static inline bool
--- a/js/src/jsinfer.cpp
+++ b/js/src/jsinfer.cpp
@@ -2027,31 +2027,31 @@ TypeCompartment::newAllocationSiteTypeOb
     }
 
     return res;
 }
 
 static inline jsid
 GetAtomId(JSContext *cx, JSScript *script, const jsbytecode *pc, unsigned offset)
 {
-    unsigned index = js_GetIndexFromBytecode(cx, script, (jsbytecode*) pc, offset);
+    unsigned index = js_GetIndexFromBytecode(script, (jsbytecode*) pc, offset);
     return MakeTypeId(cx, ATOM_TO_JSID(script->getAtom(index)));
 }
 
 static inline JSObject *
 GetScriptObject(JSContext *cx, JSScript *script, const jsbytecode *pc, unsigned offset)
 {
-    unsigned index = js_GetIndexFromBytecode(cx, script, (jsbytecode*) pc, offset);
+    unsigned index = js_GetIndexFromBytecode(script, (jsbytecode*) pc, offset);
     return script->getObject(index);
 }
 
 static inline const Value &
 GetScriptConst(JSContext *cx, JSScript *script, const jsbytecode *pc)
 {
-    unsigned index = js_GetIndexFromBytecode(cx, script, (jsbytecode*) pc, 0);
+    unsigned index = js_GetIndexFromBytecode(script, (jsbytecode*) pc, 0);
     return script->getConst(index);
 }
 
 bool
 types::UseNewType(JSContext *cx, JSScript *script, jsbytecode *pc)
 {
     JS_ASSERT(cx->typeInferenceEnabled());
 
@@ -3951,23 +3951,34 @@ ScriptAnalysis::analyzeTypesBytecode(JSC
         }
         state.hasGetSet = false;
         JS_ASSERT(!state.hasHole);
         break;
       }
 
       case JSOP_ENTERWITH:
       case JSOP_ENTERBLOCK:
+      case JSOP_ENTERLET0:
         /*
          * Scope lookups can occur on the values being pushed here. We don't track
          * the value or its properties, and just monitor all name opcodes in the
          * script.
          */
         break;
 
+      case JSOP_ENTERLET1:
+        /*
+         * JSOP_ENTERLET1 enters a let block with an unrelated value on top of
+         * the stack (such as the condition to a switch) whose constraints must
+         * be propagated. The other values are ignored for the same reason as
+         * JSOP_ENTERLET0.
+         */
+        poppedTypes(pc, 0)->addSubset(cx, &pushed[defCount - 1]);
+        break;
+
       case JSOP_ITER: {
         /*
          * Use a per-script type set to unify the possible target types of all
          * 'for in' or 'for each' loops in the script. We need to mark the
          * value pushed by the ITERNEXT appropriately, but don't track the SSA
          * information to connect that ITERNEXT with the appropriate ITER.
          * This loses some precision when a script mixes 'for in' and
          * 'for each' loops together, oh well.
@@ -4016,16 +4027,19 @@ ScriptAnalysis::analyzeTypesBytecode(JSC
       case JSOP_DELNAME:
         pushed[0].addType(cx, Type::BooleanType());
         break;
 
       case JSOP_LEAVEBLOCKEXPR:
         poppedTypes(pc, 0)->addSubset(cx, &pushed[0]);
         break;
 
+      case JSOP_LEAVEFORLETIN:
+        break;
+
       case JSOP_CASE:
       case JSOP_CASEX:
         poppedTypes(pc, 1)->addSubset(cx, &pushed[0]);
         break;
 
       case JSOP_GENERATOR:
           if (script->function()) {
             if (script->hasGlobal()) {
@@ -4549,17 +4563,17 @@ AnalyzeNewScriptProperties(JSContext *cx
         JSObject *obj = *pbaseobj;
 
         if (op == JSOP_SETPROP && uses->u.which == 1) {
             /*
              * Don't use GetAtomId here, we need to watch for SETPROP on
              * integer properties and bail out. We can't mark the aggregate
              * JSID_VOID type property as being in a definite slot.
              */
-            unsigned index = js_GetIndexFromBytecode(cx, script, pc, 0);
+            unsigned index = js_GetIndexFromBytecode(script, pc, 0);
             jsid id = ATOM_TO_JSID(script->getAtom(index));
             if (MakeTypeId(cx, id) != id)
                 return false;
             if (id == id_prototype(cx) || id == id___proto__(cx) || id == id_constructor(cx))
                 return false;
 
             /*
              * Ensure that if the properties named here could have a setter or
@@ -5416,16 +5430,18 @@ IgnorePushed(const jsbytecode *pc, unsig
       case JSOP_HOLE:
         return (index == 0);
       case JSOP_FILTER:
         return (index == 1);
 
       /* Storage for 'with' and 'let' blocks not monitored. */
       case JSOP_ENTERWITH:
       case JSOP_ENTERBLOCK:
+      case JSOP_ENTERLET0:
+      case JSOP_ENTERLET1:
         return true;
 
       /* We don't keep track of the iteration state for 'for in' or 'for each in' loops. */
       case JSOP_ITER:
       case JSOP_ITERNEXT:
       case JSOP_MOREITER:
       case JSOP_ENDITER:
         return true;
--- a/js/src/jsinterp.cpp
+++ b/js/src/jsinterp.cpp
@@ -1852,19 +1852,16 @@ js::Interpret(JSContext *cx, StackFrame 
         switchOp = intN(op);
         goto do_switch;
 #endif
     }
 
 /* No-ops for ease of decompilation. */
 ADD_EMPTY_CASE(JSOP_NOP)
 ADD_EMPTY_CASE(JSOP_UNUSED0)
-ADD_EMPTY_CASE(JSOP_UNUSED1)
-ADD_EMPTY_CASE(JSOP_UNUSED2)
-ADD_EMPTY_CASE(JSOP_UNUSED3)
 ADD_EMPTY_CASE(JSOP_CONDSWITCH)
 ADD_EMPTY_CASE(JSOP_TRY)
 #if JS_HAS_XML_SUPPORT
 ADD_EMPTY_CASE(JSOP_STARTXML)
 ADD_EMPTY_CASE(JSOP_STARTXMLEXPR)
 #endif
 ADD_EMPTY_CASE(JSOP_LOOPHEAD)
 END_EMPTY_CASES
@@ -5104,87 +5101,103 @@ BEGIN_CASE(JSOP_GETFUNNS)
     if (!cx->fp()->scopeChain().getGlobal()->getFunctionNamespace(cx, &rval))
         goto error;
     PUSH_COPY(rval);
 }
 END_CASE(JSOP_GETFUNNS)
 #endif /* JS_HAS_XML_SUPPORT */
 
 BEGIN_CASE(JSOP_ENTERBLOCK)
+BEGIN_CASE(JSOP_ENTERLET0)
+BEGIN_CASE(JSOP_ENTERLET1)
 {
     JSObject *obj;
     LOAD_OBJECT(0, obj);
     JS_ASSERT(obj->isStaticBlock());
-    JS_ASSERT(regs.fp()->base() + OBJ_BLOCK_DEPTH(cx, obj) == regs.sp);
-    Value *vp = regs.sp + OBJ_BLOCK_COUNT(cx, obj);
-    JS_ASSERT(regs.sp < vp);
-    JS_ASSERT(vp <= regs.fp()->slots() + script->nslots);
-    SetValueRangeToUndefined(regs.sp, vp);
-    regs.sp = vp;
+    JS_ASSERT(regs.fp()->maybeBlockChain() == obj->staticBlockScopeChain());
+
+    if (op == JSOP_ENTERBLOCK) {
+        JS_ASSERT(regs.fp()->base() + OBJ_BLOCK_DEPTH(cx, obj) == regs.sp);
+        Value *vp = regs.sp + OBJ_BLOCK_COUNT(cx, obj);
+        JS_ASSERT(regs.sp < vp);
+        JS_ASSERT(vp <= regs.fp()->slots() + script->nslots);
+        SetValueRangeToUndefined(regs.sp, vp);
+        regs.sp = vp;
+    } else if (op == JSOP_ENTERLET0) {
+        JS_ASSERT(regs.fp()->base() + OBJ_BLOCK_DEPTH(cx, obj) + OBJ_BLOCK_COUNT(cx, obj)
+                  == regs.sp);
+    } else if (op == JSOP_ENTERLET1) {
+        JS_ASSERT(regs.fp()->base() + OBJ_BLOCK_DEPTH(cx, obj) + OBJ_BLOCK_COUNT(cx, obj)
+                  == regs.sp - 1);
+    }
 
 #ifdef DEBUG
     JS_ASSERT(regs.fp()->maybeBlockChain() == obj->staticBlockScopeChain());
 
     /*
      * The young end of fp->scopeChain may omit blocks if we haven't closed
      * over them, but if there are any closure blocks on fp->scopeChain, they'd
      * better be (clones of) ancestors of the block we're entering now;
      * anything else we should have popped off fp->scopeChain when we left its
      * static scope.
      */
     JSObject *obj2 = &regs.fp()->scopeChain();
     while (obj2->isWith())
         obj2 = obj2->internalScopeChain();
     if (obj2->isBlock() &&
-        obj2->getPrivate() == js_FloatingFrameIfGenerator(cx, regs.fp())) {
+        obj2->getPrivate() == js_FloatingFrameIfGenerator(cx, regs.fp()))
+    {
         JSObject *youngestProto = obj2->getProto();
         JS_ASSERT(youngestProto->isStaticBlock());
         JSObject *parent = obj;
         while ((parent = parent->scopeChain()) != youngestProto)
             JS_ASSERT(parent);
     }
 #endif
 
     regs.fp()->setBlockChain(obj);
 }
 END_CASE(JSOP_ENTERBLOCK)
 
+BEGIN_CASE(JSOP_LEAVEBLOCK)
+BEGIN_CASE(JSOP_LEAVEFORLETIN)
 BEGIN_CASE(JSOP_LEAVEBLOCKEXPR)
-BEGIN_CASE(JSOP_LEAVEBLOCK)
 {
-#ifdef DEBUG
     JS_ASSERT(regs.fp()->blockChain().isStaticBlock());
-    uintN blockDepth = OBJ_BLOCK_DEPTH(cx, &regs.fp()->blockChain());
+    DebugOnly<uintN> blockDepth = OBJ_BLOCK_DEPTH(cx, &regs.fp()->blockChain());
     JS_ASSERT(blockDepth <= StackDepth(script));
-#endif
+
     /*
      * If we're about to leave the dynamic scope of a block that has been
      * cloned onto fp->scopeChain, clear its private data, move its locals from
      * the stack into the clone, and pop it off the chain.
      */
     JSObject &obj = regs.fp()->scopeChain();
     if (obj.getProto() == &regs.fp()->blockChain()) {
         JS_ASSERT(obj.isClonedBlock());
         if (!js_PutBlockObject(cx, JS_TRUE))
             goto error;
     }
 
-    /* Pop the block chain, too. */
     regs.fp()->setBlockChain(regs.fp()->blockChain().staticBlockScopeChain());
 
-    /* Move the result of the expression to the new topmost stack slot. */
-    Value *vp = NULL;  /* silence GCC warnings */
-    if (op == JSOP_LEAVEBLOCKEXPR)
-        vp = &regs.sp[-1];
-    regs.sp -= GET_UINT16(regs.pc);
-    if (op == JSOP_LEAVEBLOCKEXPR) {
+    if (op == JSOP_LEAVEBLOCK) {
+        /* Pop the block's slots. */
+        regs.sp -= GET_UINT16(regs.pc);
+        JS_ASSERT(regs.fp()->base() + blockDepth == regs.sp);
+    } else if (op == JSOP_LEAVEBLOCKEXPR) {
+        /* Pop the block's slots maintaining the topmost expr. */
+        Value *vp = &regs.sp[-1];
+        regs.sp -= GET_UINT16(regs.pc);
         JS_ASSERT(regs.fp()->base() + blockDepth == regs.sp - 1);
         regs.sp[-1] = *vp;
     } else {
-        JS_ASSERT(regs.fp()->base() + blockDepth == regs.sp);
+        /* Another op will pop; nothing to do here. */
+        len = JSOP_LEAVEFORLETIN_LENGTH;
+        DO_NEXT_OP(len);
     }
 }
 END_CASE(JSOP_LEAVEBLOCK)
 
 #if JS_HAS_GENERATORS
 BEGIN_CASE(JSOP_GENERATOR)
 {
     JS_ASSERT(!cx->isExceptionPending());
--- a/js/src/jsobj.cpp
+++ b/js/src/jsobj.cpp
@@ -3673,30 +3673,38 @@ block_setProperty(JSContext *cx, JSObjec
     /*
      * The value in *vp will be written back to the slot in obj that was
      * allocated when this let binding was defined.
      */
     return true;
 }
 
 const Shape *
-JSObject::defineBlockVariable(JSContext *cx, jsid id, intN index)
+JSObject::defineBlockVariable(JSContext *cx, jsid id, intN index, bool *redeclared)
 {
     JS_ASSERT(isStaticBlock());
 
+    *redeclared = false;
+
+    /* Inline JSObject::addProperty in order to trap the redefinition case. */
+    Shape **spp = nativeSearch(cx, id, true);
+    if (SHAPE_FETCH(spp)) {
+        *redeclared = true;
+        return NULL;
+    }
+
     /*
-     * Use JSPROP_ENUMERATE to aid the disassembler, and don't convert this
-     * object to dictionary mode so that we can clone the block's shape later.
+     * Don't convert this object to dictionary mode so that we can clone the
+     * block's shape later.
      */
     uint32_t slot = JSSLOT_FREE(&BlockClass) + index;
-    const Shape *shape = addProperty(cx, id,
-                                     block_getProperty, block_setProperty,
-                                     slot, JSPROP_ENUMERATE | JSPROP_PERMANENT,
-                                     Shape::HAS_SHORTID, index,
-                                     /* allowDictionary = */ false);
+    const Shape *shape = addPropertyInternal(cx, id, block_getProperty, block_setProperty,
+                                             slot, JSPROP_ENUMERATE | JSPROP_PERMANENT,
+                                             Shape::HAS_SHORTID, index, spp,
+                                             /* allowDictionary = */ false);
     if (!shape)
         return NULL;
     return shape;
 }
 
 JSBool
 JSObject::nonNativeSetProperty(JSContext *cx, jsid id, js::Value *vp, JSBool strict)
 {
@@ -4186,18 +4194,21 @@ js_XDRBlockObject(JSXDRState *xdr, JSObj
          */
         for (uintN i = 0; i < count; i++) {
             JSAtom *atom;
 
             /* XDR the real id. */
             if (!js_XDRAtom(xdr, &atom))
                 return false;
 
-            if (!obj->defineBlockVariable(cx, ATOM_TO_JSID(atom), i))
+            bool redeclared;
+            if (!obj->defineBlockVariable(cx, ATOM_TO_JSID(atom), i, &redeclared)) {
+                JS_ASSERT(!redeclared);
                 return false;
+            }
         }
     } else {
         AutoShapeVector shapes(cx);
         shapes.growBy(count);
 
         for (Shape::Range r(obj->lastProperty()); !r.empty(); r.popFront()) {
             shape = &r.front();
             shapes[shape->shortid()] = shape;
--- a/js/src/jsobj.h
+++ b/js/src/jsobj.h
@@ -1336,17 +1336,17 @@ struct JSObject : js::gc::Cell
     inline JSObject *thisObject(JSContext *cx);
 
     static bool thisObject(JSContext *cx, const js::Value &v, js::Value *vp);
 
     inline JSObject *getThrowTypeError() const;
 
     bool swap(JSContext *cx, JSObject *other);
 
-    const js::Shape *defineBlockVariable(JSContext *cx, jsid id, intN index);
+    const js::Shape *defineBlockVariable(JSContext *cx, jsid id, intN index, bool *redeclared);
 
     inline bool isArguments() const;
     inline bool isArrayBuffer() const;
     inline bool isNormalArguments() const;
     inline bool isStrictArguments() const;
     inline bool isArray() const;
     inline bool isDenseArray() const;
     inline bool isSlowArray() const;
--- a/js/src/jsopcode.cpp
+++ b/js/src/jsopcode.cpp
@@ -152,18 +152,17 @@ GetJumpOffset(jsbytecode *pc, jsbytecode
 
     type = JOF_OPTYPE(*pc);
     if (JOF_TYPE_IS_EXTENDED_JUMP(type))
         return GET_JUMPX_OFFSET(pc2);
     return GET_JUMP_OFFSET(pc2);
 }
 
 uintN
-js_GetIndexFromBytecode(JSContext *cx, JSScript *script, jsbytecode *pc,
-                        ptrdiff_t pcoff)
+js_GetIndexFromBytecode(JSScript *script, jsbytecode *pc, ptrdiff_t pcoff)
 {
     JSOp op = JSOp(*pc);
     JS_ASSERT(js_CodeSpec[op].length >= 1 + pcoff + UINT16_LEN);
 
     /*
      * We need to detect index base prefix. It presents when resetbase
      * follows the bytecode.
      */
@@ -215,44 +214,66 @@ js_GetVariableBytecodeLength(jsbytecode 
       do_lookup:
         /* Structure: default-jump case-count (case1-value case1-jump) ... */
         pc += jmplen;
         ncases = GET_UINT16(pc);
         return 1 + jmplen + INDEX_LEN + ncases * (INDEX_LEN + jmplen);
     }
 }
 
+static uint32_t
+NumBlockSlots(JSScript *script, jsbytecode *pc)
+{
+    JS_ASSERT(*pc == JSOP_ENTERBLOCK || *pc == JSOP_ENTERLET0 || *pc == JSOP_ENTERLET1);
+    JS_STATIC_ASSERT(JSOP_ENTERBLOCK_LENGTH == JSOP_ENTERLET0_LENGTH);
+    JS_STATIC_ASSERT(JSOP_ENTERBLOCK_LENGTH == JSOP_ENTERLET1_LENGTH);
+
+    JSObject *obj = NULL;
+    GET_OBJECT_FROM_BYTECODE(script, pc, 0, obj);
+    return OBJ_BLOCK_COUNT(NULL, obj);
+}
+
 uintN
-js_GetVariableStackUses(JSOp op, jsbytecode *pc)
+js::StackUses(JSScript *script, jsbytecode *pc)
 {
-    JS_ASSERT(*pc == op);
+    JSOp op = (JSOp) *pc;
+    const JSCodeSpec &cs = js_CodeSpec[op];
+    if (cs.nuses >= 0)
+        return cs.nuses;
+
     JS_ASSERT(js_CodeSpec[op].nuses == -1);
     switch (op) {
       case JSOP_POPN:
         return GET_UINT16(pc);
       case JSOP_LEAVEBLOCK:
         return GET_UINT16(pc);
       case JSOP_LEAVEBLOCKEXPR:
         return GET_UINT16(pc) + 1;
+      case JSOP_ENTERLET0:
+        return NumBlockSlots(script, pc);
+      case JSOP_ENTERLET1:
+        return NumBlockSlots(script, pc) + 1;
       default:
         /* stack: fun, this, [argc arguments] */
         JS_ASSERT(op == JSOP_NEW || op == JSOP_CALL || op == JSOP_EVAL ||
                   op == JSOP_FUNCALL || op == JSOP_FUNAPPLY);
         return 2 + GET_ARGC(pc);
     }
 }
 
 uintN
-js_GetEnterBlockStackDefs(JSContext *cx, JSScript *script, jsbytecode *pc)
+js::StackDefs(JSScript *script, jsbytecode *pc)
 {
-    JSObject *obj;
-
-    JS_ASSERT(*pc == JSOP_ENTERBLOCK);
-    GET_OBJECT_FROM_BYTECODE(script, pc, 0, obj);
-    return OBJ_BLOCK_COUNT(cx, obj);
+    JSOp op = (JSOp) *pc;
+    const JSCodeSpec &cs = js_CodeSpec[op];
+    if (cs.ndefs >= 0)
+        return cs.ndefs;
+
+    uint32_t n = NumBlockSlots(script, pc);
+    return op == JSOP_ENTERLET1 ? n + 1 : n;
 }
 
 static const char * countBaseNames[] = {
     "interp",
     "mjit",
     "mjit_calls",
     "mjit_code",
     "mjit_pics"
@@ -478,18 +499,22 @@ ToDisassemblySource(JSContext *cx, jsval
         if (clasp == &BlockClass) {
             char *source = JS_sprintf_append(NULL, "depth %d {", OBJ_BLOCK_DEPTH(cx, obj));
             if (!source)
                 return false;
 
             Shape::Range r = obj->lastProperty()->all();
             while (!r.empty()) {
                 const Shape &shape = r.front();
+                JSAtom *atom = JSID_IS_INT(shape.propid())
+                               ? cx->runtime->atomState.emptyAtom
+                               : JSID_TO_ATOM(shape.propid());
+
                 JSAutoByteString bytes;
-                if (!js_AtomToPrintableString(cx, JSID_TO_ATOM(shape.propid()), &bytes))
+                if (!js_AtomToPrintableString(cx, atom, &bytes))
                     return false;
 
                 r.popFront();
                 source = JS_sprintf_append(source, "%s: %d%s",
                                            bytes.ptr(), shape.shortid(),
                                            !r.empty() ? ", " : "");
                 if (!source)
                     return false;
@@ -567,17 +592,17 @@ js_Disassemble1(JSContext *cx, JSScript 
         ptrdiff_t off = GetJumpOffset(pc, pc);
         Sprint(sp, " %u (%+d)", loc + (intN) off, (intN) off);
         break;
       }
 
       case JOF_ATOM:
       case JOF_OBJECT:
       case JOF_REGEXP: {
-        uintN index = js_GetIndexFromBytecode(cx, script, pc, 0);
+        uintN index = js_GetIndexFromBytecode(script, pc, 0);
         jsval v;
         if (type == JOF_ATOM) {
             if (op == JSOP_DOUBLE) {
                 v = script->getConst(index);
             } else {
                 JSAtom *atom = script->getAtom(index);
                 v = STRING_TO_JSVAL(atom);
             }
@@ -663,17 +688,17 @@ js_Disassemble1(JSContext *cx, JSScript 
 
       case JOF_LOCAL:
         Sprint(sp, " %u", GET_SLOTNO(pc));
         break;
 
       case JOF_SLOTATOM:
       case JOF_SLOTOBJECT: {
         Sprint(sp, " %u", GET_SLOTNO(pc));
-        uintN index = js_GetIndexFromBytecode(cx, script, pc, SLOTNO_LEN);
+        uintN index = js_GetIndexFromBytecode(script, pc, SLOTNO_LEN);
         jsval v;
         if (type == JOF_SLOTATOM) {
             JSAtom *atom = script->getAtom(index);
             v = STRING_TO_JSVAL(atom);
         } else {
             v = OBJECT_TO_JSVAL(script->getObject(index));
         }
 
@@ -1276,17 +1301,16 @@ GetOff(SprintStack *ss, uintN i)
     ptrdiff_t off;
     jsbytecode *pc;
     char *bytes;
 
     off = ss->offsets[i];
     if (off >= 0)
         return off;
 
-    JS_ASSERT(off <= -2);
     JS_ASSERT(ss->printer->pcstack);
     if (off <= -2 && ss->printer->pcstack) {
         pc = ss->printer->pcstack[-2 - off];
         bytes = DecompileExpression(ss->sprinter.context, ss->printer->script,
                                     ss->printer->fun, pc);
         if (!bytes)
             return 0;
         if (bytes != FAILED_EXPRESSION_DECOMPILER) {
@@ -1356,16 +1380,25 @@ PushOff(SprintStack *ss, ptrdiff_t off, 
                                 : (op == JSOP_GETELEM2) ? JSOP_GETELEM
                                 : op);
     ss->bytecodes[top] = pc;
     ss->top = ++top;
     AddParenSlop(ss);
     return JS_TRUE;
 }
 
+static bool
+PushStr(SprintStack *ss, const char *str, JSOp op)
+{
+    ptrdiff_t off = SprintCString(&ss->sprinter, str);
+    if (off < 0)
+        return false;
+    return PushOff(ss, off, op);
+}
+
 static ptrdiff_t
 PopOffPrec(SprintStack *ss, uint8_t prec, jsbytecode **ppc = NULL)
 {
     uintN top;
     const JSCodeSpec *topcs;
     ptrdiff_t off;
 
     if (ppc)
@@ -1668,17 +1701,19 @@ GetArgOrVarAtom(JSPrinter *jp, uintN slo
 
 static const char *
 GetLocalInSlot(SprintStack *ss, jsint i, jsint slot, JSObject *obj)
 {
     for (Shape::Range r(obj->lastProperty()); !r.empty(); r.popFront()) {
         const Shape &shape = r.front();
 
         if (shape.shortid() == slot) {
-            LOCAL_ASSERT(JSID_IS_ATOM(shape.propid()));
+            /* Ignore the empty destructuring dummy. */
+            if (!JSID_IS_ATOM(shape.propid()))
+                continue;
 
             JSAtom *atom = JSID_TO_ATOM(shape.propid());
             const char *rval = QuoteString(&ss->sprinter, atom, 0);
             if (!rval)
                 return NULL;
 
             RETRACT(&ss->sprinter, rval);
             return rval;
@@ -1714,18 +1749,17 @@ GetLocal(SprintStack *ss, jsint i)
     // hoping the right object is found.
     if (off <= -2 && ss->printer->pcstack) {
         jsbytecode *pc = ss->printer->pcstack[-2 - off];
 
         JS_ASSERT(ss->printer->script->code <= pc);
         JS_ASSERT(pc < (ss->printer->script->code + ss->printer->script->length));
 
         if (JSOP_ENTERBLOCK == (JSOp)*pc) {
-            jsatomid j = js_GetIndexFromBytecode(ss->sprinter.context,
-                                                 ss->printer->script, pc, 0);
+            jsatomid j = js_GetIndexFromBytecode(ss->printer->script, pc, 0);
             JSObject *obj = script->getObject(j);
 
             if (obj->isBlock()) {
                 jsint depth = OBJ_BLOCK_DEPTH(cx, obj);
                 jsint count = OBJ_BLOCK_COUNT(cx, obj);
 
                 if (jsuint(i - depth) < jsuint(count))
                     return GetLocalInSlot(ss, i, jsint(i - depth), obj);
@@ -1768,27 +1802,42 @@ IsVarSlot(JSPrinter *jp, jsbytecode *pc,
     JS_ASSERT(slot < StackDepth(jp->script));
     *indexp = slot;
     return JS_FALSE;
 }
 
 #define LOAD_ATOM(PCOFF)                                                      \
     GET_ATOM_FROM_BYTECODE(jp->script, pc, PCOFF, atom)
 
+typedef Vector<JSAtom *, 8> AtomVector;
+typedef AtomVector::Range AtomRange;
+
 #if JS_HAS_DESTRUCTURING
 
 #define LOCAL_ASSERT(expr)  LOCAL_ASSERT_RV(expr, NULL)
 #define LOAD_OP_DATA(pc)    (oplen = (cs = &js_CodeSpec[op=(JSOp)*pc])->length)
 
 static jsbytecode *
-DecompileDestructuring(SprintStack *ss, jsbytecode *pc, jsbytecode *endpc);
-
+DecompileDestructuring(SprintStack *ss, jsbytecode *pc, jsbytecode *endpc,
+                       AtomRange *letNames = NULL);
+
+/*
+ * Decompile a single element of a compound {}/[] destructuring lhs, sprinting
+ * the result in-place (without pushing/popping the stack) and advancing the pc
+ * to either the next element or the final pop.
+ *
+ * For normal (SRC_DESTRUCT) destructuring, the names of assigned/initialized
+ * variables are read from their slots. However, for SRC_DESTRUCTLET, the slots
+ * have not been pushed yet; the caller must pass the names to use via
+ * 'letNames'. Each variable initialized in this destructuring lhs results in
+ * popping a name from 'letNames'.
+ */
 static jsbytecode *
-DecompileDestructuringLHS(SprintStack *ss, jsbytecode *pc, jsbytecode *endpc,
-                          JSBool *hole)
+DecompileDestructuringLHS(SprintStack *ss, jsbytecode *pc, jsbytecode *endpc, JSBool *hole,
+                          AtomRange *letNames = NULL)
 {
     JSPrinter *jp;
     JSOp op;
     const JSCodeSpec *cs;
     uintN oplen;
     jsint i;
     const char *lval, *xval;
     JSAtom *atom;
@@ -1799,34 +1848,84 @@ DecompileDestructuringLHS(SprintStack *s
 
     switch (op) {
       case JSOP_POP:
         *hole = JS_TRUE;
         if (SprintPut(&ss->sprinter, ", ", 2) < 0)
             return NULL;
         break;
 
+      case JSOP_PICK:
+        /*
+         * For 'let ([x, y] = y)', the emitter generates
+         *
+         *     push evaluation of y
+         *     dup
+         *   1 one
+         *   2 getelem
+         *   3 pick
+         *   4 two
+         *     getelem
+         *     pick
+         *     pop
+         *
+         * Thus 'x' consists of 1 - 3. The caller (DecompileDestructuring or
+         * DecompileGroupAssignment) will have taken care of 1 - 2, so pc is
+         * now pointing at 3. The pick indicates a primitive let var init so
+         * pop a name and advance the pc to 4.
+         */
+        LOCAL_ASSERT(letNames && !letNames->empty());
+        if (!QuoteString(&ss->sprinter, letNames->popCopyFront(), 0))
+            return NULL;
+        break;
+
       case JSOP_DUP:
-        pc = DecompileDestructuring(ss, pc, endpc);
+      {
+        /* Compound lhs, e.g., '[x,y]' in 'let [[x,y], z] = a;'. */
+        pc = DecompileDestructuring(ss, pc, endpc, letNames);
         if (!pc)
             return NULL;
         if (pc == endpc)
             return pc;
         LOAD_OP_DATA(pc);
+
+        /*
+         * By its post-condition, DecompileDestructuring pushed one string
+         * containing the whole decompiled lhs. Our post-condition is to sprint
+         * in-place so pop/concat this pushed string.
+         */
         lval = PopStr(ss, JSOP_NOP);
         if (SprintCString(&ss->sprinter, lval) < 0)
             return NULL;
+
         LOCAL_ASSERT(*pc == JSOP_POP);
+
+        /*
+         * To put block slots in the right place, the emitter follows a
+         * compound lhs with a pick (if at least one slot was pushed). The pick
+         * is not part of the compound lhs so DecompileDestructuring did not
+         * advance over it but it is part of the lhs so advance over it here.
+         */
+        jsbytecode *nextpc = pc + JSOP_POP_LENGTH;
+        LOCAL_ASSERT(nextpc <= endpc);
+        if (letNames && *nextpc == JSOP_PICK) {
+            LOCAL_ASSERT(nextpc < endpc);
+            pc = nextpc;
+            LOAD_OP_DATA(pc);
+        }
         break;
+      }
 
       case JSOP_SETARG:
       case JSOP_SETLOCAL:
+        LOCAL_ASSERT(!letNames);
         LOCAL_ASSERT(pc[oplen] == JSOP_POP || pc[oplen] == JSOP_POPN);
         /* FALL THROUGH */
       case JSOP_SETLOCALPOP:
+        LOCAL_ASSERT(!letNames);
         if (op == JSOP_SETARG) {
             atom = GetArgOrVarAtom(jp, GET_SLOTNO(pc));
             LOCAL_ASSERT(atom);
             if (!QuoteString(&ss->sprinter, atom, 0))
                 return NULL;
         } else if (IsVarSlot(jp, pc, &i)) {
             atom = GetArgOrVarAtom(jp, i);
             LOCAL_ASSERT(atom);
@@ -1844,16 +1943,17 @@ DecompileDestructuringLHS(SprintStack *s
             LOAD_OP_DATA(pc);
             if (op == JSOP_POPN)
                 return pc;
             LOCAL_ASSERT(op == JSOP_POP);
         }
         break;
 
       default: {
+        LOCAL_ASSERT(!letNames);
         /*
          * We may need to auto-parenthesize the left-most value decompiled
          * here, so add back PAREN_SLOP temporarily.  Then decompile until the
          * opcode that would reduce the stack depth to (ss->top-1), which we
          * pass to Decompile encoded as -(ss->top-1) - 1 or just -ss->top for
          * the nb parameter.
          */
         ptrdiff_t todo = ss->sprinter.offset;
@@ -1888,69 +1988,64 @@ DecompileDestructuringLHS(SprintStack *s
     }
 
     LOCAL_ASSERT(pc < endpc);
     pc += oplen;
     return pc;
 }
 
 /*
- * Starting with a SRC_DESTRUCT-annotated JSOP_DUP, decompile a destructuring
- * left-hand side object or array initialiser, including nested destructuring
- * initialisers.  On successful return, the decompilation will be pushed on ss
- * and the return value will point to the POP or GROUP bytecode following the
- * destructuring expression.
+ * Decompile a destructuring lhs object or array initialiser, including nested
+ * destructuring initialisers. On return a single string is pushed containing
+ * the entire lhs (regardless of how many variables were bound). Thus, the
+ * caller must take care of fixing up the decompiler stack.
  *
- * At any point, if pc is equal to endpc and would otherwise advance, we stop
- * immediately and return endpc.
+ * See DecompileDestructuringLHS for description of 'letNames'.
  */
 static jsbytecode *
-DecompileDestructuring(SprintStack *ss, jsbytecode *pc, jsbytecode *endpc)
+DecompileDestructuring(SprintStack *ss, jsbytecode *pc, jsbytecode *endpc,
+                       AtomRange *letNames)
 {
-    ptrdiff_t head;
-    JSContext *cx;
-    JSPrinter *jp;
-    JSOp op;
-    const JSCodeSpec *cs;
-    uintN oplen;
-    jsint i, lasti;
-    jsdouble d;
-    const char *lval;
-    JSAtom *atom;
-    jssrcnote *sn;
-    JSBool hole;
-
     LOCAL_ASSERT(*pc == JSOP_DUP);
     pc += JSOP_DUP_LENGTH;
 
+    JSContext *cx = ss->sprinter.context;
+    JSPrinter *jp = ss->printer;
+    jsbytecode *startpc = pc;
+
     /*
      * Set head so we can rewrite '[' to '{' as needed.  Back up PAREN_SLOP
      * chars so the destructuring decompilation accumulates contiguously in
      * ss->sprinter starting with "[".
      */
-    head = SprintPut(&ss->sprinter, "[", 1);
+    ptrdiff_t head = SprintPut(&ss->sprinter, "[", 1);
     if (head < 0 || !PushOff(ss, head, JSOP_NOP))
         return NULL;
     ss->sprinter.offset -= PAREN_SLOP;
     LOCAL_ASSERT(head == ss->sprinter.offset - 1);
     LOCAL_ASSERT(*OFF2STR(&ss->sprinter, head) == '[');
 
-    cx = ss->sprinter.context;
-    jp = ss->printer;
-    lasti = -1;
+    int lasti = -1;
 
     while (pc < endpc) {
 #if JS_HAS_DESTRUCTURING_SHORTHAND
         ptrdiff_t nameoff = -1;
 #endif
 
+        const JSCodeSpec *cs;
+        uintN oplen;
+        JSOp op;
         LOAD_OP_DATA(pc);
 
+        int i;
+        double d;
         switch (op) {
           case JSOP_POP:
+            /* Empty destructuring lhs. */
+            LOCAL_ASSERT(startpc == pc);
             pc += oplen;
             goto out;
 
           /* Handle the optimized number-pushing opcodes. */
           case JSOP_ZERO:   d = i = 0; goto do_getelem;
           case JSOP_ONE:    d = i = 1; goto do_getelem;
           case JSOP_UINT16: d = i = GET_UINT16(pc); goto do_getelem;
           case JSOP_UINT24: d = i = GET_UINT24(pc); goto do_getelem;
@@ -1958,17 +2053,18 @@ DecompileDestructuring(SprintStack *ss, 
           case JSOP_INT32:  d = i = GET_INT32(pc);  goto do_getelem;
 
           case JSOP_DOUBLE:
             GET_DOUBLE_FROM_BYTECODE(jp->script, pc, 0, d);
             LOCAL_ASSERT(JSDOUBLE_IS_FINITE(d) && !JSDOUBLE_IS_NEGZERO(d));
             i = (jsint)d;
 
           do_getelem:
-            sn = js_GetSrcNote(jp->script, pc);
+          {
+            jssrcnote *sn = js_GetSrcNote(jp->script, pc);
             pc += oplen;
             if (pc == endpc)
                 return pc;
             LOAD_OP_DATA(pc);
             LOCAL_ASSERT(op == JSOP_GETELEM);
 
             /* Distinguish object from array by opcode or source note. */
             if (sn && SN_TYPE(sn) == SRC_INITPROP) {
@@ -1981,20 +2077,22 @@ DecompileDestructuring(SprintStack *ss, 
 
                 /* Fill in any holes (holes at the end don't matter). */
                 while (++lasti < i) {
                     if (SprintPut(&ss->sprinter, ", ", 2) < 0)
                         return NULL;
                 }
             }
             break;
+          }
 
           case JSOP_GETPROP:
           case JSOP_LENGTH:
           {
+            JSAtom *atom;
             LOAD_ATOM(0);
             *OFF2STR(&ss->sprinter, head) = '{';
 #if JS_HAS_DESTRUCTURING_SHORTHAND
             nameoff = ss->sprinter.offset;
 #endif
             if (!QuoteString(&ss->sprinter, atom, IsIdentifier(atom) ? 0 : (jschar)'\''))
                 return NULL;
             if (SprintPut(&ss->sprinter, ": ", 2) < 0)
@@ -2010,17 +2108,18 @@ DecompileDestructuring(SprintStack *ss, 
         if (pc == endpc)
             return pc;
 
         /*
          * Decompile the left-hand side expression whose bytecode starts at pc
          * and continues for a bounded number of bytecodes or stack operations
          * (and which in any event stops before endpc).
          */
-        pc = DecompileDestructuringLHS(ss, pc, endpc, &hole);
+        JSBool hole;
+        pc = DecompileDestructuringLHS(ss, pc, endpc, &hole, letNames);
         if (!pc)
             return NULL;
 
 #if JS_HAS_DESTRUCTURING_SHORTHAND
         if (nameoff >= 0) {
             ptrdiff_t offset, initlen;
 
             offset = ss->sprinter.offset;
@@ -2055,32 +2154,32 @@ DecompileDestructuring(SprintStack *ss, 
         /*
          * We should stop if JSOP_DUP is either without notes or its note is
          * not SRC_CONTINUE. The former happens when JSOP_DUP duplicates the
          * last destructuring reference implementing an op= assignment like in
          * '([t] = z).y += x'. In the latter case the note is SRC_DESTRUCT and
          * means another destructuring initialiser abuts this one like in
          * '[a] = [b] = c'.
          */
-        sn = js_GetSrcNote(jp->script, pc);
+        jssrcnote *sn = js_GetSrcNote(jp->script, pc);
         if (!sn)
             break;
         if (SN_TYPE(sn) != SRC_CONTINUE) {
-            LOCAL_ASSERT(SN_TYPE(sn) == SRC_DESTRUCT);
+            LOCAL_ASSERT(SN_TYPE(sn) == SRC_DESTRUCT || SN_TYPE(sn) == SRC_DESTRUCTLET);
             break;
         }
 
         if (!hole && SprintPut(&ss->sprinter, ", ", 2) < 0)
             return NULL;
 
         pc += JSOP_DUP_LENGTH;
     }
 
 out:
-    lval = OFF2STR(&ss->sprinter, head);
+    const char *lval = OFF2STR(&ss->sprinter, head);
     if (SprintPut(&ss->sprinter, (*lval == '[') ? "]" : "}", 1) < 0)
         return NULL;
     return pc;
 }
 
 static jsbytecode *
 DecompileGroupAssignment(SprintStack *ss, jsbytecode *pc, jsbytecode *endpc,
                          jssrcnote *sn, ptrdiff_t *todop)
@@ -2141,18 +2240,16 @@ DecompileGroupAssignment(SprintStack *ss
 
 #undef LOCAL_ASSERT
 #undef LOAD_OP_DATA
 
 #endif /* JS_HAS_DESTRUCTURING */
 
 #define LOCAL_ASSERT(expr)    LOCAL_ASSERT_RV(expr, false)
 
-typedef Vector<JSAtom *, 8> AtomVector;
-
 /*
  * The names of the vars of a let block/expr are stored as the ids of the
  * shapes of the block object. Shapes are stored in a singly-linked list in
  * reverse order of addition. This function takes care of putting the names
  * back in declaration order.
  */
 static bool
 GetBlockNames(JSContext *cx, JSObject *blockObj, AtomVector *atoms)
@@ -2163,17 +2260,19 @@ GetBlockNames(JSContext *cx, JSObject *b
         return false;
 
     uintN i = numAtoms;
     for (Shape::Range r = blockObj->lastProperty()->all(); !r.empty(); r.popFront()) {
         const Shape &shape = r.front();
         LOCAL_ASSERT(shape.hasShortID());
         --i;
         LOCAL_ASSERT((uintN)shape.shortid() == i);
-        (*atoms)[i] = JSID_TO_ATOM(shape.propid());
+        (*atoms)[i] = JSID_IS_INT(shape.propid())
+                      ? cx->runtime->atomState.emptyAtom
+                      : JSID_TO_ATOM(shape.propid());
     }
 
     LOCAL_ASSERT(i == 0);
     return true;
 }
 
 static bool
 PushBlockNames(JSContext *cx, SprintStack *ss, const AtomVector &atoms)
@@ -2181,19 +2280,38 @@ PushBlockNames(JSContext *cx, SprintStac
     for (size_t i = 0; i < atoms.length(); i++) {
         const char *name = QuoteString(&ss->sprinter, atoms[i], 0);
         if (!name || !PushOff(ss, STR2OFF(&ss->sprinter, name), JSOP_ENTERBLOCK))
             return false;
     }
     return true;
 }
 
+/*
+ * In the scope of a let, the variables' (decompiler) stack slots must contain
+ * the corresponding variable's name. This function updates the N top slots
+ * with the N variable names stored in 'atoms'.
+ */
+static bool
+AssignBlockNamesToPushedSlots(JSContext *cx, SprintStack *ss, const AtomVector &atoms)
+{
+    /* For simplicity, just pop and push. */
+    LOCAL_ASSERT(atoms.length() <= (uintN)ss->top);
+    for (size_t i = 0; i < atoms.length(); ++i)
+        PopStr(ss, JSOP_NOP);
+    return PushBlockNames(cx, ss, atoms);
+}
+
+static const char SkipString[] = "/*skip*/";
+static const char DestructuredString[] = "/*destructured*/";
+static const unsigned DestructuredStringLength = ArrayLength(DestructuredString) - 1;
+
 static ptrdiff_t
-SprintLet(JSContext *cx, JSPrinter *jp, SprintStack *ss, jsbytecode *pc, ptrdiff_t bodyLength,
-          const char *headChars)
+SprintLetBody(JSContext *cx, JSPrinter *jp, SprintStack *ss, jsbytecode *pc, ptrdiff_t bodyLength,
+              const char *headChars)
 {
     if (pc[bodyLength] == JSOP_LEAVEBLOCK) {
         js_printf(jp, "\tlet (%s) {\n", headChars);
         jp->indent += 4;
         if (!Decompile(ss, pc, bodyLength))
             return -1;
         jp->indent -= 4;
         js_printf(jp, "\t}\n");
@@ -2240,26 +2358,25 @@ GetTokenForAssignment(JSPrinter *jp, jss
         token = "";
     }
     *lastlvalpc = NULL;
     *lastrvalpc = NULL;
     return token;
 }
 
 static ptrdiff_t
-SprintNormalFor(JSContext *cx, JSPrinter *jp, SprintStack *ss,
-                const char *init, jsbytecode *initpc,
-                jsbytecode **ppc, ptrdiff_t *plen)
+SprintNormalFor(JSContext *cx, JSPrinter *jp, SprintStack *ss, const char *initPrefix,
+                const char *init, jsbytecode *initpc, jsbytecode **ppc, ptrdiff_t *plen)
 {
     jsbytecode *pc = *ppc;
     jssrcnote *sn = js_GetSrcNote(jp->script, pc);
     JS_ASSERT(SN_TYPE(sn) == SRC_FOR);
 
     /* Print the keyword and the possibly empty init-part. */
-    js_printf(jp, "\tfor (");
+    js_printf(jp, "\tfor (%s", initPrefix);
     SprintOpcodePermanent(jp, init, initpc);
     js_printf(jp, ";");
 
     /* Skip the JSOP_NOP or JSOP_POP bytecode. */
     JS_ASSERT(*pc == JSOP_NOP || *pc == JSOP_POP);
     pc += JSOP_NOP_LENGTH;
 
     /* Get the cond, next, and loop-closing tail offsets. */
@@ -2517,26 +2634,26 @@ Decompile(SprintStack *ss, jsbytecode *p
             pc += cs->length;
             if (pc >= endpc)
                 break;
             op = (JSOp) *pc;
             cs = &js_CodeSpec[op];
         }
         saveop = op;
         len = oplen = cs->length;
-        nuses = js_GetStackUses(cs, op, pc);
+        nuses = StackUses(jp->script, pc);
 
         /*
          * Here it is possible that nuses > ss->top when the op has a hidden
          * source note. But when nb < 0 we assume that the caller knows that
          * Decompile would never meet such opcodes.
          */
         if (nb < 0) {
             LOCAL_ASSERT(ss->top >= nuses);
-            uintN ndefs = js_GetStackDefs(cx, cs, op, jp->script, pc);
+            uintN ndefs = StackDefs(jp->script, pc);
             if ((uintN) -(nb + 1) == ss->top - nuses + ndefs)
                 return pc;
         }
 
         /*
          * Save source literal associated with JS now before the following
          * rewrite changes op. See bug 380197.
          */
@@ -2666,16 +2783,22 @@ Decompile(SprintStack *ss, jsbytecode *p
 
               case 1:
                 rval = PopStrDupe(ss, op, &rvalpc);
                 todo = SprintCString(&ss->sprinter, token);
                 SprintOpcode(ss, rval, rvalpc, pc, todo);
                 break;
 
               case 0:
+                sn = js_GetSrcNote(jp->script, pc);
+                if (sn && SN_TYPE(sn) == SRC_CONTINUE) {
+                    /* Hoisted let decl (e.g. 'y' in 'let (x) { let y; }'). */
+                    todo = SprintCString(&ss->sprinter, SkipString);
+                    break;
+                }
                 todo = SprintCString(&ss->sprinter, token);
                 break;
 
               default:
                 todo = -2;
                 break;
             }
         } else {
@@ -2707,17 +2830,17 @@ Decompile(SprintStack *ss, jsbytecode *p
                     js_printf(jp, ");\n");
                     pc += tail;
                     len = js_CodeSpec[*pc].length;
                     todo = -2;
                     break;
 
                   case SRC_FOR:
                     /* for loop with empty initializer. */
-                    todo = SprintNormalFor(cx, jp, ss, "", NULL, &pc, &len);
+                    todo = SprintNormalFor(cx, jp, ss, "", "", NULL, &pc, &len);
                     break;
 
                   case SRC_ENDBRACE:
                     jp->indent -= 4;
                     js_printf(jp, "\t}\n");
                     break;
 
                   case SRC_FUNCDEF:
@@ -2870,18 +2993,17 @@ Decompile(SprintStack *ss, jsbytecode *p
                      * ss->sprinter.offset so that our consumer can find the
                      * empty group assignment decompilation.
                      */
                     if (newtop == oldtop) {
                         ss->sprinter.offset = todo;
                     } else {
                         /*
                          * Kill newtop before the end_groupassignment: label by
-                         * retracting/popping early.  Control will either jump
-                         * to do_letheadbody: or else break from our case.
+                         * retracting/popping early.
                          */
                         LOCAL_ASSERT(newtop < oldtop);
                         ss->sprinter.offset = GetOff(ss, newtop);
                         ss->top = newtop;
                     }
 
                   end_groupassignment:
                     LOCAL_ASSERT(*pc == JSOP_POPN);
@@ -2903,41 +3025,19 @@ Decompile(SprintStack *ss, jsbytecode *p
                     pc2 = pc + oplen;
 
                     if (*pc2 == JSOP_NOP) {
                         sn = js_GetSrcNote(jp->script, pc2);
                         if (sn) {
                             if (SN_TYPE(sn) == SRC_FOR) {
                                 op = JSOP_NOP;
                                 pc = pc2;
-                                todo = SprintNormalFor(cx, jp, ss, rval, rvalpc, &pc, &len);
+                                todo = SprintNormalFor(cx, jp, ss, "", rval, rvalpc, &pc, &len);
                                 break;
                             }
-
-                            if (SN_TYPE(sn) == SRC_DECL) {
-                                if (ss->top == StackDepth(jp->script)) {
-                                    /*
-                                     * This must be an empty destructuring
-                                     * in the head of a let whose body block
-                                     * is also empty.
-                                     */
-                                    pc = pc2 + JSOP_NOP_LENGTH;
-                                    len = js_GetSrcNoteOffset(sn, 0);
-                                    LOCAL_ASSERT(pc[len] == JSOP_LEAVEBLOCK);
-                                    js_printf(jp, "\tlet (%s) {\n", rval);
-                                    js_printf(jp, "\t}\n");
-                                    break;
-                                }
-                                todo = SprintCString(&ss->sprinter, rval);
-                                if (todo < 0 || !PushOff(ss, todo, JSOP_NOP))
-                                    return NULL;
-                                op = JSOP_POP;
-                                pc = pc2 + JSOP_NOP_LENGTH;
-                                goto do_letheadbody;
-                            }
                         } else {
                             /*
                              * An unnannotated NOP following a POPN must be the
                              * third part of for(;;) loop head. If the POPN's
                              * immediate operand is 0, then we may have no slot
                              * on the sprint-stack in which to push our result
                              * string. In this case the result can be recovered
                              * at ss->sprinter.base + ss->sprinter.offset.
@@ -2983,17 +3083,17 @@ Decompile(SprintStack *ss, jsbytecode *p
               case JSOP_POPV:
                 sn = js_GetSrcNote(jp->script, pc);
                 switch (sn ? SN_TYPE(sn) : SRC_NULL) {
                   case SRC_FOR:
                     /* Force parens around 'in' expression at 'for' front. */
                     if (ss->opcodes[ss->top-1] == JSOP_IN)
                         op = JSOP_LSH;
                     rval = PopStr(ss, op, &rvalpc);
-                    todo = SprintNormalFor(cx, jp, ss, rval, rvalpc, &pc, &len);
+                    todo = SprintNormalFor(cx, jp, ss, "", rval, rvalpc, &pc, &len);
                     break;
 
                   case SRC_PCDELTA:
                     /* Comma operator: use JSOP_POP for correct precedence. */
                     op = JSOP_POP;
 
                     /* Pop and save to avoid blowing stack depth budget. */
                     lval = PopStrDupe(ss, op, &lvalpc);
@@ -3018,32 +3118,21 @@ Decompile(SprintStack *ss, jsbytecode *p
                     SprintOpcode(ss, rval, rvalpc, pushpc, todo);
                     break;
 
                   case SRC_HIDDEN:
                     /* Hide this pop, it's from a goto in a with or for/in. */
                     todo = -2;
                     break;
 
-                  case SRC_DECL:
-                  {
-                    /* This pop is at the end of the let block/expr head. */
-                    pc += JSOP_POP_LENGTH;
-#if JS_HAS_DESTRUCTURING
-                  do_letheadbody:
-#endif
-                    DupBuffer head(cx);
-                    if (!Dup(POP_STR(), &head))
-                        return NULL;
-
-                    len = js_GetSrcNoteOffset(sn, 0);
-                    saveop = (JSOp) pc[len];
-                    todo = SprintLet(cx, jp, ss, pc, len, head.begin());
-                  }
-                  break;
+                  case SRC_CONTINUE:
+                    /* Pop the stack, don't print: end of a for-let-in. */
+                    (void) PopOff(ss, op);
+                    todo = -2;
+                    break;
 
                   default:
                   {
                     /* Turn off parens around a yield statement. */
                     if (ss->opcodes[ss->top-1] == JSOP_YIELD)
                         op = JSOP_NOP;
 
                     jsbytecode *rvalpc;
@@ -3231,16 +3320,167 @@ Decompile(SprintStack *ss, jsbytecode *p
                 top -= depth;
                 ss->top = top;
                 ss->sprinter.offset = GetOff(ss, top);
                 if (op == JSOP_LEAVEBLOCKEXPR)
                     todo = SprintCString(&ss->sprinter, rval);
                 break;
               }
 
+              case JSOP_ENTERLET0:
+              {
+                LOAD_OBJECT(0);
+
+                AtomVector atoms(cx);
+                if (!GetBlockNames(cx, obj, &atoms))
+                    return NULL;
+
+                sn = js_GetSrcNote(jp->script, pc);
+                LOCAL_ASSERT(SN_TYPE(sn) == SRC_DECL);
+                ptrdiff_t letData = js_GetSrcNoteOffset(sn, 0);
+                bool groupAssign = LetDataToGroupAssign(letData);
+                uintN letDepth = OBJ_BLOCK_DEPTH(cx, obj);
+                LOCAL_ASSERT(letDepth == (uintN)ss->top - OBJ_BLOCK_COUNT(cx, obj));
+                LOCAL_ASSERT(atoms.length() == OBJ_BLOCK_COUNT(cx, obj));
+
+                /*
+                 * Build the list of decompiled rhs expressions. Do this before
+                 * sprinting the let-head since GetStr can inject stuff on top
+                 * of the stack (in case js_DecompileValueGenerator).
+                 */
+                Vector<const char *> rhsExprs(cx);
+                if (!rhsExprs.resize(atoms.length()))
+                    return false;
+                for (size_t i = 0; i < rhsExprs.length(); ++i) {
+                    rhsExprs[i] = GetStr(ss, letDepth + i);
+                    if (!rhsExprs[i])
+                        return false;
+                }
+
+                /* Build the let head starting at headBegin. */
+                ptrdiff_t headBegin = ss->sprinter.offset;
+
+                /*
+                 * For group assignment, prepend the '[lhs-vars] = [' here,
+                 * append rhsExprs in the next loop and append ']' after.
+                 */
+                if (groupAssign) {
+                    if (Sprint(&ss->sprinter, "[") < 0)
+                        return false;
+                    for (size_t i = 0; i < atoms.length(); ++i) {
+                        if (i && Sprint(&ss->sprinter, ", ") < 0)
+                            return false;
+                        if (!QuoteString(&ss->sprinter, atoms[i], 0))
+                            return false;
+                    }
+                    if (Sprint(&ss->sprinter, "] = [") < 0)
+                        return false;
+                }
+
+                for (size_t i = 0; i < atoms.length(); ++i) {
+                    const char *rhs = rhsExprs[i];
+                    if (!strcmp(rhs, SkipString))
+                        continue;
+
+                    if (i && Sprint(&ss->sprinter, ", ") < 0)
+                        return false;
+
+                    if (groupAssign) {
+                        if (SprintCString(&ss->sprinter, rhs) < 0)
+                            return false;
+                    } else if (!strncmp(rhs, DestructuredString, DestructuredStringLength)) {
+                        if (SprintCString(&ss->sprinter, rhs + DestructuredStringLength) < 0)
+                            return false;
+                    } else {
+                        JS_ASSERT(atoms[i] != cx->runtime->atomState.emptyAtom);
+                        if (!QuoteString(&ss->sprinter, atoms[i], 0))
+                            return false;
+                        if (*rhs) {
+                            uint8_t prec = js_CodeSpec[ss->opcodes[letDepth + i]].prec;
+                            const char *fmt = prec && prec < js_CodeSpec[JSOP_SETLOCAL].prec
+                                              ? " = (%s)"
+                                              : " = %s";
+                            if (Sprint(&ss->sprinter, fmt, rhs) < 0)
+                                return false;
+                        }
+                    }
+                }
+
+                if (groupAssign && Sprint(&ss->sprinter, "]") < 0)
+                    return false;
+
+                /* Clone the let head chars before clobbering the stack. */
+                DupBuffer head(cx);
+                if (!Dup(OFF2STR(&ss->sprinter, headBegin), &head))
+                    return NULL;
+                if (!AssignBlockNamesToPushedSlots(cx, ss, atoms))
+                    return NULL;
+
+                /* Detect 'for (let ...)' desugared into 'let (...) {for}'. */
+                jsbytecode *nextpc = pc + JSOP_ENTERLET0_LENGTH;
+                if (*nextpc == JSOP_NOP) {
+                    jssrcnote *nextsn = js_GetSrcNote(jp->script, nextpc);
+                    if (nextsn && SN_TYPE(nextsn) == SRC_FOR) {
+                        pc = nextpc;
+                        todo = SprintNormalFor(cx, jp, ss, "let ", head.begin(), pc, &pc, &len);
+                        break;
+                    }
+                }
+
+                /* Decompile the body and then complete the let block/expr. */
+                len = LetDataToOffset(letData);
+                pc = nextpc;
+                saveop = (JSOp) pc[len];
+                todo = SprintLetBody(cx, jp, ss, pc, len, head.begin());
+                break;
+              }
+
+              /*
+               * With 'for (let lhs in rhs)' and 'switch (c) { let-decl }',
+               * placeholder slots have already been pushed (by JSOP_UNDEFINED).
+               * In both the for-let-in and switch-hoisted-let cases:
+               *  - there is a non-let slot on top of the stack (hence enterlet1)
+               *  - there is no further special let-handling required:
+               *    for-let-in will decompile the let head when it decompiles
+               *    the loop body prologue; there is no let head to decompile
+               *    with switch.
+               * Hence, the only thing to do is update the let vars' slots with
+               * their names, taking care to preserve the iter/condition value
+               * on top of the stack.
+               */
+              case JSOP_ENTERLET1:
+              {
+                LOAD_OBJECT(0);
+
+                AtomVector atoms(cx);
+                if (!GetBlockNames(cx, obj, &atoms))
+                    return NULL;
+
+                LOCAL_ASSERT(js_GetSrcNote(jp->script, pc) == NULL);
+                LOCAL_ASSERT(ss->top - 1 == OBJ_BLOCK_DEPTH(cx, obj) + OBJ_BLOCK_COUNT(cx, obj));
+                jsbytecode *nextpc = pc + JSOP_ENTERLET1_LENGTH;
+                if (*nextpc == JSOP_GOTO || *nextpc == JSOP_GOTOX) {
+                    LOCAL_ASSERT(SN_TYPE(js_GetSrcNote(jp->script, nextpc)) == SRC_FOR_IN);
+                } else {
+                    LOCAL_ASSERT(*nextpc == JSOP_CONDSWITCH ||
+                                 *nextpc == JSOP_TABLESWITCH || *nextpc == JSOP_TABLESWITCHX ||
+                                 *nextpc == JSOP_LOOKUPSWITCH || *nextpc == JSOP_LOOKUPSWITCHX);
+                }
+
+                DupBuffer rhs(cx);
+                if (!Dup(PopStr(ss, JSOP_NOP), &rhs))
+                    return NULL;
+                if (!AssignBlockNamesToPushedSlots(cx, ss, atoms))
+                    return NULL;
+                if (!PushStr(ss, rhs.begin(), op))
+                    return NULL;
+                todo = -2;
+                break;
+              }
+
               case JSOP_GETFCSLOT:
               case JSOP_CALLFCSLOT:
               {
                 if (!jp->fun)
                     jp->fun = jp->script->getCallerFunction();
 
                 if (!jp->localNames) {
                     JS_ASSERT(fun == jp->fun);
@@ -3586,17 +3826,17 @@ Decompile(SprintStack *ss, jsbytecode *p
                             ss->sprinter.offset = ss->offsets[ss->top] - PAREN_SLOP;
                             if (Sprint(&ss->sprinter, " %s (%s in %s)",
                                        foreach ? js_for_each_str : js_for_str,
                                        lval, rval) < 0) {
                                 return NULL;
                             }
 
                             /*
-                             * Do not AddParentSlop here, as we will push the
+                             * Do not AddParenSlop here, as we will push the
                              * top-most offset again, which will add paren slop
                              * for us. We must push to balance the stack budget
                              * when nesting for heads in a comprehension.
                              */
                             todo = ss->offsets[ss->top - 1];
                         } else {
                             todo = Sprint(&ss->sprinter, " %s (%s in %s)",
                                           foreach ? js_for_each_str : js_for_str,
@@ -3852,35 +4092,116 @@ Decompile(SprintStack *ss, jsbytecode *p
                     return NULL;
                 }
                 /* FALL THROUGH */
 
               case JSOP_DUP:
 #if JS_HAS_DESTRUCTURING
                 sn = js_GetSrcNote(jp->script, pc);
                 if (sn) {
-                    LOCAL_ASSERT(SN_TYPE(sn) == SRC_DESTRUCT);
-                    pc = DecompileDestructuring(ss, pc, endpc);
-                    if (!pc)
-                        return NULL;
-                    len = 0;
-                    lval = POP_STR();
-                    op = saveop = JSOP_ENUMELEM;
-                    rval = POP_STR();
-
-                    if (strcmp(rval, forelem_cookie) == 0) {
-                        todo = Sprint(&ss->sprinter, ss_format,
-                                      VarPrefix(sn), lval);
-
-                        // Skip POP so the SRC_FOR_IN code can pop for itself.
-                        if (*pc == JSOP_POP)
-                            len = JSOP_POP_LENGTH;
+                    if (SN_TYPE(sn) == SRC_DESTRUCT) {
+                        pc = DecompileDestructuring(ss, pc, endpc);
+                        if (!pc)
+                            return NULL;
+
+                        lval = POP_STR();  /* Pop the decompiler result. */
+                        rval = POP_STR();  /* Pop the initializer expression. */
+
+                        if (strcmp(rval, forelem_cookie) == 0) {
+                            todo = Sprint(&ss->sprinter, ss_format,
+                                          VarPrefix(sn), lval);
+
+                            /* Skip POP so the SRC_FOR_IN code can pop for itself. */
+                            if (*pc == JSOP_POP)
+                                len = JSOP_POP_LENGTH;
+                        } else {
+                            todo = Sprint(&ss->sprinter, "%s%s = %s",
+                                          VarPrefix(sn), lval, rval);
+                        }
+
+                        op = saveop = JSOP_ENUMELEM;
+                        len = 0;
                     } else {
-                        todo = Sprint(&ss->sprinter, "%s%s = %s",
-                                      VarPrefix(sn), lval, rval);
+                        LOCAL_ASSERT(SN_TYPE(sn) == SRC_DESTRUCTLET);
+
+                        ptrdiff_t offsetToLet = js_GetSrcNoteOffset(sn, 0);
+                        LOCAL_ASSERT(*(pc + offsetToLet) == JSOP_ENTERLET0);
+
+                        GET_OBJECT_FROM_BYTECODE(jp->script, pc + offsetToLet, 0, obj);
+
+                        uint32_t blockDepth = OBJ_BLOCK_DEPTH(cx, obj);
+                        LOCAL_ASSERT(blockDepth < ss->top);
+                        LOCAL_ASSERT(ss->top <= blockDepth + OBJ_BLOCK_COUNT(cx, obj));
+
+                        AtomVector atoms(cx);
+                        if (!GetBlockNames(cx, obj, &atoms))
+                            return NULL;
+
+                        /*
+                         * Skip any initializers preceding this one. E.g., in
+                         *   let (w=1, x=2, [y,z] = a) { ... }
+                         * skip 'w' and 'x' for the JSOP_DUP of '[y,z] = a'.
+                         */
+                        AtomRange letNames = atoms.all();
+                        uint32_t curDepth = ss->top - 1 /* initializer */;
+                        for (uint32_t i = blockDepth; i < curDepth; ++i)
+                            letNames.popFront();
+
+                        /*
+                         * Pop and copy the rhs before it gets clobbered.
+                         * Use JSOP_SETLOCAL's precedence since this is =.
+                         */
+                        DupBuffer rhs(cx);
+                        if (!Dup(PopStr(ss, JSOP_SETLOCAL), &rhs))
+                            return NULL;
+
+                        /* Destructure, tracking how many vars were bound. */
+                        size_t remainBefore = letNames.remain();
+                        pc = DecompileDestructuring(ss, pc, endpc, &letNames);
+                        if (!pc)
+                            return NULL;
+                        size_t remainAfter = letNames.remain();
+
+                        /*
+                         * Merge the lhs and rhs and prefix with a cookie to
+                         * tell enterlet0 not to prepend "name = ".
+                         */
+                        const char *lhs = PopStr(ss, JSOP_NOP);
+                        ptrdiff_t off = Sprint(&ss->sprinter, "%s%s = %s",
+                                               DestructuredString, lhs, rhs.begin());
+                        if (off < 0 || !PushOff(ss, off, JSOP_NOP))
+                            return NULL;
+
+                        /*
+                         * Only one slot has been pushed (holding the entire
+                         * decompiled destructuring expression). However, the
+                         * abstract depth needs one slot per bound var, so push
+                         * empty strings for the remainder. We don't have to
+                         * worry about empty destructuring because the parser
+                         * ensures that there is always at least one pushed
+                         * slot for each destructuring lhs.
+                         */
+                        LOCAL_ASSERT(remainBefore >= remainAfter);
+                        LOCAL_ASSERT(remainBefore > remainAfter || remainAfter > 0);
+                        for (size_t i = remainBefore - 1; i > remainAfter; --i) {
+                            if (!PushStr(ss, SkipString, JSOP_NOP))
+                                return NULL;
+                        }
+
+                        LOCAL_ASSERT(*pc == JSOP_POP);
+                        pc += JSOP_POP_LENGTH;
+
+                        /* Eat up the JSOP_UNDEFINED following empty destructuring. */
+                        if (remainBefore == remainAfter) {
+                            LOCAL_ASSERT(*pc == JSOP_UNDEFINED);
+                            pc += JSOP_UNDEFINED_LENGTH;
+                        }
+
+                        len = 0;
+                        todo = -2;
                     }
                     break;
                 }
 #endif
 
                 rval = GetStr(ss, ss->top-1);
                 saveop = (JSOp) ss->opcodes[ss->top-1];
                 todo = SprintCString(&ss->sprinter, rval);
@@ -5529,18 +5850,18 @@ js_ReconstructStackDepth(JSContext *cx, 
 }
 
 #define LOCAL_ASSERT(expr)      LOCAL_ASSERT_RV(expr, -1);
 
 static intN
 SimulateOp(JSContext *cx, JSScript *script, JSOp op, const JSCodeSpec *cs,
            jsbytecode *pc, jsbytecode **pcstack, uintN &pcdepth)
 {
-    uintN nuses = js_GetStackUses(cs, op, pc);
-    uintN ndefs = js_GetStackDefs(cx, cs, op, script, pc);
+    uintN nuses = StackUses(script, pc);
+    uintN ndefs = StackDefs(script, pc);
     LOCAL_ASSERT(pcdepth >= nuses);
     pcdepth -= nuses;
     LOCAL_ASSERT(pcdepth + ndefs <= StackDepth(script));
 
     /*
      * Fill the slots that the opcode defines withs its pc unless it just
      * reshuffles the stack. In the latter case we want to preserve the
      * opcode that generated the original value.
--- a/js/src/jsopcode.h
+++ b/js/src/jsopcode.h
@@ -327,92 +327,65 @@ js_puts(JSPrinter *jp, const char *s);
 /*
  * Get index operand from the bytecode using a bytecode analysis to deduce the
  * the index register. This function is infallible, in spite of taking cx as
  * its first parameter; it uses only cx->runtime when calling JS_GetTrapOpcode.
  * The GET_*_FROM_BYTECODE macros that call it pick up cx from their caller's
  * lexical environments.
  */
 uintN
-js_GetIndexFromBytecode(JSContext *cx, JSScript *script, jsbytecode *pc,
-                        ptrdiff_t pcoff);
+js_GetIndexFromBytecode(JSScript *script, jsbytecode *pc, ptrdiff_t pcoff);
 
 /*
  * A slower version of GET_ATOM when the caller does not want to maintain
  * the index segment register itself.
  */
 #define GET_ATOM_FROM_BYTECODE(script, pc, pcoff, atom)                       \
     JS_BEGIN_MACRO                                                            \
         JS_ASSERT(*(pc) != JSOP_DOUBLE);                                      \
-        uintN index_ = js_GetIndexFromBytecode(cx, (script), (pc), (pcoff));  \
+        uintN index_ = js_GetIndexFromBytecode((script), (pc), (pcoff));      \
         (atom) = (script)->getAtom(index_);                                   \
     JS_END_MACRO
 
 #define GET_DOUBLE_FROM_BYTECODE(script, pc, pcoff, dbl)                      \
     JS_BEGIN_MACRO                                                            \
-        uintN index_ = js_GetIndexFromBytecode(cx, (script), (pc), (pcoff));  \
+        uintN index_ = js_GetIndexFromBytecode((script), (pc), (pcoff));      \
         JS_ASSERT(index_ < (script)->consts()->length);                       \
         (dbl) = (script)->getConst(index_).toDouble();                        \
     JS_END_MACRO
 
 #define GET_OBJECT_FROM_BYTECODE(script, pc, pcoff, obj)                      \
     JS_BEGIN_MACRO                                                            \
-        uintN index_ = js_GetIndexFromBytecode(cx, (script), (pc), (pcoff));  \
+        uintN index_ = js_GetIndexFromBytecode((script), (pc), (pcoff));      \
         obj = (script)->getObject(index_);                                    \
     JS_END_MACRO
 
 #define GET_FUNCTION_FROM_BYTECODE(script, pc, pcoff, fun)                    \
     JS_BEGIN_MACRO                                                            \
-        uintN index_ = js_GetIndexFromBytecode(cx, (script), (pc), (pcoff));  \
+        uintN index_ = js_GetIndexFromBytecode((script), (pc), (pcoff));      \
         fun = (script)->getFunction(index_);                                  \
     JS_END_MACRO
 
 #define GET_REGEXP_FROM_BYTECODE(script, pc, pcoff, obj)                      \
     JS_BEGIN_MACRO                                                            \
-        uintN index_ = js_GetIndexFromBytecode(cx, (script), (pc), (pcoff));  \
+        uintN index_ = js_GetIndexFromBytecode((script), (pc), (pcoff));      \
         obj = (script)->getRegExp(index_);                                    \
     JS_END_MACRO
 
-/*
- * Find the number of stack slots used by a variadic opcode such as JSOP_CALL
- * (for such ops, JSCodeSpec.nuses is -1).
- */
-extern uintN
-js_GetVariableStackUses(JSOp op, jsbytecode *pc);
+#ifdef __cplusplus
+namespace js {
 
-/*
- * Find the number of stack slots defined by JSOP_ENTERBLOCK (for this op,
- * JSCodeSpec.ndefs is -1).
- */
 extern uintN
-js_GetEnterBlockStackDefs(JSContext *cx, JSScript *script, jsbytecode *pc);
+StackUses(JSScript *script, jsbytecode *pc);
 
-#ifdef __cplusplus /* Aargh, libgjs, bug 492720. */
-static JS_INLINE uintN
-js_GetStackUses(const JSCodeSpec *cs, JSOp op, jsbytecode *pc)
-{
-    JS_ASSERT(cs == &js_CodeSpec[op]);
-    if (cs->nuses >= 0)
-        return cs->nuses;
-    return js_GetVariableStackUses(op, pc);
-}
+extern uintN
+StackDefs(JSScript *script, jsbytecode *pc);
 
-static JS_INLINE uintN
-js_GetStackDefs(JSContext *cx, const JSCodeSpec *cs, JSOp op, JSScript *script,
-                jsbytecode *pc)
-{
-    JS_ASSERT(cs == &js_CodeSpec[op]);
-    if (cs->ndefs >= 0)
-        return cs->ndefs;
-
-    /* Only JSOP_ENTERBLOCK has a variable number of stack defs. */
-    JS_ASSERT(op == JSOP_ENTERBLOCK);
-    return js_GetEnterBlockStackDefs(cx, script, pc);
-}
-#endif
+}  /* namespace js */
+#endif  /* __cplusplus */
 
 /*
  * Decompilers, for script, function, and expression pretty-printing.
  */
 extern JSBool
 js_DecompileScript(JSPrinter *jp, JSScript *script);
 
 extern JSBool
--- a/js/src/jsopcode.tbl
+++ b/js/src/jsopcode.tbl
@@ -266,17 +266,18 @@ OPDEF(JSOP_DECARG,    98, "decarg",     
 OPDEF(JSOP_ARGINC,    99, "arginc",     NULL,         3,  0,  1, 15,  JOF_QARG |JOF_NAME|JOF_INC|JOF_POST|JOF_TMPSLOT3)
 OPDEF(JSOP_ARGDEC,   100, "argdec",     NULL,         3,  0,  1, 15,  JOF_QARG |JOF_NAME|JOF_DEC|JOF_POST|JOF_TMPSLOT3)
 
 OPDEF(JSOP_INCLOCAL,  101,"inclocal",   NULL,         3,  0,  1, 15,  JOF_LOCAL|JOF_NAME|JOF_INC|JOF_TMPSLOT3)
 OPDEF(JSOP_DECLOCAL,  102,"declocal",   NULL,         3,  0,  1, 15,  JOF_LOCAL|JOF_NAME|JOF_DEC|JOF_TMPSLOT3)
 OPDEF(JSOP_LOCALINC,  103,"localinc",   NULL,         3,  0,  1, 15,  JOF_LOCAL|JOF_NAME|JOF_INC|JOF_POST|JOF_TMPSLOT3)
 OPDEF(JSOP_LOCALDEC,  104,"localdec",   NULL,         3,  0,  1, 15,  JOF_LOCAL|JOF_NAME|JOF_DEC|JOF_POST|JOF_TMPSLOT3)
 
-OPDEF(JSOP_UNUSED1,   105,"unused0",    NULL,         1,  0,  0,  0,  JOF_BYTE)
+/* Leave a for-let-in block leaving its storage pushed (to be popped after enditer). */
+OPDEF(JSOP_LEAVEFORLETIN, 105,"leaveforletin",NULL,   1,  0,  0,  0,  JOF_BYTE)
 
 /* The argument is the offset to the next statement and is used by IonMonkey. */
 OPDEF(JSOP_LABEL,     106,"label",     NULL,          3,  0,  0,  0,  JOF_JUMP)
 OPDEF(JSOP_LABELX,    107,"labelx",    NULL,          5,  0,  0,  0,  JOF_JUMPX)
 
 /* Like JSOP_FUNAPPLY but for f.call instead of f.apply. */
 OPDEF(JSOP_FUNCALL,   108,"funcall",    NULL,         3, -1,  1, 18,  JOF_UINT16|JOF_INVOKE|JOF_TYPESET)
 
@@ -435,18 +436,21 @@ OPDEF(JSOP_XMLTAGEXPR,    178,"xmltagexp
 OPDEF(JSOP_XMLELTEXPR,    179,"xmleltexpr", NULL,     1,  1,  1,  0,  JOF_BYTE)
 OPDEF(JSOP_XMLCDATA,      180,"xmlcdata",   NULL,     3,  0,  1, 19,  JOF_ATOM)
 OPDEF(JSOP_XMLCOMMENT,    181,"xmlcomment", NULL,     3,  0,  1, 19,  JOF_ATOM)
 OPDEF(JSOP_XMLPI,         182,"xmlpi",      NULL,     3,  1,  1, 19,  JOF_ATOM)
 OPDEF(JSOP_DELDESC,       183,"deldesc",    NULL,     1,  2,  1, 15,  JOF_BYTE|JOF_ELEM|JOF_DEL)
 
 OPDEF(JSOP_CALLPROP,      184,"callprop",   NULL,     3,  1,  2, 18,  JOF_ATOM|JOF_PROP|JOF_TYPESET|JOF_CALLOP|JOF_TMPSLOT3)
 
-OPDEF(JSOP_UNUSED2,       185,"unused1",    NULL,     1,  0,  0,  0, JOF_BYTE)
-OPDEF(JSOP_UNUSED3,       186,"unused2",    NULL,     1,  0,  0,  0, JOF_BYTE)
+/* Enter a let block/expr whose slots are at the top of the stack. */
+OPDEF(JSOP_ENTERLET0,     185,"enterlet0",  NULL,     3, -1, -1,  0,  JOF_OBJECT)
+
+/* Enter a let block/expr whose slots are 1 below the top of the stack. */
+OPDEF(JSOP_ENTERLET1,     186,"enterlet1",  NULL,     3, -1, -1,  0,  JOF_OBJECT)
 
 /*
  * Opcode to hold 24-bit immediate integer operands.
  */
 OPDEF(JSOP_UINT24,        187,"uint24",     NULL,     4,  0,  1, 16,  JOF_UINT24)
 
 /*
  * Opcodes to allow 24-bit atom or object indexes. Whenever an index exceeds
--- a/js/src/jsreflect.cpp
+++ b/js/src/jsreflect.cpp
@@ -1618,17 +1618,17 @@ class ASTSerializer
     bool functionArgs(ParseNode *pn, ParseNode *pnargs, ParseNode *pndestruct, ParseNode *pnbody,
                       NodeVector &args);
 
     bool sourceElement(ParseNode *pn, Value *dst);
 
     bool declaration(ParseNode *pn, Value *dst);
     bool variableDeclaration(ParseNode *pn, bool let, Value *dst);
     bool variableDeclarator(ParseNode *pn, VarDeclKind *pkind, Value *dst);
-    bool letHead(ParseNode *pn, NodeVector &dtors);
+    bool let(ParseNode *pn, bool expr, Value *dst);
 
     bool optStatement(ParseNode *pn, Value *dst) {
         if (!pn) {
             dst->setMagic(JS_SERIALIZE_NO_NODE);
             return true;
         }
         return statement(pn, dst);
     }
@@ -1958,35 +1958,47 @@ ASTSerializer::variableDeclarator(ParseN
 
     Value left, right;
     return pattern(pnleft, pkind, &left) &&
            optExpression(pnright, &right) &&
            builder.variableDeclarator(left, right, &pn->pn_pos, dst);
 }
 
 bool
-ASTSerializer::letHead(ParseNode *pn, NodeVector &dtors)
+ASTSerializer::let(ParseNode *pn, bool expr, Value *dst)
 {
-    if (!dtors.reserve(pn->pn_count))
+    ParseNode *letHead = pn->pn_left;
+    LOCAL_ASSERT(letHead->isArity(PN_LIST));
+
+    ParseNode *letBody = pn->pn_right;
+    LOCAL_ASSERT(letBody->isKind(PNK_LEXICALSCOPE));
+
+    NodeVector dtors(cx);
+    if (!dtors.reserve(letHead->pn_count))
         return false;
 
     VarDeclKind kind = VARDECL_LET_HEAD;
 
-    for (ParseNode *next = pn->pn_head; next; next = next->pn_next) {
+    for (ParseNode *next = letHead->pn_head; next; next = next->pn_next) {
         Value child;
         /*
          * Unlike in |variableDeclaration|, this does not update |kind|; since let-heads do
          * not contain const declarations, declarators should never have PND_CONST set.
          */
         if (!variableDeclarator(next, &kind, &child))
             return false;
         dtors.infallibleAppend(child);
     }
 
-    return true;
+    Value v;
+    return expr
+           ? expression(letBody->pn_expr, &v) &&
+             builder.letExpression(dtors, v, &pn->pn_pos, dst)
+           : statement(letBody->pn_expr, &v) &&
+             builder.letStatement(dtors, v, &pn->pn_pos, dst);
 }
 
 bool
 ASTSerializer::switchCase(ParseNode *pn, Value *dst)
 {
     NodeVector stmts(cx);
 
     Value expr;
@@ -2073,55 +2085,48 @@ ASTSerializer::forInit(ParseNode *pn, Va
 {
     if (!pn) {
         dst->setMagic(JS_SERIALIZE_NO_NODE);
         return true;
     }
 
     return (pn->isKind(PNK_VAR) || pn->isKind(PNK_CONST))
            ? variableDeclaration(pn, false, dst)
-           : pn->isKind(PNK_LET)
-           ? variableDeclaration(pn, true, dst)
            : expression(pn, dst);
 }
 
 bool
 ASTSerializer::statement(ParseNode *pn, Value *dst)
 {
     JS_CHECK_RECURSION(cx, return false);
     switch (pn->getKind()) {
       case PNK_FUNCTION:
       case PNK_VAR:
       case PNK_CONST:
+        return declaration(pn, dst);
+
       case PNK_LET:
-        return declaration(pn, dst);
+        return pn->isArity(PN_BINARY)
+               ? let(pn, false, dst)
+               : declaration(pn, dst);
 
       case PNK_NAME:
         LOCAL_ASSERT(pn->isUsed());
         return statement(pn->pn_lexdef, dst);
 
       case PNK_SEMI:
         if (pn->pn_kid) {
             Value expr;
             return expression(pn->pn_kid, &expr) &&
                    builder.expressionStatement(expr, &pn->pn_pos, dst);
         }
         return builder.emptyStatement(&pn->pn_pos, dst);
 
       case PNK_LEXICALSCOPE:
         pn = pn->pn_expr;
-        if (pn->isKind(PNK_LET)) {
-            NodeVector dtors(cx);
-            Value stmt;
-
-            return letHead(pn->pn_left, dtors) &&
-                   statement(pn->pn_right, &stmt) &&
-                   builder.letStatement(dtors, stmt, &pn->pn_pos, dst);
-        }
-
         if (!pn->isKind(PNK_STATEMENTLIST))
             return statement(pn, dst);
         /* FALL THROUGH */
 
       case PNK_STATEMENTLIST:
         return blockStatement(pn, dst);
 
       case PNK_IF:
@@ -2171,19 +2176,19 @@ ASTSerializer::statement(ParseNode *pn, 
 
         bool isForEach = pn->pn_iflags & JSITER_FOREACH;
 
         if (head->isKind(PNK_FORIN)) {
             Value var, expr;
 
             return (!head->pn_kid1
                     ? pattern(head->pn_kid2, NULL, &var)
-                    : variableDeclaration(head->pn_kid1,
-                                          head->pn_kid1->isKind(PNK_LET),
-                                          &var)) &&
+                    : head->pn_kid1->isKind(PNK_LEXICALSCOPE)
+                      ? variableDeclaration(head->pn_kid1->pn_expr, true, &var)
+                      : variableDeclaration(head->pn_kid1, false, &var)) &&
                    expression(head->pn_kid3, &expr) &&
                    builder.forInStatement(var, expr, stmt, isForEach, &pn->pn_pos, dst);
         }
 
         Value init, test, update;
 
         return forInit(head->pn_kid1, &init) &&
                optExpression(head->pn_kid2, &test) &&
@@ -2628,27 +2633,18 @@ ASTSerializer::expression(ParseNode *pn,
 
       case PNK_ARRAYCOMP:
         /* NB: it's no longer the case that pn_count could be 2. */
         LOCAL_ASSERT(pn->pn_count == 1);
         LOCAL_ASSERT(pn->pn_head->isKind(PNK_LEXICALSCOPE));
 
         return comprehension(pn->pn_head->pn_expr, dst);
 
-      case PNK_LEXICALSCOPE:
-      {
-        pn = pn->pn_expr;
-
-        NodeVector dtors(cx);
-        Value expr;
-
-        return letHead(pn->pn_left, dtors) &&
-               expression(pn->pn_right, &expr) &&
-               builder.letExpression(dtors, expr, &pn->pn_pos, dst);
-      }
+      case PNK_LET:
+        return let(pn, true, dst);
 
 #ifdef JS_HAS_XML_SUPPORT
       case PNK_XMLUNARY:
         JS_ASSERT(pn->isOp(JSOP_XMLNAME) ||
                   pn->isOp(JSOP_SETXMLNAME) ||
                   pn->isOp(JSOP_BINDXMLNAME));
         return expression(pn->pn_kid, dst);
 
--- a/js/src/jsxdrapi.h
+++ b/js/src/jsxdrapi.h
@@ -221,17 +221,17 @@ JS_XDRFindClassById(JSXDRState *xdr, uin
  * Bytecode version number. Increment the subtrahend whenever JS bytecode
  * changes incompatibly.
  *
  * This version number is XDR'd near the front of xdr bytecode and
  * aborts deserialization if there is a mismatch between the current
  * and saved versions. If deserialization fails, the data should be
  * invalidated if possible.
  */
-#define JSXDR_BYTECODE_VERSION      (0xb973c0de - 99)
+#define JSXDR_BYTECODE_VERSION      (0xb973c0de - 100)
 
 /*
  * Library-private functions.
  */
 extern JSBool
 js_XDRAtom(JSXDRState *xdr, JSAtom **atomp);
 
 JS_END_EXTERN_C
--- a/js/src/methodjit/Compiler.cpp
+++ b/js/src/methodjit/Compiler.cpp
@@ -2673,16 +2673,18 @@ mjit::Compiler::generateMethod()
           END_CASE(JSOP_STOP)
 
           BEGIN_CASE(JSOP_GETXPROP)
             if (!jsop_xname(script->getAtom(fullAtomIndex(PC))))
                 return Compile_Error;
           END_CASE(JSOP_GETXPROP)
 
           BEGIN_CASE(JSOP_ENTERBLOCK)
+          BEGIN_CASE(JSOP_ENTERLET0)
+          BEGIN_CASE(JSOP_ENTERLET1)
             enterBlock(script->getObject(fullAtomIndex(PC)));
           END_CASE(JSOP_ENTERBLOCK);
 
           BEGIN_CASE(JSOP_LEAVEBLOCK)
             leaveBlock();
           END_CASE(JSOP_LEAVEBLOCK)
 
           BEGIN_CASE(JSOP_CALLLOCAL)
@@ -7129,29 +7131,29 @@ mjit::Compiler::jumpAndRun(Jump j, jsbyt
 }
 
 void
 mjit::Compiler::enterBlock(JSObject *obj)
 {
     /* For now, don't bother doing anything for this opcode. */
     frame.syncAndForgetEverything();
     masm.move(ImmPtr(obj), Registers::ArgReg1);
-    uint32_t n = js_GetEnterBlockStackDefs(cx, script, PC);
     INLINE_STUBCALL(stubs::EnterBlock, REJOIN_NONE);
-    frame.enterBlock(n);
+    if (*PC == JSOP_ENTERBLOCK)
+        frame.enterBlock(StackDefs(script, PC));
 }
 
 void
 mjit::Compiler::leaveBlock()
 {
     /*
      * Note: After bug 535912, we can pass the block obj directly, inline
      * PutBlockObject, and do away with the muckiness in PutBlockObject.
      */
-    uint32_t n = js_GetVariableStackUses(JSOP_LEAVEBLOCK, PC);
+    uint32_t n = StackUses(script, PC);
     prepareStubCall(Uses(n));
     INLINE_STUBCALL(stubs::LeaveBlock, REJOIN_NONE);
     frame.leaveBlock(n);
 }
 
 // Creates the new object expected for constructors, and places it in |thisv|.
 // It is broken down into the following operations:
 //   CALLEE
--- a/js/src/methodjit/LoopState.cpp
+++ b/js/src/methodjit/LoopState.cpp
@@ -1864,17 +1864,17 @@ LoopState::analyzeLoopBody(unsigned fram
 
             if (constrainedLoop && !definiteArrayAccess(objValue, elemValue))
                 constrainedLoop = false;
             break;
           }
 
           case JSOP_SETPROP:
           case JSOP_SETMETHOD: {
-            JSAtom *atom = script->getAtom(js_GetIndexFromBytecode(cx, script, pc, 0));
+            JSAtom *atom = script->getAtom(js_GetIndexFromBytecode(script, pc, 0));
             jsid id = MakeTypeId(cx, ATOM_TO_JSID(atom));
 
             TypeSet *objTypes = analysis->poppedTypes(pc, 1);
             if (objTypes->unknownObject()) {
                 unknownModset = true;
                 break;
             }
 
@@ -2180,17 +2180,17 @@ LoopState::getEntryValue(const CrossSSAV
         if (!tmp)
             return false;
         *pslot = frame.outerSlot(tmp);
         *pconstant = 0;
         return true;
       }
 
       case JSOP_GETPROP: {
-        JSAtom *atom = script->getAtom(js_GetIndexFromBytecode(cx, script, pc, 0));
+        JSAtom *atom = script->getAtom(js_GetIndexFromBytecode(script, pc, 0));
         jsid id = ATOM_TO_JSID(atom);
         CrossSSAValue objcv(cv.frame, analysis->poppedValue(v.pushedOffset(), 0));
         FrameEntry *tmp = invariantProperty(objcv, id);
         if (!tmp)
             return false;
         *pslot = frame.outerSlot(tmp);
         *pconstant = 0;
         return true;
--- a/js/src/methodjit/StubCalls.cpp
+++ b/js/src/methodjit/StubCalls.cpp
@@ -1816,22 +1816,25 @@ stubs::FastInstanceOf(VMFrame &f)
 void JS_FASTCALL
 stubs::EnterBlock(VMFrame &f, JSObject *obj)
 {
     FrameRegs &regs = f.regs;
     StackFrame *fp = f.fp();
 
     JS_ASSERT(!f.regs.inlined());
     JS_ASSERT(obj->isStaticBlock());
-    JS_ASSERT(fp->base() + OBJ_BLOCK_DEPTH(cx, obj) == regs.sp);
-    Value *vp = regs.sp + OBJ_BLOCK_COUNT(cx, obj);
-    JS_ASSERT(regs.sp < vp);
-    JS_ASSERT(vp <= fp->slots() + fp->script()->nslots);
-    SetValueRangeToUndefined(regs.sp, vp);
-    regs.sp = vp;
+
+    if (*regs.pc == JSOP_ENTERBLOCK) {
+        JS_ASSERT(fp->base() + OBJ_BLOCK_DEPTH(cx, obj) == regs.sp);
+        Value *vp = regs.sp + OBJ_BLOCK_COUNT(cx, obj);
+        JS_ASSERT(regs.sp < vp);
+        JS_ASSERT(vp <= fp->slots() + fp->script()->nslots);
+        SetValueRangeToUndefined(regs.sp, vp);
+        regs.sp = vp;
+    }
 
 #ifdef DEBUG
     JSContext *cx = f.cx;
     JS_ASSERT(fp->maybeBlockChain() == obj->staticBlockScopeChain());
 
     /*
      * The young end of fp->scopeChain() may omit blocks if we haven't closed
      * over them, but if there are any closure blocks on fp->scopeChain(), they'd
--- a/js/src/tests/js1_8/regress/regress-465567-01.js
+++ b/js/src/tests/js1_8/regress/regress-465567-01.js
@@ -44,20 +44,20 @@ var expect = '';
 printBugNumber(BUGNUMBER);
 printStatus (summary);
 
 expect = '99999';
 
 jit(true);
 
 for (let j = 0; j < 5; ++j) {
+  var e;
   e = 9;
   print(actual += '' + e);
   e = 47;
   if (e & 0) {
-    var e;
     let e;
   }
 }
 
 jit(false);
 
 reportCompare(expect, actual, summary);
--- a/js/src/tests/js1_8_5/extensions/reflect-parse.js
+++ b/js/src/tests/js1_8_5/extensions/reflect-parse.js
@@ -570,17 +570,17 @@ function testVarPatternCombinations(make
         assertBlockDecl("let " + pattSrcs[i].join(",") + ";", letDecl(pattPatts[i]));
 
         assertDecl("const " + pattSrcs[i].join(",") + ";", constDecl(pattPatts[i]));
 
         // variable declarations in for-loop heads
         assertStmt("for (var " + pattSrcs[i].join(",") + "; foo; bar);",
                    forStmt(varDecl(pattPatts[i]), ident("foo"), ident("bar"), emptyStmt));
         assertStmt("for (let " + pattSrcs[i].join(",") + "; foo; bar);",
-                   forStmt(letDecl(pattPatts[i]), ident("foo"), ident("bar"), emptyStmt));
+                   letStmt(pattPatts[i], forStmt(null, ident("foo"), ident("bar"), emptyStmt)));
         assertStmt("for (const " + pattSrcs[i].join(",") + "; foo; bar);",
                    forStmt(constDecl(pattPatts[i]), ident("foo"), ident("bar"), emptyStmt));
     }
 }
 
 testVarPatternCombinations(function (n) ("{a" + n + ":x" + n + "," + "b" + n + ":y" + n + "," + "c" + n + ":z" + n + "} = 0"),
                            function (n) ({ id: objPatt([{ key: ident("a" + n), value: ident("x" + n) },
                                                         { key: ident("b" + n), value: ident("y" + n) },
--- a/js/src/tests/js1_8_5/extensions/regress-672804-1.js
+++ b/js/src/tests/js1_8_5/extensions/regress-672804-1.js
@@ -1,12 +1,12 @@
 // Any copyright is dedicated to the Public Domain.
 // http://creativecommons.org/licenses/publicdomain/
 
 var a = 0;
 function f() {
     let (a = let (x = 1) x) {}
 }
 
-trap(f, 3, 'assertEq(evalInFrame(1, "a"), 0)');
+trap(f, 4, 'assertEq(evalInFrame(1, "a"), 0)');
 f();
 
 reportCompare(0, 0, 'ok');
--- a/js/src/tests/js1_8_5/extensions/regress-672804-3.js
+++ b/js/src/tests/js1_8_5/extensions/regress-672804-3.js
@@ -1,11 +1,11 @@
 // Any copyright is dedicated to the Public Domain.
 // http://creativecommons.org/licenses/publicdomain/
 
 var e = [], x = {b: []};
 function f() {
     let (a = [[] for (x in e)], {b: []} = x) {}
 }
-trap(f, 4, '');
+trap(f, 3, '');
 f();
 
 reportCompare(0, 0, 'ok');