/**
 * 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 "arithmetic.h"

#include "checker/ETSAnalyzerHelpers.h"
#include "checker/types/ets/etsObjectType.h"
#include "checker/types/ets/etsObjectTypeConstants.h"
#include "checker/types/ets/etsTupleType.h"
#include "checker/types/globalTypesHolder.h"
#include "checker/types/typeError.h"
#include "ir/ets/etsUnionType.h"
#include "lexer/token/token.h"
#include "compiler/lowering/util.h"

namespace ark::es2panda::checker {

struct BinaryArithmOperands {
    ir::BinaryExpression *expr;
    checker::Type *typeL;
    checker::Type *typeR;
    checker::Type *reducedL;
    checker::Type *reducedR;
};

struct BinaryOperatorParams {
    ir::Expression *left;
    ir::Expression *right;
    ir::Expression *expr;
    lexer::TokenType operationType;
    lexer::SourcePosition pos;
    bool isEqualOp;
};

struct TypeParams {
    checker::Type *leftType;
    checker::Type *rightType;
    Type *unboxedL;
    Type *unboxedR;
};

struct RelationalContext {
    lexer::TokenType op;
    lexer::SourcePosition pos;
};

enum class BinaryExpressionValidity {
    NO_ERR = 0U,
    LHS_ERR = 1U << 0U,
    RHS_ERR = 1U << 1U,
    EITHER_ERR = 1U << 2U,
    // NOLINTNEXTLINE(hicpp-signed-bitwise)
    BOTH_ERR = LHS_ERR | RHS_ERR,
};

static BinaryArithmOperands GetBinaryOperands(ETSChecker *checker, ir::BinaryExpression *expr)
{
    auto typeL = expr->Left()->Check(checker);
    auto typeR = expr->Right()->Check(checker);
    auto unboxedL = checker->MaybeUnboxType(typeL);
    auto unboxedR = checker->MaybeUnboxType(typeR);
    return {expr, typeL, typeR, unboxedL, unboxedR};
}

static void LogOperatorCannotBeApplied(ETSChecker *checker, lexer::TokenType op, Type *typeL, Type *typeR,
                                       lexer::SourcePosition pos)
{
    checker->LogError(diagnostic::BINOP_INVALID_TYPE, {TokenToString(op), typeL, typeR}, pos);
}

static void LogOperatorCannotBeApplied(ETSChecker *checker, BinaryArithmOperands const &ops)
{
    LogOperatorCannotBeApplied(checker, ops.expr->OperatorType(), ops.typeL, ops.typeR, ops.expr->Start());
}

static inline void RepairTypeErrorsInOperands(ETSChecker *checker, Type **left, Type **right)
{
    if (IsTypeError(*left)) {
        *left = checker->GetNonConstantType(*right);
    }
    if (IsTypeError(*right)) {
        *right = checker->GetNonConstantType(*left);
    }
}

static inline BinaryArithmOperands RepairTypeErrorsInOperands(ETSChecker *checker, BinaryArithmOperands const &ops)
{
    BinaryArithmOperands res = ops;
    RepairTypeErrorsInOperands(checker, &res.typeL, &res.typeR);
    RepairTypeErrorsInOperands(checker, &res.reducedL, &res.reducedR);
    return res;
}

static inline void RepairTypeErrorWithDefault(Type **type, Type *dflt)
{
    if (IsTypeError(*type)) {
        *type = dflt;
    }
}

static bool IsNegativeBigIntLiteralExpression(const ir::Expression *expr)
{
    if (expr == nullptr || !expr->IsUnaryExpression()) {
        return false;
    }

    auto const *unaryExpr = expr->AsUnaryExpression();
    return unaryExpr->OperatorType() == lexer::TokenType::PUNCTUATOR_MINUS && unaryExpr->Argument()->IsBigIntLiteral();
}

static bool CheckOpArgsTypeEq(ETSChecker *checker, Type *left, Type *right, Type *type)
{
    return ((left != nullptr) && (right != nullptr) && checker->IsTypeIdenticalTo(left, type) &&
            checker->IsTypeIdenticalTo(right, type));
}

static bool FindOpArgsType(ETSChecker *checker, Type *left, Type *right, Type *target)
{
    return (checker->Relation()->IsSupertypeOf(target, left) || checker->Relation()->IsSupertypeOf(target, right));
}

bool ETSChecker::CheckIfNumeric(Type *type)
{
    if (type == nullptr) {
        return false;
    }
    if (type->IsETSPrimitiveType()) {
        // NOTE(rsipka): Deprecated operations #28006, char should be removed from ETS_CONVERTIBLE_TO_NUMERIC flags
        return type->HasTypeFlag(TypeFlag::ETS_CONVERTIBLE_TO_NUMERIC) && !type->HasTypeFlag(TypeFlag::CHAR);
    }
    auto *unboxed = MaybeUnboxInRelation(type);
    // NOTE(rsipka): Deprecated operations #28006, char should be removed from ETS_CONVERTIBLE_TO_NUMERIC flags
    return (unboxed != nullptr) && unboxed->HasTypeFlag(TypeFlag::ETS_CONVERTIBLE_TO_NUMERIC) &&
           !unboxed->HasTypeFlag(TypeFlag::CHAR);
}

bool ETSChecker::CheckIfFloatingPoint(Type *type)
{
    if (type == nullptr) {
        return false;
    }
    auto *unboxed = MaybeUnboxInRelation(type);
    return (unboxed != nullptr) && (unboxed->IsFloatType() || unboxed->IsDoubleType());
}

static Type *EffectivePrimitiveTypeOfNumericOp(ETSChecker *checker, Type *left, Type *right)
{
    if (left->IsDoubleType() || right->IsDoubleType()) {
        return checker->GlobalDoubleType();
    }
    if (left->IsFloatType() || right->IsFloatType()) {
        return checker->GlobalFloatType();
    }
    if (left->IsLongType() || right->IsLongType()) {
        return checker->GlobalLongType();
    }
    // NOTE(dkofanov): Deprecated operations on 'char' #28006
    if (left->IsCharType() && right->IsCharType()) {
        return checker->GlobalCharType();
    }
    return checker->GlobalIntType();  // return int in case of primitive types by default
}

static Type *TryConvertToComparablePrimitiveType(ETSChecker *checker, Type *type)
{
    if (type == nullptr) {
        return nullptr;
    }

    if (type->IsETSUndefinedType()) {
        return nullptr;
    }

    if (type->IsETSNumericEnumType()) {
        // Pull out the type argument to BaseEnum
        if (type->AsETSObjectType()->SuperType() != nullptr &&
            !type->AsETSObjectType()->SuperType()->TypeArguments().empty()) {
            auto *baseEnumArg = type->AsETSObjectType()->SuperType()->TypeArguments()[0];
            return checker->MaybeUnboxInRelation(baseEnumArg);
        }
        return checker->GlobalIntType();
    }

    if (type->IsETSStringEnumType()) {
        return checker->GlobalETSStringLiteralType();
    }
    return checker->MaybeUnboxInRelation(type);
}

static Type *EffectiveTypeOfNumericOp(ETSChecker *checker, Type *left, Type *right)
{
    ES2PANDA_ASSERT(checker->CheckIfNumeric(left) && checker->CheckIfNumeric(right));

    auto bothBoxed = left->IsETSUnboxableObject() && right->IsETSUnboxableObject();
    if (!bothBoxed) {
        return EffectivePrimitiveTypeOfNumericOp(checker, left, right);
    }

    auto globalTypesHolder = checker->GetGlobalTypesHolder();
    if (FindOpArgsType(checker, left, right, globalTypesHolder->GlobalDoubleBuiltinType())) {
        return globalTypesHolder->GlobalDoubleBuiltinType();
    }
    if (FindOpArgsType(checker, left, right, globalTypesHolder->GlobalFloatBuiltinType())) {
        return globalTypesHolder->GlobalFloatBuiltinType();
    }
    if (FindOpArgsType(checker, left, right, globalTypesHolder->GlobalLongBuiltinType())) {
        return globalTypesHolder->GlobalLongBuiltinType();
    }
    return globalTypesHolder->GlobalIntegerBuiltinType();  // return Int for Byte, Short, Int
}

// NOTE(dkofanov): Deprecated operations on 'char' #28006
static Type *BinaryGetPromotedType(ETSChecker *checker, Type *left, Type *right, bool const promote)
{
    Type *const unboxedL = TryConvertToComparablePrimitiveType(checker, left);
    Type *const unboxedR = TryConvertToComparablePrimitiveType(checker, right);
    if (unboxedL == nullptr || unboxedR == nullptr) {
        return nullptr;
    }

    Type *typeL = left;
    Type *typeR = right;

    bool const bothBoxed = !typeL->IsETSPrimitiveType() && !typeR->IsETSPrimitiveType();

    if (!promote) {
        return typeR;
    }

    if (!bothBoxed) {
        if (unboxedL->IsETSEnumType() || unboxedR->IsETSEnumType()) {
            return nullptr;
        }
        if (!typeL->IsETSPrimitiveType()) {
            typeL = checker->MaybeUnboxType(typeL);
        }
        if (!typeR->IsETSPrimitiveType()) {
            typeR = checker->MaybeUnboxType(typeR);
        }
    }

    if (checker->CheckIfNumeric(typeL) && checker->CheckIfNumeric(typeR)) {
        return EffectiveTypeOfNumericOp(checker, typeL, typeR);
    }
    if (checker->CheckIfNumeric(typeR)) {
        return typeR;
    }

    return typeL;
}

bool ETSChecker::CheckBinaryOperatorForBigInt(Type *left, Type *right, lexer::TokenType op)
{
    if ((left == nullptr) || (right == nullptr)) {
        return false;
    }

    // Allow operations between BigInt and numeric types - number will be converted to BigInt
    bool leftIsBigInt = left->IsETSBigIntType();
    bool rightIsBigInt = right->IsETSBigIntType();
    // Allow if either operand is BigInt.
    // The non-BigInt operand will be converted to BigInt during lowering.
    if ((leftIsBigInt && CheckIfNumeric(right)) || (rightIsBigInt && CheckIfNumeric(left))) {
        switch (op) {
            case lexer::TokenType::PUNCTUATOR_GREATER_THAN:
            case lexer::TokenType::PUNCTUATOR_LESS_THAN:
            case lexer::TokenType::PUNCTUATOR_GREATER_THAN_EQUAL:
            case lexer::TokenType::PUNCTUATOR_LESS_THAN_EQUAL:
            case lexer::TokenType::PUNCTUATOR_EQUAL:
            case lexer::TokenType::PUNCTUATOR_NOT_EQUAL:
            case lexer::TokenType::PUNCTUATOR_STRICT_EQUAL:
            case lexer::TokenType::PUNCTUATOR_NOT_STRICT_EQUAL:
                return true;
            default:
                break;
        }
    }

    if (!leftIsBigInt || !rightIsBigInt) {
        return false;
    }

    switch (op) {
        case lexer::TokenType::KEYW_INSTANCEOF:
        case lexer::TokenType::PUNCTUATOR_UNSIGNED_RIGHT_SHIFT:
            // This is handled in the main CheckBinaryOperator function
            return false;
        default:
            break;
    }

    return true;
}

bool ETSChecker::CheckBinaryPlusMultDivOperandsForUnionType(const Type *leftType, const Type *rightType,
                                                            const ir::Expression *left, const ir::Expression *right)
{
    std::stringstream ss;
    if (leftType->IsETSUnionType()) {
        LogError(diagnostic::BINOP_ON_UNION, {leftType}, left->Start());
        return false;
    }
    if (rightType->IsETSUnionType()) {
        LogError(diagnostic::BINOP_ON_UNION, {rightType}, right->Start());
        return false;
    }
    return true;
}

void ETSChecker::SetGenerateValueOfFlags(std::tuple<checker::Type *, checker::Type *, Type *, Type *> types,
                                         std::tuple<ir::Expression *, ir::Expression *> nodes)
{
    auto [leftType, rightType, _, __] = types;
    auto [left, right] = nodes;
    if (leftType->IsETSEnumType()) {
        left->AddAstNodeFlags(ir::AstNodeFlags::GENERATE_VALUE_OF);
    }
    if (rightType->IsETSEnumType()) {
        right->AddAstNodeFlags(ir::AstNodeFlags::GENERATE_VALUE_OF);
    }
}

static bool TypeIsAppropriateForArithmetic(const checker::Type *type, ETSChecker *checker)
{
    return type->HasTypeFlag(TypeFlag::ETS_CONVERTIBLE_TO_NUMERIC) ||
           (type->IsETSObjectType() &&
            checker->Relation()->IsSupertypeOf(checker->GetGlobalTypesHolder()->GlobalNumericBuiltinType(), type));
}

static checker::Type *CheckBinaryOperatorForNumericEnums(ETSChecker *checker, checker::Type *const leftType,
                                                         checker::Type *const rightType)
{
    if (!leftType->IsETSEnumType() && !rightType->IsETSEnumType()) {
        return nullptr;
    }
    if (TypeIsAppropriateForArithmetic(leftType, checker) && TypeIsAppropriateForArithmetic(rightType, checker)) {
        Type *leftNumeric;
        if (leftType->IsETSNumericEnumType()) {
            leftNumeric = checker->MaybeBoxInRelation(TryConvertToComparablePrimitiveType(checker, leftType));
        } else {
            leftNumeric = leftType;
        }

        Type *rightNumeric;
        if (rightType->IsETSNumericEnumType()) {
            rightNumeric = checker->MaybeBoxInRelation(TryConvertToComparablePrimitiveType(checker, rightType));
        } else {
            rightNumeric = rightType;
        }

        return EffectiveTypeOfNumericOp(checker, leftNumeric, rightNumeric);
    }

    return nullptr;
}

checker::Type *ETSChecker::CheckBinaryOperatorMulDivMod(
    std::tuple<ir::Expression *, ir::Expression *, lexer::TokenType, lexer::SourcePosition> op, bool isEqualOp,
    std::tuple<checker::Type *, checker::Type *, Type *, Type *> types)
{
    auto [left, right, operationType, pos] = op;
    auto [leftType, rightType, unboxedL, unboxedR] = types;

    // Try to handle errors on a lower level
    RepairTypeErrorsInOperands(this, &leftType, &rightType);
    RepairTypeErrorsInOperands(this, &unboxedL, &unboxedR);
    ERROR_TYPE_CHECK(this, leftType, return GlobalTypeError());

    auto const promotedType = BinaryGetPromotedType(this, leftType, rightType, !isEqualOp);
    if (!CheckBinaryPlusMultDivOperandsForUnionType(leftType, rightType, left, right)) {
        return GlobalTypeError();
    }

    if (promotedType == nullptr || !CheckIfNumeric(leftType) || !CheckIfNumeric(rightType)) {
        auto type = CheckBinaryOperatorForNumericEnums(this, leftType, rightType);
        if (type != nullptr) {
            return type;
        }
        LogError(diagnostic::OP_NONNUMERIC, {}, pos);
        return GlobalTypeError();
    }

    return promotedType;
}

checker::Type *ETSChecker::CheckBinaryBitwiseOperatorForNumericEnums(checker::Type *const leftType,
                                                                     checker::Type *const rightType)
{
    if (!leftType->IsETSEnumType() && !rightType->IsETSEnumType()) {
        return nullptr;
    }

    auto checkForEnumType = [=](auto type, bool hasLong) -> bool {
        ETSObjectFlags floatingPointType = hasLong ? ETSObjectFlags::BUILTIN_DOUBLE : ETSObjectFlags::BUILTIN_FLOAT;
        ETSObjectFlags integralType = hasLong ? ETSObjectFlags::BUILTIN_LONG : ETSObjectFlags::BUILTIN_INT;
        return (type->AsETSNumericEnumType()->CheckBuiltInType(this, floatingPointType) ||
                type->AsETSNumericEnumType()->CheckBuiltInType(this, integralType));
    };

    if (TypeIsAppropriateForArithmetic(leftType, this) && TypeIsAppropriateForArithmetic(rightType, this)) {
        if (leftType->IsETSNumericEnumType() && rightType->IsETSNumericEnumType()) {
            if (checkForEnumType(leftType, false) || checkForEnumType(rightType, false)) {
                return GlobalIntBuiltinType();
            }
            if (checkForEnumType(leftType, true) || checkForEnumType(rightType, true)) {
                return GlobalLongBuiltinType();
            }
        }
        if (leftType->IsFloatType() || rightType->IsFloatType()) {
            return GlobalIntBuiltinType();
        }
        if (leftType->IsDoubleType() || rightType->IsDoubleType()) {
            return GlobalLongBuiltinType();
        }
        if (leftType->IsLongType() || rightType->IsLongType()) {
            return GlobalLongBuiltinType();
        }
        return GlobalIntBuiltinType();
    }
    return nullptr;
}

checker::Type *ETSChecker::CheckBinaryOperatorExponentiation(
    std::tuple<ir::Expression *, ir::Expression *, lexer::TokenType, lexer::SourcePosition> op, bool isEqualOp,
    std::tuple<checker::Type *, checker::Type *, Type *, Type *> types)
{
    auto [left, right, operationType, pos] = op;
    auto [leftType, rightType, unboxedL, unboxedR] = types;

    // Try to handle errors on a lower level
    RepairTypeErrorsInOperands(this, &leftType, &rightType);
    RepairTypeErrorsInOperands(this, &unboxedL, &unboxedR);
    ERROR_TYPE_CHECK(this, leftType, return GlobalTypeError());

    if (IsNegativeBigIntLiteralExpression(right)) {
        LogError(diagnostic::EXPONENTIATION_BIGINT_NEGATIVE_EXPONENT, {}, right->Start());
        return GlobalTypeError();
    }

    auto const promotedType = BinaryGetPromotedType(this, leftType, rightType, !isEqualOp);
    if (promotedType == nullptr || !CheckIfNumeric(leftType) || !CheckIfNumeric(rightType)) {
        if (operationType == lexer::TokenType::PUNCTUATOR_EXPONENTIATION) {
            LogError(diagnostic::EXPONENTIATION_TYPE_LIMITATION, {}, pos);
        }
        return GlobalTypeError();
    }

    // Exponentiation of non-bigint numeric types always produces double
    return GlobalDoubleType();
}

static checker::Type *CheckBinaryOperatorPlusForEnums(ETSChecker *checker, checker::Type *const leftType,
                                                      checker::Type *const rightType)
{
    if (auto numericType = CheckBinaryOperatorForNumericEnums(checker, leftType, rightType); numericType != nullptr) {
        return numericType;
    }
    if ((leftType->IsETSStringEnumType() && (rightType->IsETSStringType() || rightType->IsETSStringEnumType())) ||
        (rightType->IsETSStringEnumType() && (leftType->IsETSStringType() || leftType->IsETSStringEnumType()))) {
        return checker->GlobalETSStringLiteralType();
    }
    return nullptr;
}

checker::Type *ETSChecker::CheckBinaryOperatorPlus(
    std::tuple<ir::Expression *, ir::Expression *, lexer::TokenType, lexer::SourcePosition> op, bool isEqualOp,
    std::tuple<checker::Type *, checker::Type *, Type *, Type *> types)
{
    auto [left, right, operationType, pos] = op;
    auto [leftType, rightType, unboxedL, unboxedR] = types;

    // Try to handle errors on a lower level
    RepairTypeErrorsInOperands(this, &leftType, &rightType);
    RepairTypeErrorsInOperands(this, &unboxedL, &unboxedR);
    ERROR_TYPE_CHECK(this, leftType, return GlobalTypeError());

    if (leftType->IsETSStringType() || rightType->IsETSStringType()) {
        if (operationType == lexer::TokenType::PUNCTUATOR_MINUS ||
            operationType == lexer::TokenType::PUNCTUATOR_MINUS_EQUAL) {
            LogError(diagnostic::OP_NONNUMERIC, {}, pos);
            return GlobalTypeError();
        }

        return HandleStringConcatenation(leftType, rightType);
    }

    if (!CheckBinaryPlusMultDivOperandsForUnionType(leftType, rightType, left, right)) {
        return GlobalTypeError();
    }
    auto const promotedType = BinaryGetPromotedType(this, leftType, rightType, !isEqualOp);
    if (promotedType == nullptr || !CheckIfNumeric(rightType) || !CheckIfNumeric(leftType)) {
        auto type = CheckBinaryOperatorPlusForEnums(this, leftType, rightType);
        if (type != nullptr) {
            return type;
        }
        LogError(diagnostic::BINOP_NONARITHMETIC_TYPE, {}, pos);
        return GlobalTypeError();
    }

    return promotedType;
}

[[maybe_unused]] static checker::Type *GetBitwiseCompatibleType(ETSChecker *checker, Type *const type)
{
    switch (checker->ETSType(type)) {
        case TypeFlag::BYTE: {
            return checker->GlobalByteType();
        }
        case TypeFlag::SHORT: {
            return checker->GlobalShortType();
        }
        case TypeFlag::CHAR: {
            return checker->GlobalCharType();
        }
        case TypeFlag::INT:
        case TypeFlag::FLOAT: {
            return checker->GlobalIntType();
        }
        case TypeFlag::LONG:
        case TypeFlag::DOUBLE: {
            return checker->GlobalLongType();
        }
        default: {
            ES2PANDA_UNREACHABLE();
        }
    }
    return nullptr;
}

// NOTE(dkofanov): Deprecated operations on 'char' #28006
checker::Type *ETSChecker::CheckBinaryOperatorShift(
    std::tuple<ir::Expression *, ir::Expression *, lexer::TokenType, lexer::SourcePosition> op, bool isEqualOp,
    std::tuple<checker::Type *, checker::Type *, Type *, Type *> types)
{
    auto [left, right, operationType, pos] = op;
    auto [leftType, rightType, unboxedL, unboxedR] = types;

    RepairTypeErrorWithDefault(&leftType, GlobalIntBuiltinType());
    RepairTypeErrorWithDefault(&rightType, GlobalIntBuiltinType());
    RepairTypeErrorWithDefault(&unboxedL, GlobalIntType());
    RepairTypeErrorWithDefault(&unboxedR, GlobalIntType());

    if (leftType->IsETSUnionType() || rightType->IsETSUnionType()) {
        LogError(diagnostic::BINOP_UNION, {}, pos);
        return GlobalTypeError();
    }

    auto promotedLeftType = GetUnaryOperatorPromotedType(leftType, !isEqualOp);
    auto promotedRightType = GetUnaryOperatorPromotedType(rightType, !isEqualOp);
    if (promotedLeftType == nullptr || promotedRightType == nullptr || !CheckIfNumeric(promotedLeftType) ||
        !CheckIfNumeric(promotedRightType)) {
        auto type = CheckBinaryBitwiseOperatorForNumericEnums(leftType, rightType);
        if (type != nullptr) {
            return type;
        }
        LogError(diagnostic::OP_NONNUMERIC, {}, pos);
        return GlobalTypeError();
    }

    auto isPrim = promotedLeftType->IsETSPrimitiveType();
    auto unboxedProm = MaybeUnboxType(promotedLeftType);
    if (unboxedProm->IsFloatType() || unboxedProm->IsIntType()) {
        return isPrim ? GlobalIntType() : GetGlobalTypesHolder()->GlobalIntegerBuiltinType();
    }

    if (unboxedProm->IsLongType() || unboxedProm->IsDoubleType()) {
        return isPrim ? GlobalLongType() : GetGlobalTypesHolder()->GlobalLongBuiltinType();
    }

    if (unboxedProm->IsByteType() || unboxedProm->IsShortType()) {
        return promotedLeftType;
    }

    ES2PANDA_UNREACHABLE();
    return nullptr;
}

// NOTE(dkofanov): Deprecated operations on 'char' #28006
checker::Type *ETSChecker::CheckBinaryOperatorBitwise(
    std::tuple<ir::Expression *, ir::Expression *, lexer::TokenType, lexer::SourcePosition> op, bool isEqualOp,
    std::tuple<checker::Type *, checker::Type *, Type *, Type *> types)
{
    auto [left, right, operationType, pos] = op;
    auto [leftType, rightType, unboxedL, unboxedR] = types;

    RepairTypeErrorsInOperands(this, &leftType, &rightType);
    RepairTypeErrorsInOperands(this, &unboxedL, &unboxedR);
    ERROR_TYPE_CHECK(this, leftType, return GlobalTypeError());

    if (leftType->IsETSUnionType() || rightType->IsETSUnionType()) {
        LogError(diagnostic::BINOP_UNION, {}, pos);
        return GlobalTypeError();
    }

    if (CheckOpArgsTypeEq(this, unboxedL, unboxedR, GlobalETSBooleanType())) {
        return GetGlobalTypesHolder()->GlobalETSBooleanBuiltinType();
    }

    auto const promotedType = BinaryGetPromotedType(this, leftType, rightType, !isEqualOp);
    if (promotedType == nullptr || !CheckIfNumeric(rightType) || !CheckIfNumeric(leftType)) {
        auto type = CheckBinaryBitwiseOperatorForNumericEnums(leftType, rightType);
        if (type != nullptr) {
            return type;
        }
        LogError(diagnostic::OP_NONNUMERIC, {}, pos);
        return GlobalTypeError();
    }
    SetGenerateValueOfFlags(types, {left, right});

    auto isPrim = promotedType->IsETSPrimitiveType();
    auto unboxedProm = MaybeUnboxType(promotedType);
    if (unboxedProm->IsFloatType() || unboxedProm->IsIntType()) {
        return isPrim ? GlobalIntType() : GetGlobalTypesHolder()->GlobalIntegerBuiltinType();
    }

    if (unboxedProm->IsLongType() || unboxedProm->IsDoubleType()) {
        return isPrim ? GlobalLongType() : GetGlobalTypesHolder()->GlobalLongBuiltinType();
    }

    if (unboxedProm->IsByteType() || unboxedProm->IsShortType()) {
        return promotedType;
    }
    return nullptr;
}

checker::Type *ETSChecker::CheckBinaryOperatorLogical(ir::Expression *left, ir::Expression *right,
                                                      checker::Type *leftType, checker::Type *rightType, Type *unboxedL,
                                                      Type *unboxedR)
{
    RepairTypeErrorsInOperands(this, &leftType, &rightType);
    RepairTypeErrorsInOperands(this, &unboxedL, &unboxedR);
    ERROR_TYPE_CHECK(this, leftType, return GlobalTypeError());

    // Don't do any boxing for primitive type when another operand is Enum. Enum will become primitive type later.
    if (leftType->IsETSEnumType() || rightType->IsETSEnumType()) {
        left->RemoveAstNodeFlags(ir::AstNodeFlags::GENERATE_VALUE_OF);
        right->RemoveAstNodeFlags(ir::AstNodeFlags::GENERATE_VALUE_OF);
        return CreateETSUnionType({MaybeBoxExpression(left), MaybeBoxExpression(right)});
    }

    if (right->IsNumberLiteral() && !left->IsNumberLiteral() && leftType->IsBuiltinNumeric()) {
        return leftType;
    }
    if (left->IsNumberLiteral() && !right->IsNumberLiteral() && rightType->IsBuiltinNumeric()) {
        return rightType;
    }

    if (IsTypeIdenticalTo(leftType, rightType)) {
        return GetNonConstantType(leftType);
    }

    return CreateETSUnionType({MaybeBoxExpression(left), MaybeBoxExpression(right)});
}

static bool ContainsNumbers(ETSChecker *checker, Type *tp)
{
    auto isSubtypeOfNumeric = [checker](Type *tp2) {
        return checker->Relation()->IsSupertypeOf(checker->GetGlobalTypesHolder()->GlobalNumericBuiltinType(), tp2);
    };
    if (isSubtypeOfNumeric(tp)) {
        return true;
    }
    if (tp->IsETSUnionType()) {
        for (auto *constituent : tp->AsETSUnionType()->ConstituentTypes()) {
            if (isSubtypeOfNumeric(constituent)) {
                return true;
            }
        }
    }

    return false;
}

static bool HasBigIntNonBigIntEqualityPairing(ETSChecker *checker, Type *leftType, Type *rightType)
{
    auto *ul = checker->MaybeUnboxType(checker->GetApparentType(leftType));
    auto *ur = checker->MaybeUnboxType(checker->GetApparentType(rightType));
    if (ul == nullptr || ur == nullptr) {
        return false;
    }
    return ul->IsETSBigIntType() != ur->IsETSBigIntType();
}

// CC-OFFNXT(huge_cyclomatic_complexity, huge_cca_cyclomatic_complexity[C++]) solid logic
// NOTE: code inside this function follows the broken logic
bool ETSChecker::CheckValidEqualReferenceType(checker::Type *const leftType, checker::Type *const rightType)
{
    auto isRelaxedType {[&](checker::Type *const type) -> bool {
        return (type->IsETSObjectType() && type->AsETSObjectType()->IsGlobalETSObjectType()) || type->IsETSAnyType() ||
               type->IsETSNullType() || type->IsETSUndefinedType();
    }};

    // Equality expression is always allowed for *magic types*
    if (isRelaxedType(leftType) || isRelaxedType(rightType)) {
        return true;
    }

    // SUPPRESS_CSA_NEXTLINE(alpha.core.AllocatorETSCheckerHint)
    if (HasBigIntNonBigIntEqualityPairing(this, leftType, rightType)) {
        return true;
    }

    // Any two types that can be numeric are comparable
    if (ContainsNumbers(this, leftType) && ContainsNumbers(this, rightType)) {
        return true;
    }

    // Boolean and any type that can be numeric or char are not comparable
    if ((FindOpArgsType(this, leftType, rightType, GetGlobalTypesHolder()->GlobalNumericBuiltinType()) ||
         FindOpArgsType(this, leftType, rightType, GetGlobalTypesHolder()->GlobalCharBuiltinType())) &&
        FindOpArgsType(this, leftType, rightType, GetGlobalTypesHolder()->GlobalETSBooleanBuiltinType())) {
        return false;
    }

    // NOTE (mxlgv): Skip for unions. Required implementation of the specification section:
    // 7.25.6 Reference Equality Based on Actual Type (Union Equality Operators)
    if (leftType->IsETSUnionType()) {
        return leftType->AsETSUnionType()->IsOverlapWith(Relation(), rightType);
    }
    if (rightType->IsETSUnionType()) {
        return rightType->AsETSUnionType()->IsOverlapWith(Relation(), leftType);
    }

    // NOTE (mxlgv): Skip for generic. Required implementation of the specification section:
    // 7.25.6 Reference Equality Based on Actual Type (Type Parameter Equality Operators)
    if (leftType->HasTypeFlag(TypeFlag::GENERIC) || rightType->HasTypeFlag(TypeFlag::GENERIC)) {
        return true;
    }

    // Equality expression can only be applied to String and String, and BigInt and BigInt
    if (leftType->IsETSStringType() || rightType->IsETSStringType() || leftType->IsETSBigIntType() ||
        rightType->IsETSBigIntType()) {
        auto *const nonConstLhs = GetNonConstantType(leftType);
        auto *const nonConstRhs = GetNonConstantType(rightType);
        if (!Relation()->IsIdenticalTo(nonConstLhs, nonConstRhs) &&
            !Relation()->IsIdenticalTo(nonConstRhs, nonConstLhs)) {
            return false;
        }
    }

    if (FindOpArgsType(this, leftType, rightType, GetGlobalTypesHolder()->GlobalNumericBuiltinType()) &&
        (leftType->IsETSEnumType() || rightType->IsETSEnumType())) {
        return true;
    }

    // 7.24.5 Enumeration Relational Operators
    return leftType->IsETSEnumType() == rightType->IsETSEnumType();
}

std::tuple<Type *, Type *> ETSChecker::CheckBinaryOperatorStrictEqual(ir::Expression *left,
                                                                      lexer::TokenType operationType,
                                                                      lexer::SourcePosition pos,
                                                                      checker::Type *leftType, checker::Type *rightType)
{
    RepairTypeErrorsInOperands(this, &leftType, &rightType);
    // We still know that operation result should be boolean, so recover.
    ERROR_TYPE_CHECK(this, leftType, return std::make_tuple(GlobalETSBooleanBuiltinType(), GlobalETSObjectType()));

    if (!IsReferenceType(leftType) || !IsReferenceType(rightType)) {
        LogError(diagnostic::BINOP_NOT_REFERENCE, {}, pos);
        return {GlobalETSBooleanType(), GlobalETSObjectType()};
    }

    Relation()->SetNode(left);
    // SUPPRESS_CSA_NEXTLINE(alpha.core.AllocatorETSCheckerHint)
    if (!CheckValidEqualReferenceType(leftType, rightType)) {
        LogOperatorCannotBeApplied(this, operationType, leftType, rightType, pos);
    } else if (!Relation()->IsCastableTo(leftType, rightType) && !Relation()->IsCastableTo(rightType, leftType)) {
        LogOperatorCannotBeApplied(this, operationType, leftType, rightType, pos);
    }

    return {GlobalETSBooleanType(), GlobalETSObjectType()};
}

static Type *HandelReferenceBinaryEquality(ETSChecker *checker, BinaryArithmOperands const &ops)
{
    [[maybe_unused]] auto const [expr, typeL, typeR, reducedL, reducedR] = ops;
    if ((typeR->IsETSNullType() && typeL->IsETSPrimitiveType()) ||
        (typeL->IsETSNullType() && typeR->IsETSPrimitiveType())) {
        return checker->CreateETSUnionType({typeL, typeR});
    }

    if (typeL->IsETSReferenceType() && typeR->IsETSReferenceType()) {
        checker->Relation()->SetNode(expr->Left());
        if (!checker->CheckValidEqualReferenceType(typeL, typeR)) {
            LogOperatorCannotBeApplied(checker, ops);
            return typeL;
        }
        return checker->CreateETSUnionType({typeL, typeR});
    }

    if ((reducedL->IsETSReferenceType() || reducedR->IsETSReferenceType()) &&
        !(typeL->IsETSNullType() || typeL->IsETSUndefinedType()) &&
        !(typeR->IsETSNullType() || typeR->IsETSUndefinedType())) {
        auto *const boxedL = checker->MaybeBoxType(typeL);
        auto *const boxedR = checker->MaybeBoxType(typeR);
        checker->Relation()->SetNode(expr->Left());
        if (!checker->CheckValidEqualReferenceType(boxedL, boxedR) ||
            (!checker->Relation()->IsCastableTo(boxedL, boxedR) &&
             !checker->Relation()->IsCastableTo(boxedR, boxedL))) {
            LogOperatorCannotBeApplied(checker, ops);
            return typeL;
        }

        return checker->CreateETSUnionType(
            {checker->MaybeBoxExpression(expr->Left()), checker->MaybeBoxExpression(expr->Right())});
    }

    return nullptr;
}

static bool IsStaticallyDistinctReference(const ir::Expression *left, const ir::Expression *right)
{
    if (left->IsETSNewClassInstanceExpression() && right->IsETSNewClassInstanceExpression()) {
        return true;
    }

    auto isScriptFunctionReference = [](const ir::Expression *expr) {
        if (!expr->IsIdentifier()) {
            return false;
        }

        auto *var = expr->Variable();
        if (var == nullptr || var->Declaration() == nullptr) {
            return false;
        }

        return var->Declaration()->IsFunctionDecl();
    };
    if (isScriptFunctionReference(left) && isScriptFunctionReference(right) && left->Variable() != right->Variable()) {
        return true;
    }

    return false;
}

static std::optional<bool> ShouldReturnPredefinedEqualityExpressionResult(ETSChecker *checker,
                                                                          const ir::Expression *left,
                                                                          const ir::Expression *right,
                                                                          checker::Type *leftType,
                                                                          checker::Type *rightType)
{
    if (IsStaticallyDistinctReference(left, right)) {
        return false;
    }

    if ((leftType->DefinitelyETSNullish() && !rightType->PossiblyETSNullish()) ||
        (rightType->DefinitelyETSNullish() && !leftType->PossiblyETSNullish())) {
        return false;
    }

    if (leftType->DefinitelyETSNullish() && rightType->DefinitelyETSNullish()) {
        return true;
    }

    if (leftType->IsETSObjectType() && rightType->IsETSObjectType() &&
        !checker->Relation()->IsCastableTo(leftType, rightType) &&
        !checker->Relation()->IsCastableTo(rightType, leftType)) {
        return false;
    }

    return std::nullopt;
}

static std::optional<bool> EvaluateEqualityExpressionResult(ETSChecker *checker, ir::Expression *left,
                                                            ir::Expression *right, checker::Type *leftType,
                                                            checker::Type *rightType)
{
    if (IsTypeError(leftType) || IsTypeError(rightType)) {
        return std::nullopt;
    }

    if (HasBigIntNonBigIntEqualityPairing(checker, leftType, rightType)) {
        return false;
    }

    leftType = checker->MaybeUnboxType(checker->GetApparentType(leftType));
    rightType = checker->MaybeUnboxType(checker->GetApparentType(rightType));
    if (leftType->IsETSEnumType() && rightType->IsETSEnumType() &&
        !checker->Relation()->IsIdenticalTo(leftType, rightType)) {
        return std::nullopt;
    }

    if (leftType->IsConstantType() && rightType->IsConstantType()) {
        return checker->Relation()->IsIdenticalTo(leftType, rightType);
    }

    return ShouldReturnPredefinedEqualityExpressionResult(checker, left, right, leftType, rightType);
}

static void MaybeLogCompileTimeEqualityWarning(ETSChecker *checker, const BinaryArithmOperands &ops,
                                               lexer::TokenType operationType, const lexer::SourcePosition &pos)
{
    ir::Expression *left = ops.expr->Left();
    ir::Expression *right = ops.expr->Right();
    auto result = EvaluateEqualityExpressionResult(checker, left, right, ops.typeL, ops.typeR);
    if (!result.has_value()) {
        return;
    }

    auto value = *result;
    if (operationType == lexer::TokenType::PUNCTUATOR_NOT_EQUAL ||
        operationType == lexer::TokenType::PUNCTUATOR_NOT_STRICT_EQUAL) {
        value = !value;
    }

    //  Check that equality expression was synthetically generated from optional chaining expression
    //  via Optional Lowering (that is structural lowering thus we don't have types on that phase)
    bool isOptionalChaining =
        left->IsIdentifier() && left->AsIdentifier()->Name().StartsWith(compiler::GENSYM_CORE) &&
        util::Helpers::FindAncestorGivenByType(ops.expr, ir::AstNodeType::CONDITIONAL_EXPRESSION) != nullptr;
    if (isOptionalChaining) {
        auto blockExpression = util::Helpers::FindAncestorGivenByType(ops.expr, ir::AstNodeType::BLOCK_EXPRESSION);
        if (blockExpression == nullptr || blockExpression->OriginalNode() == nullptr ||
            !blockExpression->OriginalNode()->IsChainExpression()) {
            isOptionalChaining = false;
        }
    }

    if (isOptionalChaining) {
        checker->LogDiagnostic(value ? diagnostic::NULLISH_OPERAND : diagnostic::NON_NULLISH_OPERAND,
                               {"optional-chaining"}, pos);
    } else {
        checker->LogDiagnostic(
            value ? diagnostic::EQUALITY_EXPRESSION_ALWAYS_TRUE : diagnostic::EQUALITY_EXPRESSION_ALWAYS_FALSE, pos);
    }
}

static Type *CheckBinaryOperatorEqual(ETSChecker *checker, BinaryArithmOperands const &ops)
{
    [[maybe_unused]] auto const [expr, typeL, typeR, reducedL, reducedR] = ops;

    ERROR_TYPE_CHECK(checker, typeL, return checker->GlobalTypeError());

    auto *const appL = checker->GetApparentType(typeL);
    auto *const appR = checker->GetApparentType(typeR);
    auto isPrimitiveLikeRef = [](Type *tp) {
        return tp->IsETSObjectType() && tp->AsETSObjectType()->IsBoxedPrimitive();
    };
    if ((appL->IsETSFunctionType() && (!appR->IsETSReferenceType() || isPrimitiveLikeRef(appR))) ||
        (appR->IsETSFunctionType() && (!appL->IsETSReferenceType() || isPrimitiveLikeRef(appL)))) {
        LogOperatorCannotBeApplied(checker, ops);
        return typeL;
    }

    if (reducedL->IsETSBooleanType() && reducedR->IsETSBooleanType()) {
        if (reducedL->IsConstantType() && reducedR->IsConstantType()) {
            return checker->GetGlobalTypesHolder()->GlobalETSBooleanBuiltinType();
        }
        if (checker->CheckIfNumeric(typeL) && checker->CheckIfNumeric(typeR) && typeL->IsETSUnboxableObject() &&
            typeR->IsETSUnboxableObject()) {
            return typeL;
        }
        return reducedL;
    }

    return HandelReferenceBinaryEquality(checker, ops);
}

// Satisfying the Chinese checker
static bool NonNumericTypesAreAppropriateForComparison(ETSChecker *checker, Type *leftType, Type *rightType)
{
    leftType = checker->MaybeUnboxType(leftType);
    rightType = checker->MaybeUnboxType(rightType);
    if (rightType->IsETSStringType() && leftType->IsETSStringType()) {
        return true;
    }
    if (leftType->IsETSEnumType() && rightType->IsETSEnumType()) {
        return checker->Relation()->IsIdenticalTo(leftType, rightType);
    }
    if ((leftType->IsETSStringEnumType() && rightType->IsETSStringType()) ||
        (leftType->IsETSStringType() && rightType->IsETSStringEnumType())) {
        return true;
    }
    if ((leftType->IsETSPrimitiveType() && rightType->IsETSNumericEnumType()) ||
        (leftType->IsETSNumericEnumType() && rightType->IsETSPrimitiveType())) {
        return true;
    }
    return false;
}

static bool IsEqualityOperator(lexer::TokenType operationType)
{
    return operationType == lexer::TokenType::PUNCTUATOR_EQUAL ||
           operationType == lexer::TokenType::PUNCTUATOR_NOT_EQUAL ||
           operationType == lexer::TokenType::PUNCTUATOR_STRICT_EQUAL ||
           operationType == lexer::TokenType::PUNCTUATOR_NOT_STRICT_EQUAL;
}

static bool HasMismatchedBooleanComparands(Type *promotedType, Type *unboxedL, Type *unboxedR)
{
    return promotedType != nullptr && unboxedL != nullptr && unboxedR != nullptr &&
           unboxedL->IsETSBooleanType() != unboxedR->IsETSBooleanType();
}

static bool CheckUnionRelationalError(ETSChecker *checker, Type *leftType, Type *rightType,
                                      const RelationalContext &ctx)
{
    if ((leftType->IsETSUnionType() || rightType->IsETSUnionType()) && !IsEqualityOperator(ctx.op)) {
        checker->LogError(diagnostic::BINOP_UNION, {}, ctx.pos);
        return true;
    }
    return false;
}

static bool CheckStringEnumBooleanConflict(ETSChecker *checker, Type *leftType, Type *rightType,
                                           const RelationalContext &ctx)
{
    auto *ubL = checker->MaybeUnboxType(leftType);
    auto *ubR = checker->MaybeUnboxType(rightType);
    if ((leftType->IsETSStringEnumType() || rightType->IsETSStringEnumType()) &&
        ((ubL && ubL->IsETSBooleanType()) || (ubR && ubR->IsETSBooleanType()))) {
        LogOperatorCannotBeApplied(checker, ctx.op, ubL ? ubL : leftType, ubR ? ubR : rightType, ctx.pos);
        return true;
    }
    return false;
}

static bool CharRelationalOperandIsValid(ETSChecker *checker, Type *leftType, Type *rightType)
{
    auto const leftUnboxed = checker->MaybeUnboxType(leftType);
    auto const rightUnboxed = checker->MaybeUnboxType(rightType);
    if (leftUnboxed == nullptr || rightUnboxed == nullptr) {
        return true;
    }
    auto const leftIsChar = leftUnboxed->IsCharType();
    auto const rightIsChar = rightUnboxed->IsCharType();
    if (!leftIsChar && !rightIsChar) {
        return true;
    }
    if (leftIsChar && rightIsChar) {
        return true;
    }
    auto const &otherUnboxed = leftIsChar ? rightUnboxed : leftUnboxed;
    // Check original type to exclude enums — enum is not a "numeric type" per spec
    auto const &otherOriginal = leftIsChar ? rightType : leftType;
    return checker->CheckIfNumeric(otherUnboxed) && !otherOriginal->IsETSEnumType() &&
           !otherOriginal->IsETSStringEnumType();
}

static bool CheckCharRelationalError(ETSChecker *checker, Type *leftType, Type *rightType, const RelationalContext &ctx)
{
    if (!CharRelationalOperandIsValid(checker, leftType, rightType)) {
        LogOperatorCannotBeApplied(checker, ctx.op, checker->GetNonConstantType(leftType),
                                   checker->GetNonConstantType(rightType), ctx.pos);
        return true;
    }
    return false;
}

// NOTE(dkofanov): Deprecated operations on 'char' #28006
std::tuple<Type *, Type *> ETSChecker::CheckBinaryOperatorLessGreater(ir::Expression *left, ir::Expression *right,
                                                                      lexer::TokenType operationType,
                                                                      lexer::SourcePosition pos, bool isEqualOp,
                                                                      checker::Type *leftType, checker::Type *rightType,
                                                                      Type *unboxedL, Type *unboxedR)
{
    RepairTypeErrorsInOperands(this, &leftType, &rightType);
    RepairTypeErrorsInOperands(this, &unboxedL, &unboxedR);
    ERROR_TYPE_CHECK(this, leftType, return std::make_tuple(GlobalETSBooleanBuiltinType(), GlobalTypeError()));

    if (CheckUnionRelationalError(this, leftType, rightType, {operationType, pos})) {
        return {GlobalETSBooleanBuiltinType(), leftType};
    }

    auto const promotedType = BinaryGetPromotedType(this, leftType, rightType, !isEqualOp);

    if (CheckStringEnumBooleanConflict(this, leftType, rightType, {operationType, pos})) {
        return {GlobalETSBooleanBuiltinType(), leftType};
    }

    if (leftType->IsETSUnionType() || rightType->IsETSUnionType()) {
        return {GlobalETSBooleanBuiltinType(),
                CreateETSUnionType({MaybeBoxExpression(left), MaybeBoxExpression(right)})};
    }

    if (HasMismatchedBooleanComparands(promotedType, unboxedL, unboxedR)) {
        // SUPPRESS_CSA_NEXTLINE(alpha.core.AllocatorETSCheckerHint)
        if (HasBigIntNonBigIntEqualityPairing(this, leftType, rightType)) {
            return {GlobalETSBooleanBuiltinType(), GlobalETSBooleanBuiltinType()};
        }
        LogOperatorCannotBeApplied(this, operationType, leftType, rightType, pos);
    }

    if (CheckCharRelationalError(this, leftType, rightType, {operationType, pos})) {
        return {GlobalETSBooleanBuiltinType(), leftType};
    }

    if (promotedType == nullptr) {
        if (!NonNumericTypesAreAppropriateForComparison(this, leftType, rightType)) {
            LogError(diagnostic::BINOP_INCOMPARABLE, {}, pos);
        }
        return {GlobalETSBooleanBuiltinType(), GlobalETSBooleanBuiltinType()};
    }

    return {GlobalETSBooleanBuiltinType(), promotedType};
}

static bool IsTypeRetainedAfterErasure(Type const *const typeToCheck, bool const checkTypeParameter = true) noexcept
{
    if (checkTypeParameter && typeToCheck->IsETSTypeParameter()) {
        return false;
    }

    // Only signatures like '(x: Any, y: Any, ...) => never' are preserved
    if (typeToCheck->IsETSFunctionType()) {
        auto *signature = typeToCheck->AsETSFunctionType()->ArrowSignature();
        if (!signature->ReturnType()->IsETSNeverType()) {
            return false;
        }
        for (auto const *param : signature->Params()) {
            if (!param->TsType()->IsETSAnyType()) {
                return false;
            }
        }
        return true;
    }

    // ETSTupleType is never preserved because has underlying implementation as internal generic tuple type
    if (typeToCheck->IsETSTupleType()) {
        return false;
    }

    // FixedArray<T> type is not preserved up to undefined - only form 'instanceof FixedArray<T|undefined>` is allowed
    if (typeToCheck->IsETSArrayType() && !typeToCheck->AsETSArrayType()->IsValueArray()) {
        auto const *const elementType = typeToCheck->AsETSArrayType()->ElementType();
        return elementType->PossiblyETSUndefined() && IsTypeRetainedAfterErasure(elementType);
    }

    if (typeToCheck->IsETSUnionType()) {
        auto *unionType = typeToCheck->AsETSUnionType();
        if (unionType->AllOfConstituentTypes([](Type const *item) { return IsTypeRetainedAfterErasure(item); })) {
            return true;
        }

        // Extracted to standalone lambda for codestyle check
        auto const predicate = [](Type const *item) -> bool { return IsTypeRetainedAfterErasure(item, false); };
        // 'T | undefined' is preserved up to undefined even when T is a type parameter,
        if (unionType->AllOfConstituentTypes(predicate)) {
            return unionType->AnyOfConstituentTypes([](Type const *item) -> bool { return item->IsUndefinedType(); });
        }
        return false;
    }

    return true;
}

static BinaryExpressionValidity AreTypesValidInInstanceofExpression(const ir::Expression *const right,
                                                                    const Type *const rightType) noexcept
{
    bool isRightTypeRetained;
    if (UNLIKELY(right->IsETSUnionType())) {
        const auto &unionTypeTypes = right->AsETSUnionType()->Types();
        isRightTypeRetained = !std::any_of(unionTypeTypes.begin(), unionTypeTypes.end(),
                                           [](auto *type) { return type->IsETSStringLiteralType(); });
    } else {
        isRightTypeRetained = !right->IsETSStringLiteralType();
    }

    isRightTypeRetained = isRightTypeRetained && IsTypeRetainedAfterErasure(rightType);
    return isRightTypeRetained ? BinaryExpressionValidity::NO_ERR : BinaryExpressionValidity::RHS_ERR;
}

constexpr std::string_view INSTANCEOF_ALWAYS_TRUE_WORD = "true";
constexpr std::string_view INSTANCEOF_ALWAYS_FALSE_WORD = "false";

static bool HasInterfaceConstituent(Type *t)
{
    if (t->IsETSUnionType()) {
        return t->AsETSUnionType()->AnyOfConstituentTypes(
            [](Type *ct) { return ct->IsETSObjectType() && ct->AsETSObjectType()->IsInterface(); });
    }
    return t->IsETSObjectType() && t->AsETSObjectType()->IsInterface();
}

static void ReportIfInstanceofTrivialKnown(ETSChecker *checker, Type *lhs, Type *rhs, lexer::SourcePosition pos)
{
    if (lhs == nullptr || rhs == nullptr || lhs->IsTypeError() || rhs->IsTypeError()) {
        return;
    }
    if (lhs == checker->GlobalETSAnyType() || rhs == checker->GlobalETSAnyType()) {
        return;
    }
    if ((lhs->IsETSObjectType() && lhs->AsETSObjectType()->IsGradual()) ||
        (rhs->IsETSObjectType() && rhs->AsETSObjectType()->IsGradual())) {
        return;
    }

    auto *const relation = checker->Relation();
    SavedTypeRelationFlagsContext const savedFlags(relation, TypeRelationFlag::IGNORE_TYPE_PARAMETERS);

    auto *intersectionType = checker->Context().GetIntersectionOfTypes(lhs, rhs);

    if (intersectionType != nullptr) {
        if (relation->IsSupertypeOf(intersectionType, lhs)) {
            checker->LogDiagnostic(diagnostic::INSTANCEOF_ALWAYS_KNOWN,
                                   util::DiagnosticMessageParams {INSTANCEOF_ALWAYS_TRUE_WORD}, pos);
        }
    } else if (!HasInterfaceConstituent(lhs) || !HasInterfaceConstituent(rhs)) {
        checker->LogDiagnostic(diagnostic::INSTANCEOF_ALWAYS_KNOWN,
                               util::DiagnosticMessageParams {INSTANCEOF_ALWAYS_FALSE_WORD}, pos);
    }
}

std::tuple<Type *, Type *> ETSChecker::CheckBinaryOperatorInstanceOf(const ir::Expression *right,
                                                                     checker::Type *leftType, checker::Type *rightType,
                                                                     lexer::SourcePosition pos)
{
    ERROR_TYPE_CHECK(this, rightType, return std::make_tuple(GlobalETSBooleanBuiltinType(), GlobalTypeError()));

    const BinaryExpressionValidity exprValidity = AreTypesValidInInstanceofExpression(right, rightType);
    switch (exprValidity) {
        case BinaryExpressionValidity::NO_ERR: {
            ReportIfInstanceofTrivialKnown(this, leftType, rightType, pos);
            break;
        }
        case BinaryExpressionValidity::RHS_ERR: {
            LogError(diagnostic::INVALID_INSTANCEOF_RHS_TYPE, {right->DumpEtsSrc()}, right->Start());
            break;
        }
        default:
            ES2PANDA_UNREACHABLE();
    }

    RemoveStatus(checker::CheckerStatus::IN_INSTANCEOF_CONTEXT);

    return {GlobalETSBooleanBuiltinType(),
            exprValidity == BinaryExpressionValidity::NO_ERR ? GlobalETSObjectType() : leftType};
}

template <typename T>
static void ConvertNumberLiteralTo(ETSChecker *checker, ir::NumberLiteral *lit, Type *toType)
{
    auto &number = lit->Number();
    number.SetValue(number.GetValueAndCastTo<T>());
    lit->SetTsType(checker->GetConstantBuiltinType(toType));
}

template <typename From, typename To>
static bool CheckNumberLiteralValue(ETSChecker *checker, ir::NumberLiteral const *const lit)
{
    auto const maxTo = static_cast<From>(std::numeric_limits<To>::max());
    auto const minTo = static_cast<From>(std::numeric_limits<To>::min());
    auto const val = lit->Number().GetValue<From>();
    if (val < minTo || val > maxTo) {
        checker->LogError(diagnostic::CONSTANT_VALUE_OUT_OF_RANGE, {}, lit->Start());
        return false;
    }
    return true;
}

//  Just to reduce the size of 'ConvertNumberLiteral'
static void ConvertIntegerNumberLiteral(ETSChecker *checker, ir::NumberLiteral *lit, ETSObjectType *fromType,
                                        ETSObjectType *toType)
{
    if (toType->HasObjectFlag(ETSObjectFlags::BUILTIN_LONG)) {
        ConvertNumberLiteralTo<int64_t>(checker, lit, checker->GlobalLongBuiltinType());
    } else if (toType->HasObjectFlag(ETSObjectFlags::BUILTIN_INT)) {
        if (!fromType->HasObjectFlag(ETSObjectFlags::BUILTIN_LONG) ||
            CheckNumberLiteralValue<int64_t, int32_t>(checker, lit)) {
            ConvertNumberLiteralTo<int32_t>(checker, lit, checker->GlobalIntBuiltinType());
        }
    } else if (toType->HasObjectFlag(ETSObjectFlags::BUILTIN_SHORT)) {
        if (fromType->HasObjectFlag(ETSObjectFlags::BUILTIN_LONG) &&
            !CheckNumberLiteralValue<int64_t, int16_t>(checker, lit)) {
            return;
        }
        if (fromType->HasObjectFlag(ETSObjectFlags::BUILTIN_INT) &&
            !CheckNumberLiteralValue<int32_t, int16_t>(checker, lit)) {
            return;
        }
        ConvertNumberLiteralTo<int16_t>(checker, lit, checker->GlobalShortBuiltinType());
    } else if (toType->HasObjectFlag(ETSObjectFlags::BUILTIN_BYTE)) {
        if (fromType->HasObjectFlag(ETSObjectFlags::BUILTIN_LONG) &&
            !CheckNumberLiteralValue<int64_t, int8_t>(checker, lit)) {
            return;
        }
        if (fromType->HasObjectFlag(ETSObjectFlags::BUILTIN_INT) &&
            !CheckNumberLiteralValue<int32_t, int8_t>(checker, lit)) {
            return;
        }
        if (fromType->HasObjectFlag(ETSObjectFlags::BUILTIN_SHORT) &&
            !CheckNumberLiteralValue<int16_t, int8_t>(checker, lit)) {
            return;
        }
        ConvertNumberLiteralTo<int8_t>(checker, lit, checker->GlobalByteBuiltinType());
    }
}

static void ConvertNumberLiteral(ETSChecker *checker, ir::NumberLiteral *lit, ETSObjectType *toType)
{
    ES2PANDA_ASSERT(toType->IsBuiltinNumeric() && lit->TsType()->IsBuiltinNumeric());

    if (auto *fromType = lit->TsType()->AsETSObjectType(); !checker->Relation()->IsIdenticalTo(fromType, toType)) {
        switch (static_cast<ETSObjectFlags>(toType->ObjectFlags() & ETSObjectFlags::BUILTIN_NUMERIC)) {
            case ETSObjectFlags::BUILTIN_DOUBLE:
                ConvertNumberLiteralTo<double>(checker, lit, checker->GlobalDoubleBuiltinType());
                break;

            case ETSObjectFlags::BUILTIN_FLOAT:
                if (fromType->HasObjectFlag(ETSObjectFlags::BUILTIN_DOUBLE)) {
                    checker->LogError(diagnostic::INVALID_ASSIGNMNENT, {fromType, toType}, lit->Start());
                } else {
                    ConvertNumberLiteralTo<float>(checker, lit, checker->GlobalFloatBuiltinType());
                }
                break;

            case ETSObjectFlags::BUILTIN_LONG:
            case ETSObjectFlags::BUILTIN_INT:
            case ETSObjectFlags::BUILTIN_SHORT:
            case ETSObjectFlags::BUILTIN_BYTE:
                if (fromType->HasObjectFlag(ETSObjectFlags::BUILTIN_FLOATING_POINT)) {
                    checker->LogError(diagnostic::INVALID_ASSIGNMNENT, {fromType, toType}, lit->Start());
                } else {
                    ConvertIntegerNumberLiteral(checker, lit, fromType, toType);
                }
                break;

            default:
                ES2PANDA_UNREACHABLE();
        }
    }
}

Type *ETSChecker::CheckBinaryOperatorNullishCoalescing(ir::Expression *left, ir::Expression *right,
                                                       lexer::SourcePosition pos)
{
    auto *leftType = left->TsType();
    if (leftType->DefinitelyNotETSNullish()) {
        LogDiagnostic(diagnostic::NULLISH_COALESCING_ALWAYS_LEFT, pos);
    } else if (leftType->DefinitelyETSNullish()) {
        LogDiagnostic(diagnostic::NULLISH_COALESCING_ALWAYS_RIGHT, pos);
    }
    leftType = GetNonNullishType(leftType);
    if (leftType->IsTypeError() && HasStatus(checker::CheckerStatus::IN_TYPE_INFER)) {
        return GlobalTypeError();
    }

    ERROR_TYPE_CHECK(this, leftType, return GlobalTypeError());

    if (leftType->IsETSPrimitiveType()) {
        LogError(diagnostic::COALESCE_NOT_REF, {}, pos);
    }

    auto *rightType = MaybeBoxType(right->TsType());
    if (IsTypeIdenticalTo(leftType, rightType)) {
        return leftType;
    }

    //  If possible and required update number literal type to the proper value (identical to left-side type)
    if (right->IsNumberLiteral() && leftType->IsBuiltinNumeric()) {
        ConvertNumberLiteral(this, right->AsNumberLiteral(), leftType->AsETSObjectType());
        return leftType;
    }

    return CreateETSUnionType({leftType, rightType});
}

using CheckBinaryFunction = std::function<checker::Type *(
    ETSChecker *, std::tuple<ir::Expression *, ir::Expression *, lexer::TokenType, lexer::SourcePosition> op,
    bool isEqualOp, std::tuple<checker::Type *, checker::Type *, Type *, Type *> types)>;

std::map<lexer::TokenType, CheckBinaryFunction> &GetCheckMap()
{
    static std::map<lexer::TokenType, CheckBinaryFunction> checkMap = {
        {lexer::TokenType::PUNCTUATOR_MULTIPLY, &ETSChecker::CheckBinaryOperatorMulDivMod},
        {lexer::TokenType::PUNCTUATOR_MULTIPLY_EQUAL, &ETSChecker::CheckBinaryOperatorMulDivMod},
        {lexer::TokenType::PUNCTUATOR_DIVIDE, &ETSChecker::CheckBinaryOperatorMulDivMod},
        {lexer::TokenType::PUNCTUATOR_DIVIDE_EQUAL, &ETSChecker::CheckBinaryOperatorMulDivMod},
        {lexer::TokenType::PUNCTUATOR_MOD, &ETSChecker::CheckBinaryOperatorMulDivMod},
        {lexer::TokenType::PUNCTUATOR_MOD_EQUAL, &ETSChecker::CheckBinaryOperatorMulDivMod},

        {lexer::TokenType::PUNCTUATOR_EXPONENTIATION, &ETSChecker::CheckBinaryOperatorExponentiation},
        {lexer::TokenType::PUNCTUATOR_EXPONENTIATION_EQUAL, &ETSChecker::CheckBinaryOperatorExponentiation},

        {lexer::TokenType::PUNCTUATOR_MINUS, &ETSChecker::CheckBinaryOperatorPlus},
        {lexer::TokenType::PUNCTUATOR_MINUS_EQUAL, &ETSChecker::CheckBinaryOperatorPlus},
        {lexer::TokenType::PUNCTUATOR_PLUS, &ETSChecker::CheckBinaryOperatorPlus},
        {lexer::TokenType::PUNCTUATOR_PLUS_EQUAL, &ETSChecker::CheckBinaryOperatorPlus},

        {lexer::TokenType::PUNCTUATOR_LEFT_SHIFT, &ETSChecker::CheckBinaryOperatorShift},
        {lexer::TokenType::PUNCTUATOR_LEFT_SHIFT_EQUAL, &ETSChecker::CheckBinaryOperatorShift},
        {lexer::TokenType::PUNCTUATOR_RIGHT_SHIFT, &ETSChecker::CheckBinaryOperatorShift},
        {lexer::TokenType::PUNCTUATOR_RIGHT_SHIFT_EQUAL, &ETSChecker::CheckBinaryOperatorShift},
        {lexer::TokenType::PUNCTUATOR_UNSIGNED_RIGHT_SHIFT, &ETSChecker::CheckBinaryOperatorShift},
        {lexer::TokenType::PUNCTUATOR_UNSIGNED_RIGHT_SHIFT_EQUAL, &ETSChecker::CheckBinaryOperatorShift},

        {lexer::TokenType::PUNCTUATOR_BITWISE_OR, &ETSChecker::CheckBinaryOperatorBitwise},
        {lexer::TokenType::PUNCTUATOR_BITWISE_OR_EQUAL, &ETSChecker::CheckBinaryOperatorBitwise},
        {lexer::TokenType::PUNCTUATOR_BITWISE_AND, &ETSChecker::CheckBinaryOperatorBitwise},
        {lexer::TokenType::PUNCTUATOR_BITWISE_AND_EQUAL, &ETSChecker::CheckBinaryOperatorBitwise},
        {lexer::TokenType::PUNCTUATOR_BITWISE_XOR, &ETSChecker::CheckBinaryOperatorBitwise},
        {lexer::TokenType::PUNCTUATOR_BITWISE_XOR_EQUAL, &ETSChecker::CheckBinaryOperatorBitwise},
    };

    return checkMap;
}

// CC-OFFNXT(G.FUN.01, huge_method) solid logic
static std::tuple<Type *, Type *> CheckBinaryOperatorHelper(ETSChecker *checker,
                                                            const BinaryOperatorParams &binaryParams,
                                                            const TypeParams &typeParams)
{
    ir::Expression *left = binaryParams.left;
    ir::Expression *right = binaryParams.right;
    lexer::SourcePosition pos = binaryParams.pos;
    checker::Type *const leftType = typeParams.leftType;
    checker::Type *const rightType = typeParams.rightType;
    checker::Type *tsType {};

    BinaryArithmOperands ops = GetBinaryOperands(checker, binaryParams.expr->AsBinaryExpression());
    BinaryArithmOperands opsRepaired = RepairTypeErrorsInOperands(checker, ops);

    switch (binaryParams.operationType) {
        case lexer::TokenType::PUNCTUATOR_LOGICAL_AND:
        case lexer::TokenType::PUNCTUATOR_LOGICAL_OR: {
            tsType = checker->CheckBinaryOperatorLogical(left, right, leftType, rightType, typeParams.unboxedL,
                                                         typeParams.unboxedR);
            break;
        }
        case lexer::TokenType::PUNCTUATOR_STRICT_EQUAL:
        case lexer::TokenType::PUNCTUATOR_NOT_STRICT_EQUAL:
        case lexer::TokenType::PUNCTUATOR_EQUAL:
        case lexer::TokenType::PUNCTUATOR_NOT_EQUAL: {
            if (auto res = CheckBinaryOperatorEqual(checker, opsRepaired); res != nullptr) {
                bool canEmitWarning = true;
                if (opsRepaired.typeL->IsETSReferenceType() && opsRepaired.typeR->IsETSReferenceType()) {
                    checker->Relation()->SetNode(left);
                    canEmitWarning = checker->CheckValidEqualReferenceType(opsRepaired.typeL, opsRepaired.typeR);
                }
                if (canEmitWarning) {
                    MaybeLogCompileTimeEqualityWarning(checker, opsRepaired, binaryParams.operationType, pos);
                }
                return {checker->GetGlobalTypesHolder()->GlobalETSBooleanBuiltinType(), res};
            }
            [[fallthrough]];
        }
        case lexer::TokenType::PUNCTUATOR_LESS_THAN:
        case lexer::TokenType::PUNCTUATOR_LESS_THAN_EQUAL:
        case lexer::TokenType::PUNCTUATOR_GREATER_THAN:
        case lexer::TokenType::PUNCTUATOR_GREATER_THAN_EQUAL: {
            return checker->CheckBinaryOperatorLessGreater(left, right, binaryParams.operationType, pos,
                                                           binaryParams.isEqualOp, leftType, rightType,
                                                           typeParams.unboxedL, typeParams.unboxedR);
        }
        case lexer::TokenType::KEYW_INSTANCEOF: {
            return checker->CheckBinaryOperatorInstanceOf(right, leftType, rightType, pos);
        }
        case lexer::TokenType::PUNCTUATOR_NULLISH_COALESCING: {
            tsType = checker->CheckBinaryOperatorNullishCoalescing(left, right, pos);
            break;
        }
        default: {
            ES2PANDA_UNREACHABLE();
            break;
        }
    }

    return {tsType, tsType};
}

static void TryAddValueOfFlagToStringEnumOperand(ir::Expression *op, const ir::Expression *otherOp)
{
    auto type = op->TsType();
    auto otherType = otherOp->TsType();
    if (type->IsETSStringEnumType() && (otherType->IsETSStringType() || otherType->IsETSStringEnumType())) {
        op->AddAstNodeFlags(ir::AstNodeFlags::GENERATE_VALUE_OF);
    }
}

static void TryAddValueOfFlagToNumericEnumOperand(ir::Expression *op, const ir::Expression *otherOp)
{
    auto type = op->TsType();
    auto otherType = otherOp->TsType();
    if (type->IsETSNumericEnumType() &&
        ((otherType->IsETSObjectType() && otherType->AsETSObjectType()->IsBoxedPrimitive()) ||
         otherType->IsETSNumericEnumType())) {
        op->AddAstNodeFlags(ir::AstNodeFlags::GENERATE_VALUE_OF);
    }
}

static void CheckEnumInOperatorContext(ir::Expression *expression, lexer::TokenType opType, ir::Expression *left,
                                       ir::Expression *right, ETSChecker *checker)
{
    auto [lType, rType] = std::tuple {left->TsType(), right->TsType()};

    switch (opType) {
        case lexer::TokenType::PUNCTUATOR_GREATER_THAN:
        case lexer::TokenType::PUNCTUATOR_GREATER_THAN_EQUAL:
        case lexer::TokenType::PUNCTUATOR_LESS_THAN:
        case lexer::TokenType::PUNCTUATOR_LESS_THAN_EQUAL: {
            if (lType->IsETSEnumType() && rType->IsETSEnumType() && !checker->Relation()->IsIdenticalTo(lType, rType)) {
                checker->LogError(diagnostic::BINOP_INCOMPARABLE, {}, expression->Start());
                return;
            }
            [[fallthrough]];
        }
        case lexer::TokenType::PUNCTUATOR_EQUAL:
        case lexer::TokenType::PUNCTUATOR_NOT_EQUAL:
        case lexer::TokenType::PUNCTUATOR_STRICT_EQUAL:
        case lexer::TokenType::PUNCTUATOR_NOT_STRICT_EQUAL: {
            if (lType->IsETSEnumType() && rType->IsETSEnumType() && !checker->Relation()->IsIdenticalTo(lType, rType)) {
                checker->LogDiagnostic(diagnostic::DIFFERENT_ENUM_EQUALITY, {}, expression->Start());
            }
            [[fallthrough]];
        }
        case lexer::TokenType::PUNCTUATOR_PLUS: {
            TryAddValueOfFlagToStringEnumOperand(left, right);
            TryAddValueOfFlagToStringEnumOperand(right, left);
            [[fallthrough]];
        }
        case lexer::TokenType::PUNCTUATOR_MULTIPLY:
        case lexer::TokenType::PUNCTUATOR_DIVIDE:
        case lexer::TokenType::PUNCTUATOR_MOD:
        case lexer::TokenType::PUNCTUATOR_MINUS:
        case lexer::TokenType::PUNCTUATOR_LEFT_SHIFT:
        case lexer::TokenType::PUNCTUATOR_RIGHT_SHIFT:
        case lexer::TokenType::PUNCTUATOR_UNSIGNED_RIGHT_SHIFT:
        case lexer::TokenType::PUNCTUATOR_BITWISE_AND:
        case lexer::TokenType::PUNCTUATOR_BITWISE_OR:
        case lexer::TokenType::PUNCTUATOR_BITWISE_XOR:
        case lexer::TokenType::PUNCTUATOR_LOGICAL_AND:
        case lexer::TokenType::PUNCTUATOR_LOGICAL_OR: {
            TryAddValueOfFlagToNumericEnumOperand(left, right);
            TryAddValueOfFlagToNumericEnumOperand(right, left);
            break;
        }
        default:
            // NOTE(dkofanov): What about the '+=' operation?
            break;
    }
}

std::tuple<Type *, Type *> ETSChecker::CheckArithmeticOperations(
    ir::Expression *expr, std::tuple<ir::Expression *, ir::Expression *, lexer::TokenType, lexer::SourcePosition> op,
    bool isEqualOp, std::tuple<checker::Type *, checker::Type *, Type *, Type *> types)
{
    auto [left, right, operationType, pos] = op;
    auto [leftType, rightType, unboxedL, unboxedR] = types;

    if (leftType->IsETSUnionType()) {
        leftType = GetNonConstantType(leftType);
    }

    if (rightType->IsETSUnionType()) {
        rightType = GetNonConstantType(rightType);
    }
    CheckEnumInOperatorContext(expr, operationType, left, right, this);

    const auto &checkMap = GetCheckMap();
    if (const auto &checkMapEntry = checkMap.find(operationType); checkMapEntry != checkMap.end()) {
        const auto &checkFunc = checkMapEntry->second;
        auto tsType = checkFunc(this, std::make_tuple(left, right, operationType, pos), isEqualOp,
                                std::make_tuple(leftType, rightType, unboxedL, unboxedR));
        if (tsType == nullptr) {
            return {leftType, rightType};
        }
        if (tsType->IsETSPrimitiveType()) {
            tsType = MaybeBoxType(tsType);
        }
        return {tsType, tsType};
    }

    // SUPPRESS_CSA_NEXTLINE(alpha.core.AllocatorETSCheckerHint)
    return CheckBinaryOperatorHelper(this, {left, right, expr, operationType, pos, isEqualOp},
                                     {leftType, rightType, unboxedL, unboxedR});
}

static std::tuple<Type *, Type *> ResolveCheckBinaryOperatorForBigInt(ETSChecker *checker, Type *leftType,
                                                                      Type *rightType, lexer::TokenType operationType,
                                                                      lexer::SourcePosition pos)
{
    if (HasBigIntNonBigIntEqualityPairing(checker, leftType, rightType)) {
        switch (operationType) {
            case lexer::TokenType::PUNCTUATOR_EQUAL:
            case lexer::TokenType::PUNCTUATOR_STRICT_EQUAL:
                checker->LogDiagnostic(diagnostic::EQUALITY_EXPRESSION_ALWAYS_FALSE, pos);
                break;
            case lexer::TokenType::PUNCTUATOR_NOT_EQUAL:
            case lexer::TokenType::PUNCTUATOR_NOT_STRICT_EQUAL:
                checker->LogDiagnostic(diagnostic::EQUALITY_EXPRESSION_ALWAYS_TRUE, pos);
                break;
            default:
                break;
        }
    }
    switch (operationType) {
        case lexer::TokenType::PUNCTUATOR_GREATER_THAN:
        case lexer::TokenType::PUNCTUATOR_LESS_THAN:
        case lexer::TokenType::PUNCTUATOR_GREATER_THAN_EQUAL:
        case lexer::TokenType::PUNCTUATOR_LESS_THAN_EQUAL:
        case lexer::TokenType::PUNCTUATOR_EQUAL:
        case lexer::TokenType::PUNCTUATOR_NOT_EQUAL:
        case lexer::TokenType::PUNCTUATOR_STRICT_EQUAL:
        case lexer::TokenType::PUNCTUATOR_NOT_STRICT_EQUAL:
            return {checker->GlobalETSBooleanBuiltinType(), checker->GlobalETSBooleanBuiltinType()};
        default:
            return {leftType, rightType};
    }
}

std::tuple<Type *, Type *> ETSChecker::CheckBinaryOperator(ir::Expression *left, ir::Expression *right,
                                                           ir::Expression *expr, lexer::TokenType operationType,
                                                           lexer::SourcePosition pos, bool forcePromotion)
{
    // SUPPRESS_CSA_NEXTLINE(alpha.core.AllocatorETSCheckerHint)
    checker::Type *leftType = left->Check(this);

    if (leftType == nullptr) {
        LogError(diagnostic::BINOP_UNEXPECTED_ERROR, {}, left->Start());
        auto rightType = right->Check(this);
        return {rightType, rightType};
    }

    if (operationType == lexer::TokenType::KEYW_INSTANCEOF) {
        AddStatus(checker::CheckerStatus::IN_INSTANCEOF_CONTEXT);
    }

    Context().CheckTestSmartCastCondition(operationType);
    // SUPPRESS_CSA_NEXTLINE(alpha.core.AllocatorETSCheckerHint)
    checker::Type *rightType = right->Check(this);

    if (right->IsTypeNode()) {
        rightType = right->AsTypeNode()->GetType(this);
    }

    if (operationType == lexer::TokenType::KEYW_INSTANCEOF) {
        RemoveStatus(checker::CheckerStatus::IN_INSTANCEOF_CONTEXT);
    }

    if (rightType == nullptr) {
        LogError(diagnostic::BINOP_UNEXPECTED_ERROR, {}, pos);
        return {leftType, leftType};
    }

    const bool isLogicalExtendedOperator = (operationType == lexer::TokenType::PUNCTUATOR_LOGICAL_AND) ||
                                           (operationType == lexer::TokenType::PUNCTUATOR_LOGICAL_OR);
    Type *unboxedL =
        isLogicalExtendedOperator ? MaybeUnboxConditionalInRelation(leftType) : MaybeUnboxInRelation(leftType);
    Type *unboxedR =
        isLogicalExtendedOperator ? MaybeUnboxConditionalInRelation(rightType) : MaybeUnboxInRelation(rightType);

    ES2PANDA_ASSERT(operationType != lexer::TokenType::PUNCTUATOR_SUBSTITUTION);
    bool isEqualOp = lexer::Token::IsBinaryLvalueToken(operationType) && !forcePromotion;

    if ((operationType == lexer::TokenType::PUNCTUATOR_EXPONENTIATION ||
         operationType == lexer::TokenType::PUNCTUATOR_EXPONENTIATION_EQUAL) &&
        IsNegativeBigIntLiteralExpression(right)) {
        LogError(diagnostic::EXPONENTIATION_BIGINT_NEGATIVE_EXPONENT, {}, right->Start());
        return {GlobalTypeError(), GlobalTypeError()};
    }

    if (leftType->IsETSBigIntType() && rightType->IsETSBigIntType() &&
        operationType == lexer::TokenType::PUNCTUATOR_DIVIDE && right->IsBigIntLiteral() &&
        IsBigIntZeroLiteral(right)) {
        LogError(diagnostic::BIGINT_DIVISION_BY_ZERO, {}, right->Start());
        return {GlobalTypeError(), GlobalTypeError()};
    }

    if (CheckBinaryOperatorForBigInt(leftType, rightType, operationType)) {
        // SUPPRESS_CSA_NEXTLINE(alpha.core.AllocatorETSCheckerHint)
        return ResolveCheckBinaryOperatorForBigInt(this, leftType, rightType, operationType, pos);
    }

    // SUPPRESS_CSA_NEXTLINE(alpha.core.AllocatorETSCheckerHint)
    return CheckArithmeticOperations(expr, std::make_tuple(left, right, operationType, pos), isEqualOp,
                                     std::make_tuple(leftType, rightType, unboxedL, unboxedR));
}

}  // namespace ark::es2panda::checker