Skip to content

yield keyword does not work in for loop expression #3665

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
GabrielRatener opened this issue Oct 5, 2014 · 52 comments · Fixed by #3734
Closed

yield keyword does not work in for loop expression #3665

GabrielRatener opened this issue Oct 5, 2014 · 52 comments · Fixed by #3734
Labels

Comments

@GabrielRatener
Copy link
Contributor

When one tries to do the following

array = [1, 2, 3]
outvar = for i in array
  yield i * 2
  i * 2

Inside a gennerator function, the loop gets wrapped in another generator closure and an iterator is returned to outvar instead of an array, I presume this is a bug.

@jashkenas jashkenas added the bug label Oct 6, 2014
@jashkenas
Copy link
Owner

This will need to be addressed before the next release goes out. Let's add a failing test case for it — and other situations where we generate "hidden" function wrappers.

@GabrielRatener
Copy link
Contributor Author

I propose in the case that yield or any other es6 feature is detected by the lexer, this tells the compiler to use es6 scoping:

if (true){  // run code in block scope
    let var1, var2, var3; // declare scope variables here

    // compute value within block scope here ...

    // return value or set it to variable outside block scope
}

Any other ideas?

@xixixao
Copy link
Contributor

xixixao commented Oct 8, 2014

@GabrielRatener I doubt that approach would fly with Jeremy and it might be more work.

@GabrielRatener
Copy link
Contributor Author

@xixixao I have been puting some thought into alternatives to current function closures that could be used, and this is the best I could come up with. This seems to be a difficult bug to fix.

The tricky part is determining when to return or set a value directly, otherwise block scopes with let should work just like function scopes with var.

@akre54
Copy link

akre54 commented Oct 9, 2014

@GabrielRatener are you sure? Looks right to me.

➜  CoffeeScript (master) ✗ cat generate-test.coffee && ./bin/coffee -bcp generate-test.coffee
->
  array = [1, 2, 3]
  outvar = for i in array
    yield i * 2
    i * 2
// Generated by CoffeeScript 1.8.0
(function*() {
  var array, i, outvar;
  array = [1, 2, 3];
  return outvar = (function*() {
    var _i, _len, _results;
    _results = [];
    for (_i = 0, _len = array.length; _i < _len; _i++) {
      i = array[_i];
      (yield i) * 2;
      _results.push(i * 2);
    }
    return _results;
  })();
});

@jashkenas
Copy link
Owner

@akre54 -- do we have a good test for it already?

@akre54
Copy link

akre54 commented Oct 9, 2014

yield i for i in [3..4]

@akre54
Copy link

akre54 commented Oct 9, 2014

➜  CoffeeScript (master) ✗ ./bin/coffee --nodejs --harmony
coffee> gen = -> outvar = (yield (i * 2)) for i in [1..3]
[Function]
coffee> x = gen()
{}
coffee> x.next()
{ value: 2, done: false }
coffee> x.next()
{ value: 4, done: false }
coffee> x.next()
{ value: 6, done: false }
coffee> x.next()
{ value: [ undefined, undefined, undefined ], done: true }
coffee> x.next()
{ value: undefined, done: true }

@jashkenas
Copy link
Owner

{ value: [ undefined, undefined, undefined ], done: true }

Is that a problem?

@akre54
Copy link

akre54 commented Oct 9, 2014

Nah it just means I didn't pass anything to the next call:

coffee> x = gen()
{}
coffee> x.next('a')
{ value: 2, done: false }
coffee> x.next('b')
{ value: 4, done: false }
coffee> x.next('c')
{ value: 6, done: false }
coffee> x.next('d')
{ value: [ 'b', 'c', 'd' ], done: true }

@jashkenas
Copy link
Owner

Now I'm just confused, but whatever. Sounds like this is invalid.

@jashkenas jashkenas added invalid and removed bug labels Oct 9, 2014
@akre54
Copy link

akre54 commented Oct 9, 2014

Simpler:

coffee> gen = -> yield i for i in [1..3]
[Function]
coffee> x = gen()
{}
coffee> x.next('a')
{ value: 1, done: false }
coffee> x.next('b')
{ value: 2, done: false }
coffee> x.next('c')
{ value: 3, done: false }
coffee> x.next('d')
{ value: [ 'b', 'c', 'd' ], done: true }


coffee> gen = -> yield 1; yield 2; yield 3
[Function]
coffee> x = gen()
{}
coffee> x.next('a')
{ value: 1, done: false }
coffee> x.next('b')
{ value: 2, done: false }
coffee> x.next('c')
{ value: 3, done: false }
coffee> x.next('d')
{ value: 'd', done: true }

@GabrielRatener
Copy link
Contributor Author

@akre54 @jashkenas Sorry for the erroneous information, the closure created is actually another generator.

This still does not work though. In the case of gen = -> yield i for i in [1..3] no closures are created in this generator, which is why it works as expected. Try running the following in Node v0.11.11 with the harmony flag:

gogo = ->
    yield 2
    yield 3
    a = for i in [1..3]
        yield i
        6
    yield 4
    yield a

b = gogo()

console.log b.next()
console.log b.next()
console.log b.next()
console.log b.next()
console.log b.next()
console.log b.next()
console.log b.next()
console.log b.next()

This gives the following output:

{ value: 2, done: false }
{ value: 3, done: false }
{ value: 4, done: false }
{ value: {}, done: false }
{ value: undefined, done: true }

/home/peter/Apps/new/coffeescript/test.js:34
  console.log(b.next());
                ^
Error: Generator has already finished
    at GeneratorFunctionPrototype.next (native)
    at Object.<anonymous> (/home/peter/Apps/new/coffeescript/test.js:34:17)
    at Object.<anonymous> (/home/peter/Apps/new/coffeescript/test.js:40:4)
    at Module._compile (module.js:449:26)
    at Object.Module._extensions..js (module.js:467:10)
    at Module.load (module.js:349:32)
    at Function.Module._load (module.js:305:12)
    at Function.Module.runMain (module.js:490:10)
    at startup (node.js:123:16)
    at node.js:1128:3

Remember, generator functions return iterators, not the expressions to the right of the return statements they contain.

This is still a bug.

@jashkenas jashkenas reopened this Oct 9, 2014
@jashkenas jashkenas added bug and removed invalid labels Oct 9, 2014
@jashkenas
Copy link
Owner

@alubbe — What do you think about this wrinkle?

@akre54
Copy link

akre54 commented Oct 9, 2014

This looks like it has more to do with list comprehensions getting wrapped in a closure than any yield-specific changes. Without the closure the yield would work as intended.

I think it's downright odd that assigning the return value of the comprehension to a variable gets a closure (x = (i for i in [1..3])) but a non-returning comprehension doesn't.

@akre54
Copy link

akre54 commented Oct 9, 2014

@GabrielRatener The simple answer is to call next on the value yielded by a (since it is also a generator itself).

Simplifying your example:

gogo = ->
    a = for i in [1..3]
        yield i
        'a'
    yield a
    yield 'b'

outer = gogo()


console.log first = outer.next()
# { value: {}, done: false }  (the generator)

inner = first.value

console.log inner.next()
 # { value: 1, done: false }

console.log inner.next()
# { value: 2, done: false }

console.log inner.next()
# { value: 3, done: false }

console.log inner.next()
# { value: [ 'a', 'a', 'a' ], done: true }

console.log outer.next()
# { value: 'b', done: false }

console.log outer.next()
# { value: undefined, done: true }

@jashkenas
Copy link
Owner

@akre54 — You're missing the point here. It's not odd that CoffeeScript sometimes has to closure-wrap a complex expression ... that's part of our compilation strategy.

It's also not a simple answer to call next on the inner value. There's no obvious function there in the CoffeeScript source ... so we have to think about whether it makes sense to yield from within a comprehension that's also a value in itself. And how we could make that possible.

@akre54
Copy link

akre54 commented Oct 9, 2014

Sure. But I don't think you're getting what I'm saying. Why does

->
  for i in [1..3]
    yield i
    'a'
  'b'

... not get a closure? Keeping the two consistent (dropping the extra closure from assignments) would solve this issue, no?

@GabrielRatener
Copy link
Contributor Author

@akre54 Because the for loop is not being used as an expression in that case.

CS compiler detects when statements are used as expressions, and then wraps them in closures accordingly.

@akre54
Copy link

akre54 commented Oct 9, 2014

No no I got that. What I'm saying is that it used to be that comprehensions assigned as expressions would still live in the same block scope (only wrapped in a for loop) as the outer body, the way that non-expression comprehensions are currently.

Reverting to the old behavior would solve this issue with yield, but I imagine there was some reason the change was made in the first place (off the top of my head I can't think of anything and issue search is failing me). I know there was some discussion around explicitly asking for a closure using either the stabby arrow syntax or the do keyword, but again my search is failing me.

Edit: #959, #423, etc.

@GabrielRatener
Copy link
Contributor Author

Closure wrapping is used by CS to turn javascript statements into javascript expressions.

I'l let someone else give a better explanation.

@connec
Copy link
Collaborator

connec commented Oct 10, 2014

A comprehension expression in coffeescript compiles to a number of Javascript statements. A closure is needed whenever a comprehension is used where Javascript statements would be invalid (e.g. as part of a larger expression).

###
no closure
###
i for i in [1..2]
-> i for i in [1..2]
-> a ; i for i in [1..2]

###
closure
###
return (i for i in [1..2])
a = (i for i in [1..2])
a = for i in [1..2] then i

(i for i in [1..2])\
.concat\
(i for i in [1..2])

Hopefully the last example makes it clear why closures are sometimes needed: when a comprehension is used as a generic expression as part of a more complex expression, the closure is needed to evaluate the comprehension at the right point.

The first case where there is no closure seems to be an optimisation: bare comprehension expressions at the end of a function don't need a closure, though it could have one.

It seems it would be possible to optimise away the closure in the basic assignment case by compiling a = (i for i in [1..2]) to something like:

var a, i, _i;
a = [];
for (i = _i = 1; _i <= 2; i = ++_i) {
  a.push(i);
}

However, when the assignment is part of a generic expression f(a = for ...) the closure would still be needed.

@GabrielRatener
Copy link
Contributor Author

I guess yield* can be used as an expression in JS to obtain the result returned by a generator closure. This is a lot easier to fix than I anticipated.

@Artazor
Copy link
Contributor

Artazor commented Oct 10, 2014

Hi!

First of all it was surprise for me that

... yield 2 * 2 ...

is compiled into

... (yield 2) * 2

I think in most cases the intention of writing the former is

... (yield (2 * 2)) ...

since in JavaScript

assert((function*(){ yield 2*2; })().next().value === 4);

But it is another story (probably an issue).

The other more serious problem that I see is collecting the argument of the yield in for-as-an-expression case instead of yield's result that is the result of asynchronous operation, in case if yields are used in async/await pattern.

Consider already mentioned 'so' library. Imagine the following example:

so = require 'so'
fs = require 'then-fs'

readJSON = so (path) -> JSON.parse yield fs.readFile path, 'utf8'
writeJSON = so (path, value) -> yield fs.writeFile path, JSON.stringify(value), 'utf8'

main = so ->
    names = ['a', 'b', 'c']
    all = for name in names
        path = "#{name}.json"
        yield readJSON path  # <--- HERE
    yield writeJSON 'all.json', all

main().done()

@alubbe, what would you expect to see as the result of this program?
If we will collect the argument of the yield then the usage of yield in this example is meaningless.
What do you think?

+1 for yield* and generator closure.

@akre54
Copy link

akre54 commented Oct 10, 2014

yield* is a much better solution, I had no idea that existed. Thanks @alubbe

@GabrielRatener
Copy link
Contributor Author

@alubbe yield* and generator closures seems like the obvious way to go for fixing this bug. Thank you!!!!!

@Artazor I agree that yield should have a lower priority in the order of operations, lets open a new issue about this.

@Artazor
Copy link
Contributor

Artazor commented Oct 10, 2014

@GabrielRatener I've opened an issue: #3674
Btw, what do you think about the later problem?
I think, that collecting yield results is more consistent rather collecting its arguments, and would yield less confusions when used in async/await pattern. Just like @akre54 shown in his example above it should simply collect what is passed to gen. next()

@akre54
Copy link

akre54 commented Oct 10, 2014

For what it's worth, CoffeeScript master doesn't support the yield* operator. Should it?

@GabrielRatener
Copy link
Contributor Author

@Artazor Thanks for opening the issue. Since yield statements are also expressions, one would expect a list of the yield results as all in @akre54 's example, right?

Would that not be consistent with current CS loop expressions?

@GabrielRatener
Copy link
Contributor Author

@akre54 CS supports yield from which is the JS equivalent of yield*, so why would CS need yield*?

@Artazor
Copy link
Contributor

Artazor commented Oct 10, 2014

@GabrielRatener, yes exactly!

I'm only afraid that @jashkenas said 'Sounds good' for the @alubbe's comment where he expects

f = ->
  a = for i in [1..3]
    yield i * 2

for a to equal [2,4,6] at the end of running b=f() and 4 iterations of b.next()

I suppose that after 4 iterations of b.next() the a should be [undefined, undefined, undefined]

@akre54
Copy link

akre54 commented Oct 10, 2014

@GabrielRatener yes of course. right again :)

@GabrielRatener
Copy link
Contributor Author

@Artazor Yes, you are right, probably just a thinko on their part.

@alubbe
Copy link
Contributor

alubbe commented Oct 13, 2014

Okay, so the order precendence is fixed now, see #3677

I've also tried to add yield*, but got lost in the behemoth that is for-loop compilation, its normal form vs its assigned one and plucking direct calls.

Basically, it should check whether the for loop is occuring in an assignment and whether isGenerator is truthy - if so, add yield*. At this point, I am not sure where that check belongs.

@jashkenas @michaelficarra Any pointers?

@GabrielRatener
Copy link
Contributor Author

@alubbe Will your PR fix all expression closures containing yield inside a generator?

@alubbe
Copy link
Contributor

alubbe commented Oct 13, 2014

We are testing for and can confirm precedence regarding
+ - * / << >> & | || && ** ^ // or and < > == != <= >= is isnt

@Artazor
Copy link
Contributor

Artazor commented Nov 21, 2014

@alubbe Any progress?

@alubbe
Copy link
Contributor

alubbe commented Nov 21, 2014

Unfortunately, no. Like I said, I know what needs to be fixed and how, but I have no idea what's the appropriate place within the code base.
Could you help us out, @jashkenas @michaelficarra ?

@vendethiel
Copy link
Collaborator

Ah, yeah, yield * is definitely the way to go. Looks good to me.

@alubbe It's not pretty. It might actually be better to have generator as a scope property. But then, you need to skip compiler-generated scopes.

@carlsmith
Copy link
Contributor

Appreciate the hard work @alubbe. Thank you.

@jashkenas
Copy link
Owner

So, I'm imagining you'll probably want to make the change in the hairy heart of CoffeeScript: For#compileNode.

You can tell if it's being used as a value or as a statement (what you mean by "is occurring within an assignment") by checking the value of o.level.

@epidemian
Copy link
Contributor

Notice that switch expressions can also compile to IIFEs, so that will also have to be changed if we want to support using yield inside switch.

@jashkenas
Copy link
Owner

Then I guess I take it back about where this should be done.

You'll want to insert the detection / closure-conversion-fixing into Base#compileClosure ... which is supposed to be the single place that handles closure-wrapping. Give that a shot.

@vendethiel
Copy link
Collaborator

@jashkenas the problem is that we don't currently mark compiler-generated closures, so in the case of nested comprehensions we might need to detect if the outerleast non-generated scope is yielding or not

@vendethiel
Copy link
Collaborator

Also, that For method could really get some lifting on the string generation things :)

@jashkenas
Copy link
Owner

All compiler-generated closures are created in that method I've mentioned above. Hopefully the machinery can simply be inserted there. If not, a tag can easily be added.

@alubbe
Copy link
Contributor

alubbe commented Nov 21, 2014

Should be fixed now - #3734
Feedback would be most welcome!

@pepkin88
Copy link

@jashkenas @alubbe Unfortunately there are cases, where this issue is still not fixed. Namely, when in switch or for expression yield and @ (or this or =>) is used.
yield produces a wrapping, self executing generator function, @ causes passing this to that generator function, but the necessary yield* is not inserted.

This code (minimal for the sake of example):

->
  foo = switch
    when true then yield =>
  bar = (yield @ for item in arr)

compiles to this:

(function*() {
  var bar, foo, item;
  foo = (function*() {
    switch (false) {
      case !true:
        return (yield (function(_this) {
          return function() {};
        })(this));
    }
  }).call(this);
  return bar = (function*() {
    var i, len, results;
    results = [];
    for (i = 0, len = arr.length; i < len; i++) {
      item = arr[i];
      results.push((yield this));
    }
    return results;
  }).call(this);
});

but should compile to this:

(function*() {
  var bar, foo, item;
  foo = (yield* (function*() { // <-- here
    switch (false) {
      case !true:
        return (yield (function(_this) {
          return function() {};
        })(this));
    }
  }).call(this));
  return bar = (yield* (function*() { // <-- here
    var i, len, results;
    results = [];
    for (i = 0, len = arr.length; i < len; i++) {
      item = arr[i];
      results.push((yield this));
    }
    return results;
  }).call(this));
});

Notice the yield* in 3rd and 11th lines.

@alubbe
Copy link
Contributor

alubbe commented Feb 26, 2015

Hey @pepkin88
Thanks for the clear and structured error report.

I have found and fixed the issue locally, and will write some more tests over the weekend to check for further issues with this.

Could you create a separate issue for this bug please?

@pepkin88
Copy link

Ok, I will.


Created: #3882

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.