* Copyright (c) 2025-2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "asyncMethodLoweringStackless.h"
#include "checker/ETSchecker.h"
#include "compiler/lowering/scopesInit/scopesInitPhase.h"
#include "compiler/lowering/util.h"
namespace ark::es2panda::compiler {
static constexpr std::string_view LOWERING_NAME = "async-method-lowering-stackless";
std::string_view AsyncMethodLoweringStackless::Name() const
{
return "AsyncMethodLoweringStackless";
}
static void CheckNode(public_lib::Context *ctx, ir::AstNode *node)
{
ES2PANDA_ASSERT(ctx);
ES2PANDA_ASSERT(node);
ES2PANDA_ASSERT(node->Parent());
auto checker = ctx->GetChecker()->AsETSChecker();
auto binder = checker->VarBinder()->AsETSBinder();
auto scope = NearestScope(node->Parent());
auto bscope = varbinder::LexicalScope<varbinder::Scope>::Enter(binder, scope);
CheckLoweredNode(binder, checker, node);
}
static ArenaVector<ir::Statement *> CreatePrologue(public_lib::Context *ctx)
{
ES2PANDA_ASSERT(ctx);
const auto alloc = ctx->Allocator();
const auto dispatchCall = util::NodeAllocator::ForceSetParent<ir::ETSIntrinsicNode>(
alloc, "asyncdispatch", ArenaVector<ir::Expression *>());
ES2PANDA_ASSERT(dispatchCall);
const auto dispatchStmt = util::NodeAllocator::ForceSetParent<ir::ExpressionStatement>(alloc, dispatchCall);
ES2PANDA_ASSERT(dispatchStmt);
return {dispatchStmt};
}
static void AddPrologueToMethodBody(public_lib::Context *ctx, ir::BlockStatement *block)
{
ES2PANDA_ASSERT(ctx);
ES2PANDA_ASSERT(block);
const auto statements = block->Statements();
const auto prologue = CreatePrologue(ctx);
const auto prologueSize = prologue.size();
ArenaVector<ir::Statement *> newStatements = {};
newStatements.reserve(statements.size() + prologueSize);
newStatements.insert(newStatements.end(), prologue.begin(), prologue.end());
newStatements.insert(newStatements.end(), statements.begin(), statements.end());
block->SetStatements(std::move(newStatements));
}
static ir::Expression *CreateAsyncContextResolveValue(public_lib::Context *ctx, ir::ReturnStatement *stmt)
{
ES2PANDA_ASSERT(ctx);
ES2PANDA_ASSERT(stmt);
const auto alloc = ctx->Allocator();
const auto parser = ctx->parser->AsETSParser();
auto arg = stmt->Argument();
if (!arg) {
return alloc->New<ir::UndefinedLiteral>();
}
ES2PANDA_ASSERT(arg);
const auto argType = arg->TsType();
ES2PANDA_ASSERT(argType);
auto argClone = arg->Clone(alloc, nullptr)->AsExpression();
ES2PANDA_ASSERT(argClone);
if (argType->IsETSUndefinedType()) {
const auto argStmt = util::NodeAllocator::ForceSetParent<ir::ExpressionStatement>(alloc, argClone);
ES2PANDA_ASSERT(argStmt);
const auto undefinedLit = alloc->New<ir::UndefinedLiteral>();
ES2PANDA_ASSERT(undefinedLit);
const auto undefinedLitStmt = util::NodeAllocator::ForceSetParent<ir::ExpressionStatement>(alloc, undefinedLit);
ES2PANDA_ASSERT(undefinedLitStmt);
return util::NodeAllocator::ForceSetParent<ir::BlockExpression>(
alloc, ArenaVector<ir::Statement *>({argStmt, undefinedLitStmt}));
}
const auto resolveValIdent = Gensym(alloc);
ES2PANDA_ASSERT(resolveValIdent);
const auto resolveValDecl = parser->CreateFormattedStatement(
"let @@I1 : @@T2 = @@E3", resolveValIdent->Clone(alloc, nullptr), argType, argClone);
ES2PANDA_ASSERT(resolveValDecl);
const auto resolveValIdentStmt =
util::NodeAllocator::ForceSetParent<ir::ExpressionStatement>(alloc, resolveValIdent);
ES2PANDA_ASSERT(resolveValIdentStmt);
const auto resolveVal = util::NodeAllocator::ForceSetParent<ir::BlockExpression>(
alloc, ArenaVector<ir::Statement *>({resolveValDecl, resolveValIdentStmt}));
ES2PANDA_ASSERT(resolveVal);
return resolveVal;
}
static ir::ReturnStatement *CreateReturnFromAsync(public_lib::Context *ctx, ir::Expression *val,
checker::Type *explicitTypeAnno, bool isResolve)
{
ES2PANDA_ASSERT(ctx);
ES2PANDA_ASSERT(val);
ES2PANDA_ASSERT(explicitTypeAnno);
const auto parser = ctx->parser->AsETSParser();
const auto alloc = ctx->Allocator();
const auto completeSignature = isResolve ? "asyncresolve" : "asyncreject";
const auto completeCall = util::NodeAllocator::ForceSetParent<ir::ETSIntrinsicNode>(
alloc, completeSignature, ArenaVector<ir::Expression *>({val}));
ES2PANDA_ASSERT(completeCall);
const auto returnArgDecl = parser->CreateFormattedStatement("@@E1 as @@T2", completeCall, explicitTypeAnno);
ES2PANDA_ASSERT(returnArgDecl);
const auto returnArg =
util::NodeAllocator::ForceSetParent<ir::BlockExpression>(alloc, ArenaVector<ir::Statement *>({returnArgDecl}));
ES2PANDA_ASSERT(returnArg);
const auto returnStmt = util::NodeAllocator::ForceSetParent<ir::ReturnStatement>(alloc, returnArg);
ES2PANDA_ASSERT(returnStmt);
return returnStmt;
}
static ir::ReturnStatement *HandleReturnStatement(public_lib::Context *ctx, [[maybe_unused]] ir::ScriptFunction *func,
ir::ReturnStatement *stmt)
{
ES2PANDA_ASSERT(ctx);
ES2PANDA_ASSERT(stmt);
ES2PANDA_ASSERT(ctx);
ES2PANDA_ASSERT(stmt);
const auto val = CreateAsyncContextResolveValue(ctx, stmt);
ES2PANDA_ASSERT(val);
const auto promiseT = func->Signature()->ReturnType();
ES2PANDA_ASSERT(promiseT);
const auto returnStmt = CreateReturnFromAsync(ctx, val, promiseT, true);
ES2PANDA_ASSERT(returnStmt);
returnStmt->SetParent(stmt->Parent());
CheckNode(ctx, returnStmt);
return returnStmt;
}
static void AddMissingReturnStatement(public_lib::Context *ctx, ir::ScriptFunction *func)
{
ES2PANDA_ASSERT(func);
ES2PANDA_ASSERT(func->HasBody());
ES2PANDA_ASSERT(func->Body()->IsBlockStatement());
if (func->HasReturnStatement()) {
return;
}
auto checker = ctx->GetChecker()->AsETSChecker();
const auto alloc = ctx->Allocator();
const auto funcReturnType = func->Signature()->ReturnType();
const auto promiseUndefinedType = checker->CreatePromiseOf(checker->GlobalETSUndefinedType());
const auto relation = checker->Relation();
if (!relation->IsSupertypeOf(funcReturnType, promiseUndefinedType)) {
return;
}
const auto undefinedLit = alloc->New<ir::UndefinedLiteral>();
ES2PANDA_ASSERT(undefinedLit);
auto returnStmt = util::NodeAllocator::ForceSetParent<ir::ReturnStatement>(alloc, undefinedLit);
ES2PANDA_ASSERT(returnStmt);
auto body = func->Body()->AsBlockStatement();
ES2PANDA_ASSERT(body);
body->AddStatement(returnStmt);
func->AddFlag(ir::ScriptFunctionFlags::HAS_RETURN);
CheckNode(ctx, returnStmt);
}
static ArenaVector<ir::CatchClause *> CreateCatchClauses(public_lib::Context *ctx, ir::ScriptFunction *func)
{
ES2PANDA_ASSERT(ctx);
const auto alloc = ctx->Allocator();
const auto errIdent = Gensym(alloc);
ES2PANDA_ASSERT(errIdent);
const auto promiseT = func->Signature()->ReturnType();
ES2PANDA_ASSERT(promiseT);
ES2PANDA_ASSERT(ctx->GetChecker()->AsETSChecker()->IsPromiseType(promiseT));
const auto returnStmt = CreateReturnFromAsync(ctx, errIdent, promiseT, false);
ES2PANDA_ASSERT(returnStmt);
const auto catchBody = util::NodeAllocator::ForceSetParent<ir::BlockStatement>(
alloc, alloc, ArenaVector<ir::Statement *>({returnStmt}));
ES2PANDA_ASSERT(catchBody);
const auto catchClause =
util::NodeAllocator::ForceSetParent<ir::CatchClause>(alloc, errIdent->Clone(alloc, nullptr), catchBody);
ES2PANDA_ASSERT(catchClause);
return ArenaVector<ir::CatchClause *>({catchClause});
}
static void WrapBodyInTryCatchBlock(public_lib::Context *ctx, ir::ScriptFunction *func, ir::BlockStatement *body)
{
ES2PANDA_ASSERT(ctx);
ES2PANDA_ASSERT(func);
ES2PANDA_ASSERT(body);
ES2PANDA_ASSERT(func == body->Parent());
ES2PANDA_ASSERT(func->IsAsyncFunc());
const auto alloc = ctx->Allocator();
ClearTypesVariablesAndScopes(body);
const auto tryStatement = util::NodeAllocator::ForceSetParent<ir::TryStatement>(
alloc, body, CreateCatchClauses(ctx, func), nullptr,
ArenaVector<std::pair<compiler::LabelPair, const ir::Statement *>>({}));
ES2PANDA_ASSERT(tryStatement);
const auto newBody = util::NodeAllocator::ForceSetParent<ir::BlockStatement>(
alloc, alloc, ArenaVector<ir::Statement *>({tryStatement}));
ES2PANDA_ASSERT(newBody);
newBody->SetScope(func->Scope());
newBody->SetParent(func);
func->SetBody(newBody);
CheckNode(ctx, newBody);
}
static void TransformAsyncFunctionBody(public_lib::Context *ctx, ir::ScriptFunction *func, ir::BlockStatement *body)
{
ES2PANDA_ASSERT(ctx);
ES2PANDA_ASSERT(func);
ES2PANDA_ASSERT(body);
ES2PANDA_ASSERT(func == body->Parent());
ES2PANDA_ASSERT(func->IsAsyncFunc());
AddPrologueToMethodBody(ctx, body);
AddMissingReturnStatement(ctx, func);
const std::function<ir::AstNode *(ir::AstNode *)> transformer = [&](ir::AstNode *node) -> ir::AstNode * {
ES2PANDA_ASSERT(node);
* NOTE(knazarov): since we iterate nodes in postorder, deepest
* ScriptFunction will already be transformed. Thus, break early.
*/
if (node->IsScriptFunction()) {
return node;
}
* NOTE(knazarov): first, transform children, since `return` and
* `await` can be nested.
*/
node->TransformChildren(transformer, LOWERING_NAME);
if (node->IsReturnStatement()) {
const auto loweringResult = HandleReturnStatement(ctx, func, node->AsReturnStatement());
ES2PANDA_ASSERT(loweringResult);
return loweringResult;
}
return node;
};
body->TransformChildren(transformer, LOWERING_NAME);
}
static void IteratorCallback(public_lib::Context *ctx, ir::AstNode *node)
{
ES2PANDA_ASSERT(ctx);
ES2PANDA_ASSERT(node);
if (node->IsScriptFunction()) {
auto scriptFunction = node->AsScriptFunction();
if (!scriptFunction->IsAsyncFunc()) {
return;
}
ES2PANDA_ASSERT(scriptFunction->Signature());
ES2PANDA_ASSERT(scriptFunction->Signature()->ReturnType());
ES2PANDA_ASSERT(ctx->GetChecker()->AsETSChecker()->IsPromiseType(scriptFunction->Signature()->ReturnType()));
auto scriptFunctionBody = scriptFunction->Body();
if (scriptFunctionBody == nullptr) {
return;
}
ES2PANDA_ASSERT(scriptFunctionBody->IsBlockStatement());
auto body = scriptFunctionBody->AsBlockStatement();
TransformAsyncFunctionBody(ctx, scriptFunction, body);
WrapBodyInTryCatchBlock(ctx, scriptFunction, body);
RefineSourceRanges(body);
}
}
bool AsyncMethodLoweringStackless::PerformForProgram(parser::Program *program)
{
if (!Context()->config->options->IsStacklessCoros()) {
return true;
}
* NOTE(knazarov): iterate postorder, since we want to transform
* deepest ScriptFunctions first.
*/
program->Ast()->IterateRecursivelyPostorder([ctx = Context()](ir::AstNode *node) { IteratorCallback(ctx, node); });
return true;
}
bool AsyncMethodLoweringStackless::PreconditionForProgram(const parser::Program *program)
{
return !program->Ast()->IsAnyChild([](const ir::AstNode *node) -> bool {
* NOTE(knazarov): No asyncImpl functions should be generated by the lowerings
*/
const auto isAsyncImplFunc = node->IsScriptFunction() && node->AsScriptFunction()->IsAsyncImplFunc();
if (isAsyncImplFunc) {
return true;
}
return false;
});
}
}