Bug 666396 - Implemement yield*. r=jorendorff, r=Waldo
authorAndy Wingo <wingo@igalia.com>
Thu, 19 Sep 2013 15:26:26 +0200
changeset 147942 89406858afdfba834f3aaacce927037565712d2f
parent 147941 33c41758167d588fdd9ba891e14abdb2c72eca9d
child 147943 1b66f13a5c5a283f0b082e353ab139d800bcba6d
push id34037
push userryanvm@gmail.com
push dateThu, 19 Sep 2013 18:48:45 +0000
treeherdermozilla-inbound@1b66f13a5c5a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff, Waldo
bugs666396
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 666396 - Implemement yield*. r=jorendorff, r=Waldo
js/src/frontend/BytecodeEmitter.cpp
js/src/frontend/ParseNode.h
js/src/frontend/Parser.cpp
js/src/jit-test/lib/iteration.js
js/src/jit-test/tests/debug/Environment-callee-01.js
js/src/jit-test/tests/debug/Environment-callee-03.js
js/src/jit-test/tests/debug/Frame-01.js
js/src/jit-test/tests/debug/Frame-eval-18.js
js/src/jit-test/tests/debug/Frame-onPop-star-generators-01.js
js/src/jit-test/tests/debug/Frame-onPop-star-generators-02.js
js/src/jit-test/tests/debug/Frame-onPop-star-generators-03.js
js/src/jit-test/tests/debug/Script-getChildScripts-01.js
js/src/jit-test/tests/debug/breakpoint-11.js
js/src/jit-test/tests/debug/onNewScript-02.js
js/src/jit-test/tests/debug/resumption-06.js
js/src/jsiter.cpp
js/src/jsreflect.cpp
js/src/tests/ecma_6/Generators/delegating-yield-1.js
js/src/tests/ecma_6/Generators/delegating-yield-10.js
js/src/tests/ecma_6/Generators/delegating-yield-11.js
js/src/tests/ecma_6/Generators/delegating-yield-2.js
js/src/tests/ecma_6/Generators/delegating-yield-3.js
js/src/tests/ecma_6/Generators/delegating-yield-4.js
js/src/tests/ecma_6/Generators/delegating-yield-5.js
js/src/tests/ecma_6/Generators/delegating-yield-6.js
js/src/tests/ecma_6/Generators/delegating-yield-7.js
js/src/tests/ecma_6/Generators/delegating-yield-8.js
js/src/tests/ecma_6/Generators/delegating-yield-9.js
js/src/tests/ecma_6/Generators/iteration.js
js/src/vm/Interpreter.cpp
--- a/js/src/frontend/BytecodeEmitter.cpp
+++ b/js/src/frontend/BytecodeEmitter.cpp
@@ -268,16 +268,22 @@ EmitJump(ExclusiveContext *cx, BytecodeE
 
     jsbytecode *code = bce->code(offset);
     code[0] = jsbytecode(op);
     SET_JUMP_OFFSET(code, off);
     UpdateDepth(cx, bce, offset);
     return offset;
 }
 
+static ptrdiff_t
+EmitCall(ExclusiveContext *cx, BytecodeEmitter *bce, JSOp op, uint16_t argc)
+{
+    return Emit3(cx, bce, op, ARGC_HI(argc), ARGC_LO(argc));
+}
+
 /* XXX too many "... statement" L10N gaffes below -- fix via js.msg! */
 const char js_with_statement_str[] = "with statement";
 const char js_finally_block_str[]  = "finally block";
 const char js_script_str[]         = "script";
 
 static const char * const statementName[] = {
     "label statement",       /* LABEL */
     "if statement",          /* IF */
@@ -1637,20 +1643,20 @@ CheckSideEffects(ExclusiveContext *cx, B
             if (pn->isOp(JSOP_NOT)) {
                 /* ! does not convert its operand via toString or valueOf. */
                 return CheckSideEffects(cx, bce, pn->pn_kid, answer);
             }
             /* FALL THROUGH */
 
           default:
             /*
-             * All of PNK_INC, PNK_DEC, PNK_THROW, and PNK_YIELD have direct
-             * effects. Of the remaining unary-arity node types, we can't
-             * easily prove that the operand never denotes an object with a
-             * toString or valueOf method.
+             * All of PNK_INC, PNK_DEC, PNK_THROW, PNK_YIELD, and PNK_YIELD_STAR
+             * have direct effects. Of the remaining unary-arity node types, we
+             * can't easily prove that the operand never denotes an object with
+             * a toString or valueOf method.
              */
             *answer = true;
             return true;
         }
         MOZ_ASSUME_UNREACHABLE("We have a returning default case");
 
       case PN_NAME:
         /*
@@ -4888,16 +4894,167 @@ EmitReturn(ExclusiveContext *cx, Bytecod
         if (Emit1(cx, bce, JSOP_RETRVAL) < 0)
             return false;
     }
 
     return true;
 }
 
 static bool
+EmitYieldStar(ExclusiveContext *cx, BytecodeEmitter *bce, ParseNode *iter)
+{
+    JS_ASSERT(bce->sc->isFunctionBox());
+    JS_ASSERT(bce->sc->asFunctionBox()->isStarGenerator());
+
+    if (!EmitTree(cx, bce, iter))                                // ITER
+        return false;
+
+    int depth = bce->stackDepth;
+    JS_ASSERT(depth >= 1);
+
+    // Initial send value is undefined.
+    if (Emit1(cx, bce, JSOP_UNDEFINED) < 0)                      // ITER RECEIVED
+        return false;
+    ptrdiff_t initialSend = -1;
+    if (EmitBackPatchOp(cx, bce, &initialSend) < 0)              // goto initialSend
+        return false;
+
+    // Try prologue.                                             // ITER RESULT
+    StmtInfoBCE stmtInfo(cx);
+    PushStatementBCE(bce, &stmtInfo, STMT_TRY, bce->offset());
+    ptrdiff_t noteIndex = NewSrcNote(cx, bce, SRC_TRY);
+    if (noteIndex < 0 || Emit1(cx, bce, JSOP_TRY) < 0)
+        return false;
+    ptrdiff_t tryStart = bce->offset();                          // tryStart:
+    JS_ASSERT(bce->stackDepth == depth + 1);
+
+    // Yield RESULT as-is, without re-boxing.
+    if (Emit1(cx, bce, JSOP_YIELD) < 0)                          // ITER RECEIVED
+        return false;
+
+    // Try epilogue.
+    if (!SetSrcNoteOffset(cx, bce, noteIndex, 0, bce->offset() - tryStart + JSOP_TRY_LENGTH))
+        return false;
+    if (NewSrcNote(cx, bce, SRC_HIDDEN) < 0)
+        return false;
+    ptrdiff_t subsequentSend = -1;
+    if (EmitBackPatchOp(cx, bce, &subsequentSend) < 0)           // goto subsequentSend
+        return false;
+    ptrdiff_t tryEnd = bce->offset();                            // tryEnd:
+
+    // Catch location.
+    // THROW? = 'throw' in ITER                                  // ITER
+    bce->stackDepth = (unsigned) depth;
+    if (Emit1(cx, bce, JSOP_EXCEPTION) < 0)                      // ITER EXCEPTION
+        return false;
+    if (Emit1(cx, bce, JSOP_SWAP) < 0)                           // EXCEPTION ITER
+        return false;
+    if (Emit1(cx, bce, JSOP_DUP) < 0)                            // EXCEPTION ITER ITER
+        return false;
+    if (!EmitAtomOp(cx, cx->names().throw_, JSOP_STRING, bce))   // EXCEPTION ITER ITER "throw"
+        return false;
+    if (Emit1(cx, bce, JSOP_SWAP) < 0)                           // EXCEPTION ITER "throw" ITER
+        return false;
+    if (Emit1(cx, bce, JSOP_IN) < 0)                             // EXCEPTION ITER THROW?
+        return false;
+    // if (THROW?) goto delegate
+    ptrdiff_t checkThrow = EmitJump(cx, bce, JSOP_IFNE, 0);      // EXCEPTION ITER
+    if (checkThrow < 0)
+        return false;
+    if (Emit1(cx, bce, JSOP_POP) < 0)                            // EXCEPTION
+        return false;
+    if (Emit1(cx, bce, JSOP_THROW) < 0)                          // throw EXCEPTION
+        return false;
+
+    SetJumpOffsetAt(bce, checkThrow);                            // delegate:
+    // RESULT = ITER.throw(EXCEPTION)                            // EXCEPTION ITER
+    bce->stackDepth = (unsigned) depth + 1;
+    if (Emit1(cx, bce, JSOP_DUP) < 0)                            // EXCEPTION ITER ITER
+        return false;
+    if (Emit1(cx, bce, JSOP_DUP) < 0)                            // EXCEPTION ITER ITER ITER
+        return false;
+    if (!EmitAtomOp(cx, cx->names().throw_, JSOP_CALLPROP, bce)) // EXCEPTION ITER ITER THROW
+        return false;
+    if (Emit1(cx, bce, JSOP_SWAP) < 0)                           // EXCEPTION ITER THROW ITER
+        return false;
+    if (Emit1(cx, bce, JSOP_NOTEARG) < 0)                        // EXCEPTION ITER THROW ITER
+        return false;
+    if (Emit2(cx, bce, JSOP_PICK, (jsbytecode)3) < 0)            // ITER THROW ITER EXCEPTION
+        return false;
+    if (Emit1(cx, bce, JSOP_NOTEARG) < 0)                        // ITER THROW ITER EXCEPTION
+        return false;
+    if (EmitCall(cx, bce, JSOP_CALL, 1) < 0)                     // ITER RESULT
+        return false;
+    JS_ASSERT(bce->stackDepth == depth + 1);
+    ptrdiff_t checkResult = -1;
+    if (EmitBackPatchOp(cx, bce, &checkResult) < 0)              // goto checkResult
+        return false;
+
+    // Catch epilogue.
+    if (!PopStatementBCE(cx, bce))
+        return false;
+    // This is a peace offering to ReconstructPCStack.  See the note in EmitTry.
+    if (Emit1(cx, bce, JSOP_NOP) < 0)
+        return false;
+    if (!bce->tryNoteList.append(JSTRY_CATCH, depth, tryStart, tryEnd))
+        return false;
+
+    // After the try/catch block: send the received value to the iterator.
+    if (!BackPatch(cx, bce, initialSend, bce->code().end(), JSOP_GOTO)) // initialSend:
+        return false;
+    if (!BackPatch(cx, bce, subsequentSend, bce->code().end(), JSOP_GOTO)) // subsequentSend:
+        return false;
+
+    // Send location.
+    // result = iter.next(received)                              // ITER RECEIVED
+    if (Emit1(cx, bce, JSOP_SWAP) < 0)                           // RECEIVED ITER
+        return false;
+    if (Emit1(cx, bce, JSOP_DUP) < 0)                            // RECEIVED ITER ITER
+        return false;
+    if (Emit1(cx, bce, JSOP_DUP) < 0)                            // RECEIVED ITER ITER ITER
+        return false;
+    if (!EmitAtomOp(cx, cx->names().next, JSOP_CALLPROP, bce))   // RECEIVED ITER ITER NEXT
+        return false;
+    if (Emit1(cx, bce, JSOP_SWAP) < 0)                           // RECEIVED ITER NEXT ITER
+        return false;
+    if (Emit1(cx, bce, JSOP_NOTEARG) < 0)                        // RECEIVED ITER NEXT ITER
+        return false;
+    if (Emit2(cx, bce, JSOP_PICK, (jsbytecode)3) < 0)            // ITER NEXT ITER RECEIVED
+        return false;
+    if (Emit1(cx, bce, JSOP_NOTEARG) < 0)                        // ITER NEXT ITER RECEIVED
+        return false;
+    if (EmitCall(cx, bce, JSOP_CALL, 1) < 0)                     // ITER RESULT
+        return false;
+    JS_ASSERT(bce->stackDepth == depth + 1);
+
+    if (!BackPatch(cx, bce, checkResult, bce->code().end(), JSOP_GOTO)) // checkResult:
+        return false;
+    // if (!result.done) goto tryStart;                          // ITER RESULT
+    if (Emit1(cx, bce, JSOP_DUP) < 0)                            // ITER RESULT RESULT
+        return false;
+    if (!EmitAtomOp(cx, cx->names().done, JSOP_GETPROP, bce))    // ITER RESULT DONE
+        return false;
+    // if (!DONE) goto tryStart;
+    if (EmitJump(cx, bce, JSOP_IFEQ, tryStart - bce->offset()) < 0) // ITER RESULT
+        return false;
+
+    // result.value
+    if (Emit1(cx, bce, JSOP_SWAP) < 0)                           // RESULT ITER
+        return false;
+    if (Emit1(cx, bce, JSOP_POP) < 0)                            // RESULT
+        return false;
+    if (!EmitAtomOp(cx, cx->names().value, JSOP_GETPROP, bce))   // VALUE
+        return false;
+
+    JS_ASSERT(bce->stackDepth == depth);
+
+    return true;
+}
+
+static bool
 EmitStatementList(ExclusiveContext *cx, BytecodeEmitter *bce, ParseNode *pn, ptrdiff_t top)
 {
     JS_ASSERT(pn->isArity(PN_LIST));
 
     StmtInfoBCE stmtInfo(cx);
     PushStatementBCE(bce, &stmtInfo, STMT_BLOCK, top);
 
     ParseNode *pnchild = pn->pn_head;
@@ -5189,17 +5346,17 @@ EmitCallOrNew(ExclusiveContext *cx, Byte
         } else {
             if (!EmitArray(cx, bce, pn2->pn_next, argc))
                 return false;
         }
         bce->emittingForInit = oldEmittingForInit;
     }
 
     if (!spread) {
-        if (Emit3(cx, bce, pn->getOp(), ARGC_HI(argc), ARGC_LO(argc)) < 0)
+        if (EmitCall(cx, bce, pn->getOp(), argc) < 0)
             return false;
     } else {
         if (Emit1(cx, bce, pn->getOp()) < 0)
             return false;
     }
     CheckTypeSet(cx, bce, pn->getOp());
     if (pn->isOp(JSOP_EVAL) || pn->isOp(JSOP_SPREADEVAL)) {
         uint32_t lineNum = bce->parser->tokenStream.srcCoords.lineNum(pn->pn_pos.begin);
@@ -5884,16 +6041,20 @@ frontend::EmitTree(ExclusiveContext *cx,
         if (!EmitVariables(cx, bce, pn, InitializeVars))
             return false;
         break;
 
       case PNK_RETURN:
         ok = EmitReturn(cx, bce, pn);
         break;
 
+      case PNK_YIELD_STAR:
+        ok = EmitYieldStar(cx, bce, pn->pn_kid);
+        break;
+
       case PNK_YIELD:
         JS_ASSERT(bce->sc->isFunctionBox());
         if (bce->sc->asFunctionBox()->isStarGenerator()) {
             if (!EmitPrepareIteratorResult(cx, bce))
                 return false;
         }
         if (pn->pn_kid) {
             if (!EmitTree(cx, bce, pn->pn_kid))
--- a/js/src/frontend/ParseNode.h
+++ b/js/src/frontend/ParseNode.h
@@ -120,16 +120,17 @@ class UpvarCookie
     F(DELETE) \
     F(TRY) \
     F(CATCH) \
     F(CATCHLIST) \
     F(FINALLY) \
     F(THROW) \
     F(DEBUGGER) \
     F(YIELD) \
+    F(YIELD_STAR) \
     F(GENEXP) \
     F(ARRAYCOMP) \
     F(ARRAYPUSH) \
     F(LEXICALSCOPE) \
     F(LET) \
     F(SEQ) \
     F(FORIN) \
     F(FORHEAD) \
--- a/js/src/frontend/Parser.cpp
+++ b/js/src/frontend/Parser.cpp
@@ -4572,26 +4572,24 @@ Parser<ParseHandler>::yieldExpression()
 
     switch (pc->generatorKind()) {
       case StarGenerator:
       {
         JS_ASSERT(pc->sc->isFunctionBox());
 
         pc->lastYieldOffset = begin;
 
-        bool isDelegatingYield = tokenStream.matchToken(TOK_MUL);
+        ParseNodeKind kind = tokenStream.matchToken(TOK_MUL) ? PNK_YIELD_STAR : PNK_YIELD;
 
         // ES6 generators require a value.
         Node exprNode = assignExpr();
         if (!exprNode)
             return null();
 
-        // FIXME: Plumb isDelegatingYield appropriately.
-        (void) isDelegatingYield;
-        return handler.newUnary(PNK_YIELD, JSOP_YIELD, begin, exprNode);
+        return handler.newUnary(kind, JSOP_NOP, begin, exprNode);
       }
 
       case NotGenerator:
         // We are in code that has not seen a yield, but we are in JS 1.7 or
         // later.  Try to transition to being a legacy generator.
         JS_ASSERT(tokenStream.versionNumber() >= JSVERSION_1_7);
         JS_ASSERT(pc->lastYieldOffset == ParseContext<ParseHandler>::NoYieldOffset);
 
@@ -4643,17 +4641,17 @@ Parser<ParseHandler>::yieldExpression()
                 return null();
             break;
           default:
             exprNode = assignExpr();
             if (!exprNode)
                 return null();
         }
 
-        return handler.newUnary(PNK_YIELD, JSOP_YIELD, begin, exprNode);
+        return handler.newUnary(PNK_YIELD, JSOP_NOP, begin, exprNode);
       }
     }
 
     MOZ_ASSUME_UNREACHABLE("yieldExpr");
 }
 
 template <>
 ParseNode *
@@ -6089,17 +6087,17 @@ ParseNode *
 Parser<FullParseHandler>::generatorExpr(ParseNode *kid)
 {
     JS_ASSERT(tokenStream.isCurrentTokenType(TOK_FOR));
 
     /* Create a |yield| node for |kid|. */
     ParseNode *pn = UnaryNode::create(PNK_YIELD, &handler);
     if (!pn)
         return null();
-    pn->setOp(JSOP_YIELD);
+    pn->setOp(JSOP_NOP);
     pn->setInParens(true);
     pn->pn_pos = kid->pn_pos;
     pn->pn_kid = kid;
     pn->pn_hidden = true;
 
     /* Make a new node for the desugared generator function. */
     ParseNode *genfn = CodeNode::create(PNK_FUNCTION, &handler);
     if (!genfn)
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/lib/iteration.js
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+load(libdir + "asserts.js");
+
+if (typeof assertIteratorResult === 'undefined') {
+    var assertIteratorResult = function assertIteratorResult(result, value, done) {
+        assertEq(typeof result, "object");
+        assertDeepEq(result.value, value);
+        assertDeepEq(result.done, done);
+    }
+}
--- a/js/src/jit-test/tests/debug/Environment-callee-01.js
+++ b/js/src/jit-test/tests/debug/Environment-callee-01.js
@@ -41,8 +41,11 @@ check('eval("debugger");', 'object', nul
 g.eval('function k() { eval("debugger;"); }');
 check('k()', 'declarative', gw.makeDebuggeeValue(g.k));
 
 g.eval('function m() { debugger; yield true; }');
 check('m().next();', 'declarative', gw.makeDebuggeeValue(g.m));
 
 g.eval('function n() { let (x = 1) { debugger; } }');
 check('n()', 'declarative', null);
+
+g.eval('function* o() { debugger; yield true; }');
+check('o().next();', 'declarative', gw.makeDebuggeeValue(g.o));
--- a/js/src/jit-test/tests/debug/Environment-callee-03.js
+++ b/js/src/jit-test/tests/debug/Environment-callee-03.js
@@ -1,26 +1,31 @@
 // Environments of different instances of the same generator have the same
 // callee. I love this job.
 
 var g = newGlobal();
 var dbg = new Debugger;
 var gw = dbg.addDebuggee(g);
 
-g.eval('function f(x) { debugger; yield x; }');
-g.eval('var g = f(2);');
-g.eval('var h = f(3);');
-
 function check(gen, label) {
   print("check(" + label + ")");
   var hits;
   dbg.onDebuggerStatement = function (frame) {
     hits++;
     var env = frame.environment;
     assertEq(env.callee, gw.makeDebuggeeValue(g.f));
   };
   hits = 0;
   gen.next();
   assertEq(hits, 1);
 }
 
+g.eval('function f(x) { debugger; yield x; }');
+g.eval('var g = f(2);');
+g.eval('var h = f(3);');
 check(g.g, 'g.g');
 check(g.h, 'g.h');
+
+g.eval('function* f(x) { debugger; yield x; }');
+g.eval('var g = f(2);');
+g.eval('var h = f(3);');
+check(g.g, 'g.g');
+check(g.h, 'g.h');
--- a/js/src/jit-test/tests/debug/Frame-01.js
+++ b/js/src/jit-test/tests/debug/Frame-01.js
@@ -24,8 +24,11 @@ test("new function() { debugger; };", {t
 test("new function () { (function() { debugger; })(); }", {type: "call", generator: false, constructing: false});
 test("eval('debugger;');", {type: "eval", generator: false, constructing: false});
 test("this.eval('debugger;');  // indirect eval", {type: "eval", generator: false, constructing: false});
 test("(function () { eval('debugger;'); })();", {type: "eval", generator: false, constructing: false});
 test("new function () { eval('debugger'); }", {type: "eval", generator: false, constructing: false});
 test("function gen() { debugger; yield 1; debugger; }\n" +
      "for (var x in gen()) {}\n",
      {type: "call", generator: true, constructing: false}, 2);
+test("var iter = (function* stargen() { debugger; yield 1; debugger; })();\n" +
+     "iter.next(); iter.next();",
+     {type: "call", generator: true, constructing: false}, 2);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-eval-18.js
@@ -0,0 +1,12 @@
+// yield is not allowed in eval in a star generator.
+
+load(libdir + 'asserts.js');
+
+var g = newGlobal();
+var dbg = new Debugger(g);
+
+dbg.onDebuggerStatement = function (frame) {
+    assertThrowsInstanceOf(function() { frame.eval('yield 10;') }, SyntaxError);
+};
+
+g.eval("(function*g(){ debugger; })()");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onPop-star-generators-01.js
@@ -0,0 +1,20 @@
+// Returning {throw:} from an onPop handler when yielding works and
+// does not close the generator-iterator.
+
+load(libdir + "iteration.js");
+
+var g = newGlobal();
+var dbg = new Debugger;
+var gw = dbg.addDebuggee(g);
+dbg.onDebuggerStatement = function handleDebugger(frame) {
+    frame.onPop = function (c) {
+        return {throw: "fit"};
+    };
+};
+g.eval("function* g() { for (var i = 0; i < 10; i++) { debugger; yield i; } }");
+g.eval("var it = g();");
+var rv = gw.evalInGlobal("it.next();");
+assertEq(rv.throw, "fit");
+
+dbg.enabled = false;
+assertIteratorResult(g.it.next(), 1, false);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onPop-star-generators-02.js
@@ -0,0 +1,19 @@
+// Throwing an exception from an onPop handler when yielding terminates the debuggee
+// but does not close the generator-iterator.
+
+load(libdir + 'iteration.js')
+
+var g = newGlobal();
+var dbg = new Debugger;
+var gw = dbg.addDebuggee(g);
+dbg.onDebuggerStatement = function handleDebugger(frame) {
+    frame.onPop = function (c) {
+        throw "fit";
+    };
+};
+g.eval("function* g() { for (var i = 0; i < 10; i++) { debugger; yield i; } }");
+g.eval("var it = g();");
+assertEq(gw.evalInGlobal("it.next();"), null);
+
+dbg.enabled = false;
+assertIteratorResult(g.it.next(), 1, false);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onPop-star-generators-03.js
@@ -0,0 +1,42 @@
+// Each resumption of an ES6 generator gets a fresh frame, whose onPop
+// handler fires the next time the generator yields.  This is not the
+// behavior the spec requests, but it's what we do for the moment, and
+// it's good to check that at least we don't crash.
+
+load(libdir + 'iteration.js');
+
+var g = newGlobal();
+var dbg = new Debugger(g);
+var log;
+
+var debuggerFrames = [];
+var poppedFrames = [];
+dbg.onDebuggerStatement = function handleDebugger(frame) {
+    log += 'd';
+    assertEq(frame.type, "call");
+
+    assertEq(debuggerFrames.indexOf(frame), -1);
+    assertEq(poppedFrames.indexOf(frame), -1);
+    debuggerFrames.push(frame);
+
+    if (frame.eval('i').return % 3 == 0) {
+        frame.onPop = function handlePop(c) {
+            log += ')' + c.return.value;
+            assertEq(debuggerFrames.indexOf(this) != -1, true);
+            assertEq(poppedFrames.indexOf(this), -1);
+            poppedFrames.push(this);
+        };
+    }
+};
+
+g.eval("function* g() { for (var i = 0; i < 10; i++) { debugger; yield i; } }");
+log ='';
+g.eval("var t = 0, iter = g();");
+for (var j = 0; j < 10; j++)
+    g.eval("t += iter.next().value;");
+assertIteratorResult(g.eval("iter.next()"), undefined, true);
+assertEq(g.eval("t"), 45);
+
+// FIXME: Should equal this, but see bug 917809.
+// assertEq(log, "d)0ddd)3ddd)6ddd)9");
+assertEq(log, "d)undefinedddd)undefinedddd)undefinedddd)undefined");
--- a/js/src/jit-test/tests/debug/Script-getChildScripts-01.js
+++ b/js/src/jit-test/tests/debug/Script-getChildScripts-01.js
@@ -33,10 +33,12 @@ test("function q() {} function qq() {} d
 test("[0].map(function id(a) { return a; }); debugger;",
      "S[S]");
 test("Function('return 2+2;')(); debugger;",
      "S");
 test("var obj = {get x() { return 0; }, set x(v) {}}; debugger;",
      "S[SS]");
 test("function r(n) { for (var i = 0; i < n; i++) yield i; } debugger;",
      "S[S]");
+test("function* qux(n) { for (var i = 0; i < n; i++) yield i; } debugger;",
+     "S[S]");
 test("var it = (3 for (p in obj)); debugger;",
      "S[S]");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-11.js
@@ -0,0 +1,40 @@
+// Setting a breakpoint in a generator function works, and we can
+// traverse the stack and evaluate expressions in the context of older
+// generator frames.
+
+var g = newGlobal();
+var dbg = Debugger(g);
+dbg.onDebuggerStatement = function (frame) {
+    function hit(frame) {
+        assertEq(frame.generator, true);
+        assertEq(frame.older.generator, true);
+        frame.older.eval("q += 16");
+    }
+
+    var s = frame.script;
+    var offs = s.getLineOffsets(g.line0 + 9);
+    for (var i = 0; i < offs.length; i++)
+        s.setBreakpoint(offs[i], {hit: hit});
+};
+
+g.eval("line0 = Error().lineNumber;\n" +
+       "function* g(x) {\n" +   // + 1
+       "    var q = 10;\n" +    // + 2
+       "    yield* x;\n" +      // + 3
+       "    return q;\n" +      // + 4
+       "}\n" +                  // + 5
+       "function* range(n) {\n" + // + 6
+       "    debugger;\n" +      // + 7
+       "    for (var i = 0; i < n; i++)\n" + // + 8
+       "        yield i;\n" +   // + 9  <-- breakpoint
+       "    return;\n" + // so that line 9 only has the yield
+       "}");
+
+g.eval("var iter = g(range(2))");
+g.eval("var first = iter.next().value");
+g.eval("var second = iter.next().value");
+g.eval("var third = iter.next().value");
+
+assertEq(g.first, 0);
+assertEq(g.second, 1);
+assertEq(g.third, 42);
--- a/js/src/jit-test/tests/debug/onNewScript-02.js
+++ b/js/src/jit-test/tests/debug/onNewScript-02.js
@@ -44,16 +44,19 @@ test(function () { g.eval("var obj = {ge
 test(function () { return g.Function("a", "b", "return b - a;"); });
 
 // cloning a function with nested functions
 test(function () { g.clone(evaluate("(function(x) { return x + 1; })", {compileAndGo: false})); });
 
 // eval declaring a generator
 test(function () { g.eval("function r(n) { for (var i=0;i<n;i++) yield i; }"); });
 
+// eval declaring a star generator
+test(function () { g.eval("function* sg(n) { for (var i=0;i<n;i++) yield i; }"); });
+
 // eval with a generator-expression
 test(function () { g.eval("var it = (obj[p] for (p in obj));"); });
 
 // eval creating several instances of a closure
 test(function () { g.eval("for (var i = 0; i < 7; i++)\n" +
                           "    obj = function () { return obj; };\n"); });
 
 // non-strict-mode direct eval
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/resumption-06.js
@@ -0,0 +1,20 @@
+// |jit-test| debug
+// Forced return from a star generator frame.
+
+load(libdir + 'asserts.js')
+load(libdir + 'iteration.js')
+
+var g = newGlobal();
+g.debuggeeGlobal = this;
+g.eval("var dbg = new Debugger(debuggeeGlobal);" +
+       "dbg.onDebuggerStatement = function () { return {return: '!'}; };");
+
+function* gen() {
+    yield '1';
+    debugger;  // Force return here. The value is ignored.
+    yield '2';
+}
+var iter = gen();
+assertIteratorResult(iter.next(), '1', false);
+assertEq(iter.next(), '!');
+assertThrowsInstanceOf(iter.next.bind(iter), TypeError);
--- a/js/src/jsiter.cpp
+++ b/js/src/jsiter.cpp
@@ -1798,22 +1798,22 @@ template<typename T, NativeImpl Impl>
 static bool
 NativeMethod(JSContext *cx, unsigned argc, Value *vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     return CallNonGenericMethod<IsObjectOfType<T>, Impl>(cx, args);
 }
 
 #define JSPROP_ROPERM   (JSPROP_READONLY | JSPROP_PERMANENT)
-#define JS_METHOD(name, T, impl, len, perms) JS_FN(name, (NativeMethod<T,impl>), len, perms)
+#define JS_METHOD(name, T, impl, len, attrs) JS_FN(name, (NativeMethod<T,impl>), len, attrs)
 
 static const JSFunctionSpec star_generator_methods[] = {
     JS_FN("iterator", iterator_iterator, 0, 0),
-    JS_METHOD("next", StarGeneratorObject, star_generator_next, 1, JSPROP_ROPERM),
-    JS_METHOD("throw", StarGeneratorObject, star_generator_throw, 1, JSPROP_ROPERM),
+    JS_METHOD("next", StarGeneratorObject, star_generator_next, 1, 0),
+    JS_METHOD("throw", StarGeneratorObject, star_generator_throw, 1, 0),
     JS_FS_END
 };
 
 static const JSFunctionSpec legacy_generator_methods[] = {
     JS_FN("iterator", iterator_iterator, 0, 0),
     // "send" is an alias for "next".
     JS_METHOD("next", LegacyGeneratorObject, legacy_generator_next, 1, JSPROP_ROPERM),
     JS_METHOD("send", LegacyGeneratorObject, legacy_generator_next, 1, JSPROP_ROPERM),
--- a/js/src/jsreflect.cpp
+++ b/js/src/jsreflect.cpp
@@ -91,16 +91,18 @@ char const * const js::nodeTypeNames[] =
 
 static char const * const callbackNames[] = {
 #define ASTDEF(ast, str, method) method,
 #include "jsast.tbl"
 #undef ASTDEF
     NULL
 };
 
+enum YieldKind { Delegating, NotDelegating };
+
 typedef AutoValueVector NodeVector;
 
 /*
  * ParseNode is a somewhat intricate data structure, and its invariants have
  * evolved, making it more likely that there could be a disconnect between the
  * parser and the AST serializer. We use these macros to check invariants on a
  * parse node and raise a dynamic error on failure.
  */
@@ -596,17 +598,17 @@ class NodeBuilder
     bool arrayExpression(NodeVector &elts, TokenPos *pos, MutableHandleValue dst);
 
     bool spreadExpression(HandleValue expr, TokenPos *pos, MutableHandleValue dst);
 
     bool objectExpression(NodeVector &elts, TokenPos *pos, MutableHandleValue dst);
 
     bool thisExpression(TokenPos *pos, MutableHandleValue dst);
 
-    bool yieldExpression(HandleValue arg, TokenPos *pos, MutableHandleValue dst);
+    bool yieldExpression(HandleValue arg, YieldKind kind, TokenPos *pos, MutableHandleValue dst);
 
     bool comprehensionBlock(HandleValue patt, HandleValue src, bool isForEach, bool isForOf, TokenPos *pos,
                             MutableHandleValue dst);
 
     bool comprehensionExpression(HandleValue body, NodeVector &blocks, HandleValue filter,
                                  TokenPos *pos, MutableHandleValue dst);
 
     bool generatorExpression(HandleValue body, NodeVector &blocks, HandleValue filter,
@@ -1237,23 +1239,33 @@ NodeBuilder::thisExpression(TokenPos *po
     RootedValue cb(cx, callbacks[AST_THIS_EXPR]);
     if (!cb.isNull())
         return callback(cb, pos, dst);
 
     return newNode(AST_THIS_EXPR, pos, dst);
 }
 
 bool
-NodeBuilder::yieldExpression(HandleValue arg, TokenPos *pos, MutableHandleValue dst)
+NodeBuilder::yieldExpression(HandleValue arg, YieldKind kind, TokenPos *pos, MutableHandleValue dst)
 {
     RootedValue cb(cx, callbacks[AST_YIELD_EXPR]);
+    RootedValue delegateVal(cx);
+
+    switch (kind) {
+      case Delegating:
+        delegateVal = BooleanValue(true);
+        break;
+      case NotDelegating:
+        delegateVal = BooleanValue(false);
+        break;
+    }
+
     if (!cb.isNull())
-        return callback(cb, opt(arg), pos, dst);
-
-    return newNode(AST_YIELD_EXPR, pos, "argument", arg, dst);
+        return callback(cb, opt(arg), delegateVal, pos, dst);
+    return newNode(AST_YIELD_EXPR, pos, "argument", arg, "delegate", delegateVal, dst);
 }
 
 bool
 NodeBuilder::comprehensionBlock(HandleValue patt, HandleValue src, bool isForEach, bool isForOf, TokenPos *pos,
                                 MutableHandleValue dst)
 {
     RootedValue isForEachVal(cx, BooleanValue(isForEach));
     RootedValue isForOfVal(cx, BooleanValue(isForOf));
@@ -2592,23 +2604,32 @@ ASTSerializer::expression(ParseNode *pn,
       case PNK_STRING:
       case PNK_REGEXP:
       case PNK_NUMBER:
       case PNK_TRUE:
       case PNK_FALSE:
       case PNK_NULL:
         return literal(pn, dst);
 
+      case PNK_YIELD_STAR:
+      {
+        JS_ASSERT(pn->pn_pos.encloses(pn->pn_kid->pn_pos));
+
+        RootedValue arg(cx);
+        return expression(pn->pn_kid, &arg) &&
+               builder.yieldExpression(arg, Delegating, &pn->pn_pos, dst);
+      }
+
       case PNK_YIELD:
       {
         JS_ASSERT_IF(pn->pn_kid, pn->pn_pos.encloses(pn->pn_kid->pn_pos));
 
         RootedValue arg(cx);
         return optExpression(pn->pn_kid, &arg) &&
-               builder.yieldExpression(arg, &pn->pn_pos, dst);
+               builder.yieldExpression(arg, NotDelegating, &pn->pn_pos, dst);
       }
 
       case PNK_ARRAYCOMP:
         JS_ASSERT(pn->pn_pos.encloses(pn->pn_head->pn_pos));
 
         /* 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));
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Generators/delegating-yield-1.js
@@ -0,0 +1,38 @@
+// This file was written by Andy Wingo <wingo@igalia.com> and originally
+// contributed to V8 as generators-objects.js, available here:
+//
+// http://code.google.com/p/v8/source/browse/branches/bleeding_edge/test/mjsunit/harmony/generators-objects.js
+
+// Test that yield* re-yields received results without re-boxing.
+
+function results(results) {
+    var i = 0;
+    function next() {
+        return results[i++];
+    }
+    return { next: next }
+}
+
+function* yield_results(expected) {
+    return yield* results(expected);
+}
+
+function collect_results(iter) {
+    var ret = [];
+    var result;
+    do {
+        result = iter.next();
+        ret.push(result);
+    } while (!result.done);
+    return ret;
+}
+
+// We have to put a full result for the end, because the return will re-box.
+var expected = [{value: 1}, 13, "foo", {value: 34, done: true}];
+
+// Sanity check.
+assertDeepEq(expected, collect_results(results(expected)));
+assertDeepEq(expected, collect_results(yield_results(expected)));
+
+if (typeof reportCompare == "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Generators/delegating-yield-10.js
@@ -0,0 +1,49 @@
+// Errors accessing next, done, or value don't cause an exception to be
+// thrown into the iterator of a yield*.
+
+function* g(n) { for (var i=0; i<n; i++) yield i; }
+function* delegate(iter) { return yield* iter; }
+
+var log = "", inner, outer;
+
+// That var is poisoooooon, p-poison poison...
+var Poison = new Error;
+
+function log_calls(method) {
+    return function () {
+        log += "x"
+        return method.call(this);
+    }
+}
+
+function poison(receiver, prop) {
+    Object.defineProperty(receiver, prop, { get: function () { throw Poison } });
+}
+
+// Poison inner.next.
+inner = g(10);
+outer = delegate(inner);
+inner.throw = log_calls(inner.throw);
+poison(inner, 'next')
+assertThrowsValue(outer.next.bind(outer), Poison);
+assertEq(log, "");
+
+// Poison result value from inner.
+inner = g(10);
+outer = delegate(inner);
+inner.next = function () { return { done: true, get value() { throw Poison} } };
+inner.throw = log_calls(inner.throw);
+assertThrowsValue(outer.next.bind(outer), Poison);
+assertEq(log, "");
+
+// Poison result done from inner.
+inner = g(10);
+outer = delegate(inner);
+inner.next = function () { return { get done() { throw Poison }, value: 42 } };
+inner.throw = log_calls(inner.throw);
+assertThrowsValue(outer.next.bind(outer), Poison);
+assertEq(log, "");
+
+// mischief managed.
+if (typeof reportCompare == "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Generators/delegating-yield-11.js
@@ -0,0 +1,19 @@
+// The first call to yield* passes one arg to "next".
+
+function Iter() {
+    function next() {
+        if (arguments.length != 1)
+            throw Error;
+        return { value: 42, done: true }
+    }
+
+    this.next = next;
+}
+
+function* delegate(iter) { return yield* iter; }
+
+var iter = delegate(new Iter());
+assertDeepEq(iter.next(), {value:42, done:true});
+
+if (typeof reportCompare == "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Generators/delegating-yield-2.js
@@ -0,0 +1,71 @@
+// Test yield* with iter.throw and monkeypatching.
+
+function* g1() { return (yield 1); }
+function* g2() { try { yield 1; } catch (e) { yield e; } }
+function* delegate(iter) { return yield* iter; }
+var GeneratorObjectPrototype = Object.getPrototypeOf(g1).prototype;
+var GeneratorObjectPrototype_throw = GeneratorObjectPrototype.throw;
+
+// An uncaught delegated throw.
+var inner = g1();
+var outer = delegate(inner);
+assertIteratorResult(1, false, outer.next());
+assertThrowsValue(function () { outer.throw(42) }, 42);
+assertThrowsInstanceOf(function () { outer.throw(42) }, TypeError);
+
+// A caught delegated throw.
+inner = g2();
+outer = delegate(inner);
+assertIteratorResult(1, false, outer.next());
+assertIteratorResult(42, false, outer.throw(42));
+assertThrowsValue(function () { outer.throw(42) }, 42);
+assertThrowsInstanceOf(function () { outer.throw(42) }, TypeError);
+
+// What would be an uncaught delegated throw, but with a monkeypatched iterator.
+inner = g1();
+outer = delegate(inner);
+assertIteratorResult(1, false, outer.next());
+inner.throw = function(e) { return e*2; };
+assertEq(84, outer.throw(42));
+assertIteratorResult(undefined, true, outer.next());
+
+// Monkeypatching inner.next.
+inner = g1();
+outer = delegate(inner);
+inner.next = function() { return { value: 13, done: true } };
+assertIteratorResult(13, true, outer.next());
+
+// What would be a caught delegated throw, but with a monkeypunched prototype.
+inner = g2();
+outer = delegate(inner);
+assertIteratorResult(1, false, outer.next());
+delete GeneratorObjectPrototype.throw;
+var outer_throw_42 = GeneratorObjectPrototype_throw.bind(outer, 42);
+assertThrowsValue(outer_throw_42, 42);
+assertThrowsInstanceOf(outer_throw_42, TypeError);
+
+// Monkeypunch a different throw handler.
+inner = g2();
+outer = delegate(inner);
+outer_throw_42 = GeneratorObjectPrototype_throw.bind(outer, 42);
+assertIteratorResult(1, false, outer.next());
+GeneratorObjectPrototype.throw = function(e) { return e*2; }
+assertEq(84, outer_throw_42());
+assertEq(84, outer_throw_42());
+// This continues indefinitely.
+assertEq(84, outer_throw_42());
+assertIteratorResult(undefined, true, outer.next());
+
+// The same, but restoring the original pre-monkey throw.
+inner = g2();
+outer = delegate(inner);
+outer_throw_42 = GeneratorObjectPrototype_throw.bind(outer, 42);
+assertIteratorResult(1, false, outer.next());
+assertEq(84, outer_throw_42());
+assertEq(84, outer_throw_42());
+GeneratorObjectPrototype.throw = GeneratorObjectPrototype_throw;
+assertIteratorResult(42, false, outer_throw_42());
+assertIteratorResult(undefined, true, outer.next());
+
+if (typeof reportCompare == "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Generators/delegating-yield-3.js
@@ -0,0 +1,40 @@
+// Test yield* with iter.next and monkeypatching.
+
+function* g(n) { for (var i=0; i<n; i++) yield i; }
+function* delegate(iter) { return yield* iter; }
+var GeneratorObjectPrototype = Object.getPrototypeOf(g).prototype;
+var GeneratorObjectPrototype_next = GeneratorObjectPrototype.next;
+
+// Monkeypatch next on an iterator.
+var inner = g(20);
+var outer = delegate(inner);
+assertIteratorResult(0, false, outer.next());
+assertIteratorResult(1, false, outer.next());
+inner.next = function() { return 0; };
+// 42 yielded directly without re-boxing.
+assertEq(0, outer.next());
+// Outer generator not terminated.
+assertEq(0, outer.next());
+// Restore.
+inner.next = GeneratorObjectPrototype_next;
+assertIteratorResult(2, false, outer.next());
+// Repatch.
+inner.next = function() { return { value: 42, done: true }; };
+assertIteratorResult(42, true, outer.next());
+
+// Monkeypunch next on the prototype.
+var inner = g(20);
+var outer = delegate(inner);
+assertIteratorResult(0, false, outer.next());
+assertIteratorResult(1, false, outer.next());
+GeneratorObjectPrototype.next = function() { return 0; };
+// 42 yielded directly without re-boxing.
+assertEq(0, GeneratorObjectPrototype_next.call(outer));
+// Outer generator not terminated.
+assertEq(0, GeneratorObjectPrototype_next.call(outer));
+// Restore.
+GeneratorObjectPrototype.next = GeneratorObjectPrototype_next;
+assertIteratorResult(2, false, outer.next());
+
+if (typeof reportCompare == "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Generators/delegating-yield-4.js
@@ -0,0 +1,18 @@
+// With yield*, inner and outer iterators can be invoked separately.
+
+function* g(n) { for (var i=0; i<n; i++) yield i; }
+function* delegate(iter) { return yield* iter; }
+
+var inner = g(20);
+var outer1 = delegate(inner);
+var outer2 = delegate(inner);
+
+assertIteratorResult(0, false, outer1.next());
+assertIteratorResult(1, false, outer2.next());
+assertIteratorResult(2, false, inner.next());
+assertIteratorResult(3, false, outer1.next());
+assertIteratorResult(4, false, outer2.next());
+assertIteratorResult(5, false, inner.next());
+
+if (typeof reportCompare == "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Generators/delegating-yield-5.js
@@ -0,0 +1,33 @@
+// Test that a deep yield* chain re-yields received results without
+// re-boxing.
+
+function results(results) {
+    var i = 0;
+    function next() {
+        return results[i++];
+    }
+    return { next: next }
+}
+
+function* yield_results(expected, n) {
+    return yield* n ? yield_results(expected, n - 1) : results(expected);
+}
+
+function collect_results(iter) {
+    var ret = [];
+    var result;
+    do {
+        result = iter.next();
+        ret.push(result);
+    } while (!result.done);
+    return ret;
+}
+
+// We have to put a full result for the end, because the return will re-box.
+var expected = [{value: 1}, 13, "foo", {value: 34, done: true}];
+
+assertDeepEq(expected, collect_results(results(expected)));
+assertDeepEq(expected, collect_results(yield_results(expected, 20)));
+
+if (typeof reportCompare == "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Generators/delegating-yield-6.js
@@ -0,0 +1,49 @@
+// Test that each yield* loop just checks "done", and "value" is only
+// fetched once at the end.
+
+var log = "";
+
+function collect_results(iter) {
+    var ret = [];
+    var result;
+    do {
+        result = iter.next();
+        ret.push(result);
+    } while (!result.done);
+    return ret;
+}
+
+function Iter(val, count) {
+    function next() {
+        return {
+            get done() { log += "d"; return count-- == 0; },
+            get value() { log += "v"; return val; }
+        }
+    }
+
+    this.next = next;
+}
+
+function* delegate(iter) { return yield* iter; }
+
+var inner = new Iter(42, 5);
+var outer = delegate(inner);
+
+// Five values, and one terminal value.
+outer.next();
+outer.next();
+outer.next();
+outer.next();
+outer.next();
+outer.next();
+
+assertEq(log, "ddddddv");
+
+// Outer's dead, man.  Outer's dead.
+assertThrowsInstanceOf(outer.next.bind(outer), TypeError);
+
+// No more checking the iterator.
+assertEq(log, "ddddddv");
+
+if (typeof reportCompare == "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Generators/delegating-yield-7.js
@@ -0,0 +1,33 @@
+// The iteratee of yield* can be a proxy.
+
+function results(results) {
+    var i = 0;
+    function next() {
+        return results[i++];
+    }
+    return { next: next }
+}
+
+function* yield_results(expected) {
+    return yield* Proxy(results(expected), {});
+}
+
+function collect_results(iter) {
+    var ret = [];
+    var result;
+    do {
+        result = iter.next();
+        ret.push(result);
+    } while (!result.done);
+    return ret;
+}
+
+// We have to put a full result for the end, because the return will re-box.
+var expected = [{value: 1}, 13, "foo", {value: 34, done: true}];
+
+// Sanity check.
+assertDeepEq(expected, collect_results(results(expected)));
+assertDeepEq(expected, collect_results(yield_results(expected)));
+
+if (typeof reportCompare == "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Generators/delegating-yield-8.js
@@ -0,0 +1,44 @@
+// Test that yield* can appear in a loop, and alongside yield.
+
+function* countdown(n) {
+    while (n > 0) {
+        yield n;
+        yield* countdown(--n);
+    }
+    return 34;
+}
+
+function collect_results(iter) {
+    var ret = [];
+    var result;
+    do {
+        result = iter.next();
+        ret.push(result);
+    } while (!result.done);
+    return ret;
+}
+
+var expected = [
+    // yield in countdown(3), n == 3
+    {value: 3, done: false},
+    // yield in yield* countdown(2), n == 2
+    {value: 2, done: false},
+    // yield in nested yield* countdown(1), n == 1
+    {value: 1, done: false},
+    // countdown(0) yields no values
+    // second go-through of countdown(2) loop, n == 1
+    {value: 1, done: false},
+    // second go-through of countdown(3) loop, n == 2
+    {value: 2, done: false},
+    // yield in yield* countdown(1), n == 1
+    {value: 1, done: false},
+    // third go-through of countdown(3) loop, n == 1
+    {value: 1, done: false},
+    // done
+    {value: 34, done: true}
+];
+
+assertDeepEq(expected, collect_results(countdown(3)));
+
+if (typeof reportCompare == "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Generators/delegating-yield-9.js
@@ -0,0 +1,37 @@
+// Test that yield* can appear in a loop, and inside yield.
+
+function* countdown(n) {
+    while (n > 0) {
+        yield (yield* countdown(--n));
+    }
+    return 34;
+}
+
+function collect_results(iter) {
+    var ret = [];
+    var result;
+    do {
+        result = iter.next();
+        ret.push(result);
+    } while (!result.done);
+    return ret;
+}
+
+var expected = [
+    // Only 34 yielded from the "yield" and the last return make it out.
+    // Three yields in countdown(3), two in countdown(2), and one in
+    // countdown(1) (called twice).
+    {value: 34, done: false},
+    {value: 34, done: false},
+    {value: 34, done: false},
+    {value: 34, done: false},
+    {value: 34, done: false},
+    {value: 34, done: false},
+    {value: 34, done: false},
+    {value: 34, done: true}, // final
+];
+
+assertDeepEq(collect_results(countdown(3)), expected);
+
+if (typeof reportCompare == "function")
+    reportCompare(true, true);
--- a/js/src/tests/ecma_6/Generators/iteration.js
+++ b/js/src/tests/ecma_6/Generators/iteration.js
@@ -53,21 +53,19 @@ function TestGenerator(g, expected_value
             assertThrowsInstanceOf(function () { iter.next(); }, TypeError);
         }
     }
 
     testNext(g);
     testSend(g);
     testThrow(g);
 
-    // FIXME: Implement yield*.  Bug 907738.
-    //
-    // testNext(function*() { return yield* g(); });
-    // testSend(function*() { return yield* g(); });
-    // testThrow(function*() { return yield* g(); });
+    testNext(function*() { return yield* g(); });
+    testSend(function*() { return yield* g(); });
+    testThrow(function*() { return yield* g(); });
 
     if (g instanceof GeneratorFunction) {
         testNext(function() { return new g(); });
         testSend(function() { return new g(); });
         testThrow(function() { return new g(); });
     }
 }
 
@@ -327,49 +325,16 @@ TestGenerator(
 TestGenerator(
     function () {
         return ({ x: 42, g: function* (a) { yield this.x } }).g(0);
     },
     [42, undefined],
     "foo",
     [42, undefined]);
 
-/* FIXME: Implement yield*.  Bug 907738.
-
-// Test that yield* re-yields received results without re-boxing.
-function TestDelegatingYield() {
-    function results(results) {
-        var i = 0;
-        function next() {
-            return results[i++];
-        }
-        return { next: next }
-    }
-    function* yield_results(expected) {
-        return yield* results(expected);
-    }
-    function collect_results(iter) {
-        var ret = [];
-        var result;
-        do {
-            result = iter.next();
-            ret.push(result);
-        } while (!result.done);
-        return ret;
-    }
-    // We have to put a full result for the end, because the return will re-box.
-    var expected = [{value: 1}, 13, "foo", {value: 34, done: true}];
-
-    // Sanity check.
-    assertDeepEq(expected, collect_results(results(expected)));
-    assertDeepEq(expected, collect_results(yield_results(expected)));
-}
-TestDelegatingYield();
-*/
-
 function TestTryCatch(instantiate) {
     function* g() { yield 1; try { yield 2; } catch (e) { yield e; } yield 3; }
     function Sentinel() {}
 
     function Test1(iter) {
         assertIteratorResult(1, false, iter.next());
         assertIteratorResult(2, false, iter.next());
         assertIteratorResult(3, false, iter.next());
@@ -420,18 +385,17 @@ function TestTryCatch(instantiate) {
         var exn = new Sentinel;
         assertIteratorResult(exn, false, iter.throw(exn));
         assertThrowsInstanceOf(function() { iter.throw(new Sentinel); }, Sentinel);
         assertThrowsInstanceOf(function() { iter.next(); }, TypeError);
     }
     Test6(instantiate(g));
 }
 TestTryCatch(function (g) { return g(); });
-// FIXME: Implement yield*.  Bug 907738.
-// TestTryCatch(function* (g) { return yield* g(); });
+TestTryCatch(function* (g) { return yield* g(); });
 
 function TestTryFinally(instantiate) {
     function* g() { yield 1; try { yield 2; } finally { yield 3; } yield 4; }
     function Sentinel() {}
     function Sentinel2() {}
 
     function Test1(iter) {
         assertIteratorResult(1, false, iter.next());
@@ -490,18 +454,17 @@ function TestTryFinally(instantiate) {
         assertIteratorResult(3, false, iter.next());
         assertIteratorResult(4, false, iter.next());
         assertThrowsInstanceOf(function() { iter.throw(new Sentinel); }, Sentinel);
         assertThrowsInstanceOf(function() { iter.next(); }, TypeError);
     }
     Test7(instantiate(g));
 }
 TestTryFinally(function (g) { return g(); });
-// FIXME: Implement yield*.  Bug 907738.
-// TestTryFinally(function* (g) { return yield* g(); });
+TestTryFinally(function* (g) { return yield* g(); });
 
 function TestNestedTry(instantiate) {
     function* g() {
         try {
             yield 1;
             try { yield 2; } catch (e) { yield e; }
             yield 3;
         } finally {
@@ -581,18 +544,17 @@ function TestNestedTry(instantiate) {
         assertThrowsInstanceOf(function() { iter.next(); }, TypeError);
 
     }
     Test7(instantiate(g));
 
     // That's probably enough.
 }
 TestNestedTry(function (g) { return g(); });
-// FIXME: Implement yield*.  Bug 907738.
-// TestNestedTry(function* (g) { return yield* g(); });
+TestNestedTry(function* (g) { return yield* g(); });
 
 function TestRecursion() {
     function TestNextRecursion() {
         function* g() { yield iter.next(); }
         var iter = g();
         return iter.next();
     }
     function TestSendRecursion() {
--- a/js/src/vm/Interpreter.cpp
+++ b/js/src/vm/Interpreter.cpp
@@ -3347,17 +3347,17 @@ default:
              * to the beginning of catch or finally or to [enditer] closing
              * the for-in loop.
              */
             regs.pc = (script)->main() + tn->start + tn->length;
             regs.sp = regs.spForStackDepth(tn->stackDepth);
 
             switch (tn->kind) {
               case JSTRY_CATCH:
-                  JS_ASSERT(*regs.pc == JSOP_ENTERBLOCK);
+                JS_ASSERT(*regs.pc == JSOP_ENTERBLOCK || *regs.pc == JSOP_EXCEPTION);
 
                 /* Catch cannot intercept the closing of a generator. */
                   if (JS_UNLIKELY(cx->getPendingException().isMagic(JS_GENERATOR_CLOSING)))
                     break;
 
                 /*
                  * Don't clear exceptions to save cx->exception from GC
                  * until it is pushed to the stack via [exception] in the