* Copyright (c) 2021-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 <algorithm>
#include "unionLowering.h"
#include "compiler/lowering/scopesInit/scopesInitPhase.h"
#include "compiler/lowering/util.h"
#include "varbinder/ETSBinder.h"
#include "checker/ETSchecker.h"
#include "util/nameMangler.h"
namespace ark::es2panda::compiler {
static void ReplaceAll(std::string &str, std::string_view substr, std::string_view replacement)
{
for (size_t pos = str.find(substr, 0); pos != std::string::npos; pos = str.find(substr, pos)) {
str.replace(pos, substr.size(), replacement);
pos += replacement.size();
}
}
static std::string GetAccessClassName(const checker::ETSUnionType *unionType)
{
std::stringstream ss;
unionType->ToString(ss, false);
std::string newName = util::NameMangler::GetInstance()->CreateMangledNameForUnionProperty(ss.str());
ReplaceAll(newName, "[]", "[$]$");
return newName;
}
static ir::ClassDefinition *GetUnionAccessClass(public_lib::Context *ctx, varbinder::VarBinder *varbinder,
std::string const &name)
{
auto *checker = ctx->GetChecker()->AsETSChecker();
auto *allocator = ctx->Allocator();
if (auto foundVar = checker->Scope()->FindLocal(util::StringView(name), varbinder::ResolveBindingOptions::BINDINGS);
foundVar != nullptr) {
return foundVar->Declaration()->Node()->AsClassDefinition();
}
util::UString unionFieldClassName(util::StringView(name), allocator);
auto *ident = ctx->AllocNode<ir::Identifier>(unionFieldClassName.View(), allocator);
auto [decl, var] = varbinder->NewVarDecl<varbinder::ClassDecl>(ident->Start(), ident->Name());
ES2PANDA_ASSERT(ident != nullptr);
ident->SetVariable(var);
auto classCtx = varbinder::LexicalScope<varbinder::ClassScope>(varbinder);
auto *classDef =
ctx->AllocNode<ir::ClassDefinition>(ctx->Allocator(), ident, ir::ClassDefinitionModifiers::CLASS_DECL,
ir::ModifierFlags::ABSTRACT, Language(Language::Id::ETS));
ES2PANDA_ASSERT(classDef != nullptr);
classDef->SetScope(classCtx.GetScope());
auto *classDecl = ctx->AllocNode<ir::ClassDeclaration>(classDef, allocator);
ES2PANDA_ASSERT(classDecl != nullptr);
classDef->Scope()->BindNode(classDecl->Definition());
decl->BindNode(classDef);
var->SetScope(classDef->Scope());
varbinder->AsETSBinder()->BuildClassDefinition(classDef);
auto globalBlock = varbinder->Program()->Ast();
classDecl->SetParent(globalBlock);
globalBlock->AddStatement(classDecl);
classDecl->Check(checker);
return classDef;
}
static ir::ETSParameterExpression *CloneParamAndProcessType(public_lib::Context *ctx, checker::Signature *signature,
ir::ETSParameterExpression *declParam, size_t index)
{
auto *allocator = ctx->Allocator();
auto *newParam = declParam->Clone(allocator, nullptr)->AsETSParameterExpression();
newParam->SetInitializer(nullptr);
checker::Type *resolvedType = nullptr;
auto *declSignature = (signature->Function() != nullptr) ? signature->Function()->Signature() : nullptr;
if (declParam->IsRestParameter()) {
auto *restVar = (declSignature != nullptr) ? declSignature->RestVar() : signature->RestVar();
if (restVar != nullptr) {
resolvedType = restVar->TsType();
}
} else {
auto const &sigParams = (declSignature != nullptr) ? declSignature->Params() : signature->Params();
if (index < sigParams.size() && sigParams[index] != nullptr) {
resolvedType = sigParams[index]->TsType();
}
}
if (resolvedType != nullptr) {
auto *typeAnno = ctx->AllocNode<ir::OpaqueTypeNode>(resolvedType, allocator);
newParam->SetTypeAnnotation(typeAnno);
if (newParam->IsRestParameter()) {
typeAnno->SetParent(newParam->RestParameter());
} else if (newParam->Ident() != nullptr) {
typeAnno->SetParent(newParam->Ident());
} else {
typeAnno->SetParent(newParam);
}
}
return newParam;
}
static std::tuple<varbinder::LocalVariable *, checker::Signature *> CreateNamedAccessMethod(
public_lib::Context *ctx, varbinder::VarBinder *varbinder, ir::MemberExpression *expr,
checker::Signature *signature)
{
auto *allocator = ctx->Allocator();
auto *checker = ctx->GetChecker()->AsETSChecker();
auto apparentType = checker->GetApparentType(checker->GetNonNullishType(expr->Object()->TsType()));
ES2PANDA_ASSERT(apparentType != nullptr);
auto unionType = apparentType->AsETSUnionType();
auto *const accessClass = GetUnionAccessClass(ctx, varbinder, GetAccessClassName(unionType));
auto methodName = expr->TsType()->AsETSFunctionType()->Name();
auto *methodIdent = ctx->AllocNode<ir::Identifier>(methodName, allocator);
ArenaVector<ir::Expression *> params {allocator->Adapter()};
auto *sigFunc = signature->Function();
ES2PANDA_ASSERT(sigFunc != nullptr);
auto const &declParams = sigFunc->Params();
for (size_t i = 0; i < declParams.size(); ++i) {
auto *declParam = declParams[i]->AsETSParameterExpression();
auto *newParam = CloneParamAndProcessType(ctx, signature, declParam, i);
params.emplace_back(newParam);
}
auto *declSignature = (sigFunc != nullptr) ? sigFunc->Signature() : nullptr;
auto *returnType = (declSignature != nullptr && declSignature->ReturnType() != nullptr)
? declSignature->ReturnType()
: signature->ReturnType();
auto *returnTypeAnno = ctx->AllocNode<ir::OpaqueTypeNode>(returnType, allocator);
auto *func = ctx->AllocNode<ir::ScriptFunction>(
allocator, ir::ScriptFunction::ScriptFunctionData {
nullptr, ir::FunctionSignature(nullptr, std::move(params), returnTypeAnno),
ir::ScriptFunctionFlags::METHOD, ir::ModifierFlags::PUBLIC});
ES2PANDA_ASSERT(func != nullptr && methodIdent != nullptr);
func->SetIdent(methodIdent->Clone(allocator, nullptr));
auto *funcExpr = ctx->AllocNode<ir::FunctionExpression>(func);
auto *method =
ctx->AllocNode<ir::MethodDefinition>(ir::MethodDefinitionKind::METHOD, methodIdent, funcExpr,
ir::ModifierFlags::PUBLIC | ir::ModifierFlags::ABSTRACT, allocator, false);
ArenaVector<ir::AstNode *> methodDecl {allocator->Adapter()};
methodDecl.push_back(method);
accessClass->AddProperties(std::move(methodDecl));
{
auto clsCtx =
varbinder::LexicalScope<varbinder::ClassScope>::Enter(varbinder, accessClass->Scope()->AsClassScope());
auto boundCtx = varbinder::BoundContext(varbinder->AsETSBinder()->GetRecordTable(), accessClass, true);
CheckLoweredNode(varbinder->AsETSBinder(), checker, method);
}
ES2PANDA_ASSERT(method->Id() != nullptr && method->TsType() != nullptr);
return {method->Id()->Variable()->AsLocalVariable(),
method->TsType()->AsETSFunctionType()->CallSignatures().front()};
}
static varbinder::LocalVariable *CreateNamedAccessProperty(public_lib::Context *ctx, varbinder::VarBinder *varbinder,
ir::MemberExpression *expr)
{
auto *const allocator = ctx->Allocator();
auto *checker = ctx->GetChecker()->AsETSChecker();
auto apparentType = checker->GetApparentType(checker->GetNonNullishType(expr->Object()->TsType()));
ES2PANDA_ASSERT(apparentType != nullptr);
auto unionType = apparentType->AsETSUnionType();
auto *const accessClass = GetUnionAccessClass(ctx, varbinder, GetAccessClassName(unionType));
auto propName = expr->Property()->AsIdentifier()->Name();
auto fieldType = expr->TsType();
auto uncheckedType = expr->UncheckedType();
auto *typeToSet = uncheckedType == nullptr ? fieldType : uncheckedType;
auto *fieldIdent = ctx->AllocNode<ir::Identifier>(propName, allocator);
auto *field =
ctx->AllocNode<ir::ClassProperty>(fieldIdent, nullptr, nullptr, ir::ModifierFlags::PUBLIC, allocator, false);
ES2PANDA_ASSERT(field != nullptr);
auto [decl, var] = varbinder->NewVarDecl<varbinder::LetDecl>(fieldIdent->Start(), fieldIdent->Name());
var->AddFlag(varbinder::VariableFlags::PROPERTY);
var->SetTsType(typeToSet);
fieldIdent->SetVariable(var);
field->SetTsType(typeToSet);
decl->BindNode(field);
ArenaVector<ir::AstNode *> fieldDecl {allocator->Adapter()};
fieldDecl.push_back(field);
accessClass->AddProperties(std::move(fieldDecl));
return var->AsLocalVariable();
}
static varbinder::LocalVariable *CreateNamedAccess(public_lib::Context *ctx, varbinder::VarBinder *varbinder,
ir::MemberExpression *expr)
{
auto type = expr->TsType();
auto name = expr->Property()->AsIdentifier()->Name();
auto *checker = ctx->GetChecker()->AsETSChecker();
auto *apparentType = checker->GetApparentType(checker->GetNonNullishType(expr->Object()->TsType()));
ES2PANDA_ASSERT(apparentType != nullptr);
auto unionType = apparentType->AsETSUnionType();
auto *const accessClass = GetUnionAccessClass(ctx, varbinder, GetAccessClassName(unionType));
auto *classScope = accessClass->Scope()->AsClassScope();
if (auto *var = classScope->FindLocal(name, varbinder::ResolveBindingOptions::ALL_NON_STATIC); var != nullptr) {
return var->AsLocalVariable();
}
if (type->IsETSMethodType() && !type->IsETSArrowType()) {
auto parent = expr->Parent()->AsCallExpression();
ES2PANDA_ASSERT(parent->Callee() == expr && parent->Signature()->HasFunction());
auto [var, sig] = CreateNamedAccessMethod(ctx, varbinder, expr, parent->Signature());
parent->AsCallExpression()->SetSignature(sig);
return var;
}
auto fieldCtx = varbinder::LexicalScope<varbinder::LocalScope>::Enter(varbinder, classScope->InstanceFieldScope());
return CreateNamedAccessProperty(ctx, varbinder, expr);
}
static void HandleUnionPropertyAccess(public_lib::Context *ctx, varbinder::VarBinder *vbind, ir::MemberExpression *expr)
{
if (expr->PropVar() != nullptr) {
return;
}
expr->SetPropVar(CreateNamedAccess(ctx, vbind, expr));
ES2PANDA_ASSERT(expr->PropVar() != nullptr);
}
bool UnionLowering::PerformForProgram(parser::Program *program)
{
program->Ast()->TransformChildrenRecursively(
[ctx = Context()](checker::AstNodePtr ast) -> checker::AstNodePtr {
if (ast->IsMemberExpression() && ast->AsMemberExpression()->Object()->TsType() != nullptr) {
auto *checker = ctx->GetChecker()->AsETSChecker();
auto *objType =
checker->GetApparentType(checker->GetNonNullishType(ast->AsMemberExpression()->Object()->TsType()));
if (objType->IsETSUnionType()) {
HandleUnionPropertyAccess(ctx, checker->VarBinder(), ast->AsMemberExpression());
return ast;
}
}
return ast;
},
Name());
return true;
}
bool UnionLowering::PostconditionForProgram(const parser::Program *program)
{
auto *checker = Context()->GetChecker()->AsETSChecker();
bool current = !program->Ast()->IsAnyChild([checker](ir::AstNode *ast) {
if (!ast->IsMemberExpression() || ast->AsMemberExpression()->Object()->TsType() == nullptr) {
return false;
}
auto *objType =
checker->GetApparentType(checker->GetNonNullishType(ast->AsMemberExpression()->Object()->TsType()));
auto *parent = ast->Parent();
if (!parent->IsCallExpression() || parent->AsCallExpression()->Signature() == nullptr ||
parent->AsCallExpression()->Signature()->HasFunction()) {
return false;
}
return objType->IsETSUnionType() && ast->AsMemberExpression()->PropVar() == nullptr;
});
if (!current || Options()->GetCompilationMode() != CompilationMode::GEN_STD_LIB) {
return current;
}
return true;
}
}