Fix #4875: Asynchronous iterators (#4893)

* async iterators

* tests; refactor 'For' grammar rules

* async iterator tests

* formatting
This commit is contained in:
zdenko 2018-04-08 22:42:54 +02:00 committed by Geoffrey Booth
parent 47c491ffa1
commit 1f9cd4eaf7
7 changed files with 263 additions and 166 deletions

View File

@ -1348,77 +1348,94 @@
// Comprehensions can either be normal, with a block of expressions to execute,
// or postfix, with a single expression.
For: [
o('Statement ForBody',
o('Statement ForBody',
function() {
return new For($1,
$2);
return $2.addBody($1);
}),
o('Expression ForBody',
o('Expression ForBody',
function() {
return new For($1,
$2);
return $2.addBody($1);
}),
o('ForBody Block',
o('ForBody Block',
function() {
return new For($2,
$1);
return $1.addBody($2);
}),
o('ForLineBody Block',
function() {
return new For($2,
$1);
return $1.addBody($2);
})
],
ForBody: [
o('FOR Range',
function() {
return {
return new For([],
{
source: LOC(2)(new Value($2))
};
});
}),
o('FOR Range BY Expression',
function() {
return {
return new For([],
{
source: LOC(2)(new Value($2)),
step: $4
};
});
}),
o('ForStart ForSource',
function() {
$2.own = $1.own;
$2.ownTag = $1.ownTag;
$2.name = $1[0];
$2.index = $1[1];
return $2;
return $1.addSource($2);
})
],
ForLineBody: [
o('FOR Range BY ExpressionLine',
function() {
return {
return new For([],
{
source: LOC(2)(new Value($2)),
step: $4
};
});
}),
o('ForStart ForLineSource',
function() {
$2.own = $1.own;
$2.ownTag = $1.ownTag;
$2.name = $1[0];
$2.index = $1[1];
return $2;
return $1.addSource($2);
})
],
ForStart: [
o('FOR ForVariables',
function() {
return $2;
return new For([],
{
name: $2[0],
index: $2[1]
});
}),
o('FOR AWAIT ForVariables',
function() {
var index,
name;
[name,
index] = $3;
return new For([],
{
name,
index,
await: true,
awaitTag: LOC(2)(new Literal($2))
});
}),
o('FOR OWN ForVariables',
function() {
$3.own = true;
$3.ownTag = LOC(2)(new Literal($2));
return $3;
var index,
name;
[name,
index] = $3;
return new For([],
{
name,
index,
own: true,
ownTag: LOC(2)(new Literal($2))
});
})
],
// An array of all accepted values for a variable inside the loop.

View File

@ -3889,8 +3889,8 @@
if ((node instanceof Op && node.isAwait()) || node instanceof AwaitReturn) {
this.isAsync = true;
}
if (this.isGenerator && this.isAsync) {
return node.error("function can't contain both yield and await");
if (node instanceof For && node.isAwait()) {
return this.isAsync = true;
}
});
}
@ -5361,25 +5361,47 @@
exports.For = For = (function() {
class For extends While {
constructor(body, source) {
var attribute, j, len1, ref1, ref2, ref3;
super();
({source: this.source, guard: this.guard, step: this.step, name: this.name, index: this.index} = source);
this.addBody(body);
this.addSource(source);
}
isAwait() {
var ref1;
return (ref1 = this.await) != null ? ref1 : false;
}
addBody(body) {
this.body = Block.wrap([body]);
this.own = source.own != null;
this.object = source.object != null;
this.from = source.from != null;
return this;
}
addSource(source) {
var attr, attribs, attribute, j, k, len1, len2, ref1, ref2, ref3, ref4;
({source: this.source = false} = source);
attribs = ["name", "index", "guard", "step", "own", "ownTag", "await", "awaitTag", "object", "from"];
for (j = 0, len1 = attribs.length; j < len1; j++) {
attr = attribs[j];
this[attr] = (ref1 = source[attr]) != null ? ref1 : this[attr];
}
if (!this.source) {
return this;
}
if (this.from && this.index) {
this.index.error('cannot use index with for-from');
}
if (this.own && !this.object) {
source.ownTag.error(`cannot use own with for-${(this.from ? 'from' : 'in')}`);
this.ownTag.error(`cannot use own with for-${(this.from ? 'from' : 'in')}`);
}
if (this.object) {
[this.name, this.index] = [this.index, this.name];
}
if (((ref1 = this.index) != null ? typeof ref1.isArray === "function" ? ref1.isArray() : void 0 : void 0) || ((ref2 = this.index) != null ? typeof ref2.isObject === "function" ? ref2.isObject() : void 0 : void 0)) {
if (((ref2 = this.index) != null ? typeof ref2.isArray === "function" ? ref2.isArray() : void 0 : void 0) || ((ref3 = this.index) != null ? typeof ref3.isObject === "function" ? ref3.isObject() : void 0 : void 0)) {
this.index.error('index cannot be a pattern matching expression');
}
if (this.await && !this.from) {
this.awaitTag.error('await must be used with for-from');
}
this.range = this.source instanceof Value && this.source.base instanceof Range && !this.source.properties.length && !this.from;
this.pattern = this.name instanceof Value;
if (this.range && this.index) {
@ -5389,21 +5411,21 @@
this.name.error('cannot pattern match over range loops');
}
this.returns = false;
ref3 = ['source', 'guard', 'step', 'name', 'index'];
ref4 = ['source', 'guard', 'step', 'name', 'index'];
// Move up any comments in the “`for` line”, i.e. the line of code with `for`,
// from any child nodes of that line up to the `for` node itself so that these
// comments get output, and get output above the `for` loop.
for (j = 0, len1 = ref3.length; j < len1; j++) {
attribute = ref3[j];
for (k = 0, len2 = ref4.length; k < len2; k++) {
attribute = ref4[k];
if (!this[attribute]) {
continue;
}
this[attribute].traverseChildren(true, (node) => {
var comment, k, len2, ref4;
var comment, l, len3, ref5;
if (node.comments) {
ref4 = node.comments;
for (k = 0, len2 = ref4.length; k < len2; k++) {
comment = ref4[k];
ref5 = node.comments;
for (l = 0, len3 = ref5.length; l < len3; l++) {
comment = ref5[l];
// These comments are buried pretty deeply, so if they happen to be
// trailing comments the line they trail will be unrecognizable when
// were done compiling this `for` loop; so just shift them up to
@ -5415,6 +5437,7 @@
});
moveComments(this[attribute], this);
}
return this;
}
// Welcome to the hairiest method in all of CoffeeScript. Handles the inner
@ -5422,7 +5445,7 @@
// comprehensions. Some of the generated code can be shared in common, and
// some cannot.
compileNode(o) {
var body, bodyFragments, compare, compareDown, declare, declareDown, defPart, down, forPartFragments, fragments, guardPart, idt1, increment, index, ivar, kvar, kvarAssign, last, lvar, name, namePart, ref, ref1, resultPart, returnResult, rvar, scope, source, step, stepNum, stepVar, svar, varPart;
var body, bodyFragments, compare, compareDown, declare, declareDown, defPart, down, forClose, forCode, forPartFragments, fragments, guardPart, idt1, increment, index, ivar, kvar, kvarAssign, last, lvar, name, namePart, ref, ref1, resultPart, returnResult, rvar, scope, source, step, stepNum, stepVar, svar, varPart;
body = Block.wrap([this.body]);
ref1 = body.expressions, [last] = slice1.call(ref1, -1);
if ((last != null ? last.jumps() : void 0) instanceof Return) {
@ -5540,7 +5563,12 @@
guardPart = `\n${idt1}if (!${utility('hasProp', o)}.call(${svar}, ${kvar})) continue;`;
}
} else if (this.from) {
forPartFragments = [this.makeCode(`${kvar} of ${svar}`)];
if (this.await) {
forPartFragments = new Op('await', new Parens(new Literal(`${kvar} of ${svar}`)));
forPartFragments = forPartFragments.compileToFragments(o, LEVEL_TOP);
} else {
forPartFragments = [this.makeCode(`${kvar} of ${svar}`)];
}
}
bodyFragments = body.compileToFragments(merge(o, {
indent: idt1
@ -5552,7 +5580,9 @@
if (resultPart) {
fragments.push(this.makeCode(resultPart));
}
fragments = fragments.concat(this.makeCode(this.tab), this.makeCode('for ('), forPartFragments, this.makeCode(`) {${guardPart}${varPart}`), bodyFragments, this.makeCode(this.tab), this.makeCode('}'));
forCode = this.await ? 'for ' : 'for (';
forClose = this.await ? '' : ')';
fragments = fragments.concat(this.makeCode(this.tab), this.makeCode(forCode), forPartFragments, this.makeCode(`${forClose} {${guardPart}${varPart}`), bodyFragments, this.makeCode(this.tab), this.makeCode('}'));
if (returnResult) {
fragments.push(this.makeCode(returnResult));
}

File diff suppressed because one or more lines are too long

View File

@ -660,26 +660,31 @@ grammar =
# Comprehensions can either be normal, with a block of expressions to execute,
# or postfix, with a single expression.
For: [
o 'Statement ForBody', -> new For $1, $2
o 'Expression ForBody', -> new For $1, $2
o 'ForBody Block', -> new For $2, $1
o 'ForLineBody Block', -> new For $2, $1
o 'Statement ForBody', -> $2.addBody $1
o 'Expression ForBody', -> $2.addBody $1
o 'ForBody Block', -> $1.addBody $2
o 'ForLineBody Block', -> $1.addBody $2
]
ForBody: [
o 'FOR Range', -> source: (LOC(2) new Value($2))
o 'FOR Range BY Expression', -> source: (LOC(2) new Value($2)), step: $4
o 'ForStart ForSource', -> $2.own = $1.own; $2.ownTag = $1.ownTag; $2.name = $1[0]; $2.index = $1[1]; $2
o 'FOR Range', -> new For [], source: (LOC(2) new Value($2))
o 'FOR Range BY Expression', -> new For [], source: (LOC(2) new Value($2)), step: $4
o 'ForStart ForSource', -> $1.addSource $2
]
ForLineBody: [
o 'FOR Range BY ExpressionLine', -> source: (LOC(2) new Value($2)), step: $4
o 'ForStart ForLineSource', -> $2.own = $1.own; $2.ownTag = $1.ownTag; $2.name = $1[0]; $2.index = $1[1]; $2
o 'FOR Range BY ExpressionLine', -> new For [], source: (LOC(2) new Value($2)), step: $4
o 'ForStart ForLineSource', -> $1.addSource $2
]
ForStart: [
o 'FOR ForVariables', -> $2
o 'FOR OWN ForVariables', -> $3.own = yes; $3.ownTag = (LOC(2) new Literal($2)); $3
o 'FOR ForVariables', -> new For [], name: $2[0], index: $2[1]
o 'FOR AWAIT ForVariables', ->
[name, index] = $3
new For [], {name, index, await: yes, awaitTag: (LOC(2) new Literal($2))}
o 'FOR OWN ForVariables', ->
[name, index] = $3
new For [], {name, index, own: yes, ownTag: (LOC(2) new Literal($2))}
]
# An array of all accepted values for a variable inside the loop.

View File

@ -2610,8 +2610,8 @@ exports.Code = class Code extends Base
@isGenerator = yes
if (node instanceof Op and node.isAwait()) or node instanceof AwaitReturn
@isAsync = yes
if @isGenerator and @isAsync
node.error "function can't contain both yield and await"
if node instanceof For and node.isAwait()
@isAsync = yes
children: ['params', 'body']
@ -3615,15 +3615,27 @@ exports.StringWithInterpolations = class StringWithInterpolations extends Base
exports.For = class For extends While
constructor: (body, source) ->
super()
{@source, @guard, @step, @name, @index} = source
@body = Block.wrap [body]
@own = source.own?
@object = source.object?
@from = source.from?
@addBody body
@addSource source
children: ['body', 'source', 'guard', 'step']
isAwait: -> @await ? no
addBody: (body) ->
@body = Block.wrap [body]
this
addSource: (source) ->
{@source = no} = source
attribs = ["name", "index", "guard", "step", "own", "ownTag", "await", "awaitTag", "object", "from"]
@[attr] = source[attr] ? @[attr] for attr in attribs
return this unless @source
@index.error 'cannot use index with for-from' if @from and @index
source.ownTag.error "cannot use own with for-#{if @from then 'from' else 'in'}" if @own and not @object
@ownTag.error "cannot use own with for-#{if @from then 'from' else 'in'}" if @own and not @object
[@name, @index] = [@index, @name] if @object
@index.error 'index cannot be a pattern matching expression' if @index?.isArray?() or @index?.isObject?()
@awaitTag.error 'await must be used with for-from' if @await and not @from
@range = @source instanceof Value and @source.base instanceof Range and not @source.properties.length and not @from
@pattern = @name instanceof Value
@index.error 'indexes do not apply to range loops' if @range and @index
@ -3642,8 +3654,7 @@ exports.For = class For extends While
comment.newLine = comment.unshift = yes for comment in node.comments
moveComments node, @[attribute]
moveComments @[attribute], @
children: ['body', 'source', 'guard', 'step']
this
# Welcome to the hairiest method in all of CoffeeScript. Handles the inner
# loop, filtering, stepping, and result saving for array, object, and range
@ -3721,15 +3732,21 @@ exports.For = class For extends While
forPartFragments = [@makeCode("#{kvar} in #{svar}")]
guardPart = "\n#{idt1}if (!#{utility 'hasProp', o}.call(#{svar}, #{kvar})) continue;" if @own
else if @from
forPartFragments = [@makeCode("#{kvar} of #{svar}")]
if @await
forPartFragments = new Op 'await', new Parens new Literal "#{kvar} of #{svar}"
forPartFragments = forPartFragments.compileToFragments o, LEVEL_TOP
else
forPartFragments = [@makeCode("#{kvar} of #{svar}")]
bodyFragments = body.compileToFragments merge(o, indent: idt1), LEVEL_TOP
if bodyFragments and bodyFragments.length > 0
bodyFragments = [].concat @makeCode('\n'), bodyFragments, @makeCode('\n')
fragments = [@makeCode(defPart)]
fragments.push @makeCode(resultPart) if resultPart
fragments = fragments.concat @makeCode(@tab), @makeCode( 'for ('),
forPartFragments, @makeCode(") {#{guardPart}#{varPart}"), bodyFragments,
forCode = if @await then 'for ' else 'for ('
forClose = if @await then '' else ')'
fragments = fragments.concat @makeCode(@tab), @makeCode( forCode),
forPartFragments, @makeCode("#{forClose} {#{guardPart}#{varPart}"), bodyFragments,
@makeCode(@tab), @makeCode('}')
fragments.push @makeCode(returnResult) if returnResult
fragments

View File

@ -0,0 +1,32 @@
# This is always fulfilled.
winLater = (val, ms) ->
new Promise (resolve) -> setTimeout (-> resolve val), ms
# This is always rejected.
failLater = (val, ms) ->
new Promise (resolve, reject) -> setTimeout (-> reject new Error val), ms
createAsyncIterable = (syncIterable) ->
for elem in syncIterable
yield await winLater elem, 50
test "async iteration", ->
foo = (x for await x from createAsyncIterable [1,2,3])
arrayEq foo, [1, 2, 3]
test "async generator functions", ->
foo = (val) ->
yield await winLater val + 1, 50
bar = (val) ->
yield await failLater val - 1, 50
a = await foo(41).next()
eq a.value, 42
try
b = do -> await bar(41).next()
b.catch (err) ->
eq "40", err.message
catch err
ok no

View File

@ -1230,28 +1230,6 @@ test "CoffeeScript keywords cannot be used as local names in import list aliases
^^^^^^
'''
test "function cannot contain both `await` and `yield`", ->
assertErrorFormat '''
f = () ->
yield 5
await a
''', '''
[stdin]:3:3: error: function can't contain both yield and await
await a
^^^^^^^
'''
test "function cannot contain both `await` and `yield from`", ->
assertErrorFormat '''
f = () ->
yield from a
await b
''', '''
[stdin]:3:3: error: function can't contain both yield and await
await b
^^^^^^^
'''
test "cannot have `await` outside a function", ->
assertErrorFormat '''
await 1