Skip to content
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

Add a for .. from .. loop for generators, see #3832 #4306

Closed
wants to merge 14 commits into from

Conversation

atg
Copy link

@atg atg commented Sep 12, 2016

This implements a for x from g loop as argued for in this comment.

Currently CoffeeScript supports generators using yield and yield from, but it doesn't support a way to use these generators like for (x of g) in ES6.

CoffeeScript already has for in (for Arrays) and for of (for Objects), however users expect both to generate es5-compatible code. It's impossible to modify either of these syntaxes to have all three of es5 compatibility, efficiency and support for generators/iterables.

That means the choice is either a compile-time flag or a new loop syntax. A compile-time flag is likely to create subtle bugs: if the code is expected to be compiled in es6 mode but is actually compiled in es5 mode, then a for in loop over an iterator will be a normal for loop for (var i = 0; i < iterator.length; i++). Iterators are objects, and .length on an iterator is usually 0. The loop will therefore silently fail to run, with no error message.

That leaves a new loop syntax.

The best argument for introducing a new for loop syntax for generators is that they are fundamentally different from either arrays or objects. Iterating over a generator mutates that generator, the next time you iterate over it, it will be empty. Generators cannot be iterated over in reverse order (because they are not indexed). Generators can be infinite.

So aside from being the only option, a new syntax has the benefit of making it clear from the code which loops are meant for Arrays, and which loops are meant for generators (or any iterable).

As for the specific syntax, for x from y is a nice complement to yield from y.

@lydell
Copy link
Collaborator

lydell commented Sep 12, 2016

I like this. 👍

@@ -2178,6 +2180,9 @@ exports.For = class For extends While
if @object
forPartFragments = [@makeCode("#{kvar} in #{svar}")]
guardPart = "\n#{idt1}if (!#{utility 'hasProp', o}.call(#{svar}, #{kvar})) continue;" if @own
if @from
forPartFragments = [@makeCode("#{kvar} of #{svar}")]
guardPart = "\n"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean the guardPart? Yeah, I'll delete that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@atg
Copy link
Author

atg commented Sep 12, 2016

I'm actually starting to understand how For#compileNode works now. I've refactored my first try so that @index and @name are not flipped, and added support for patterns.

@jashkenas
Copy link
Owner

I like this too. @lydell should we merge it?

@lydell
Copy link
Collaborator

lydell commented Sep 26, 2016

I get this error when running the tests:

$ npm run test-harmony

> [email protected] test-harmony /home/lydell/forks/coffeescript
> node --harmony ./bin/cake test

failed 1 and passed 755 tests in 10.75 seconds 

  for-from comprehensions over generators 
  AssertionError: Expected  to deep equal 70,20
  ...

  function () {
    var array1, array2, array3, array4, gen, iterator, x;
    array1 = [50, 30, 70, 20];
    gen = function*() {
      return (yield* array1);
    };
    array2 = [];
    array3 = [];
    array4 = [];
    iterator = gen();
    for (x of iterator) {
      array2.push(x);
      if (x === 30) {
        break;
      }
    }
    for (x of iterator) {
      array3.push(x);
    }
    for (x of iterator) {
      array4.push(x);
    }
    arrayEq(array2, [50, 30]);
    arrayEq(array3, [70, 20]);
    return arrayEq(array4, []);
  }

@lydell
Copy link
Collaborator

lydell commented Sep 29, 2016

@atg I'd like to merge this too. Could you please look into the failing test whenever you get the time? :)

@Jamesernator
Copy link

Jamesernator commented Oct 3, 2016

I'd like to merge this too. Could you please look into the failing test whenever you get the time? :)

I took a look at this, it seems like there's a bug in V8 with resuming iterators in a for-of loop (it failed in both Chrome and Node), it works fine on SpiderMonkey though.

@atg
Copy link
Author

atg commented Oct 29, 2016

Oops, didn't see the updates. I'll see if I can get this up to date.

@atg
Copy link
Author

atg commented Oct 29, 2016

@lydell OK the branch is conflict-free now.

The test is ok insofar as CoffeeScript is generating the right code. But the code doesn't run as expected.

As @Jamesernator said, the test works in Firefox but not in v8. So I tried in Safari, which agrees with v8 that the test is wrong.

I dug into the spec, and it's difficult to understand, but from what I can determine my test & spidermonkey are wrong and v8/nitro are doing the right thing. This is the relevant section.

You can see that all paths lead to IteratorClose(). If you loop over a generator object once and break in the middle, then it will be closed forever. Personally I think this is a silly decision because it shuts the door on many cool uses of generators. But arguing with the spec is like arguing with a wall. ¯_(ツ)_/¯

@GeoffreyBooth
Copy link
Collaborator

@atg so does the test pass (in Node 6.9.1 or 7.0.0, with --harmony)? We can’t merge in a PR with broken tests.

@atg
Copy link
Author

atg commented Oct 29, 2016

I'll change the test to accept either the firefox or v8/nitro behaviour as a pass.

@atg
Copy link
Author

atg commented Oct 29, 2016

$ node --harmony ./bin/cake test
passed 771 tests in 9.13 seconds

@@ -123,3 +123,10 @@ test "#3001: `own` shouldn't be allowed in a `for`-`in` loop", ->

test "#2994: single-line `if` requires `then`", ->
cantCompile "if b else x"

test "indexes are not supported in for-from loops", ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put these tests in error_messages.coffee.

# Different JS engines have different opinions on the value of array3:
# https://github.com/jashkenas/coffeescript/pull/4306#issuecomment-257066877
# As a temporary measure, either result is accepted.
ok array3.length == 0 or array3.join(',') == '70,20'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please always use is instead of ==.

@error "reserved word '#{id}'", length: id.length

if tag is 'IDENTIFIER'
if @seenFor and id is 'from'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be refactored so that we don't branch this if block again? In other words, don't indent the following 15ish lines.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. You can copy the tag is 'IDENTIFIER' into each of the ifs, but then the else if condition becomes comically long (see below).

I do agree that probably the if at the bottom of the big if (if id in RESERVED) should broken out and returned to the main body of the method.

if tag is 'IDENTIFIER' and @seenFor and id is 'from'
  tag = 'FORFROM'
  @seenFor = no
else if tag is 'IDENTIFIER' and (id in JS_KEYWORDS or id in COFFEE_KEYWORDS) and
   not (@exportSpecifierList and id in COFFEE_KEYWORDS)
  tag = id.toUpperCase()
  if tag is 'WHEN' and @tag() in LINE_BREAK
    tag = 'LEADING_WHEN'
  else if tag is 'FOR'
    @seenFor = yes
  else if tag is 'UNLESS'
    tag = 'IF'
  else if tag is 'IMPORT'
    @seenImport = yes
  else if tag is 'EXPORT'
    @seenExport = yes
  else if tag in UNARY
    tag = 'UNARY'
  else if tag in RELATION
    if tag isnt 'INSTANCEOF' and @seenFor
      tag = 'FOR' + tag
      @seenFor = no
    else
      tag = 'RELATION'
      if @value() is '!'
        poppedToken = @tokens.pop()
        id = '!' + id

if tag is 'IDENTIFIER' and id in RESERVED
  @error "reserved word '#{id}'", length: id.length

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@atg
Copy link
Author

atg commented Nov 1, 2016

Alright, do it or don't. I got places to be.

@@ -123,3 +123,6 @@ test "#3001: `own` shouldn't be allowed in a `for`-`in` loop", ->

test "#2994: single-line `if` requires `then`", ->
cantCompile "if b else x"

test "own is not supported in for-from loops", ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also go in error_messages.coffee.

GeoffreyBooth added a commit that referenced this pull request Nov 8, 2016
* Added support for for-from loop, see #3832

* for-from: remove extra newline and add support for ranges

* for-from: tidy up the lexer

* for-from: add support for patterns

* for-from: fix bad alignment

* for-from: add two more tests

* for-from: fix test "for-from loops over generators"

See explanation here: #4306 (comment)

* for-from: delete leftover console.log

* Refactor the big `if` block in the lexer to be as minimal a change from `master` as we can get away with

* Cleanup to make more idiomatic, remove trailing whitespace, minor performance improvements

* for-from: move code from one file to another

* for-from: clean up whitespace

* for-from: lexer bikeshedding

* Move "own is not supported in for-from loops" test into error_messages.coffee; improve error message so that "own" is underlined

* Revert unnecessary changes, to minimize the lines of code modified by this PR
@GeoffreyBooth
Copy link
Collaborator

Closed per #4355

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

Successfully merging this pull request may close these issues.

6 participants