* 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 "evaluate/helpers.h"
#include "evaluate/entityDeclarator-inl.h"
#include "evaluate/scopedDebugInfoPlugin-inl.h"
#include "evaluate/debugInfoDeserialization/debugInfoDeserializer.h"
#include "checker/ETSchecker.h"
#include "parser/program/program.h"
#include "compiler/lowering/phase.h"
#include "compiler/lowering/util.h"
#include "compiler/lowering/scopesInit/scopesInitPhase.h"
#include "ir/statements/blockStatement.h"
namespace ark::es2panda::evaluate {
namespace {
ir::VariableDeclaration *CreateVariableDeclaration(checker::ETSChecker *checker, ir::Identifier *ident,
ir::Expression *init)
{
auto *declarator = checker->AllocNode<ir::VariableDeclarator>(ir::VariableDeclaratorFlag::CONST, ident, init);
ES2PANDA_ASSERT(declarator != nullptr);
ArenaVector<ir::VariableDeclarator *> declarators(1, declarator, checker->Allocator()->Adapter());
auto *declaration = checker->AllocNode<ir::VariableDeclaration>(
ir::VariableDeclaration::VariableDeclarationKind::CONST, checker->Allocator(), std::move(declarators));
declarator->SetParent(declaration);
return declaration;
}
* @brief Break function's last statement into variable declaration and return statement.
* Hence we ensure that expression will return result, and local variables
* could be updated by inserting `DebuggerAPI.setLocal<>` calls between result and return.
* @param checker used for allocation purposes only.
* @param methodName used for returned variable name generation.
* @param lastStatement function's last statement to break.
* @returns pair of created AST nodes for variable declaration and return statement.
*/
std::pair<ir::VariableDeclaration *, ir::ReturnStatement *> BreakLastStatement(checker::ETSChecker *checker,
util::StringView methodName,
ir::ExpressionStatement *lastStatement)
{
static constexpr std::string_view GENERATED_VAR_SUFFIX = "_generated_var";
ES2PANDA_ASSERT(checker);
ES2PANDA_ASSERT(lastStatement);
auto *allocator = checker->Allocator();
auto returnVariableNameView = [methodName, allocator]() {
std::stringstream ss;
ss << methodName << GENERATED_VAR_SUFFIX;
util::UString variableName(ss.str(), allocator);
return variableName.View();
}();
auto *variableIdent = checker->AllocNode<ir::Identifier>(returnVariableNameView, allocator);
auto *exprInit = lastStatement->AsExpressionStatement()->GetExpression();
auto *variableDeclaration = CreateVariableDeclaration(checker, variableIdent, exprInit);
auto *returnStatement = checker->AllocNode<ir::ReturnStatement>(variableIdent->Clone(allocator, nullptr));
lastStatement->SetParent(nullptr);
return std::make_pair(variableDeclaration, returnStatement);
}
}
ScopedDebugInfoPlugin::ScopedDebugInfoPlugin(public_lib::Context *ctx)
: e2pctx_(ctx),
checker_(e2pctx_->GetChecker()->AsETSChecker()),
context_(*e2pctx_->config->options),
irCheckHelper_(checker_, e2pctx_->parserProgram->VarBinder()->AsETSBinder()),
debugInfoStorage_(*e2pctx_->config->options, checker_->Allocator()),
debugInfoDeserializer_(*this),
pathResolver_(debugInfoStorage_),
prologueEpilogueMap_(checker_->Allocator()->Adapter()),
proxyProgramsCache_(checker_->Allocator()),
entityDeclarator_(*this)
{
ES2PANDA_ASSERT(e2pctx_->parserProgram);
auto isContextValid = debugInfoStorage_.FillEvaluateContext(context_);
if (!isContextValid) {
LOG(FATAL, ES2PANDA) << "Can't create evaluate context" << std::endl;
}
CreateContextPrograms();
}
void ScopedDebugInfoPlugin::PreCheck()
{
irCheckHelper_.PreCheck();
context_.FindEvaluationMethod(GetEvaluatedExpressionProgram());
}
void ScopedDebugInfoPlugin::PostCheck()
{
ES2PANDA_ASSERT(prologueEpilogueMap_.empty());
[[maybe_unused]] auto inserted = InsertReturnStatement();
LOG(DEBUG, ES2PANDA) << "Evaluation method will return: " << std::boolalpha << inserted << std::noboolalpha;
}
bool ScopedDebugInfoPlugin::InsertReturnStatement()
{
auto *lastStatement = context_.lastStatement;
if (lastStatement == nullptr) {
return false;
}
auto *returnType = lastStatement->GetExpression()->TsType();
if (returnType == nullptr || !returnType->IsETSPrimitiveType()) {
return false;
}
auto *evalMethodStatements = context_.methodStatements;
auto &statementsList = evalMethodStatements->StatementsForUpdates();
auto lastStatementIter = std::find(statementsList.rbegin(), statementsList.rend(), lastStatement);
ES2PANDA_ASSERT(lastStatementIter != statementsList.rend());
auto *scope = compiler::NearestScope(lastStatement);
auto *scriptFunction = evalMethodStatements->Parent()->AsScriptFunction();
auto [variableDeclaration, returnStatement] =
BreakLastStatement(checker_, scriptFunction->Id()->Name(), lastStatement);
variableDeclaration->SetParent(evalMethodStatements);
*lastStatementIter = variableDeclaration;
returnStatement->SetParent(evalMethodStatements);
statementsList.emplace_back(returnStatement);
scriptFunction->AddFlag(ir::ScriptFunctionFlags::HAS_RETURN);
auto *signature = scriptFunction->Signature();
signature->AddSignatureFlag(checker::SignatureFlags::NEED_RETURN_TYPE);
signature->SetReturnType(returnType);
auto newNodeInitializer = [this, scope](ir::AstNode *node) {
auto *varBinder = GetETSBinder();
helpers::DoScopedAction(checker_, varBinder, GetEvaluatedExpressionProgram(), scope, nullptr,
[this, varBinder, scope, node]() {
compiler::InitScopesPhaseETS::RunExternalNode(node, varBinder);
varBinder->HandleCustomNodes(node);
varBinder->ResolveReferencesForScope(node, scope);
node->Check(checker_);
});
};
newNodeInitializer(variableDeclaration);
newNodeInitializer(returnStatement);
return true;
}
void ScopedDebugInfoPlugin::AddPrologueEpilogue(ir::BlockStatement *block)
{
auto iter = prologueEpilogueMap_.find(block);
if (iter == prologueEpilogueMap_.end()) {
return;
}
auto &statements = block->StatementsForUpdates();
for (auto *stmt : iter->second.first) {
statements.insert(statements.begin(), stmt);
}
for (auto *stmt : iter->second.second) {
statements.emplace_back(stmt);
}
prologueEpilogueMap_.erase(iter);
}
varbinder::Variable *ScopedDebugInfoPlugin::FindIdentifier(ir::Identifier *ident)
{
ES2PANDA_ASSERT(ident);
helpers::SafeStateScope s(checker_, GetETSBinder());
auto *var = FindLocalVariable(ident);
if (var != nullptr) {
return var;
}
var = FindGlobalVariable(ident);
if (var != nullptr) {
return var;
}
var = FindClass(ident);
if (var != nullptr) {
return var;
}
return FindGlobalFunction(ident);
}
varbinder::Variable *ScopedDebugInfoPlugin::FindClass(ir::Identifier *ident)
{
ES2PANDA_ASSERT(ident);
LOG(DEBUG, ES2PANDA) << "ScopedDebugInfoPlugin: FindClass " << ident->Name();
auto *importerProgram = GetETSBinder()->Program();
const auto &identName = ident->Name();
auto importPath = pathResolver_.FindNamedImportAll(context_.sourceFilePath.Utf8(), identName.Utf8());
if (!importPath.empty()) {
ES2PANDA_UNREACHABLE();
return nullptr;
}
auto classId = debugInfoStorage_.FindClass(context_.sourceFilePath.Utf8(), identName.Utf8());
if (classId.IsValid()) {
return entityDeclarator_.ImportGlobalEntity(
context_.sourceFilePath, identName, importerProgram, identName,
[classId](auto *deserializer, auto *program, auto declSourcePath, auto declName) {
return deserializer->CreateIrClass(classId, program, declSourcePath, declName);
});
}
auto optFoundEntity = pathResolver_.FindImportedEntity(context_.sourceFilePath.Utf8(), identName.Utf8());
if (!optFoundEntity) {
return nullptr;
}
const auto &[entitySourceFile, entitySourceName] = optFoundEntity.value();
classId = debugInfoStorage_.FindClass(entitySourceFile, entitySourceName);
if (!classId.IsValid()) {
return nullptr;
}
return entityDeclarator_.ImportGlobalEntity(
entitySourceFile, entitySourceName, importerProgram, identName,
[classId](auto *deserializer, auto *program, auto declSourcePath, auto declName) {
return deserializer->CreateIrClass(classId, program, declSourcePath, declName);
});
}
varbinder::Variable *ScopedDebugInfoPlugin::FindGlobalFunction(ir::Identifier *ident)
{
ES2PANDA_ASSERT(ident);
LOG(DEBUG, ES2PANDA) << "ScopedDebugInfoPlugin: FindGlobalFunction " << ident->Name();
auto *allocator = Allocator();
auto *importerProgram = GetETSBinder()->Program();
auto identName = ident->Name();
ArenaVector<std::pair<parser::Program *, ArenaVector<ir::AstNode *>>> createdMethods(allocator->Adapter());
createdMethods.emplace_back(GetProgram(context_.sourceFilePath), ArenaVector<ir::AstNode *>(allocator->Adapter()));
auto &fromContextFile = createdMethods.back().second;
auto *var = entityDeclarator_.ImportGlobalEntity(
context_.sourceFilePath, identName, importerProgram, identName,
[&fromContextFile](auto *deserializer, auto *program, auto declSourcePath, auto declName) {
return deserializer->CreateIrGlobalMethods(fromContextFile, program, declSourcePath, declName);
});
ArenaVector<EntityInfo> importedFunctions(allocator->Adapter());
pathResolver_.FindImportedFunctions(importedFunctions, context_.sourceFilePath.Utf8(), identName.Utf8());
for (const auto &[funcSourceFile, funcSourceName] : importedFunctions) {
createdMethods.emplace_back(GetProgram(funcSourceFile), ArenaVector<ir::AstNode *>(allocator->Adapter()));
auto &fromImported = createdMethods.back().second;
auto *importedVar = entityDeclarator_.ImportGlobalEntity(
funcSourceFile, funcSourceName, importerProgram, identName,
[&fromImported](auto *deserializer, auto *program, auto declSourcePath, auto declName) {
return deserializer->CreateIrGlobalMethods(fromImported, program, declSourcePath, declName);
});
if (importedVar != nullptr) {
ES2PANDA_ASSERT(var == nullptr || var == importedVar);
var = importedVar;
}
}
for (auto &[program, methods] : createdMethods) {
auto *globalClass = program->GlobalClass();
auto *globalClassScope = program->GlobalClassScope();
for (auto *method : methods) {
irCheckHelper_.CheckNewNode(method, globalClassScope, globalClass, program);
}
}
return var;
}
varbinder::Variable *ScopedDebugInfoPlugin::FindGlobalVariable(ir::Identifier *ident)
{
ES2PANDA_ASSERT(ident);
LOG(DEBUG, ES2PANDA) << "ScopedDebugInfoPlugin: FindGlobalVariable " << ident->Name();
auto *importerProgram = GetETSBinder()->Program();
auto identName = ident->Name();
auto *var = entityDeclarator_.ImportGlobalEntity(context_.sourceFilePath, identName, importerProgram, identName,
&DebugInfoDeserializer::CreateIrGlobalVariable);
if (var != nullptr) {
return var;
}
auto optFoundEntity = pathResolver_.FindImportedEntity(context_.sourceFilePath.Utf8(), identName.Utf8());
if (!optFoundEntity) {
return nullptr;
}
const auto &[entitySourceFile, entitySourceName] = optFoundEntity.value();
return entityDeclarator_.ImportGlobalEntity(entitySourceFile, entitySourceName, importerProgram, identName,
&DebugInfoDeserializer::CreateIrGlobalVariable);
}
varbinder::Variable *ScopedDebugInfoPlugin::FindLocalVariable(ir::Identifier *ident)
{
ES2PANDA_ASSERT(ident);
if (helpers::GetEnclosingBlock(ident) != context_.methodStatements) {
return nullptr;
}
LOG(DEBUG, ES2PANDA) << "ScopedDebugInfoPlugin: FindLocalVariable " << ident->Name();
const auto &localVariableTable = context_.extractor->GetLocalVariableTable(context_.methodId);
return debugInfoDeserializer_.CreateIrLocalVariable(ident, localVariableTable, context_.bytecodeOffset);
}
void ScopedDebugInfoPlugin::CreateContextPrograms()
{
debugInfoStorage_.EnumerateContextFiles([this](auto sourceFilePath, auto, auto, auto moduleName) {
CreateEmptyProgram(sourceFilePath, moduleName);
return true;
});
}
parser::Program *ScopedDebugInfoPlugin::CreateEmptyProgram(std::string_view sourceFilePath, std::string_view moduleName)
{
parser::Program *program =
e2pctx_->parser->GetImportPathManager()->SetupProgramForDebugInfoPlugin(sourceFilePath, moduleName);
proxyProgramsCache_.AddProgram(program);
return program;
}
parser::Program *ScopedDebugInfoPlugin::GetProgram(util::StringView fileName)
{
auto *program = proxyProgramsCache_.GetProgram(fileName);
ES2PANDA_ASSERT(program);
return program;
}
parser::Program *ScopedDebugInfoPlugin::GetEvaluatedExpressionProgram()
{
auto *program = GetETSBinder()->GetContext()->parserProgram;
ES2PANDA_ASSERT(program);
return program;
}
varbinder::ETSBinder *ScopedDebugInfoPlugin::GetETSBinder()
{
return e2pctx_->parserProgram->VarBinder()->AsETSBinder();
}
ArenaAllocator *ScopedDebugInfoPlugin::Allocator()
{
return checker_->Allocator();
}
}