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 for await of #1559

Open
wants to merge 2 commits into
base: static_h
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions include/hermes/VM/RuntimeModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ union RuntimeModuleFlags {
/// Whether this runtime module's epilogue should be hidden in
/// runtime.getEpilogues().
bool hidesEpilogue : 1;

/// Whether we want to ignore ES6 promise checks.
/// This is used to force compiling modules with ES6 promises even if the
/// runtime is not configured to support them.
bool ignoreES6PromiseChecks : 1;
};
uint8_t flags;
RuntimeModuleFlags() : flags(0) {}
Expand Down
74 changes: 73 additions & 1 deletion lib/IRGen/ESTreeIRGen-stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ void ESTreeIRGen::genStatement(ESTree::Node *stmt) {
}

if (auto *FOS = llvh::dyn_cast<ESTree::ForOfStatementNode>(stmt)) {
return genForOfStatement(FOS);
return FOS->_await ? genAsyncForOfStatement(FOS) : genForOfStatement(FOS);
}

if (auto *Ret = llvh::dyn_cast<ESTree::ReturnStatementNode>(stmt)) {
Expand Down Expand Up @@ -867,6 +867,78 @@ void ESTreeIRGen::genForOfStatement(ESTree::ForOfStatementNode *forOfStmt) {
Builder.setInsertionBlock(exitBlock);
}

void ESTreeIRGen::genAsyncForOfStatement(
ESTree::ForOfStatementNode *forOfStmt) {
emitScopeDeclarations(forOfStmt->getScope());

auto *function = Builder.getInsertionBlock()->getParent();
auto *getNextBlock = Builder.createBasicBlock(function);
auto *bodyBlock = Builder.createBasicBlock(function);
auto *exitBlock = Builder.createBasicBlock(function);

// Initialize the goto labels.
curFunction()->initLabel(forOfStmt, exitBlock, getNextBlock);

auto *exprValue = genExpression(forOfStmt->_right);
const IteratorRecordSlow iteratorRecord = emitGetAsyncIteratorSlow(exprValue);

Builder.createBranchInst(getNextBlock);

// Attempt to retrieve the next value. If iteration is complete, finish the
// loop. This stays outside the SurroundingTry below because exceptions in
// `.next()` should not call `.return()` on the iterator.
Builder.setInsertionBlock(getNextBlock);
auto *nextResult = genYieldOrAwaitExpr(emitIteratorNextSlow(iteratorRecord));
auto *done = emitIteratorCompleteSlow(nextResult);
Builder.createCondBranchInst(done, exitBlock, bodyBlock);

Builder.setInsertionBlock(bodyBlock);
auto *nextValue = emitIteratorValueSlow(nextResult);

emitTryCatchScaffolding(
getNextBlock,
// emitBody.
[this, forOfStmt, nextValue, &iteratorRecord, getNextBlock](
BasicBlock *catchBlock) {
// Generate IR for the body of Try
SurroundingTry thisTry{
curFunction(),
forOfStmt,
catchBlock,
{},
[this, &iteratorRecord, getNextBlock](
ESTree::Node *,
ControlFlowChange cfc,
BasicBlock *continueTarget) {
// Only emit the iteratorClose if this is a
// 'break' or if the target of the control flow
// change is outside the current loop. If
// continuing the existing loop, do not close
// the iterator.
if (cfc == ControlFlowChange::Break ||
continueTarget != getNextBlock)
emitIteratorCloseSlow(iteratorRecord, false);
}};

// Note: obtaining the value is not protected, but storing it is.
createLRef(forOfStmt->_left, false).emitStore(nextValue);

genStatement(forOfStmt->_body);
Builder.setLocation(SourceErrorManager::convertEndToLocation(
forOfStmt->_body->getSourceRange()));
},
// emitNormalCleanup.
[]() {},
// emitHandler.
[this, &iteratorRecord](BasicBlock *) {
auto *catchReg = Builder.createCatchInst();
emitIteratorCloseSlow(iteratorRecord, true);
Builder.createThrowInst(catchReg);
});

Builder.setInsertionBlock(exitBlock);
}

void ESTreeIRGen::genForOfFastArrayStatement(
ESTree::ForOfStatementNode *forOfStmt,
flow::ArrayType *type) {
Expand Down
28 changes: 28 additions & 0 deletions lib/IRGen/ESTreeIRGen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,12 @@ Value *ESTreeIRGen::emitIteratorSymbol() {
"iterator");
}

Value *ESTreeIRGen::emitAsyncIteratorSymbol() {
return Builder.createLoadPropertyInst(
Builder.createGetBuiltinClosureInst(BuiltinMethod::globalThis_Symbol),
"asyncIterator");
}

ESTreeIRGen::IteratorRecordSlow ESTreeIRGen::emitGetIteratorSlow(Value *obj) {
auto *method = Builder.createLoadPropertyInst(obj, emitIteratorSymbol());
auto *iterator = Builder.createCallInst(
Expand All @@ -491,6 +497,28 @@ ESTreeIRGen::IteratorRecordSlow ESTreeIRGen::emitGetIteratorSlow(Value *obj) {
return {iterator, nextMethod};
}

ESTreeIRGen::IteratorRecordSlow ESTreeIRGen::emitGetAsyncIteratorSlow(
Value *obj) {
auto *asyncIteratorMethod =
Builder.createLoadPropertyInst(obj, emitAsyncIteratorSymbol());
auto *syncIteratorMethod =
Builder.createLoadPropertyInst(obj, emitIteratorSymbol());

auto *wrapper = Builder.createLoadPropertyInst(
Builder.getGlobalObject(), "HermesAsyncIteratorsInternal");
wrapper = Builder.createLoadPropertyInst(
wrapper, "_makeAsyncIterator");

auto *iterator = Builder.createCallInst(
wrapper,
Builder.getLiteralUndefined(),
Builder.getLiteralUndefined(),
{obj, asyncIteratorMethod, syncIteratorMethod});
auto *nextMethod = Builder.createLoadPropertyInst(iterator, "next");

return {iterator, nextMethod};
}

Value *ESTreeIRGen::emitIteratorNextSlow(IteratorRecordSlow iteratorRecord) {
auto *nextResult = Builder.createCallInst(
iteratorRecord.nextMethod,
Expand Down
13 changes: 13 additions & 0 deletions lib/IRGen/ESTreeIRGen.h
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ class ESTreeIRGen {
void genForOfFastArrayStatement(
ESTree::ForOfStatementNode *forOfStmt,
flow::ArrayType *type);
void genAsyncForOfStatement(ESTree::ForOfStatementNode *forOfStmt);
void genWhileLoop(ESTree::WhileStatementNode *loop);
void genDoWhileLoop(ESTree::DoWhileStatementNode *loop);

Expand Down Expand Up @@ -1210,6 +1211,9 @@ class ESTreeIRGen {
/// \return the internal value @@iterator
Value *emitIteratorSymbol();

/// \return the internal value @@asyncIterator
Value *emitAsyncIteratorSymbol();

/// IteratorRecord as defined in ES2018 7.4.1 GetIterator
struct IteratorRecordSlow {
Value *iterator;
Expand All @@ -1228,6 +1232,15 @@ class ESTreeIRGen {
/// \return (iterator, nextMethod)
IteratorRecordSlow emitGetIteratorSlow(Value *obj);

/// Call obj[@@asyncIterator], which should return an async iterator,
/// and return the iterator itself and its \c next() method.
///
/// NOTE: This API is slow and should only be used if it is necessary to
/// provide a value to the `next()` method on the iterator.
///
/// \return (iterator, nextMethod)
IteratorRecordSlow emitGetAsyncIteratorSlow(Value *obj);

/// ES2018 7.4.2 IteratorNext
/// https://www.ecma-international.org/ecma-262/9.0/index.html#sec-iteratornext
///
Expand Down
42 changes: 42 additions & 0 deletions lib/InternalJavaScript/04-AsyncIterator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
var initAsyncIterators = function() {
function _makeAsyncIterator(iterable, asyncIterMethod, syncIterMethod) {
if (asyncIterMethod) {
return asyncIterMethod.call(iterable);
}

let syncIterator = syncIterMethod.call(iterable);
async function handleResult(result) {
if (result.done) {
return result;
}
const value = result.value;
const resolvedValue = value instanceof Promise ? await value : value;
return { done: false, value: resolvedValue };
}

return {
async next() {
const result = syncIterator.next();
return handleResult(result);
},
async return(value) {
const result = typeof syncIterator.return === 'function' ? syncIterator.return(value) : { done: true, value };
return handleResult(result);
},
async throw(error) {
const result = typeof syncIterator.throw === 'function' ? syncIterator.throw(error) : { done: true, value: error };
return handleResult(result);
}
};
}

var HermesAsyncIteratorsInternal = {
_makeAsyncIterator
};
Object.freeze(HermesAsyncIteratorsInternal);
globalThis.HermesAsyncIteratorsInternal = HermesAsyncIteratorsInternal;
};

if (HermesInternal?.hasPromise?.()) {
initAsyncIterators();
}
2 changes: 0 additions & 2 deletions lib/Sema/SemanticResolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -479,8 +479,6 @@ void SemanticResolver::visit(ESTree::ForInStatementNode *node) {
}

void SemanticResolver::visit(ESTree::ForOfStatementNode *node) {
if (compile_ && node->_await)
sm_.error(node->getStartLoc(), "for await is not supported");
visitForInOf(node, node, node->_left, node->_right, node->_body);
}

Expand Down
3 changes: 2 additions & 1 deletion lib/VM/Runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,7 @@ CallResult<HermesValue> Runtime::runBytecode(
assert(builtinsFrozen_ && "Builtins must be frozen by now.");
}

if (bytecode->getBytecodeOptions().hasAsync && !hasES6Promise_) {
if (!flags.ignoreES6PromiseChecks && bytecode->getBytecodeOptions().hasAsync && !hasES6Promise_) {
return raiseTypeError(
"Cannot execute a bytecode having async functions when Promise is disabled.");
}
Expand Down Expand Up @@ -1191,6 +1191,7 @@ Handle<JSObject> Runtime::runInternalJavaScript() {
RuntimeModuleFlags flags;
flags.persistent = true;
flags.hidesEpilogue = true;
flags.ignoreES6PromiseChecks = true;
auto res = runBytecode(
std::move(bcResult.first),
flags,
Expand Down
38 changes: 38 additions & 0 deletions test/hermes/for-await-of-async-iterator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

// RUN: %hermes -Xes6-promise %s | %FileCheck --match-full-lines %s
// RUN: %hermes -O0 -Xes6-promise %s | %FileCheck --match-full-lines %s
// RUN: %hermes -lazy -Xes6-promise %s | %FileCheck --match-full-lines %s

// Custom async iterable using Symbol.asyncIterator
const asyncIterable = {
[Symbol.asyncIterator]: function() {
let i = 1;
const max = 3;
return {
next: function() {
if (i <= max) {
return Promise.resolve({ value: i++, done: false });
} else {
return Promise.resolve({ done: true });
}
}
};
}
};

// Test for await of loop with custom async iterable
(async function testCustomAsyncIterable() {
sum = 0;
for await (const value of asyncIterable) {
sum += value;
}
print(sum);
})();

//CHECK: 6
36 changes: 36 additions & 0 deletions test/hermes/for-await-of-return.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

// RUN: %hermes -Xes6-promise %s | %FileCheck --match-full-lines %s
// RUN: %hermes -O0 -Xes6-promise %s | %FileCheck --match-full-lines %s
// RUN: %hermes -lazy -Xes6-promise %s | %FileCheck --match-full-lines %s

// Custom async iterable using Symbol.asyncIterator
const asyncIterable = {
[Symbol.asyncIterator]: function() {
return {
next: function() {
return Promise.resolve({ value:0, done: false });
},
return: function() {
print('Cleanup performed');
return Promise.resolve({ done: true });
}
};
}
};

// Using break and triggering return() in for await...of loop
(async function testBreakWithReturn() {
for await (const value of asyncIterable) {
if (value === 0) {
break;
}
}
})();

//CHECK: Cleanup performed
23 changes: 23 additions & 0 deletions test/hermes/for-await-of-sync-iterator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

// RUN: %hermes -Xes6-promise %s | %FileCheck --match-full-lines %s
// RUN: %hermes -O0 -Xes6-promise %s | %FileCheck --match-full-lines %s
// RUN: %hermes -lazy -Xes6-promise %s | %FileCheck --match-full-lines %s

// For await of loop should work with a sync iterable
const syncIterable = [1,2,3];

(async function testCustomSyncIterable() {
sum = 0;
for await (const value of syncIterable) {
sum += value;
}
print(sum);
})();

//CHECK: 6