From 410840e6795b03ab8499f5afe9bd91905576c07b Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Wed, 21 Aug 2024 08:40:38 +0000 Subject: [PATCH 01/14] Add for-await-of implementation via IR This PR adds support for the `for await` loop through Intermediate Representation (IR). **Testing:** - Using the Test262 suite. Currently, `for-await-of` and async generators implementations achieve approximately a 98.46% success rate on `for-await-of` tests. This result is based on 1232 total tests - 20+ hand-made tests involving different scenarios were all passing - By manually reviewing the failing tests in the suite, many issues are related to missing features or other failing tests (e.g., arrow functions, non-async generators not working in all cases). Currently, Hermes is not 100% compliant and some functionalities do not pass Test262 test cases. However, most features are available and functioning correctly. --- --- lib/IRGen/ESTreeIRGen-stmt.cpp | 74 +++++++++++++++++++++- lib/IRGen/ESTreeIRGen.cpp | 28 ++++++++ lib/IRGen/ESTreeIRGen.h | 5 ++ lib/InternalJavaScript/04-AsyncIterator.js | 39 ++++++++++++ lib/Sema/SemanticResolver.cpp | 2 - 5 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 lib/InternalJavaScript/04-AsyncIterator.js diff --git a/lib/IRGen/ESTreeIRGen-stmt.cpp b/lib/IRGen/ESTreeIRGen-stmt.cpp index b20af3078e5..3ea1c54bc64 100644 --- a/lib/IRGen/ESTreeIRGen-stmt.cpp +++ b/lib/IRGen/ESTreeIRGen-stmt.cpp @@ -54,7 +54,7 @@ void ESTreeIRGen::genStatement(ESTree::Node *stmt) { } if (auto *FOS = llvh::dyn_cast(stmt)) { - return genForOfStatement(FOS); + return FOS->_await ? genAsyncForOfStatement(FOS) : genForOfStatement(FOS); } if (auto *Ret = llvh::dyn_cast(stmt)) { @@ -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) { diff --git a/lib/IRGen/ESTreeIRGen.cpp b/lib/IRGen/ESTreeIRGen.cpp index 411d2fea2fb..79ff2c18763 100644 --- a/lib/IRGen/ESTreeIRGen.cpp +++ b/lib/IRGen/ESTreeIRGen.cpp @@ -459,6 +459,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( @@ -470,6 +476,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, + /* newTarget */ Builder.getLiteralUndefined(), + /* this */ 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, diff --git a/lib/IRGen/ESTreeIRGen.h b/lib/IRGen/ESTreeIRGen.h index 552d99ed504..dd237fcf5b1 100644 --- a/lib/IRGen/ESTreeIRGen.h +++ b/lib/IRGen/ESTreeIRGen.h @@ -556,6 +556,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); @@ -1197,6 +1198,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; @@ -1214,6 +1218,7 @@ class ESTreeIRGen { /// /// \return (iterator, nextMethod) IteratorRecordSlow emitGetIteratorSlow(Value *obj); + IteratorRecordSlow emitGetAsyncIteratorSlow(Value *obj); /// ES2018 7.4.2 IteratorNext /// https://www.ecma-international.org/ecma-262/9.0/index.html#sec-iteratornext diff --git a/lib/InternalJavaScript/04-AsyncIterator.js b/lib/InternalJavaScript/04-AsyncIterator.js new file mode 100644 index 00000000000..2cb2a5434a3 --- /dev/null +++ b/lib/InternalJavaScript/04-AsyncIterator.js @@ -0,0 +1,39 @@ +(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; + // If the value is a promise, wait for it to resolve. + 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; +})(); diff --git a/lib/Sema/SemanticResolver.cpp b/lib/Sema/SemanticResolver.cpp index 995fddb716b..d90a60fb03a 100644 --- a/lib/Sema/SemanticResolver.cpp +++ b/lib/Sema/SemanticResolver.cpp @@ -459,8 +459,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); } From 63a16c80e8c6b40b708b0200ded8690231772492 Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Fri, 23 Aug 2024 15:46:17 +0000 Subject: [PATCH 02/14] Add async generators This PR introduces support for async generators through AST transformations, aligning with the design and rules of ES6 classes transformations. Key changes: - Added an `AsyncGenerator` class that converts any async generator to a normal generator. This follows the same logic implemented by [Babel](https://babeljs.io/docs/babel-plugin-transform-async-generator-functions). - Added helper functions as part of the internal bytecode. Tested by executing multiple scripts involving: - nested async/generator functions, - `for await...of` loop with and without generators - `for await...of` loop with array of promises **Test262 results:** 98.46%. --- --- include/hermes/AST/AsyncGenerator.h | 14 ++ include/hermes/AST/TransformationsBase.h | 144 ++++++++++++++++ lib/AST/AsyncGenerator.cpp | 185 +++++++++++++++++++++ lib/AST/CMakeLists.txt | 1 + lib/AST/ES6Class.cpp | 137 ++------------- lib/InternalJavaScript/04-AsyncIterator.js | 88 +++++++++- lib/Sema/SemResolve.cpp | 3 + 7 files changed, 447 insertions(+), 125 deletions(-) create mode 100644 include/hermes/AST/AsyncGenerator.h create mode 100644 include/hermes/AST/TransformationsBase.h create mode 100644 lib/AST/AsyncGenerator.cpp diff --git a/include/hermes/AST/AsyncGenerator.h b/include/hermes/AST/AsyncGenerator.h new file mode 100644 index 00000000000..225bc01d74d --- /dev/null +++ b/include/hermes/AST/AsyncGenerator.h @@ -0,0 +1,14 @@ +#ifndef HERMES_AST_ASYNCGENERATORS_H +#define HERMES_AST_ASYNCGENERATORS_H + +#include "hermes/AST/ESTree.h" + +namespace hermes { + +/// Recursively transforms the ESTree Node tree such that async generators +/// are converted into generators +void transformAsyncGenerators(Context &context, ESTree::Node *node); + +} // namespace hermes + +#endif diff --git a/include/hermes/AST/TransformationsBase.h b/include/hermes/AST/TransformationsBase.h new file mode 100644 index 00000000000..aac03bfb592 --- /dev/null +++ b/include/hermes/AST/TransformationsBase.h @@ -0,0 +1,144 @@ +#include "hermes/AST/RecursiveVisitor.h" +#include "hermes/Parser/JSLexer.h" +#include "llvh/ADT/StringRef.h" + +namespace { +using namespace hermes; + +/// Mutable vector that helps dealing with arrays of nodes safely. +/// Once done with the vector, it can create an ESTree::NodeList +/// representation which is used by the ESTree API in several places. +class NodeVector { + public: + using Storage = llvh::SmallVector; + + NodeVector() = default; + NodeVector(std::initializer_list nodes) { + for (auto &node : nodes) { + _storage.push_back(node); + } + } + + NodeVector(ESTree::NodeList &list) { + for (auto &node : list) { + _storage.push_back(&node); + } + } + + ~NodeVector() = default; + + size_t size() const { + return _storage.size(); + } + + Storage::const_iterator begin() const { + return _storage.begin(); + } + + Storage::const_iterator end() const { + return _storage.end(); + } + + void append(ESTree::Node *node) { + _storage.emplace_back(node); + } + + void prepend(ESTree::Node *node) { + _storage.insert(_storage.begin(), node); + } + + ESTree::NodeList toNodeList() const { + ESTree::NodeList nodeList; + for (auto &node : _storage) { + nodeList.push_back(*node); + } + return nodeList; + } + + private: + Storage _storage; +}; + +class TransformationsBase + : public ESTree::RecursionDepthTracker { + public: + static constexpr bool kEnableNodeListMutation = true; + + TransformationsBase(Context &context) + : context_(context), + identLet_(context.getIdentifier("let").getUnderlyingPointer()) {} + + void recursionDepthExceeded(ESTree::Node *n) { + context_.getSourceErrorManager().error( + n->getEndLoc(), "Too many nested expressions/statements/declarations"); + } + + protected: + Context &context_; + UniqueString *const identLet_; + + void doCopyLocation(ESTree::Node *src, ESTree::Node *dest) { + if (src != nullptr) { + dest->setStartLoc(src->getStartLoc()); + dest->setEndLoc(src->getEndLoc()); + dest->setDebugLoc(src->getDebugLoc()); + } + } + + template + T *copyLocation(ESTree::Node *src, T *dest) { + doCopyLocation(src, dest); + return dest; + } + + template + T *createTransformedNode(ESTree::Node *src, Args &&...args) { + auto *node = new (context_) T(std::forward(args)...); + return copyLocation(src, node); + } + + ESTree::IdentifierNode *makeIdentifierNode( + ESTree::Node *srcNode, + UniqueString *name) { + return createTransformedNode( + srcNode, name, nullptr, false); + } + + ESTree::IdentifierNode *makeIdentifierNode( + ESTree::Node *srcNode, + llvh::StringRef name) { + return makeIdentifierNode( + srcNode, context_.getIdentifier(name).getUnderlyingPointer()); + } + + ESTree::Node *makeSingleLetDecl( + ESTree::Node *srcNode, + ESTree::Node *identifier, + ESTree::Node *value) { + auto *variableDeclarator = + createTransformedNode( + srcNode, value, identifier); + ESTree::NodeList variableList; + variableList.push_back(*variableDeclarator); + return createTransformedNode( + srcNode, identLet_, std::move(variableList)); + } + + ESTree::Node *makeHermesInternalCall( + ESTree::Node *srcNode, + llvh::StringRef methodName, + const NodeVector ¶meters) { + auto hermesInternalIdentifier = getHermesInternalIdentifier(srcNode); + auto methodIdentifier = makeIdentifierNode(srcNode, methodName); + + auto *getPropertyNode = createTransformedNode( + srcNode, hermesInternalIdentifier, methodIdentifier, false); + return createTransformedNode( + srcNode, getPropertyNode, nullptr, parameters.toNodeList()); + } + + virtual ESTree::Node *getHermesInternalIdentifier(ESTree::Node *srcNode) { + return nullptr; + }; +}; +} // namespace diff --git a/lib/AST/AsyncGenerator.cpp b/lib/AST/AsyncGenerator.cpp new file mode 100644 index 00000000000..610be3ccfab --- /dev/null +++ b/lib/AST/AsyncGenerator.cpp @@ -0,0 +1,185 @@ +#include "hermes/AST/AsyncGenerator.h" +#include "hermes/AST/TransformationsBase.h" + +namespace hermes { + +/** + * Transforms JS async generators to generators according to babel + * transformation + * https://babeljs.io/docs/babel-plugin-transform-async-generator-functions + * + * An async generator: + * + * async function* userFunction(arguments) { + * // body containing await calls + * } + * + * is transformed into: + * + * let userFunction = (() => { + * let _ref = _wrapAsyncGenerator(function* (arguments) { // body + * containing await calls }); return function userFunction() { return + * _ref.apply(this, arguments); + * }; + * })(); + * + * and an await call inside the async generator body: + * await 1; + * + * is transformed into: + * yield _awaitAsyncGenerator(1); + * + * where _awaitAsyncGenerator and _wrapAsyncGenerator are helper functions + * defined in 04-AsyncIterator.js + * + */ +class AsyncGenerator : public TransformationsBase { + public: + AsyncGenerator(Context &context) : TransformationsBase(context) {} + + void visit(ESTree::FunctionDeclarationNode *funcDecl, ESTree::Node **ppNode) { + if (funcDecl->_async && funcDecl->_generator) { + recurseFunctionBody(funcDecl->_body, true); + auto iife = transformAsyncGeneratorFunction( + funcDecl, funcDecl->_params, funcDecl->_body); + *ppNode = makeSingleLetDecl(funcDecl, funcDecl->_id, iife); + } else { + recurseFunctionBody(funcDecl->_body, false); + } + } + + void visit(ESTree::FunctionExpressionNode *funcExpr, ESTree::Node **ppNode) { + if (funcExpr->_async && funcExpr->_generator) { + recurseFunctionBody(funcExpr->_body, true); + *ppNode = transformAsyncGeneratorFunction( + funcExpr, funcExpr->_params, funcExpr->_body); + } else { + recurseFunctionBody(funcExpr->_body, false); + } + } + + void visit(ESTree::AwaitExpressionNode *awaitExpr, ESTree::Node **ppNode) { + if (!insideAsyncGenerator) { + return; + } + + auto *awaitCall = makeHermesInternalCall( + awaitExpr, "_awaitAsyncGenerator", NodeVector{awaitExpr->_argument}); + *ppNode = createTransformedNode( + awaitExpr, awaitCall, false); + } + + void visit(ESTree::YieldExpressionNode *yieldExpr, ESTree::Node **ppNode) { + if (!insideAsyncGenerator) { + return; + } + + auto awaitExpr = + llvh::dyn_cast(yieldExpr->_argument); + if (awaitExpr) { + visit(awaitExpr->_argument); + *ppNode = createTransformedNode( + yieldExpr, awaitExpr->_argument, false); + } + } + + void visit(ESTree::Node *node) { + visitESTreeChildren(*this, node); + } + + private: + void recurseFunctionBody(ESTree::Node *body, bool insideAsyncGenerator) { + // recursively transforms all nested async generators and async expressions + bool oldInsideAsyncGenerator = this->insideAsyncGenerator; + this->insideAsyncGenerator = insideAsyncGenerator; + visitESTreeChildren(*this, body); + this->insideAsyncGenerator = oldInsideAsyncGenerator; + } + + ESTree::Node *transformAsyncGeneratorFunction( + ESTree::Node *funcNode, + ESTree::NodeList ¶ms, + ESTree::Node *body) { + auto *refFunc = createTransformedNode( + funcNode, + nullptr, // id is null for anonymous function + std::move(params), // params + body, + nullptr, // typeParameters + nullptr, // returnType + nullptr, // predicate + true, // generator + false); // async + + auto *wrappedRef = makeHermesInternalCall( + funcNode, "_wrapAsyncGenerator", NodeVector{refFunc}); + + // Create the inner function that calls apply on the wrapped reference + auto innerFuncBody = createTransformedNode( + funcNode, + NodeVector{ + createTransformedNode( + funcNode, + createTransformedNode( + funcNode, + createTransformedNode( + funcNode, + wrappedRef, + makeIdentifierNode(funcNode, "apply"), + false), + nullptr, + NodeVector{ + createTransformedNode( + funcNode), + makeIdentifierNode(funcNode, "arguments")} + .toNodeList()))} + .toNodeList()); + + auto *innerFunc = createTransformedNode( + funcNode, + nullptr, + ESTree::NodeList{}, + innerFuncBody, + nullptr, + nullptr, + nullptr, + false, + false); + + // Create the outer IIFE + auto *iife = createTransformedNode( + funcNode, + createTransformedNode( + funcNode, + nullptr, + ESTree::NodeList{}, // no params + createTransformedNode( + funcNode, + NodeVector{createTransformedNode( + funcNode, innerFunc)} + .toNodeList()), + nullptr, + nullptr, + nullptr, + false, + false), + nullptr, // typeArguments + ESTree::NodeList{}); // no arguments + + return iife; + } + + ESTree::Node *getHermesInternalIdentifier(ESTree::Node *srcNode) override { + return makeIdentifierNode(srcNode, "HermesAsyncIteratorsInternal"); + ; + }; + + bool insideAsyncGenerator = false; +}; + +void transformAsyncGenerators(Context &context, ESTree::Node *node) { + AsyncGenerator transformer(context); + visitESTreeNodeNoReplace(transformer, node); +} + +} // namespace hermes diff --git a/lib/AST/CMakeLists.txt b/lib/AST/CMakeLists.txt index 6316fcad582..b8186277514 100644 --- a/lib/AST/CMakeLists.txt +++ b/lib/AST/CMakeLists.txt @@ -7,6 +7,7 @@ add_hermes_library(hermesAST ASTBuilder.cpp ASTUtils.cpp ES6Class.cpp + AsyncGenerator.cpp ESTree.cpp ESTreeJSONDumper.cpp CommonJS.cpp diff --git a/lib/AST/ES6Class.cpp b/lib/AST/ES6Class.cpp index e98279d1045..303988e0d5c 100644 --- a/lib/AST/ES6Class.cpp +++ b/lib/AST/ES6Class.cpp @@ -6,68 +6,11 @@ */ #include "hermes/AST/ES6Class.h" -#include "hermes/AST/RecursiveVisitor.h" -#include "hermes/Parser/JSLexer.h" -#include "llvh/ADT/StringRef.h" +#include "hermes/AST/TransformationsBase.h" namespace { using namespace hermes; -/// Mutable vector that helps dealing with arrays of nodes safely. -/// Once done with the vector, it can create an ESTree::NodeList -/// representation which is used by the ESTree API in several places. -class NodeVector { - public: - using Storage = llvh::SmallVector; - - NodeVector() = default; - NodeVector(std::initializer_list nodes) { - for (auto &node : nodes) { - _storage.push_back(node); - } - } - - NodeVector(ESTree::NodeList &list) { - for (auto &node : list) { - _storage.push_back(&node); - } - } - - ~NodeVector() = default; - - size_t size() const { - return _storage.size(); - } - - Storage::const_iterator begin() const { - return _storage.begin(); - } - - Storage::const_iterator end() const { - return _storage.end(); - } - - void append(ESTree::Node *node) { - _storage.emplace_back(node); - } - - void prepend(ESTree::Node *node) { - _storage.insert(_storage.begin(), node); - } - - ESTree::NodeList toNodeList() const { - ESTree::NodeList nodeList; - for (auto &node : _storage) { - nodeList.push_back(*node); - } - - return nodeList; - } - - private: - Storage _storage; -}; - struct VisitedClass { UniqueString *className = nullptr; ESTree::Node *parentClass = nullptr; @@ -134,15 +77,15 @@ namespace hermes { /// them into plain ES5 functions. The generated AST leverages the /// HermesES6Internal object, which should be made available at runtime by /// enabling the ES6Class option. -class ES6ClassesTransformations - : public ESTree::RecursionDepthTracker { +class ES6ClassesTransformations : public TransformationsBase { public: - /// Required by ESTree::RecursiveVisitorDispatch. - static constexpr bool kEnableNodeListMutation = true; - ES6ClassesTransformations(Context &context) - : context_(context), - identVar_(context.getIdentifier("var").getUnderlyingPointer()) {} + ES6ClassesTransformations(Context &context) : TransformationsBase(context), + identVar_(context.getIdentifier("var").getUnderlyingPointer()){} + + void doVisitChildren(ESTree::Node *node) { + visitESTreeChildren(*this, node); + } void visit(ESTree::ClassDeclarationNode *classDecl, ESTree::Node **ppNode) { auto *classBody = llvh::dyn_cast(classDecl->_body); @@ -231,41 +174,11 @@ class ES6ClassesTransformations visitESTreeChildren(*this, node); } - void recursionDepthExceeded(ESTree::Node *n) { - context_.getSourceErrorManager().error( - n->getEndLoc(), "Too many nested expressions/statements/declarations"); - } - private: - Context &context_; UniqueString *const identVar_; VisitedClass *_currentProcessingClass = nullptr; const ResolvedClassMember *_currentClassMember = nullptr; - void doVisitChildren(ESTree::Node *node) { - visitESTreeChildren(*this, node); - } - - void doCopyLocation(ESTree::Node *src, ESTree::Node *dest) { - if (src != nullptr) { - dest->setStartLoc(src->getStartLoc()); - dest->setEndLoc(src->getEndLoc()); - dest->setDebugLoc(src->getDebugLoc()); - } - } - - template - T *copyLocation(ESTree::Node *src, T *dest) { - doCopyLocation(src, dest); - return dest; - } - - template - T *createTransformedNode(ESTree::Node *src, Args &&...args) { - auto *node = new (context_) T(std::forward(args)...); - return copyLocation(src, node); - } - ESTree::Node *cloneNodeInternal(ESTree::Node *node) { if (node == nullptr) { return nullptr; @@ -333,7 +246,7 @@ class ES6ClassesTransformations auto *ctorAsFunction = createClassCtor( resolvedClassId, classBody, superClass, classMembers.constructor); - auto *defineClassResult = makeHermesES6InternalCall( + auto *defineClassResult = makeHermesInternalCall( classNode, "defineClass", {copyIdentifier(ctorAsFunction->_id), superClassExpr}); @@ -375,33 +288,9 @@ class ES6ClassesTransformations srcNode, identVar_, std::move(variableList)); } - ESTree::Node *makeHermesES6InternalCall( - ESTree::Node *srcNode, - llvh::StringRef methodName, - const NodeVector ¶meters) { - auto hermesInternalIdentifier = - makeIdentifierNode(srcNode, "HermesES6Internal"); - auto methodIdentifier = makeIdentifierNode(srcNode, methodName); - - auto *getPropertyNode = createTransformedNode( - srcNode, hermesInternalIdentifier, methodIdentifier, false); - return createTransformedNode( - srcNode, getPropertyNode, nullptr, parameters.toNodeList()); - } - - ESTree::IdentifierNode *makeIdentifierNode( - ESTree::Node *srcNode, - UniqueString *name) { - return createTransformedNode( - srcNode, name, nullptr, false); - } - - ESTree::IdentifierNode *makeIdentifierNode( - ESTree::Node *srcNode, - llvh::StringRef name) { - return makeIdentifierNode( - srcNode, context_.getIdentifier(name).getUnderlyingPointer()); - } + ESTree::Node * getHermesInternalIdentifier(ESTree::Node *srcNode) override { + return makeIdentifierNode(srcNode, "HermesES6Internal");; + }; ESTree::Node *makeUndefinedNode(ESTree::Node *srcNode) { return makeIdentifierNode(srcNode, "undefined"); @@ -729,7 +618,7 @@ class ES6ClassesTransformations } auto *call = - makeHermesES6InternalCall(srcNode, hermesCallName, parameters); + makeHermesInternalCall(srcNode, hermesCallName, parameters); stmtList.append(toStatement(call)); } diff --git a/lib/InternalJavaScript/04-AsyncIterator.js b/lib/InternalJavaScript/04-AsyncIterator.js index 2cb2a5434a3..73d8dc95092 100644 --- a/lib/InternalJavaScript/04-AsyncIterator.js +++ b/lib/InternalJavaScript/04-AsyncIterator.js @@ -1,4 +1,88 @@ +// References: +// https://github.com/babel/babel/blob/dcfa42b933299644c4e78168cb4678ecbfae67ed/packages/babel-runtime-corejs3/helpers/esm/OverloadYield.js +// https://github.com/babel/babel/blob/dcfa42b933299644c4e78168cb4678ecbfae67ed/packages/babel-runtime-corejs3/helpers/esm/wrapAsyncGenerator.js +// https://github.com/babel/babel/blob/dcfa42b933299644c4e78168cb4678ecbfae67ed/packages/babel-runtime-corejs3/helpers/esm/awaitAsyncGenerator.js +// https://babel.dev/repl#?browsers=chrome%2062&build=&builtIns=false&corejs=false&spec=false&loose=false&code_lz=IYZwngdgxgBAZgV2gFwJYHsICp7vQCgEoYBvAKBhmAHdhVkYAFAJ3QFtUQBTAOma5DoANgDcu-AOTAJhANwBICjDCouQgCYwAjAAZZSlWs006DFu069-g0eIkAjGfsqGNVWvSasO3PgOFiklBOBqpuACxkAL5kQA&debug=false&forceAllTransforms=true&modules=false&shippedProposals=true&evaluate=false&fileSize=true&timeTravel=false&sourceType=module&lineWrap=true&presets=env&prettier=true&targets=&version=7.13.15&externalPlugins=&assumptions=%7B%7D + (function() { + function _OverloadYield(e, d) { + this.v = e, this.k = d; + } + + function _AsyncGenerator(e) { + var r, t; + function resume(r, t) { + try { + var n = e[r](t), + o = n.value, + u = o instanceof _OverloadYield; + Promise.resolve(u ? o.v : o).then(function (t) { + if (u) { + var i = "return" === r ? "return" : "next"; + if (!o.k || t.done) return resume(i, t); + t = e[i](t).value; + } + settle(n.done ? "return" : "normal", t); + }, function (e) { + resume("throw", e); + }); + } catch (e) { + settle("throw", e); + } + } + function settle(e, n) { + switch (e) { + case "return": + r.resolve({ + value: n, + done: !0 + }); + break; + case "throw": + r.reject(n); + break; + default: + r.resolve({ + value: n, + done: !1 + }); + } + (r = r.next) ? resume(r.key, r.arg) : t = null; + } + this._invoke = function (e, n) { + return new Promise(function (o, u) { + var i = { + key: e, + arg: n, + resolve: o, + reject: u, + next: null + }; + t ? t = t.next = i : (r = t = i, resume(e, n)); + }); + }, "function" != typeof e["return"] && (this["return"] = void 0); + } + + _AsyncGenerator.prototype[Symbol.asyncIterator] = function () { + return this; + }, _AsyncGenerator.prototype.next = function (e) { + return this._invoke("next", e); + }, _AsyncGenerator.prototype["throw"] = function (e) { + return this._invoke("throw", e); + }, _AsyncGenerator.prototype["return"] = function (e) { + return this._invoke("return", e); + }; + + function _wrapAsyncGenerator(e) { + return function () { + return new _AsyncGenerator(e.apply(this, arguments)); + }; + } + + function _awaitAsyncGenerator(e) { + return new _OverloadYield(e, 0); + } + function _makeAsyncIterator(iterable, asyncIterMethod, syncIterMethod) { if (asyncIterMethod) { return asyncIterMethod.call(iterable); @@ -32,7 +116,9 @@ } var HermesAsyncIteratorsInternal = { - _makeAsyncIterator + _makeAsyncIterator, + _wrapAsyncGenerator, + _awaitAsyncGenerator, }; Object.freeze(HermesAsyncIteratorsInternal); globalThis.HermesAsyncIteratorsInternal = HermesAsyncIteratorsInternal; diff --git a/lib/Sema/SemResolve.cpp b/lib/Sema/SemResolve.cpp index 09d3ef03f99..3da041d39ca 100644 --- a/lib/Sema/SemResolve.cpp +++ b/lib/Sema/SemResolve.cpp @@ -13,6 +13,7 @@ #include "SemanticResolver.h" #include "hermes/AST/ES6Class.h" #include "hermes/AST/ESTree.h" +#include "hermes/AST/AsyncGenerator.h" #include "hermes/Support/PerfSection.h" namespace hermes { @@ -163,6 +164,8 @@ bool resolveAST( if (astContext.getConvertES6Classes()) transformES6Classes(astContext, root); + transformAsyncGenerators(astContext, root); + PerfSection validation("Resolving JavaScript global AST"); // Resolve the entire AST. DeclCollectorMapTy declCollectorMap{}; From 21356b777a705a97bcef635dc4fef02c48de30fb Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Mon, 4 Nov 2024 11:54:36 +0100 Subject: [PATCH 03/14] add for await of --- include/hermes/VM/RuntimeModule.h | 5 ++ lib/IRGen/ESTreeIRGen-stmt.cpp | 74 +++++++++++++++++++++- lib/IRGen/ESTreeIRGen.cpp | 28 ++++++++ lib/IRGen/ESTreeIRGen.h | 13 ++++ lib/InternalJavaScript/04-AsyncIterator.js | 38 +++++++++++ lib/Sema/SemanticResolver.cpp | 2 - lib/VM/Runtime.cpp | 3 +- test/hermes/for-await-of-async-iterator.js | 38 +++++++++++ test/hermes/for-await-of-return.js | 36 +++++++++++ test/hermes/for-await-of-sync-iterator.js | 23 +++++++ 10 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 lib/InternalJavaScript/04-AsyncIterator.js create mode 100644 test/hermes/for-await-of-async-iterator.js create mode 100644 test/hermes/for-await-of-return.js create mode 100644 test/hermes/for-await-of-sync-iterator.js diff --git a/include/hermes/VM/RuntimeModule.h b/include/hermes/VM/RuntimeModule.h index f9b88a94273..eb0ee1550b4 100644 --- a/include/hermes/VM/RuntimeModule.h +++ b/include/hermes/VM/RuntimeModule.h @@ -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) {} diff --git a/lib/IRGen/ESTreeIRGen-stmt.cpp b/lib/IRGen/ESTreeIRGen-stmt.cpp index b20af3078e5..3ea1c54bc64 100644 --- a/lib/IRGen/ESTreeIRGen-stmt.cpp +++ b/lib/IRGen/ESTreeIRGen-stmt.cpp @@ -54,7 +54,7 @@ void ESTreeIRGen::genStatement(ESTree::Node *stmt) { } if (auto *FOS = llvh::dyn_cast(stmt)) { - return genForOfStatement(FOS); + return FOS->_await ? genAsyncForOfStatement(FOS) : genForOfStatement(FOS); } if (auto *Ret = llvh::dyn_cast(stmt)) { @@ -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) { diff --git a/lib/IRGen/ESTreeIRGen.cpp b/lib/IRGen/ESTreeIRGen.cpp index 9dfb667f1b0..79d55086018 100644 --- a/lib/IRGen/ESTreeIRGen.cpp +++ b/lib/IRGen/ESTreeIRGen.cpp @@ -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( @@ -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, diff --git a/lib/IRGen/ESTreeIRGen.h b/lib/IRGen/ESTreeIRGen.h index 3ca4a17f543..fe9902f8e9a 100644 --- a/lib/IRGen/ESTreeIRGen.h +++ b/lib/IRGen/ESTreeIRGen.h @@ -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); @@ -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; @@ -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 /// diff --git a/lib/InternalJavaScript/04-AsyncIterator.js b/lib/InternalJavaScript/04-AsyncIterator.js new file mode 100644 index 00000000000..184de394291 --- /dev/null +++ b/lib/InternalJavaScript/04-AsyncIterator.js @@ -0,0 +1,38 @@ +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; +}; diff --git a/lib/Sema/SemanticResolver.cpp b/lib/Sema/SemanticResolver.cpp index 278b948e1f6..58117e3f9cf 100644 --- a/lib/Sema/SemanticResolver.cpp +++ b/lib/Sema/SemanticResolver.cpp @@ -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); } diff --git a/lib/VM/Runtime.cpp b/lib/VM/Runtime.cpp index 10e43bb1316..edc8001168a 100644 --- a/lib/VM/Runtime.cpp +++ b/lib/VM/Runtime.cpp @@ -1047,7 +1047,7 @@ CallResult 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."); } @@ -1191,6 +1191,7 @@ Handle Runtime::runInternalJavaScript() { RuntimeModuleFlags flags; flags.persistent = true; flags.hidesEpilogue = true; + flags.ignoreES6PromiseChecks = true; auto res = runBytecode( std::move(bcResult.first), flags, diff --git a/test/hermes/for-await-of-async-iterator.js b/test/hermes/for-await-of-async-iterator.js new file mode 100644 index 00000000000..27a65c7a68f --- /dev/null +++ b/test/hermes/for-await-of-async-iterator.js @@ -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 \ No newline at end of file diff --git a/test/hermes/for-await-of-return.js b/test/hermes/for-await-of-return.js new file mode 100644 index 00000000000..895a9edd2ec --- /dev/null +++ b/test/hermes/for-await-of-return.js @@ -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 \ No newline at end of file diff --git a/test/hermes/for-await-of-sync-iterator.js b/test/hermes/for-await-of-sync-iterator.js new file mode 100644 index 00000000000..b90ff9f6bd4 --- /dev/null +++ b/test/hermes/for-await-of-sync-iterator.js @@ -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 \ No newline at end of file From 21774c4bf44fff911857de9f1e3cf4e3d63a0e8d Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Mon, 4 Nov 2024 13:01:55 +0100 Subject: [PATCH 04/14] enable async iterators --- lib/InternalJavaScript/04-AsyncIterator.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/InternalJavaScript/04-AsyncIterator.js b/lib/InternalJavaScript/04-AsyncIterator.js index 184de394291..036f1c75558 100644 --- a/lib/InternalJavaScript/04-AsyncIterator.js +++ b/lib/InternalJavaScript/04-AsyncIterator.js @@ -36,3 +36,7 @@ var initAsyncIterators = function() { Object.freeze(HermesAsyncIteratorsInternal); globalThis.HermesAsyncIteratorsInternal = HermesAsyncIteratorsInternal; }; + +if (HermesInternal?.hasPromise?.()) { + initAsyncIterators(); +} From e1ac9931a35ecda68d939b8d9e287910f4afc69b Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Mon, 24 Mar 2025 17:10:46 +0100 Subject: [PATCH 05/14] address suggestion --- include/hermes/FrontEndDefs/Builtins.def | 1 + include/hermes/VM/PredefinedStrings.def | 1 + lib/IRGen/ESTreeIRGen.cpp | 8 +++----- lib/InternalJavaScript/04-AsyncIterator.js | 19 +++++++++++-------- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/include/hermes/FrontEndDefs/Builtins.def b/include/hermes/FrontEndDefs/Builtins.def index ccb5c0304fb..1932e783a09 100644 --- a/include/hermes/FrontEndDefs/Builtins.def +++ b/include/hermes/FrontEndDefs/Builtins.def @@ -151,6 +151,7 @@ PRIVATE_BUILTIN(initRegexNamedGroups) JS_BUILTIN(spawnAsync) MARK_FIRST_JS_BUILTIN(spawnAsync) +JS_BUILTIN(makeAsyncIterator) #undef NORMAL_OBJECT #undef NORMAL_METHOD diff --git a/include/hermes/VM/PredefinedStrings.def b/include/hermes/VM/PredefinedStrings.def index a1490019a26..c868d14d39c 100644 --- a/include/hermes/VM/PredefinedStrings.def +++ b/include/hermes/VM/PredefinedStrings.def @@ -462,6 +462,7 @@ STR(fileName, "fileName") STR(setPromiseRejectionTrackingHook, "setPromiseRejectionTrackingHook") STR(enablePromiseRejectionTracker, "enablePromiseRejectionTracker") STR(spawnAsync, "spawnAsync") /* NOLINT */ +STR(makeAsyncIterator, "makeAsyncIterator") /* NOLINT */ STR(SHBuiltin, "$SHBuiltin") diff --git a/lib/IRGen/ESTreeIRGen.cpp b/lib/IRGen/ESTreeIRGen.cpp index 79d55086018..afc0a7228be 100644 --- a/lib/IRGen/ESTreeIRGen.cpp +++ b/lib/IRGen/ESTreeIRGen.cpp @@ -504,13 +504,11 @@ ESTreeIRGen::IteratorRecordSlow ESTreeIRGen::emitGetAsyncIteratorSlow( auto *syncIteratorMethod = Builder.createLoadPropertyInst(obj, emitIteratorSymbol()); - auto *wrapper = Builder.createLoadPropertyInst( - Builder.getGlobalObject(), "HermesAsyncIteratorsInternal"); - wrapper = Builder.createLoadPropertyInst( - wrapper, "_makeAsyncIterator"); + auto *makeAsyncIterator = Builder.createGetBuiltinClosureInst( + BuiltinMethod::HermesBuiltin_makeAsyncIterator); auto *iterator = Builder.createCallInst( - wrapper, + makeAsyncIterator, Builder.getLiteralUndefined(), Builder.getLiteralUndefined(), {obj, asyncIteratorMethod, syncIteratorMethod}); diff --git a/lib/InternalJavaScript/04-AsyncIterator.js b/lib/InternalJavaScript/04-AsyncIterator.js index 036f1c75558..df28ea2d22a 100644 --- a/lib/InternalJavaScript/04-AsyncIterator.js +++ b/lib/InternalJavaScript/04-AsyncIterator.js @@ -1,4 +1,4 @@ -var initAsyncIterators = function() { +function initAsyncIterators() { function _makeAsyncIterator(iterable, asyncIterMethod, syncIterMethod) { if (asyncIterMethod) { return asyncIterMethod.call(iterable); @@ -30,13 +30,16 @@ var initAsyncIterators = function() { }; } - var HermesAsyncIteratorsInternal = { - _makeAsyncIterator - }; - Object.freeze(HermesAsyncIteratorsInternal); - globalThis.HermesAsyncIteratorsInternal = HermesAsyncIteratorsInternal; -}; + // Register as "makeAsyncIterator" + internalBytecodeResult.makeAsyncIterator = _makeAsyncIterator; +} +// Async operations require Promise support if (HermesInternal?.hasPromise?.()) { initAsyncIterators(); -} +} else { + // Ensure the invariant is maintained and error if Promise is unavailable + internalBytecodeResult.makeAsyncIterator = function() { + throw Error("Async iterators cannot be used with Promise disabled. makeAsyncIterator not registered."); + }; +} \ No newline at end of file From 85deed26451b749e2adc67d7241e94c6b87ec054 Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Thu, 27 Mar 2025 11:18:39 +0100 Subject: [PATCH 06/14] remove ignoreES6PromiseChecks --- include/hermes/VM/RuntimeModule.h | 5 ----- lib/VM/Runtime.cpp | 1 - 2 files changed, 6 deletions(-) diff --git a/include/hermes/VM/RuntimeModule.h b/include/hermes/VM/RuntimeModule.h index f7287693f4a..48f34b4cb11 100644 --- a/include/hermes/VM/RuntimeModule.h +++ b/include/hermes/VM/RuntimeModule.h @@ -53,11 +53,6 @@ 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) {} diff --git a/lib/VM/Runtime.cpp b/lib/VM/Runtime.cpp index 6226b0dde6c..8d50a773587 100644 --- a/lib/VM/Runtime.cpp +++ b/lib/VM/Runtime.cpp @@ -1228,7 +1228,6 @@ Handle Runtime::runInternalJavaScript() { RuntimeModuleFlags flags; flags.persistent = true; flags.hidesEpilogue = true; - flags.ignoreES6PromiseChecks = true; auto res = runBytecode( std::move(bcResult.first), flags, From 5e16b472fc97da1cfcaf5dbe10a2566f871bef68 Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Thu, 27 Mar 2025 11:26:47 +0100 Subject: [PATCH 07/14] fix for-await-of test cases --- test/hermes/for-await-of-async-iterator.js | 6 +++--- test/hermes/for-await-of-return.js | 6 +++--- test/hermes/for-await-of-sync-iterator.js | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/hermes/for-await-of-async-iterator.js b/test/hermes/for-await-of-async-iterator.js index 27a65c7a68f..4204e8d628e 100644 --- a/test/hermes/for-await-of-async-iterator.js +++ b/test/hermes/for-await-of-async-iterator.js @@ -5,9 +5,9 @@ * 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 +// RUN: %hermes %s | %FileCheck --match-full-lines %s +// RUN: %hermes -O0 %s | %FileCheck --match-full-lines %s +// RUN: %hermes -lazy %s | %FileCheck --match-full-lines %s // Custom async iterable using Symbol.asyncIterator const asyncIterable = { diff --git a/test/hermes/for-await-of-return.js b/test/hermes/for-await-of-return.js index 895a9edd2ec..11ae252727d 100644 --- a/test/hermes/for-await-of-return.js +++ b/test/hermes/for-await-of-return.js @@ -5,9 +5,9 @@ * 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 +// RUN: %hermes %s | %FileCheck --match-full-lines %s +// RUN: %hermes -O0 %s | %FileCheck --match-full-lines %s +// RUN: %hermes -lazy %s | %FileCheck --match-full-lines %s // Custom async iterable using Symbol.asyncIterator const asyncIterable = { diff --git a/test/hermes/for-await-of-sync-iterator.js b/test/hermes/for-await-of-sync-iterator.js index b90ff9f6bd4..a0c836396a3 100644 --- a/test/hermes/for-await-of-sync-iterator.js +++ b/test/hermes/for-await-of-sync-iterator.js @@ -5,9 +5,9 @@ * 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 +// RUN: %hermes %s | %FileCheck --match-full-lines %s +// RUN: %hermes -O0 %s | %FileCheck --match-full-lines %s +// RUN: %hermes -lazy %s | %FileCheck --match-full-lines %s // For await of loop should work with a sync iterable const syncIterable = [1,2,3]; From f75f803ea41f06c5ca31bdda02ffb2143c33523e Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Fri, 28 Mar 2025 20:50:41 +0100 Subject: [PATCH 08/14] address suggestions --- lib/IRGen/ESTreeIRGen.cpp | 7 +--- lib/InternalJavaScript/04-AsyncIterator.js | 41 ++++++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/IRGen/ESTreeIRGen.cpp b/lib/IRGen/ESTreeIRGen.cpp index df21c1bbbd4..c4e200e65c8 100644 --- a/lib/IRGen/ESTreeIRGen.cpp +++ b/lib/IRGen/ESTreeIRGen.cpp @@ -584,11 +584,6 @@ ESTreeIRGen::IteratorRecordSlow ESTreeIRGen::emitGetIteratorSlow(Value *obj) { ESTreeIRGen::IteratorRecordSlow ESTreeIRGen::emitGetAsyncIteratorSlow( Value *obj) { - auto *asyncIteratorMethod = - Builder.createLoadPropertyInst(obj, emitAsyncIteratorSymbol()); - auto *syncIteratorMethod = - Builder.createLoadPropertyInst(obj, emitIteratorSymbol()); - auto *makeAsyncIterator = Builder.createGetBuiltinClosureInst( BuiltinMethod::HermesBuiltin_makeAsyncIterator); @@ -596,7 +591,7 @@ ESTreeIRGen::IteratorRecordSlow ESTreeIRGen::emitGetAsyncIteratorSlow( makeAsyncIterator, Builder.getLiteralUndefined(), Builder.getLiteralUndefined(), - {obj, asyncIteratorMethod, syncIteratorMethod}); + {obj}); auto *nextMethod = Builder.createLoadPropertyInst(iterator, "next"); return {iterator, nextMethod}; diff --git a/lib/InternalJavaScript/04-AsyncIterator.js b/lib/InternalJavaScript/04-AsyncIterator.js index df28ea2d22a..d2abb517b0f 100644 --- a/lib/InternalJavaScript/04-AsyncIterator.js +++ b/lib/InternalJavaScript/04-AsyncIterator.js @@ -1,30 +1,43 @@ function initAsyncIterators() { - function _makeAsyncIterator(iterable, asyncIterMethod, syncIterMethod) { - if (asyncIterMethod) { - return asyncIterMethod.call(iterable); + var HermesPromise = globalThis.Promise; + + /** + * Creates an asynchronous iterator for the given iterable. + * + * This function checks if the iterable already has an async iterator. + * If it does, it returns the existing async iterator. + * If not, it constructs an async iterator from the iterable's synchronous iterator. + * The constructed async iterator resolves any promise values within its methods. + * + * @param {Iterable} iterable - The iterable object for which to create an async iterator. + * @returns {AsyncIterator} - An asynchronous iterator for the given iterable. + */ + function _makeAsyncIterator(iterable) { + if (iterable[Symbol.asyncIterator]) { + return iterable[Symbol.asyncIterator](); } - let syncIterator = syncIterMethod.call(iterable); + var syncIterator = iterable[Symbol.iterator](); async function handleResult(result) { if (result.done) { return result; } - const value = result.value; - const resolvedValue = value instanceof Promise ? await value : value; + var value = result.value; + var resolvedValue = value instanceof HermesPromise ? await value : value; return { done: false, value: resolvedValue }; } return { async next() { - const result = syncIterator.next(); + var result = syncIterator.next(); return handleResult(result); }, async return(value) { - const result = typeof syncIterator.return === 'function' ? syncIterator.return(value) : { done: true, value }; + var 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 }; + var result = typeof syncIterator.throw === 'function' ? syncIterator.throw(error) : { done: true, value: error }; return handleResult(result); } }; @@ -34,12 +47,4 @@ function initAsyncIterators() { internalBytecodeResult.makeAsyncIterator = _makeAsyncIterator; } -// Async operations require Promise support -if (HermesInternal?.hasPromise?.()) { - initAsyncIterators(); -} else { - // Ensure the invariant is maintained and error if Promise is unavailable - internalBytecodeResult.makeAsyncIterator = function() { - throw Error("Async iterators cannot be used with Promise disabled. makeAsyncIterator not registered."); - }; -} \ No newline at end of file +initAsyncIterators(); \ No newline at end of file From aae6ac7a2b1109d67564664f49902643b657e61e Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Thu, 3 Apr 2025 11:24:59 +0200 Subject: [PATCH 09/14] temporarily avoid testing in lazy compilation --- test/hermes/for-await-of-async-iterator.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/hermes/for-await-of-async-iterator.js b/test/hermes/for-await-of-async-iterator.js index 4204e8d628e..a472c6f3b5a 100644 --- a/test/hermes/for-await-of-async-iterator.js +++ b/test/hermes/for-await-of-async-iterator.js @@ -7,7 +7,6 @@ // RUN: %hermes %s | %FileCheck --match-full-lines %s // RUN: %hermes -O0 %s | %FileCheck --match-full-lines %s -// RUN: %hermes -lazy %s | %FileCheck --match-full-lines %s // Custom async iterable using Symbol.asyncIterator const asyncIterable = { From 5567cd96c5a5f98d20918481dcd1b38b67bf14ea Mon Sep 17 00:00:00 2001 From: Aakash Patel Date: Thu, 3 Apr 2025 15:19:01 -0700 Subject: [PATCH 10/14] Add a compiler stage for AST transformation Summary: This is a compiler stage that is invoked from every place we compile JS, allowing arbitrary AST transformations between parsing and semantic resolution. Differential Revision: D72414034 --- include/hermes/AST/TransformAST.h | 25 +++++++++++++++++++++++++ lib/AST/CMakeLists.txt | 1 + lib/AST/TransformAST.cpp | 16 ++++++++++++++++ lib/BCGen/HBC/BCProviderFromSrc.cpp | 7 +++++++ lib/BCGen/HBC/HBC.cpp | 17 +++++++++++++++++ lib/CompilerDriver/CompilerDriver.cpp | 5 +++++ tools/shermes/shermes.cpp | 6 ++++++ 7 files changed, 77 insertions(+) create mode 100644 include/hermes/AST/TransformAST.h create mode 100644 lib/AST/TransformAST.cpp diff --git a/include/hermes/AST/TransformAST.h b/include/hermes/AST/TransformAST.h new file mode 100644 index 00000000000..55db15ee621 --- /dev/null +++ b/include/hermes/AST/TransformAST.h @@ -0,0 +1,25 @@ +/* + * 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. + */ + +#pragma once + +#include "hermes/AST/Context.h" +#include "hermes/AST/ESTree.h" + +namespace hermes { + +/// General purpose AST transformation which will be applied before running +/// semantic resolution in the compiler pipeline. +/// Allows adding functionality/transforms in a general way directly to the AST +/// in a way that works for lazy compilation, debugger eval, etc. +/// +/// \return the transformed node, which should be used for the remainder of +/// compilation. On failure, report an error and return nullptr. +/// The returned Node must be the same kind as the original \p root. +ESTree::Node *transformASTForCompilation(Context &context, ESTree::Node *root); + +} // namespace hermes diff --git a/lib/AST/CMakeLists.txt b/lib/AST/CMakeLists.txt index 158239c8cfa..ca00bac9de2 100644 --- a/lib/AST/CMakeLists.txt +++ b/lib/AST/CMakeLists.txt @@ -10,6 +10,7 @@ add_hermes_library(hermesAST ESTreeJSONDumper.cpp CommonJS.cpp TS2Flow.cpp + TransformAST.cpp Context.cpp NativeContext.cpp LINK_OBJLIBS diff --git a/lib/AST/TransformAST.cpp b/lib/AST/TransformAST.cpp new file mode 100644 index 00000000000..a111f310062 --- /dev/null +++ b/lib/AST/TransformAST.cpp @@ -0,0 +1,16 @@ +/* + * 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. + */ + +#include "hermes/AST/TransformAST.h" + +namespace hermes { + +ESTree::Node *transformASTForCompilation(Context &context, ESTree::Node *root) { + return root; +} + +} // namespace hermes diff --git a/lib/BCGen/HBC/BCProviderFromSrc.cpp b/lib/BCGen/HBC/BCProviderFromSrc.cpp index b1c38fc4168..cfc3fe94557 100644 --- a/lib/BCGen/HBC/BCProviderFromSrc.cpp +++ b/lib/BCGen/HBC/BCProviderFromSrc.cpp @@ -7,6 +7,7 @@ #include "hermes/BCGen/HBC/BCProviderFromSrc.h" +#include "hermes/AST/TransformAST.h" #include "hermes/BCGen/HBC/HBC.h" #include "hermes/IR/IR.h" #include "hermes/IRGen/IRGen.h" @@ -205,6 +206,12 @@ BCProviderFromSrc::create( parser.registerMagicURLs(); } + if (!parsed) + return {nullptr, getErrorString()}; + + parsed = llvh::cast( + hermes::transformASTForCompilation(*context, *parsed)); + if (!parsed || !hermes::sema::resolveAST(*context, *semCtx, *parsed, declFileList)) { return {nullptr, getErrorString()}; diff --git a/lib/BCGen/HBC/HBC.cpp b/lib/BCGen/HBC/HBC.cpp index 6ca71e5d0ce..b607d989516 100644 --- a/lib/BCGen/HBC/HBC.cpp +++ b/lib/BCGen/HBC/HBC.cpp @@ -8,6 +8,7 @@ #include "hermes/BCGen/HBC/HBC.h" #include "BytecodeGenerator.h" +#include "hermes/AST/TransformAST.h" #include "hermes/BCGen/HBC/BCProviderFromSrc.h" #include "hermes/IRGen/IRGen.h" #include "hermes/Optimizer/PassManager/Pipeline.h" @@ -169,6 +170,14 @@ static void compileLazyFunctionWorker(void *argPtr) { sema::SemContext *semCtx = provider->getSemCtx(); assert(semCtx && "missing semantic data to compile"); + if (!optParsed) { + data->success = false; + data->error = outputManager.getErrorString(); + return; + } + + optParsed = hermes::transformASTForCompilation(context, *optParsed); + // A non-null home object means the parent function context could reference // super. bool parentHadSuperBinding = lazyDataInst->getHomeObject(); @@ -306,6 +315,11 @@ static void compileEvalWorker(void *argPtr) { parser.setStrictMode(data->compileFlags.strict); auto optParsed = parser.parse(); + if (!optParsed) { + data->success = false; + data->error = outputManager.getErrorString(); + return; + } if (optParsed && parserMode != parser::LazyParse) { parser.registerMagicURLs(); @@ -317,6 +331,9 @@ static void compileEvalWorker(void *argPtr) { std::shared_ptr semCtx = std::make_shared(context, provider->shareSemCtx()); + optParsed = llvh::cast( + hermes::transformASTForCompilation(context, *optParsed)); + // A non-null home object means the parent function context could reference // super. bool parentHadSuperBinding = evalDataInst->getHomeObject() != nullptr; diff --git a/lib/CompilerDriver/CompilerDriver.cpp b/lib/CompilerDriver/CompilerDriver.cpp index 819318a1275..2516fc40161 100644 --- a/lib/CompilerDriver/CompilerDriver.cpp +++ b/lib/CompilerDriver/CompilerDriver.cpp @@ -12,6 +12,7 @@ #include "hermes/AST/Context.h" #include "hermes/AST/ESTreeJSONDumper.h" #include "hermes/AST/TS2Flow.h" +#include "hermes/AST/TransformAST.h" #include "hermes/AST2JS/AST2JS.h" #include "hermes/BCGen/HBC/BytecodeDisassembler.h" #include "hermes/BCGen/HBC/HBC.h" @@ -866,6 +867,10 @@ ESTree::NodePtr parseJS( } #endif + parsedAST = hermes::transformASTForCompilation(*context, parsedAST); + if (!parsedAST) + return nullptr; + // If we are executing in typed mode and not script, then wrap the program. if (shouldWrapInIIFE) { parsedAST = wrapInIIFE(context, llvh::cast(parsedAST)); diff --git a/tools/shermes/shermes.cpp b/tools/shermes/shermes.cpp index 8bd84260785..72e17d6bd60 100644 --- a/tools/shermes/shermes.cpp +++ b/tools/shermes/shermes.cpp @@ -13,6 +13,7 @@ #include "hermes/AST/ESTreeJSONDumper.h" #include "hermes/AST/NativeContext.h" #include "hermes/AST/TS2Flow.h" +#include "hermes/AST/TransformAST.h" #include "hermes/IR/IRVerifier.h" #include "hermes/IRGen/IRGen.h" #include "hermes/Optimizer/PassManager/PassManager.h" @@ -805,6 +806,11 @@ ESTree::NodePtr parseJS( } #endif + parsedAST = llvh::cast( + hermes::transformASTForCompilation(*context, parsedAST)); + if (!parsedAST) + return nullptr; + // If we are executing in typed mode and not script, then wrap the program. if (shouldWrapInIIFE) { parsedAST = wrapInIIFE(context, parsedAST); From b62e26d90d84bf87aff42de41b622132511b81cc Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Fri, 4 Apr 2025 12:05:29 +0200 Subject: [PATCH 11/14] address suggestion --- lib/AST/AsyncGenerator.cpp | 2 +- lib/AST/TransformAST.cpp | 4 ++++ lib/Sema/SemResolve.cpp | 4 ---- test/hermes/async-generators.js | 3 +-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/AST/AsyncGenerator.cpp b/lib/AST/AsyncGenerator.cpp index fc14dc08a61..ce2a3f5c640 100644 --- a/lib/AST/AsyncGenerator.cpp +++ b/lib/AST/AsyncGenerator.cpp @@ -178,7 +178,7 @@ class AsyncGenerator : public TransformationsBase { void transformAsyncGenerators(Context &context, ESTree::Node *node) { AsyncGenerator transformer(context); - visitESTreeNodeNoReplace(transformer, node); + visitESTreeNode(transformer, node, nullptr); } } // namespace hermes diff --git a/lib/AST/TransformAST.cpp b/lib/AST/TransformAST.cpp index a111f310062..d7bf7f7572c 100644 --- a/lib/AST/TransformAST.cpp +++ b/lib/AST/TransformAST.cpp @@ -6,10 +6,14 @@ */ #include "hermes/AST/TransformAST.h" +#include "hermes/AST/AsyncGenerator.h" namespace hermes { ESTree::Node *transformASTForCompilation(Context &context, ESTree::Node *root) { + if (context.getEnableAsyncGenerators()) { + transformAsyncGenerators(context, root); + } return root; } diff --git a/lib/Sema/SemResolve.cpp b/lib/Sema/SemResolve.cpp index efba5a4c05a..2e347b0d1ab 100644 --- a/lib/Sema/SemResolve.cpp +++ b/lib/Sema/SemResolve.cpp @@ -12,7 +12,6 @@ #include "FlowTypesDumper.h" #include "SemanticResolver.h" #include "hermes/AST/ESTree.h" -#include "hermes/AST/AsyncGenerator.h" #include "hermes/Support/PerfSection.h" namespace hermes { @@ -163,9 +162,6 @@ bool resolveAST( flow::FlowContext *flowContext, ESTree::ProgramNode *root, const DeclarationFileListTy &ambientDecls) { - if (astContext.getEnableAsyncGenerators()) { - transformAsyncGenerators(astContext, root); - } PerfSection validation("Resolving JavaScript global AST"); // Resolve the entire AST. diff --git a/test/hermes/async-generators.js b/test/hermes/async-generators.js index 3df395f93bd..2070e8f80b9 100644 --- a/test/hermes/async-generators.js +++ b/test/hermes/async-generators.js @@ -25,7 +25,6 @@ async function* mixedValuesGenerator() { })(); //CHECK: 6 -/* async function* errorHandlingGenerator() { yield 1; throw new Error('Test error'); @@ -42,4 +41,4 @@ async function* errorHandlingGenerator() { print('Caught error:', error.message); } })(); - */ + From c59c80de7127a84559bb9ce808aef972fa11f91c Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Tue, 8 Apr 2025 15:12:14 +0200 Subject: [PATCH 12/14] separate tests --- test/hermes/async-generators-throw.js | 28 +++++++++++++++++++++++++++ test/hermes/async-generators.js | 18 ----------------- 2 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 test/hermes/async-generators-throw.js diff --git a/test/hermes/async-generators-throw.js b/test/hermes/async-generators-throw.js new file mode 100644 index 00000000000..2f006df0243 --- /dev/null +++ b/test/hermes/async-generators-throw.js @@ -0,0 +1,28 @@ +/** + * 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 -Xasync-generators %s | %FileCheck --match-full-lines %s +// RUN: %hermes -Xasync-generators -O0 %s | %FileCheck --match-full-lines %s +// RUN: %hermes -Xasync-generators -lazy %s | %FileCheck --match-full-lines %s + +async function* errorHandlingGenerator() { + yield 1; + throw new Error('Async generator threw!'); + yield 2; +} + +// Test error propagation with the async generator +(async function testErrorPropagation() { + try { + for await (const value of errorHandlingGenerator()) { + print(value); + } + } catch (error) { + print('Caught error:', error.message); + } +})(); +//CHECK: Caught error: Async generator threw! diff --git a/test/hermes/async-generators.js b/test/hermes/async-generators.js index 2070e8f80b9..3a2faa4b475 100644 --- a/test/hermes/async-generators.js +++ b/test/hermes/async-generators.js @@ -24,21 +24,3 @@ async function* mixedValuesGenerator() { print(sum); })(); //CHECK: 6 - -async function* errorHandlingGenerator() { - yield 1; - throw new Error('Test error'); - yield 2; -} - -// Test error propagation with the async generator -(async function testErrorPropagation() { - try { - for await (const value of errorHandlingGenerator()) { - print(value); - } - } catch (error) { - print('Caught error:', error.message); - } -})(); - From 6b84cfc162912778eacbb1ede64725931c90aca0 Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Tue, 8 Apr 2025 16:42:13 +0200 Subject: [PATCH 13/14] WIP --- include/hermes/AST/AsyncGenerator.h | 2 +- lib/AST/AsyncGenerator.cpp | 67 ++++++++++++--------------- lib/AST/TransformAST.cpp | 2 +- test/hermes/async-generators-throw.js | 2 +- test/hermes/async-generators.js | 1 - 5 files changed, 32 insertions(+), 42 deletions(-) diff --git a/include/hermes/AST/AsyncGenerator.h b/include/hermes/AST/AsyncGenerator.h index 225bc01d74d..b8839d0a910 100644 --- a/include/hermes/AST/AsyncGenerator.h +++ b/include/hermes/AST/AsyncGenerator.h @@ -7,7 +7,7 @@ namespace hermes { /// Recursively transforms the ESTree Node tree such that async generators /// are converted into generators -void transformAsyncGenerators(Context &context, ESTree::Node *node); +ESTree::Node* transformAsyncGenerators(Context &context, ESTree::Node *node); } // namespace hermes diff --git a/lib/AST/AsyncGenerator.cpp b/lib/AST/AsyncGenerator.cpp index ce2a3f5c640..f01f43fe8bf 100644 --- a/lib/AST/AsyncGenerator.cpp +++ b/lib/AST/AsyncGenerator.cpp @@ -40,9 +40,19 @@ class AsyncGenerator : public TransformationsBase { void visit(ESTree::FunctionDeclarationNode *funcDecl, ESTree::Node **ppNode) { if (funcDecl->_async && funcDecl->_generator) { recurseFunctionBody(funcDecl->_body, true); - auto iife = transformAsyncGeneratorFunction( + auto *refFunc = transformAsyncGeneratorFunction( funcDecl, funcDecl->_params, funcDecl->_body); - *ppNode = makeSingleVarDecl(funcDecl, funcDecl->_id, iife); + + *ppNode = createTransformedNode( + funcDecl, + funcDecl->_id, + std::move(funcDecl->_params), + refFunc, + nullptr, + nullptr, + nullptr, + false, + false); } else { recurseFunctionBody(funcDecl->_body, false); } @@ -51,8 +61,19 @@ class AsyncGenerator : public TransformationsBase { void visit(ESTree::FunctionExpressionNode *funcExpr, ESTree::Node **ppNode) { if (funcExpr->_async && funcExpr->_generator) { recurseFunctionBody(funcExpr->_body, true); - *ppNode = transformAsyncGeneratorFunction( + auto *refFunc = transformAsyncGeneratorFunction( funcExpr, funcExpr->_params, funcExpr->_body); + + *ppNode = createTransformedNode( + funcExpr, + nullptr, + std::move(funcExpr->_params), + refFunc, + nullptr, + nullptr, + nullptr, + false, + false); } else { recurseFunctionBody(funcExpr->_body, false); } @@ -114,8 +135,8 @@ class AsyncGenerator : public TransformationsBase { auto *wrappedRef = makeHermesInternalCall( funcNode, "_wrapAsyncGenerator", NodeVector{refFunc}); - // Create the inner function that calls apply on the wrapped reference - auto innerFuncBody = createTransformedNode( + // Create a function body that calls apply on the wrapped function + auto appliedBody = createTransformedNode( funcNode, NodeVector{ createTransformedNode( @@ -135,38 +156,7 @@ class AsyncGenerator : public TransformationsBase { .toNodeList()))} .toNodeList()); - auto *innerFunc = createTransformedNode( - funcNode, - nullptr, - ESTree::NodeList{}, - innerFuncBody, - nullptr, - nullptr, - nullptr, - false, - false); - - // Create the outer IIFE - auto *iife = createTransformedNode( - funcNode, - createTransformedNode( - funcNode, - nullptr, - ESTree::NodeList{}, // no params - createTransformedNode( - funcNode, - NodeVector{createTransformedNode( - funcNode, innerFunc)} - .toNodeList()), - nullptr, - nullptr, - nullptr, - false, - false), - nullptr, // typeArguments - ESTree::NodeList{}); // no arguments - - return iife; + return appliedBody; } ESTree::Node *getHermesInternalIdentifier(ESTree::Node *srcNode) override { @@ -176,9 +166,10 @@ class AsyncGenerator : public TransformationsBase { bool insideAsyncGenerator = false; }; -void transformAsyncGenerators(Context &context, ESTree::Node *node) { +ESTree::Node* transformAsyncGenerators(Context &context, ESTree::Node *node) { AsyncGenerator transformer(context); visitESTreeNode(transformer, node, nullptr); + return node; } } // namespace hermes diff --git a/lib/AST/TransformAST.cpp b/lib/AST/TransformAST.cpp index d7bf7f7572c..741f78964b6 100644 --- a/lib/AST/TransformAST.cpp +++ b/lib/AST/TransformAST.cpp @@ -12,7 +12,7 @@ namespace hermes { ESTree::Node *transformASTForCompilation(Context &context, ESTree::Node *root) { if (context.getEnableAsyncGenerators()) { - transformAsyncGenerators(context, root); + root = transformAsyncGenerators(context, root); } return root; } diff --git a/test/hermes/async-generators-throw.js b/test/hermes/async-generators-throw.js index 2f006df0243..7f27aed8861 100644 --- a/test/hermes/async-generators-throw.js +++ b/test/hermes/async-generators-throw.js @@ -7,7 +7,7 @@ // RUN: %hermes -Xasync-generators %s | %FileCheck --match-full-lines %s // RUN: %hermes -Xasync-generators -O0 %s | %FileCheck --match-full-lines %s -// RUN: %hermes -Xasync-generators -lazy %s | %FileCheck --match-full-lines %s + async function* errorHandlingGenerator() { yield 1; diff --git a/test/hermes/async-generators.js b/test/hermes/async-generators.js index 3a2faa4b475..b83533faea9 100644 --- a/test/hermes/async-generators.js +++ b/test/hermes/async-generators.js @@ -7,7 +7,6 @@ // RUN: %hermes -Xasync-generators %s | %FileCheck --match-full-lines %s // RUN: %hermes -Xasync-generators -O0 %s | %FileCheck --match-full-lines %s -// RUN: %hermes -Xasync-generators -lazy %s | %FileCheck --match-full-lines %s async function* mixedValuesGenerator() { yield 1; From ca91214e669d074c026a2c41c5b79497f7b550b5 Mon Sep 17 00:00:00 2001 From: Thomas Rossi Mel Date: Thu, 10 Apr 2025 15:56:15 +0200 Subject: [PATCH 14/14] fix lazy compilation --- lib/AST/AsyncGenerator.cpp | 7 ++++++- test/hermes/async-generators-throw.js | 1 + test/hermes/async-generators.js | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/AST/AsyncGenerator.cpp b/lib/AST/AsyncGenerator.cpp index f01f43fe8bf..75810406572 100644 --- a/lib/AST/AsyncGenerator.cpp +++ b/lib/AST/AsyncGenerator.cpp @@ -132,7 +132,7 @@ class AsyncGenerator : public TransformationsBase { true, // generator false); // async - auto *wrappedRef = makeHermesInternalCall( + auto *wrappedRef = isBodyEmpty(body) ? refFunc : makeHermesInternalCall( funcNode, "_wrapAsyncGenerator", NodeVector{refFunc}); // Create a function body that calls apply on the wrapped function @@ -159,6 +159,11 @@ class AsyncGenerator : public TransformationsBase { return appliedBody; } + bool isBodyEmpty(ESTree::Node *body) { + auto *blockStmt = llvh::dyn_cast(body); + return blockStmt && blockStmt->_body.empty(); + } + ESTree::Node *getHermesInternalIdentifier(ESTree::Node *srcNode) override { return makeIdentifierNode(srcNode, "HermesAsyncIteratorsInternal"); }; diff --git a/test/hermes/async-generators-throw.js b/test/hermes/async-generators-throw.js index 7f27aed8861..02a6bb660ad 100644 --- a/test/hermes/async-generators-throw.js +++ b/test/hermes/async-generators-throw.js @@ -7,6 +7,7 @@ // RUN: %hermes -Xasync-generators %s | %FileCheck --match-full-lines %s // RUN: %hermes -Xasync-generators -O0 %s | %FileCheck --match-full-lines %s +// RUN: %hermes -Xasync-generators -lazy %s | %FileCheck --match-full-lines %s async function* errorHandlingGenerator() { diff --git a/test/hermes/async-generators.js b/test/hermes/async-generators.js index b83533faea9..3a2faa4b475 100644 --- a/test/hermes/async-generators.js +++ b/test/hermes/async-generators.js @@ -7,6 +7,7 @@ // RUN: %hermes -Xasync-generators %s | %FileCheck --match-full-lines %s // RUN: %hermes -Xasync-generators -O0 %s | %FileCheck --match-full-lines %s +// RUN: %hermes -Xasync-generators -lazy %s | %FileCheck --match-full-lines %s async function* mixedValuesGenerator() { yield 1;