Closed Bug 958949 Opened 10 years ago Closed 10 years ago

yield in finally overrides return completion from try

Categories

(Core :: JavaScript Engine, defect)

defect
Not set
normal

Tracking

()

RESOLVED FIXED
mozilla37

People

(Reporter: anba, Assigned: arai)

References

(Blocks 1 open bug)

Details

Attachments

(1 file, 2 obsolete files)

Maybe related to bug 718531?

Test case:
---
js> function*g(){ try { return 0 } finally { yield 1 } }
js> o = g(); o.next(); o.next()

Expected: ({value:0, done:true})
Actual: ({value:1, done:false})
---
Can you point out where the spec says that your expected result is correct?

By my reading, this is correct behavior:
- `return 0` sets the rval to `0`
- the finally block is executed
- `yield 1` sets the rval to the result of `GeneratorYield(CreateIterResultObject(1, false))`
- control is returned to the callee, with the current rval as the returned value
- the callee interprets the returned value as always
There are two calls to next() in the example code. The first call does return the correct result, i.e. {value: 1, done: false}, but the second call does not:
---
js> function*g(){ try { return 0 } finally { yield 1 } }
js> o = g()
({})
js> o.next()
({value:1, done:false})
js> o.next()
({value:1, done:false})
js> o.next()
typein:5:0 TypeError: generator has already finished
---
Blocks: es6
Blocks: es6:let
This shouldn't be blocking bug 950547 now that generators are out of scope of it; I'm removing it.
No longer blocks: es6:let
Now situation is a bit different, but still incorrect.

---
js> function*g(){ try { return 0 } finally { yield 1 } }
js> o = g();
({})
js> o.next()
({value:1, done:false})
js> o.next()
js> o.next()
({value:(void 0), done:true})
---

the second next() returns undefined.
(In reply to Tooru Fujisawa [:arai] from comment #4)
> the second next() returns undefined.

Oh, this is interesting. What happens is this:

(1) For the "return 0" we set the frame's rval slot to {value: 0, done: true}. We do *not* return.
(2) We execute the finally block. This yields {value: 1, done: false}.
(3) We resume and RETSUB returns to the return inside the try block, which does finalyieldrval but rval is |undefined| because we didn't save/restore the rval slot as part of (2).

This is most likely a regression from bug 987560. We could add a GeneratorObject slot for the frame's rval but it'd be nice if we could avoid that.
Flags: needinfo?(jdemooij)
Added .genrval local variable (like .generator variable),
save return value into it before executing finally-block,
and restore the value from it after retsub.

Also, changed PNK_RETURN to binary, to store name node of .genrval,
which is needed to emit JSOP_SETALIASEDVAR, where hops might be != 0.


Now bytecode for generator function `function*g(){ try { return 0 } finally { yield 1 } }` is following:

---
00000:  generator
00001:  setaliasedvar ".generator" (hops = 0, slot = 2)
00006:  initialyield 0
00010:  debugafteryield
00011:  pop
00012:  try
00013:  newobject ({value:(void 0), done:(void 0)})
00018:  zero
00019:  initprop "value"
00024:  true
00025:  initprop "done"
00030:  setaliasedvar ".genrval" (hops = 0, slot = 3)   // save return value
00035:  pop
00036:  gosub 63 (+27)
00041:  getaliasedvar ".genrval" (hops = 0, slot = 3)   // restore return value
00046:  setrval
00047:  getaliasedvar ".generator" (hops = 0, slot = 2)
00052:  finalyieldrval
00053:  gosub 63 (+10)
00058:  goto 94 (+36)
00063:  finally
00064:  newobject ({value:(void 0), done:(void 0)})
00069:  one
00070:  initprop "value"
00075:  false
00076:  initprop "done"
00081:  getaliasedvar ".generator" (hops = 0, slot = 2)
00086:  yield 1
00090:  debugafteryield
00091:  pop
00092:  retsub
00093:  nop
00094:  newobject ({value:(void 0), done:(void 0)})
00099:  undefined
00100:  initprop "value"
00105:  true
00106:  initprop "done"
00111:  setrval
00112:  getaliasedvar ".generator" (hops = 0, slot = 2)
00117:  finalyieldrval
00118:  retrval
---

No bytecode change happened if there is no finally block.

Green on try run: https://treeherder.mozilla.org/ui/#/jobs?repo=try&revision=862649f14379
Attachment #8531282 - Flags: review?(jdemooij)
just rebased :)
Attachment #8531282 - Attachment is obsolete: true
Attachment #8531282 - Flags: review?(jdemooij)
Attachment #8533519 - Flags: review?(jdemooij)
Comment on attachment 8533519 [details] [diff] [review]
Save return value for generator function into local variable before running finally-block.

Review of attachment 8533519 [details] [diff] [review]:
-----------------------------------------------------------------

Hi arai, I'm very sorry for the delay. Excellent fix, thanks so much for taking this!

Just some small comments, if you can post an updated patch I'll review quickly.

::: js/src/frontend/BytecodeEmitter.cpp
@@ +5585,5 @@
>      return EmitGoto(cx, bce, stmt, &stmt->continues, SRC_CONTINUE) >= 0;
>  }
>  
>  static bool
> +hasFinally(BytecodeEmitter *bce)

Style nit: HasFinally

Also STMT_FINALLY is confusing because it's pushed when we're emitting the try-code, not the finally-block itself. So to make this more obvious, maybe InTryBlockWithFinally()?

@@ +5637,5 @@
>      bool isGenerator = bce->sc->isFunctionBox() && bce->sc->asFunctionBox()->isGenerator();
> +    bool useGenRVal = false;
> +    if (isGenerator) {
> +        if (bce->sc->asFunctionBox()->isStarGenerator() && hasFinally(bce)) {
> +            useGenRVal = true;

Nit: I'd add a short comment here to explain to the reader what's going on:

Emit JSOP_SETALIASEDVAR .genrval to store the return value on the scope chain, so it's not lost when we yield in a finally block.

::: js/src/frontend/Parser.cpp
@@ +1086,5 @@
>              return null();
>          if (!pc->define(tokenStream, context->names().dotGenerator, generator, Definition::VAR))
>              return null();
>  
> +        if (pc->generatorKind() == StarGenerator) {

Nit: if (pc->isStarGenerator())

@@ +3119,5 @@
>           * can potentially override any static bindings introduced by statements
>           * further up the stack, we have to abort the search.
>           */
> +        if (stmt->type == STMT_WITH && atom != ct->sc->context->names().dotGenerator &&
> +            atom != ct->sc->context->names().dotGenRVal) {

Can you add a helper function somewhere to check if a name is dotGenerator or dotGenRVal and call that everywhere? Like IsDotVariable or something better.

(Either make it a static helper function with an ExclusiveContext* argument, or add a method to sc and use context.)

::: js/src/jit-test/tests/generators/bug958949.js
@@ +6,5 @@
> +function* g1() {
> +  try {
> +    return 42;
> +  } finally {
> +    yield 43;

Great tests, thank you.
Attachment #8533519 - Flags: review?(jdemooij)
Flags: needinfo?(jdemooij)
Thank you for reviewing! :)

(In reply to Jan de Mooij [:jandem] from comment #8)
> @@ +3119,5 @@
> >           * can potentially override any static bindings introduced by statements
> >           * further up the stack, we have to abort the search.
> >           */
> > +        if (stmt->type == STMT_WITH && atom != ct->sc->context->names().dotGenerator &&
> > +            atom != ct->sc->context->names().dotGenRVal) {
> 
> Can you add a helper function somewhere to check if a name is dotGenerator
> or dotGenRVal and call that everywhere? Like IsDotVariable or something
> better.
> 
> (Either make it a static helper function with an ExclusiveContext* argument,
> or add a method to sc and use context.)

Added IsDotVariable to SharedContext, and used the context property of itself to access `names()`,
In following 3 functions, previously different `ExclusiveContext *` variable was used.
 * Parser<FullParseHandler>::withStatement  --  Parser::context
 * LegacyCompExprTransplanter::transplant  --  Parser::context
 * EmitAtomOp  --  cx parameter
Since `ThreadSafeContext::names()` returns JSRuntime::commonNames,
they still return same JSAtomState instance even if those pointers refer different context instance,
as long as those contexts are created in same JSRuntime.

Is it possible situation that they are created in different JSRuntime's?
if so, this patch is wrong, and we should add a helper function in some header file.


Also, renamed test file to "yield-in-finally.js", and added one more test case with "with" statement.

Green on try run: https://treeherder.mozilla.org/ui/#/jobs?repo=try&revision=09ab93fc9aab
Attachment #8533519 - Attachment is obsolete: true
Attachment #8536957 - Flags: review?(jdemooij)
Comment on attachment 8536957 [details] [diff] [review]
Save return value for generator function into local variable before running finally-block.

Review of attachment 8536957 [details] [diff] [review]:
-----------------------------------------------------------------

Great work, thanks again.

::: js/src/frontend/SharedContext.h
@@ +206,5 @@
>      bool needStrictChecks() {
>          return strict || extraWarnings;
>      }
> +
> +    bool IsDotVariable(JSAtom *atom) {

Style nit: it's a class method so it should be isDotVariable. Also I think you can make it const:

    bool isDotVariable(JSAtom *atom) const {
Attachment #8536957 - Flags: review?(jdemooij) → review+
https://hg.mozilla.org/mozilla-central/rev/d3ce465f852c
Assignee: nobody → arai_a
Status: NEW → RESOLVED
Closed: 10 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla37
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: