archives

« Bugzilla Issues Index

#3526 — yield* broken for throw()


The semantics for the interaction between throw() and yield* seem to have changed in a recent revision of the spec. The text now is:

YieldExpression : yield * AssignmentExpression
1. Let exprRef be the result of evaluating AssignmentExpression.
2. Let value be GetValue(exprRef).
3. Let iterator be GetIterator(value).
4. ReturnIfAbrupt(iterator).
5. Let received be NormalCompletion(undefined).
6. Repeat
a. If received.[[type]] is normal, then
i. Let innerResult be IteratorNext(iterator, received.[[value]]).
ii. ReturnIfAbrupt(innerResult).
iii. Let done be IteratorComplete(innerResult).
iv. ReturnIfAbrupt(done).
v. If done is true, then
1. Return IteratorValue (innerResult).
vi. Let received be GeneratorYield(innerResult).
b. Else if received.[[type]] is throw, then
i. Let hasThrow be HasProperty(iterator, "throw").
ii. ReturnIfAbrupt(hasThrow).
iii. If hasThrow is true, then
1. Let innerResult be Invoke(iterator, "throw", «‍received.[[value]]»).
2. ReturnIfAbrupt(innerResult).
3. NOTE: Exceptions from the inner iterator throw method are propagated.
iv. Return received.
c. Else,
i. Assert: received.[[type]] is return.
ii. Let hasReturn be HasProperty(iterator, "return").
iii. ReturnIfAbrupt(hasReturn).
iv. If hasReturn is false, then return received.
v. Let innerReturnValue be Invoke(iterator, "return", «‍received.[[value]]»).
vi. ReturnIfAbrupt(innerReturnValue).
vii. If Type(innerReturnValue) is not Object, then throw a TypeError exception.
viii. Let returnValue be IteratorValue(innerReturnValue).
ix. ReturnIfAbrupt(returnValue).
ix. Return Completion{[[type]]: return , [[value]]: returnValue , [[target]]:empty}.

So now if we have the coroutine:

function* ones_coroutine() {
while (true) {
try {
yield 1;
} catch (e) {}
}
}

And the yield* wrapper:

function* wrap(iterable) {
return yield* iterable
}

I thought that the intention behind yield* was that wrap(ones_coroutine()) would be equivalent to ones_coroutine().

However now they are different; consider

var i1 = ones_coroutine()
i1.next() // { value: 1, done: false }
i1.throw('foo') // { value: 1, done: false }
i1.next() // { value: 1, done: false }

var i2 = wrap(ones_coroutine())
i2.next() // { value: 1, done: false }
i2.throw(42) // { value: undefined, done: true }
i2.next() // { value: undefined, done: true }

This is particularly egregious for async functions, consider:

var task = async(function* () { ... yield* subtask(); ... });
var subtask = async(function* () { ... });

Now if a promise yielded by subtask() fails, subtask() will not have the chance to recover from the error and continue processing, yielding further values. This seems to me to be an error in the specification.


Jan pointed out to me that i2.throw(42) would throw 42 instead of returning { done: true, value: undefined }. Thanks Jan. Still, the point stands.


This behavior is present in the latest draft too. Allen, can you confirm please?


I haven't had a chance to get into it yet. Hopefully this week


(In reply to Andy Wingo from comment #0)
> The semantics for the interaction between throw() and yield* seem to have
> changed in a recent revision of the spec. The text now is:
>

Actually, it's been this way since generator semantics for first incorporated into the spec. (I checked)

But, I agree the currently spec'd behavior is a bug.

Fix on the way.


fixed in rev32 editor's draft.

the 'ReturnIfAbrupt' in step 6.b.iii.2 should just be a 'return'


(In reply to Allen Wirfs-Brock from comment #5)
> fixed in rev32 editor's draft.
>
> the 'ReturnIfAbrupt' in step 6.b.iii.2 should just be a 'return'

Thanks for taking a look, Allen! :)

Shouldn't 6.b.iv not be executed if hasThrow is true? I mean, if the iterator has "throw", calling the throw method should update "received" and loop, seems to me.


(In reply to Andy Wingo from comment #6)

>
> Shouldn't 6.b.iv not be executed if hasThrow is true? I mean, if the
> iterator has "throw", calling the throw method should update "received" and
> loop, seems to me.

It's even more complex than that:


b. Else if received.[[type]] is throw, then
i. Let hasThrow be HasProperty(iterator, "throw").
ii. ReturnIfAbrupt(hasThrow).
iii. If hasThrow is true, then
1. Let innerResult be Invoke(iterator, "throw", «received.[[value]]»).
2. ReturnIfAbrupt(innerResult).
3. NOTE: Exceptions from the inner iterator throw method are propagated. Normal completions from an inner throw method are processed just like an inner next.
4. If Type(innerResult) is not Object, throw a TypeError exception.
5. Let done be IteratorComplete(innerResult).
6. ReturnIfAbrupt(done).
7. If done is true, return IteratorValue(innerResult).
8. Let received be GeneratorYield(innerResult).
iv. Else,
1. NOTE: If iterator does not have a throw method, this throw is going to terminate the yield* loop. But first we need to give iterator a chance to clean up.
2. Return IteratorClose(iterator, received).


What about the following example?

var x;

function* ones_coroutine() {
try {
yield 1;
} finally {
yield 2;
x = 10;
}
}

function* wrap(iterable) {
return yield* iterable;
}

x = 0;
let g = ones_coroutine();
g.next() // -> {value: 1, done: false}
g.return(42) // -> {value: 2, done: false}
g.next() // -> {value: 42, done: true}
x // -> 10

x = 0;
let h = wrap(ones_coroutine());
h.next() // -> {value: 1, done: false}
h.return(42) // -> {value: 2, done: true}
h.next() // -> {value: undefined, done: true}
x // -> 0

If I understand the rev 31 of the draft correctly, the values behind the comments // -> reflect what the draft describes. However, this means that ones_coroutine() behaves very differently to wrap(ones_coroutine()).

This semantics seems not very plausible to me. I would be expecting that all finally clauses are being run when a generator (even an inner one) is being closed by a call to return.


fixed in rev32 draft