1 /*
2 * Copyright (c) 2025 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include "evaluate/helpers.h"
17 #include "evaluate/entityDeclarator-inl.h"
18 #include "evaluate/scopedDebugInfoPlugin-inl.h"
19 #include "evaluate/debugInfoDeserialization/debugInfoDeserializer.h"
20
21 #include "checker/ETSchecker.h"
22 #include "parser/program/program.h"
23 #include "compiler/lowering/phase.h"
24 #include "compiler/lowering/util.h"
25 #include "compiler/lowering/scopesInit/scopesInitPhase.h"
26 #include "ir/statements/blockStatement.h"
27
28 namespace ark::es2panda::evaluate {
29
30 namespace {
31
CreateVariableDeclaration(checker::ETSChecker * checker,ir::Identifier * ident,ir::Expression * init)32 ir::VariableDeclaration *CreateVariableDeclaration(checker::ETSChecker *checker, ir::Identifier *ident,
33 ir::Expression *init)
34 {
35 auto *declarator = checker->AllocNode<ir::VariableDeclarator>(ir::VariableDeclaratorFlag::CONST, ident, init);
36 ES2PANDA_ASSERT(declarator != nullptr);
37
38 ArenaVector<ir::VariableDeclarator *> declarators(1, declarator, checker->Allocator()->Adapter());
39 auto *declaration = checker->AllocNode<ir::VariableDeclaration>(
40 ir::VariableDeclaration::VariableDeclarationKind::CONST, checker->Allocator(), std::move(declarators));
41
42 declarator->SetParent(declaration);
43 return declaration;
44 }
45
46 /**
47 * @brief Break function's last statement into variable declaration and return statement.
48 * Hence we ensure that expression will return result, and local variables
49 * could be updated by inserting `DebuggerAPI.setLocal<>` calls between result and return.
50 * @param checker used for allocation purposes only.
51 * @param methodName used for returned variable name generation.
52 * @param lastStatement function's last statement to break.
53 * @returns pair of created AST nodes for variable declaration and return statement.
54 */
BreakLastStatement(checker::ETSChecker * checker,util::StringView methodName,ir::ExpressionStatement * lastStatement)55 std::pair<ir::VariableDeclaration *, ir::ReturnStatement *> BreakLastStatement(checker::ETSChecker *checker,
56 util::StringView methodName,
57 ir::ExpressionStatement *lastStatement)
58 {
59 static constexpr std::string_view GENERATED_VAR_SUFFIX = "_generated_var";
60
61 ES2PANDA_ASSERT(checker);
62 ES2PANDA_ASSERT(lastStatement);
63 auto *allocator = checker->Allocator();
64
65 auto returnVariableNameView = [methodName, allocator]() {
66 std::stringstream ss;
67 ss << methodName << GENERATED_VAR_SUFFIX;
68 util::UString variableName(ss.str(), allocator);
69 return variableName.View();
70 }();
71 auto *variableIdent = checker->AllocNode<ir::Identifier>(returnVariableNameView, allocator);
72 auto *exprInit = lastStatement->AsExpressionStatement()->GetExpression();
73 auto *variableDeclaration = CreateVariableDeclaration(checker, variableIdent, exprInit);
74
75 auto *returnStatement = checker->AllocNode<ir::ReturnStatement>(variableIdent->Clone(allocator, nullptr));
76
77 // Unattach previous statement.
78 lastStatement->SetParent(nullptr);
79
80 return std::make_pair(variableDeclaration, returnStatement);
81 }
82
83 } // namespace
84
ScopedDebugInfoPlugin(parser::Program * globalProgram,checker::ETSChecker * checker,const util::Options & options)85 ScopedDebugInfoPlugin::ScopedDebugInfoPlugin(parser::Program *globalProgram, checker::ETSChecker *checker,
86 const util::Options &options)
87 : globalProgram_(globalProgram),
88 checker_(checker),
89 context_(options),
90 irCheckHelper_(checker, globalProgram->VarBinder()->AsETSBinder()),
91 debugInfoStorage_(options, checker->Allocator()),
92 debugInfoDeserializer_(*this),
93 pathResolver_(debugInfoStorage_),
94 prologueEpilogueMap_(checker->Allocator()->Adapter()),
95 proxyProgramsCache_(checker->Allocator()),
96 entityDeclarator_(*this)
97 {
98 ES2PANDA_ASSERT(globalProgram_);
99 ES2PANDA_ASSERT(checker_);
100
101 auto isContextValid = debugInfoStorage_.FillEvaluateContext(context_);
102 if (!isContextValid) {
103 LOG(FATAL, ES2PANDA) << "Can't create evaluate context" << std::endl;
104 }
105
106 CreateContextPrograms();
107 }
108
PreCheck()109 void ScopedDebugInfoPlugin::PreCheck()
110 {
111 irCheckHelper_.PreCheck();
112
113 // Find evaluation method after parse and before any checks.
114 context_.FindEvaluationMethod(GetEvaluatedExpressionProgram());
115 }
116
PostCheck()117 void ScopedDebugInfoPlugin::PostCheck()
118 {
119 ES2PANDA_ASSERT(prologueEpilogueMap_.empty());
120
121 [[maybe_unused]] auto inserted = InsertReturnStatement();
122 LOG(DEBUG, ES2PANDA) << "Evaluation method will return: " << std::boolalpha << inserted << std::noboolalpha;
123 }
124
InsertReturnStatement()125 bool ScopedDebugInfoPlugin::InsertReturnStatement()
126 {
127 auto *lastStatement = context_.lastStatement;
128 if (lastStatement == nullptr) {
129 // Last evaluation statement cannot return a value.
130 return false;
131 }
132 auto *returnType = lastStatement->GetExpression()->TsType();
133 if (returnType == nullptr || !returnType->IsETSPrimitiveType()) {
134 // NOTE(dslynko): currently expression evaluation supports only primitives.
135 // In future this condition might be replaced with `returnType is not void`.
136 return false;
137 }
138
139 // As an example, the code below will transform
140 // ```
141 // localVar += 1 // This expression returns new value of `localVar`.
142 // DebuggerAPI.setLocalInt(0, localVar) // Already generated by plugin.
143 // ```
144 // into
145 // ```
146 // const eval_file_generated_var = (localVar += 1)
147 // DebuggerAPI.setLocalInt(0, localVar)
148 // return eval_file_generated_var
149 // ```
150 // which will also modify method signature's return type.
151
152 auto *evalMethodStatements = context_.methodStatements;
153 auto &statementsList = evalMethodStatements->Statements();
154 // Omit the emplaced `DebuggerAPI.setLocal<>` calls and find the original last statement.
155 auto lastStatementIter = std::find(statementsList.rbegin(), statementsList.rend(), lastStatement);
156 ES2PANDA_ASSERT(lastStatementIter != statementsList.rend());
157
158 // Break the last user's statement into variable declaration and return statement.
159 auto *scope = compiler::NearestScope(lastStatement);
160 auto *scriptFunction = evalMethodStatements->Parent()->AsScriptFunction();
161 auto [variableDeclaration, returnStatement] =
162 BreakLastStatement(checker_, scriptFunction->Id()->Name(), lastStatement);
163
164 // Attach new nodes to statements block.
165 variableDeclaration->SetParent(evalMethodStatements);
166 *lastStatementIter = variableDeclaration;
167 returnStatement->SetParent(evalMethodStatements);
168 statementsList.emplace_back(returnStatement);
169
170 scriptFunction->AddFlag(ir::ScriptFunctionFlags::HAS_RETURN);
171 auto *signature = scriptFunction->Signature();
172 signature->AddSignatureFlag(checker::SignatureFlags::NEED_RETURN_TYPE);
173 signature->SetReturnType(returnType);
174
175 auto newNodeInitializer = [this, scope](ir::AstNode *node) {
176 auto *varBinder = GetETSBinder();
177 helpers::DoScopedAction(checker_, varBinder, GetEvaluatedExpressionProgram(), scope, nullptr,
178 [this, varBinder, scope, node]() {
179 compiler::InitScopesPhaseETS::RunExternalNode(node, varBinder);
180 varBinder->HandleCustomNodes(node);
181 varBinder->ResolveReferencesForScope(node, scope);
182 node->Check(checker_);
183 });
184 };
185 newNodeInitializer(variableDeclaration);
186 newNodeInitializer(returnStatement);
187
188 return true;
189 }
190
AddPrologueEpilogue(ir::BlockStatement * block)191 void ScopedDebugInfoPlugin::AddPrologueEpilogue(ir::BlockStatement *block)
192 {
193 auto iter = prologueEpilogueMap_.find(block);
194 if (iter == prologueEpilogueMap_.end()) {
195 return;
196 }
197
198 // Prepend prologue.
199 auto &statements = block->Statements();
200 for (auto *stmt : iter->second.first) {
201 statements.insert(statements.begin(), stmt);
202 }
203
204 // Append epilogue.
205 for (auto *stmt : iter->second.second) {
206 statements.emplace_back(stmt);
207 }
208
209 prologueEpilogueMap_.erase(iter);
210 }
211
FindIdentifier(ir::Identifier * ident)212 varbinder::Variable *ScopedDebugInfoPlugin::FindIdentifier(ir::Identifier *ident)
213 {
214 ES2PANDA_ASSERT(ident);
215
216 helpers::SafeStateScope s(checker_, GetETSBinder());
217
218 auto *var = FindLocalVariable(ident);
219 if (var != nullptr) {
220 return var;
221 }
222 var = FindGlobalVariable(ident);
223 if (var != nullptr) {
224 return var;
225 }
226 var = FindClass(ident);
227 if (var != nullptr) {
228 return var;
229 }
230 return FindGlobalFunction(ident);
231 }
232
FindClass(ir::Identifier * ident)233 varbinder::Variable *ScopedDebugInfoPlugin::FindClass(ir::Identifier *ident)
234 {
235 // The following algorithm is used:
236 // - Search for `import * as B from "C"` statements.
237 // - If found, [Not implemented yet]
238 // - Else, proceed.
239 // - Search classes which defined in the context file:
240 // - If found, recreate its structure and return.
241 // - Else, proceed.
242 // - Search through the imported entities extracted from imports/exports table:
243 // - If the class was found, create parser::Program corresponding for the import source,
244 // where the class could be recreated.
245 // - Else, return nullptr.
246
247 // NOTE: support "import * as X".
248
249 ES2PANDA_ASSERT(ident);
250 LOG(DEBUG, ES2PANDA) << "ScopedDebugInfoPlugin: FindClass " << ident->Name();
251
252 auto *importerProgram = GetETSBinder()->Program();
253 const auto &identName = ident->Name();
254
255 // Search "import * as B" statements.
256 // NOTE: separate this into a method.
257 auto importPath = pathResolver_.FindNamedImportAll(context_.sourceFilePath.Utf8(), identName.Utf8());
258 if (!importPath.empty()) {
259 ES2PANDA_UNREACHABLE();
260 return nullptr;
261 }
262
263 // Search in the context file.
264 auto classId = debugInfoStorage_.FindClass(context_.sourceFilePath.Utf8(), identName.Utf8());
265 if (classId.IsValid()) {
266 return entityDeclarator_.ImportGlobalEntity(
267 context_.sourceFilePath, identName, importerProgram, identName,
268 [classId](auto *deserializer, auto *program, auto declSourcePath, auto declName) {
269 return deserializer->CreateIrClass(classId, program, declSourcePath, declName);
270 });
271 }
272
273 // Search in imported entities.
274 auto optFoundEntity = pathResolver_.FindImportedEntity(context_.sourceFilePath.Utf8(), identName.Utf8());
275 if (!optFoundEntity) {
276 return nullptr;
277 }
278
279 const auto &[entitySourceFile, entitySourceName] = optFoundEntity.value();
280
281 classId = debugInfoStorage_.FindClass(entitySourceFile, entitySourceName);
282 if (!classId.IsValid()) {
283 // The entity is not a class.
284 return nullptr;
285 }
286
287 // Must pass the name of class as declared in the found file.
288 return entityDeclarator_.ImportGlobalEntity(
289 entitySourceFile, entitySourceName, importerProgram, identName,
290 [classId](auto *deserializer, auto *program, auto declSourcePath, auto declName) {
291 return deserializer->CreateIrClass(classId, program, declSourcePath, declName);
292 });
293 }
294
FindGlobalFunction(ir::Identifier * ident)295 varbinder::Variable *ScopedDebugInfoPlugin::FindGlobalFunction(ir::Identifier *ident)
296 {
297 // Correct overload resolution requires us to create all reachable functions with the given name,
298 // so that Checker later could choose the correct one.
299 ES2PANDA_ASSERT(ident);
300 LOG(DEBUG, ES2PANDA) << "ScopedDebugInfoPlugin: FindGlobalFunction " << ident->Name();
301
302 auto *allocator = Allocator();
303
304 auto *importerProgram = GetETSBinder()->Program();
305 auto identName = ident->Name();
306
307 ArenaVector<std::pair<parser::Program *, ArenaVector<ir::AstNode *>>> createdMethods(allocator->Adapter());
308
309 // Build every global function from the context file.
310 createdMethods.emplace_back(GetProgram(context_.sourceFilePath), ArenaVector<ir::AstNode *>(allocator->Adapter()));
311 auto &fromContextFile = createdMethods.back().second;
312
313 auto *var = entityDeclarator_.ImportGlobalEntity(
314 context_.sourceFilePath, identName, importerProgram, identName,
315 [&fromContextFile](auto *deserializer, auto *program, auto declSourcePath, auto declName) {
316 return deserializer->CreateIrGlobalMethods(fromContextFile, program, declSourcePath, declName);
317 });
318
319 // Then search in imports.
320 ArenaVector<EntityInfo> importedFunctions(allocator->Adapter());
321 pathResolver_.FindImportedFunctions(importedFunctions, context_.sourceFilePath.Utf8(), identName.Utf8());
322
323 // Build all the found functions.
324 for (const auto &[funcSourceFile, funcSourceName] : importedFunctions) {
325 createdMethods.emplace_back(GetProgram(funcSourceFile), ArenaVector<ir::AstNode *>(allocator->Adapter()));
326 auto &fromImported = createdMethods.back().second;
327
328 auto *importedVar = entityDeclarator_.ImportGlobalEntity(
329 funcSourceFile, funcSourceName, importerProgram, identName,
330 [&fromImported](auto *deserializer, auto *program, auto declSourcePath, auto declName) {
331 return deserializer->CreateIrGlobalMethods(fromImported, program, declSourcePath, declName);
332 });
333 if (importedVar != nullptr) {
334 ES2PANDA_ASSERT(var == nullptr || var == importedVar);
335 var = importedVar;
336 }
337 }
338
339 // Run Checker only after all functions are created, so that overloading could work correctly.
340 for (auto &[program, methods] : createdMethods) {
341 auto *globalClass = program->GlobalClass();
342 auto *globalClassScope = program->GlobalClassScope();
343 for (auto *method : methods) {
344 irCheckHelper_.CheckNewNode(method, globalClassScope, globalClass, program);
345 }
346 }
347
348 return var;
349 }
350
FindGlobalVariable(ir::Identifier * ident)351 varbinder::Variable *ScopedDebugInfoPlugin::FindGlobalVariable(ir::Identifier *ident)
352 {
353 ES2PANDA_ASSERT(ident);
354 LOG(DEBUG, ES2PANDA) << "ScopedDebugInfoPlugin: FindGlobalVariable " << ident->Name();
355
356 auto *importerProgram = GetETSBinder()->Program();
357 auto identName = ident->Name();
358
359 // Search in the context file.
360 auto *var = entityDeclarator_.ImportGlobalEntity(context_.sourceFilePath, identName, importerProgram, identName,
361 &DebugInfoDeserializer::CreateIrGlobalVariable);
362 if (var != nullptr) {
363 return var;
364 }
365
366 // Search within the imports.
367 auto optFoundEntity = pathResolver_.FindImportedEntity(context_.sourceFilePath.Utf8(), identName.Utf8());
368 if (!optFoundEntity) {
369 return nullptr;
370 }
371
372 const auto &[entitySourceFile, entitySourceName] = optFoundEntity.value();
373
374 // Search once again, but in the exported source. Must pass the name of entity as declared in the found file.
375 return entityDeclarator_.ImportGlobalEntity(entitySourceFile, entitySourceName, importerProgram, identName,
376 &DebugInfoDeserializer::CreateIrGlobalVariable);
377 }
378
FindLocalVariable(ir::Identifier * ident)379 varbinder::Variable *ScopedDebugInfoPlugin::FindLocalVariable(ir::Identifier *ident)
380 {
381 ES2PANDA_ASSERT(ident);
382 // Search local variables only in evaluation method.
383 if (helpers::GetEnclosingBlock(ident) != context_.methodStatements) {
384 return nullptr;
385 }
386
387 LOG(DEBUG, ES2PANDA) << "ScopedDebugInfoPlugin: FindLocalVariable " << ident->Name();
388
389 // NOTE: verify that function arguments are included.
390 const auto &localVariableTable = context_.extractor->GetLocalVariableTable(context_.methodId);
391 return debugInfoDeserializer_.CreateIrLocalVariable(ident, localVariableTable, context_.bytecodeOffset);
392 }
393
CreateContextPrograms()394 void ScopedDebugInfoPlugin::CreateContextPrograms()
395 {
396 debugInfoStorage_.EnumerateContextFiles([this](auto sourceFilePath, auto, auto, auto moduleName) {
397 CreateEmptyProgram(sourceFilePath, moduleName);
398 return true;
399 });
400 }
401
CreateEmptyProgram(std::string_view sourceFilePath,std::string_view moduleName)402 parser::Program *ScopedDebugInfoPlugin::CreateEmptyProgram(std::string_view sourceFilePath, std::string_view moduleName)
403 {
404 auto *allocator = Allocator();
405
406 // Checker doesn't yet have `VarBinder`, must retrieve it from `globalProgram_`.
407 parser::Program *program = allocator->New<parser::Program>(allocator, GetETSBinder());
408 ES2PANDA_ASSERT(program != nullptr);
409 program->SetSource({sourceFilePath, "", globalProgram_->SourceFileFolder().Utf8(), true, false});
410 program->SetPackageInfo(moduleName, util::ModuleKind::MODULE);
411 auto *emptyIdent = allocator->New<ir::Identifier>("", allocator);
412 auto *etsModule = allocator->New<ir::ETSModule>(allocator, ArenaVector<ir::Statement *>(allocator->Adapter()),
413 emptyIdent, ir::ModuleFlag::ETSSCRIPT, program);
414 program->SetAst(etsModule);
415
416 helpers::AddExternalProgram(globalProgram_, program, moduleName);
417 proxyProgramsCache_.AddProgram(program);
418
419 return program;
420 }
421
GetProgram(util::StringView fileName)422 parser::Program *ScopedDebugInfoPlugin::GetProgram(util::StringView fileName)
423 {
424 auto *program = proxyProgramsCache_.GetProgram(fileName);
425 ES2PANDA_ASSERT(program);
426 return program;
427 }
428
GetEvaluatedExpressionProgram()429 parser::Program *ScopedDebugInfoPlugin::GetEvaluatedExpressionProgram()
430 {
431 auto *program = GetETSBinder()->GetContext()->parserProgram;
432 ES2PANDA_ASSERT(program);
433 return program;
434 }
435
GetETSBinder()436 varbinder::ETSBinder *ScopedDebugInfoPlugin::GetETSBinder()
437 {
438 return globalProgram_->VarBinder()->AsETSBinder();
439 }
440
Allocator()441 ArenaAllocator *ScopedDebugInfoPlugin::Allocator()
442 {
443 return checker_->Allocator();
444 }
445
446 } // namespace ark::es2panda::evaluate
447