#include "libasr/asr.h"
#include <unordered_set>
#include <map>
#include <libasr/asr_utils.h>
#include <libasr/string_utils.h>
#include <libasr/serialization.h>
#include <libasr/assert.h>
#include <libasr/asr_verify.h>
#include <libasr/utils.h>
#include <libasr/modfile.h>
#include <libasr/runtime/lfortran_float128_quadmath.h>
#include <libasr/pass/pass_utils.h>
#include <libasr/pass/intrinsic_function_registry.h>
#include <libasr/pass/intrinsic_subroutine_registry.h>
#include <libasr/pass/intrinsic_array_function_registry.h>

#include <libasr/asr_builder.h>

namespace LCompilers {

    namespace ASRUtils  {


/**
 * @param string_expr string to be casted
 * @param array_type type of array to cast to
 * @return ASR Cast node with cast_kind = `StringToArray`
*/
inline ASR::Cast_t* cast_string_to_array(Allocator &al, ASR::expr_t* const string_expr, ASR::ttype_t* const array_type){
    LCOMPILERS_ASSERT(is_string_only(expr_type(string_expr)))
    LCOMPILERS_ASSERT(is_array_of_strings(array_type))

    ASR::ttype_t*  const array_type_dup = ASRUtils::ExprStmtDuplicator(al).duplicate_ttype(array_type);
    ASR::Array_t*  const array_t = ASR::down_cast<ASR::Array_t>(type_get_past_allocatable_pointer(array_type_dup));
    ASR::String_t* const dest_string_t = get_string_type(array_type_dup);
    if (array_t->n_dims != 1) {
        ASRBuilder b(al, string_expr->base.loc);
        ASR::expr_t* known_size = b.i32(1);
        for (size_t i = 0; i < array_t->n_dims; i++) {
            if (array_t->m_dims[i].m_start == nullptr) {
                array_t->m_dims[i].m_start = b.i32(1);
            }
            if (array_t->m_dims[i].m_length != nullptr) {
                known_size = b.Mul(known_size, array_t->m_dims[i].m_length);
            }
        }
        ASR::expr_t* string_len = b.StringLen(string_expr);
        ASR::expr_t* elem_count = b.Div(string_len, dest_string_t->m_len);
        for (size_t i = 0; i < array_t->n_dims; i++) {
            if (array_t->m_dims[i].m_length == nullptr) {
                array_t->m_dims[i].m_length = b.Div(elem_count, known_size);
                known_size = b.Mul(known_size, array_t->m_dims[i].m_length);
            }
        }
       return ASR::down_cast2<ASR::Cast_t>(
                ASR::make_Cast_t(al, string_expr->base.loc, string_expr
                            , ASR::StringToArray, array_type_dup, nullptr, nullptr));
    }
    const bool is_length_present = dest_string_t->m_len_kind == ASR::ExpressionLength;
    const bool is_size_present = !is_dimension_empty(array_type_dup); 

    // Notice We're transforming a string into array;
    // Resulting array should have proper length + size. 
    if(is_length_present && is_size_present){ // character()
       return ASR::down_cast2<ASR::Cast_t>(
                ASR::make_Cast_t(al, string_expr->base.loc, string_expr
                            , ASR::StringToArray, array_type_dup, nullptr, nullptr));
    } else if(is_length_present && !is_size_present){
        ASRBuilder b(al, string_expr->base.loc);
        ASR::expr_t* const castedString_len = extract_kind_from_ttype_t(expr_type(dest_string_t->m_len)) != 4? 
                                                b.i2i_t(b.StringLen(string_expr), expr_type(dest_string_t->m_len)) 
                                              : b.StringLen(string_expr);
        ASR::expr_t* const arr_size = b.Div(castedString_len, dest_string_t->m_len);
        array_t->m_dims[0].m_start  = b.i32(1);
        array_t->m_dims[0].m_length = arr_size;
       return ASR::down_cast2<ASR::Cast_t>(
                ASR::make_Cast_t(al, string_expr->base.loc, string_expr
                , ASR::StringToArray, array_type_dup, nullptr, nullptr));
    } else if(!is_length_present && is_size_present){
        ASRBuilder b(al, string_expr->base.loc);
        dest_string_t->m_len = b.StringLen(string_expr);
        dest_string_t->m_len_kind = ASR::ExpressionLength;
       return ASR::down_cast2<ASR::Cast_t>(
                ASR::make_Cast_t(al, string_expr->base.loc, string_expr
                , ASR::StringToArray, array_type_dup, nullptr, nullptr));
    } else {
        ASRBuilder b(al, string_expr->base.loc);
        dest_string_t->m_len = b.StringLen(string_expr);
        dest_string_t->m_len_kind = ASR::ExpressionLength;
        array_t->m_dims[0].m_start  = b.i32(1);
        array_t->m_dims[0].m_length = b.i32(1);
       return ASR::down_cast2<ASR::Cast_t>(
                ASR::make_Cast_t(al, string_expr->base.loc, string_expr
                , ASR::StringToArray, array_type_dup, nullptr, nullptr));
    }

}

// depth-first graph traversal
void visit(
    std::string const& a,
    std::map<std::string, std::vector<std::string>> const& deps,
    std::unordered_set<std::string>& visited,
    std::vector<std::string>& result
) {
    visited.insert(a);
    auto it = deps.find(a);
    if (it != deps.end()) {
        for (auto n : it->second) {
            if (!visited.count(n)) visit(n, deps, visited, result);
        }
    }
    result.push_back(a);
}

std::vector<std::string> order_deps(std::map<std::string, std::vector<std::string>> const& deps) {
    // Compute ordering: depth-first graph traversal, inserting nodes on way back

    // set containing the visited nodes
    std::unordered_set<std::string> visited;

    // vector containing result
    std::vector<std::string> result;

    for (auto d : deps) {
        if (!visited.count(d.first)) {
            visit(d.first, deps, visited, result);
        }
    }
    return result;
}

std::vector<std::string> determine_module_dependencies(
        const ASR::TranslationUnit_t &unit)
{
    std::map<std::string, std::vector<std::string>> deps;
    for (auto &item : unit.m_symtab->get_scope()) {
        if (ASR::is_a<ASR::Module_t>(*item.second)) {
            std::string name = item.first;
            ASR::Module_t *m = ASR::down_cast<ASR::Module_t>(item.second);
            deps[name] = std::vector<std::string>();
            for (size_t i=0; i < m->n_dependencies; i++) {
                std::string dep = m->m_dependencies[i];
                deps[name].push_back(dep);
            }
        }
    }
    return order_deps(deps);
}

std::vector<std::string> determine_function_definition_order(
        SymbolTable* symtab) {
    std::map<std::string, std::vector<std::string>> func_dep_graph;
    ASR::symbol_t *sym;
    for( auto itr: symtab->get_scope() ) {
        if( ASR::is_a<ASR::Function_t>(*itr.second) ) {
            std::vector<std::string> deps;
            ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(itr.second);
            for( size_t i = 0; i < func->n_dependencies; i++ ) {
                std::string dep = func->m_dependencies[i];
                // Check if the dependent variable is present in the symtab.
                // This will help us to include only local dependencies, and we
                // assume that dependencies in the parent symtab are already declared
                // earlier.
                sym = symtab->get_symbol(dep);
                if (sym != nullptr && ASR::is_a<ASR::Function_t>(*sym))
                    deps.push_back(dep);
            }
            func_dep_graph[itr.first] = deps;
        }
    }
    return ASRUtils::order_deps(func_dep_graph);
}

std::vector<std::string> determine_class_procedure_declaration_order(
        SymbolTable* symtab) {
    std::map<std::string, std::vector<std::string>> func_dep_graph;
    ASR::symbol_t *sym;
    for( auto itr: symtab->get_scope() ) {
        if( ASR::is_a<ASR::StructMethodDeclaration_t>(*itr.second) ) {
            std::vector<std::string> deps;
            ASR::symbol_t* cp = ASR::down_cast<ASR::StructMethodDeclaration_t>(itr.second)->m_proc;
            if (ASR::is_a<ASR::Function_t>(*cp)) {
                ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(cp);
                for( size_t i = 0; i < func->n_dependencies; i++ ) {
                    std::string dep = func->m_dependencies[i];
                    // Check if the dependent variable is present in the symtab.
                    // This will help us to include only local dependencies, and we
                    // assume that dependencies in the parent symtab are already declared
                    // earlier.
                    sym = symtab->get_symbol(dep);
                    if (sym != nullptr && ASR::is_a<ASR::Function_t>(*sym))
                        deps.push_back(dep);
                }
                func_dep_graph[itr.first] = deps;
            }
        }
    }
    return ASRUtils::order_deps(func_dep_graph);
}

ASR::symbol_t* get_struct_sym_from_struct_expr(ASR::expr_t* expression)
{
    // The idea behind this utility function is that every struct expression
    // must eventually resolve to either `Var` or `StructInstanceMember`.
    switch (expression->type) {
        case ASR::exprType::Var: {
            // The symbol m_v has to be `Variable` or 'Function' for a Struct expression.
            if (ASR::is_a<ASR::Variable_t>(*ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(expression)->m_v))) {
                ASR::Variable_t* var = ASR::down_cast<ASR::Variable_t>(ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(expression)->m_v));
                if (var->m_type_declaration == nullptr) {
                    return nullptr;
                }
                return ASRUtils::symbol_get_past_external(var->m_type_declaration);
            } else if (ASR::is_a<ASR::Function_t>(*ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(expression)->m_v))) {
                ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(expression)->m_v));
                if (func->m_return_var != nullptr && ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(func->m_return_var))) {
                   return ASRUtils::get_struct_sym_from_struct_expr(func->m_return_var);
                } else {
                    for (size_t i = 0; i < func->n_args; i++) {
                        ASR::expr_t* arg = func->m_args[i];
                        if (arg != nullptr) {
                            ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(arg));
                            if (struct_sym != nullptr) {
                                return struct_sym;
                            }
                        }
                    }
                    return nullptr;
                }
            } else if (ASR::is_a<ASR::Struct_t>(*ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(expression)->m_v))) {
                // If the Var is a Struct, we return the symbol of the Struct.
                return ASR::down_cast<ASR::Var_t>(expression)->m_v;
            } else {
                throw LCompilersException("Expected Var to be either Variable or Function type, but found: " +
                                    std::to_string(ASR::down_cast<ASR::Var_t>(expression)->m_v->type));
            }
        }
        case ASR::exprType::StructInstanceMember: {
            ASR::StructInstanceMember_t* struct_instance_member = ASR::down_cast<ASR::StructInstanceMember_t>(expression);
            if (ASR::is_a<ASR::Struct_t>(*ASRUtils::symbol_get_past_external(struct_instance_member->m_m))) {
                // Special case: Can have `StructInstanceMember` like `var%member` where `member` is
                // parent struct of the struct used to declare `var`.
                // Please see assignment `c%parent_t = p` in
                // `integration_tests/derived_types_73.f90` for an example.
                return struct_instance_member->m_m;
            } else {
                LCOMPILERS_ASSERT(ASR::is_a<ASR::Variable_t>(*ASRUtils::symbol_get_past_external(struct_instance_member->m_m)));
                ASR::Variable_t* var = ASR::down_cast<ASR::Variable_t>(ASRUtils::symbol_get_past_external(struct_instance_member->m_m));
                return var->m_type_declaration;
            }
        }
        case ASR::exprType::ArrayConstructor: {
            ASR::ArrayConstructor_t* array_constructor = ASR::down_cast<ASR::ArrayConstructor_t>(expression);
            // First check if type_declaration is available
            if (array_constructor->m_struct_var != nullptr) {
                return get_struct_sym_from_struct_expr(array_constructor->m_struct_var);
            }
            for (size_t i = 0; i < array_constructor->n_args; i++) {
                ASR::expr_t* arg = array_constructor->m_args[i];
                if (arg != nullptr) {
                    ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(arg));
                    if (struct_sym != nullptr) {
                        return struct_sym;
                    }
                }
            }
            if (array_constructor->m_value != nullptr) {
                // If `m_value` is not null, it means that the array constructor
                // is returning a struct type.
                return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(array_constructor->m_value));
            }
            return nullptr; // If no struct symbol found in arguments or value
        }
        case ASR::exprType::ArrayItem: {
            ASR::ArrayItem_t* array_item = ASR::down_cast<ASR::ArrayItem_t>(expression);
            return ASRUtils::get_struct_sym_from_struct_expr(array_item->m_v);
        }
        case ASR::exprType::ArraySection: {
            ASR::ArraySection_t* array_section = ASR::down_cast<ASR::ArraySection_t>(expression);
            return ASRUtils::get_struct_sym_from_struct_expr(array_section->m_v);
        }
        case ASR::exprType::FunctionCall: {
            ASR::FunctionCall_t* func_call = ASR::down_cast<ASR::FunctionCall_t>(expression);
            ASR::Function_t* func = get_function(func_call->m_name);
            if (func == nullptr || func->m_return_var == nullptr) {
                return nullptr;
            }
            return ASRUtils::get_struct_sym_from_struct_expr(func->m_return_var);
        }
        case ASR::exprType::StructConstant: {
            ASR::StructConstant_t* struct_constant = ASR::down_cast<ASR::StructConstant_t>(expression);
            return struct_constant->m_dt_sym;
        }
        case ASR::exprType::ArrayPhysicalCast: {
            ASR::ArrayPhysicalCast_t* array_physical_cast = ASR::down_cast<ASR::ArrayPhysicalCast_t>(expression);
            // `array_physical_cast->m_arg` will be non-null for Struct expressions
            LCOMPILERS_ASSERT(array_physical_cast->m_arg != nullptr);
            return ASRUtils::get_struct_sym_from_struct_expr(array_physical_cast->m_arg);
        }
        case ASR::exprType::IntegerCompare: {
            ASR::IntegerCompare_t* int_compare = ASR::down_cast<ASR::IntegerCompare_t>(expression);
            ASR::symbol_t* left_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(int_compare->m_left));
            ASR::symbol_t* right_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(int_compare->m_right));
            if (left_struct_sym != nullptr) {
                return left_struct_sym;
            } else if (right_struct_sym != nullptr) {
                return right_struct_sym;
            } else {
                return nullptr; // If no struct symbol found in either side
            }
        }
        case ASR::exprType::RealCompare: {
            ASR::RealCompare_t* real_compare = ASR::down_cast<ASR::RealCompare_t>(expression);
            ASR::symbol_t* left_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(real_compare->m_left));
            ASR::symbol_t* right_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(real_compare->m_right));
            if (left_struct_sym != nullptr) {
                return left_struct_sym;
            } else if (right_struct_sym != nullptr) {
                return right_struct_sym;
            } else {
                return nullptr; // If no struct symbol found in either side
            }
        }
        case ASR::exprType::StringCompare: {
            ASR::StringCompare_t* string_compare = ASR::down_cast<ASR::StringCompare_t>(expression);
            ASR::symbol_t* left_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(string_compare->m_left));
            ASR::symbol_t* right_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(string_compare->m_right));
            if (left_struct_sym != nullptr) {
                return left_struct_sym;
            } else if (right_struct_sym != nullptr) {
                return right_struct_sym;
            } else {
                return nullptr; // If no struct symbol found in either side
            }
        }
        case ASR::exprType::ComplexCompare: {
            ASR::ComplexCompare_t* complex_compare = ASR::down_cast<ASR::ComplexCompare_t>(expression);
            ASR::symbol_t* left_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(complex_compare->m_left));
            ASR::symbol_t* right_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(complex_compare->m_right));
            if (left_struct_sym != nullptr) {
                return left_struct_sym;
            } else if (right_struct_sym != nullptr) {
                return right_struct_sym;
            } else {
                return nullptr; // If no struct symbol found in either side
            }
        }
        case ASR::exprType::IntrinsicArrayFunction: {
            ASR::IntrinsicArrayFunction_t* intrinsic_array_function = ASR::down_cast<ASR::IntrinsicArrayFunction_t>(expression);
            for (size_t i = 0; i < intrinsic_array_function->n_args; i++) {
                ASR::expr_t* arg = intrinsic_array_function->m_args[i];
                if (arg != nullptr) {
                    ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(arg));
                    if (struct_sym != nullptr) {
                        return struct_sym;
                    }
                }
            }
            // If no struct symbol found in arguments, return nullptr
            return nullptr;
        }
        case ASR::exprType::StringPhysicalCast: {
            ASR::StringPhysicalCast_t* string_physical_cast = ASR::down_cast<ASR::StringPhysicalCast_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(string_physical_cast->m_arg));
        }
        case ASR::exprType::IntrinsicImpureFunction: {
            ASR::IntrinsicImpureFunction_t* intrinsic_impure_function = ASR::down_cast<ASR::IntrinsicImpureFunction_t>(expression);
            for (size_t i = 0; i < intrinsic_impure_function->n_args; i++) {
                ASR::expr_t* arg = intrinsic_impure_function->m_args[i];
                if (arg != nullptr) {
                    ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(arg));
                    if (struct_sym != nullptr) {
                        return struct_sym;
                    }
                }
            }
            // If no struct symbol found in arguments, return nullptr
            return nullptr;
        }
        case ASR::exprType::IntrinsicElementalFunction: {
            ASR::IntrinsicElementalFunction_t* intrinsic_elemental_function = ASR::down_cast<ASR::IntrinsicElementalFunction_t>(expression);
            for (size_t i = 0; i < intrinsic_elemental_function->n_args; i++) {
                ASR::expr_t* arg = intrinsic_elemental_function->m_args[i];
                if (arg != nullptr) {
                    ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(arg));
                    if (struct_sym != nullptr) {
                        return struct_sym;
                    }
                }
            }
            // If no struct symbol found in arguments, return nullptr
            return nullptr;
        }
        case ASR::exprType::TypeInquiry: {
            ASR::TypeInquiry_t* type_inquiry = ASR::down_cast<ASR::TypeInquiry_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(type_inquiry->m_arg));
        }
        case ASR::exprType::RealUnaryMinus: {
            ASR::RealUnaryMinus_t* real_unary_minus = ASR::down_cast<ASR::RealUnaryMinus_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(real_unary_minus->m_arg));
        }
        case ASR::exprType::IntegerUnaryMinus: {
            ASR::IntegerUnaryMinus_t* int_unary_minus = ASR::down_cast<ASR::IntegerUnaryMinus_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(int_unary_minus->m_arg));
        }
        case ASR::exprType::RealBinOp: {
            ASR::RealBinOp_t* real_bin_op = ASR::down_cast<ASR::RealBinOp_t>(expression);
            ASR::symbol_t* left_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(real_bin_op->m_left));
            ASR::symbol_t* right_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(real_bin_op->m_right));
            if (left_struct_sym != nullptr) {
                return left_struct_sym;
            } else if (right_struct_sym != nullptr) {
                return right_struct_sym;
            } else {
                return nullptr; // If no struct symbol found in either side
            }
        }
        case ASR::exprType::IntegerBinOp: {
            ASR::IntegerBinOp_t* int_bin_op = ASR::down_cast<ASR::IntegerBinOp_t>(expression);
            ASR::symbol_t* left_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(int_bin_op->m_left));
            ASR::symbol_t* right_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(int_bin_op->m_right));
            if (left_struct_sym != nullptr) {
                return left_struct_sym;
            } else if (right_struct_sym != nullptr) {
                return right_struct_sym;
            } else {
                return nullptr; // If no struct symbol found in either side
            }
        }
        case ASR::exprType::LogicalBinOp: {
            ASR::LogicalBinOp_t* logical_bin_op = ASR::down_cast<ASR::LogicalBinOp_t>(expression);
            ASR::symbol_t* left_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(logical_bin_op->m_left));
            ASR::symbol_t* right_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(logical_bin_op->m_right));
            if (left_struct_sym != nullptr) {
                return left_struct_sym;
            } else if (right_struct_sym != nullptr) {
                return right_struct_sym;
            } else {
                return nullptr; // If no struct symbol found in either side
            }
        }
        case ASR::exprType::ComplexBinOp: {
            ASR::ComplexBinOp_t* complex_bin_op = ASR::down_cast<ASR::ComplexBinOp_t>(expression);
            ASR::symbol_t* left_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(complex_bin_op->m_left));
            ASR::symbol_t* right_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(complex_bin_op->m_right));
            if (left_struct_sym != nullptr) {
                return left_struct_sym;
            } else if (right_struct_sym != nullptr) {
                return right_struct_sym;
            } else {
                return nullptr; // If no struct symbol found in either side
            }
        }
        case ASR::exprType::StringConcat: {
            ASR::StringConcat_t* string_concat = ASR::down_cast<ASR::StringConcat_t>(expression);
            ASR::symbol_t* left_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(string_concat->m_left));
            ASR::symbol_t* right_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(string_concat->m_right));
            if (left_struct_sym != nullptr) {
                return left_struct_sym;
            } else if (right_struct_sym != nullptr) {
                return right_struct_sym;
            } else {
                return nullptr; // If no struct symbol found in either side
            }
        }
        case ASR::exprType::RealConstant:
        case ASR::exprType::StringConstant:
        case ASR::exprType::IntegerConstant:
        case ASR::exprType::LogicalConstant:
        case ASR::exprType::ArrayConstant: 
        case ASR::exprType::PointerNullConstant:
        case ASR::exprType::UnsignedIntegerConstant:
        case ASR::exprType::ComplexConstant:
        {
            // These do not have a struct symbol, return nullptr
            return nullptr;
        }
        case ASR::exprType::BitCast: {
            ASR::BitCast_t* bit_cast = ASR::down_cast<ASR::BitCast_t>(expression);
            return ASRUtils::symbol_get_past_external(
                ASRUtils::get_struct_sym_from_struct_expr(bit_cast->m_mold));
        }
        case ASR::exprType::ComplexConstructor: {
            ASR::ComplexConstructor_t* complex_constructor = ASR::down_cast<ASR::ComplexConstructor_t>(expression);
            ASR::symbol_t* real_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(complex_constructor->m_re));
            ASR::symbol_t* imag_struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(complex_constructor->m_im));
            if (real_struct_sym != nullptr) {
                return real_struct_sym;
            } else if (imag_struct_sym != nullptr) {
                return imag_struct_sym;
            } else {
                return nullptr; // If no struct symbol found in either part
            }
        }
        case ASR::exprType::Cast: {
            ASR::Cast_t* cast = ASR::down_cast<ASR::Cast_t>(expression);
            if (cast->m_dest) {
                return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(cast->m_dest));
            }
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(cast->m_arg));
        }
        case ASR::exprType::ArrayReshape: {
            ASR::ArrayReshape_t* array_reshape = ASR::down_cast<ASR::ArrayReshape_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(array_reshape->m_array));
        }
        case ASR::exprType::ArraySize: {
            return nullptr;
        }
        case ASR::exprType::LogicalNot: {
            ASR::LogicalNot_t* logical_not = ASR::down_cast<ASR::LogicalNot_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(logical_not->m_arg));
        }
        case ASR::exprType::ImpliedDoLoop: {
            ASR::ImpliedDoLoop_t* imp_dl = ASR::down_cast<ASR::ImpliedDoLoop_t>(expression);
            for (size_t i = 0; i < imp_dl->n_values; i++) {
                ASR::expr_t* arg = imp_dl->m_values[i];
                if (arg != nullptr) {
                    ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(arg));
                    if (struct_sym != nullptr) {
                        return struct_sym;
                    }
                }
            }
            if (imp_dl->m_value != nullptr) {
                return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(imp_dl->m_value));
            }
            return nullptr;
        }
        case ASR::exprType::StringSection: {
            ASR::StringSection_t* string_section = ASR::down_cast<ASR::StringSection_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(string_section->m_arg));
        }
        case ASR::exprType::ArrayBroadcast: {
            ASR::ArrayBroadcast_t* array_broadcast = ASR::down_cast<ASR::ArrayBroadcast_t>(expression);
            // `array_broadcast->m_v` will be non-null for Struct expressions
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(array_broadcast->m_array));
        }
        case ASR::exprType::ArrayBound: {
            return nullptr;
        }
        case ASR::exprType::ArrayRank: {
            return nullptr;
        }
        case ASR::exprType::StructConstructor: {
            ASR::StructConstructor_t* struct_constructor = ASR::down_cast<ASR::StructConstructor_t>(expression);
            return struct_constructor->m_dt_sym;
        }
        case ASR::exprType::OverloadedBinOp: {
            ASR::OverloadedBinOp_t* overloaded_bin_op = ASR::down_cast<ASR::OverloadedBinOp_t>(expression);
            return ASRUtils::get_struct_sym_from_struct_expr(overloaded_bin_op->m_overloaded);
        }
        case ASR::exprType::OverloadedUnaryMinus: {
            ASR::OverloadedUnaryMinus_t* overloaded_unary_minus = ASR::down_cast<ASR::OverloadedUnaryMinus_t>(expression);
            return ASRUtils::get_struct_sym_from_struct_expr(overloaded_unary_minus->m_overloaded);
        }
        case ASR::exprType::OverloadedCompare: {
            ASR::OverloadedCompare_t* overloaded_compare = ASR::down_cast<ASR::OverloadedCompare_t>(expression);
            return ASRUtils::get_struct_sym_from_struct_expr(overloaded_compare->m_overloaded);
        }
        case ASR::exprType::OverloadedStringConcat: {
            ASR::OverloadedStringConcat_t* overloaded_string_concat = ASR::down_cast<ASR::OverloadedStringConcat_t>(expression);
            return ASRUtils::get_struct_sym_from_struct_expr(overloaded_string_concat->m_overloaded);
        }
        case ASR::exprType::OverloadedBoolOp: {
            ASR::OverloadedBoolOp_t* overloaded_bool_op = ASR::down_cast<ASR::OverloadedBoolOp_t>(expression);
            return ASRUtils::get_struct_sym_from_struct_expr(overloaded_bool_op->m_overloaded);
        }
        case ASR::exprType::StringItem: {
            ASR::StringItem_t* string_item = ASR::down_cast<ASR::StringItem_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(string_item->m_arg));
        }
        case ASR::exprType::ComplexRe: {
            ASR::ComplexRe_t* complex_re = ASR::down_cast<ASR::ComplexRe_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(complex_re->m_arg));
        }
        case ASR::exprType::ComplexIm: {
            ASR::ComplexIm_t* complex_im = ASR::down_cast<ASR::ComplexIm_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(complex_im->m_arg));
        }
        case ASR::exprType::StringLen: {
            ASR::StringLen_t* string_len = ASR::down_cast<ASR::StringLen_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(string_len->m_arg));
        }
        case ASR::exprType::ComplexUnaryMinus: {
            ASR::ComplexUnaryMinus_t* complex_unary_minus = ASR::down_cast<ASR::ComplexUnaryMinus_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(complex_unary_minus->m_arg));
        }
        case ASR::exprType::Iachar: {
            ASR::Iachar_t* iachar = ASR::down_cast<ASR::Iachar_t>(expression);
            return ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(iachar->m_arg));
        }
        case ASR::exprType::ListLen:
        case ASR::exprType::ListConstant:
        case ASR::exprType::ListConcat:
        case ASR::exprType::PointerAssociated:
        case ASR::exprType::PointerToCPtr:
        case ASR::exprType::GetPointer:
        case ASR::exprType::CLoc:
        case ASR::exprType::FunctionParam: {
            return nullptr;
        }
        case ASR::exprType::UnionInstanceMember: {
            ASR::UnionInstanceMember_t* union_instance_member = ASR::down_cast<ASR::UnionInstanceMember_t>(expression);
            LCOMPILERS_ASSERT(ASR::is_a<ASR::Variable_t>(*ASRUtils::symbol_get_past_external(union_instance_member->m_m)));
            ASR::Variable_t* var = ASR::down_cast<ASR::Variable_t>(ASRUtils::symbol_get_past_external(union_instance_member->m_m));
            return var->m_type_declaration;
        }
        default: {
            throw LCompilersException("get_struct_sym_from_struct_expr() not implemented for "
                                + std::to_string(expression->type));
            return nullptr;
        }
    }
}

void set_struct_sym_to_struct_expr(ASR::expr_t* expression, ASR::symbol_t* struct_sym) {
    // The idea behind this utility function is that every struct expression
    // must eventually resolve to either `Var` or `StructInstanceMember`.
    switch (expression->type) {
        case ASR::exprType::Var: {
            ASR::Var_t* var = ASR::down_cast<ASR::Var_t>(expression);
            LCOMPILERS_ASSERT(ASR::is_a<ASR::Variable_t>(*ASRUtils::symbol_get_past_external(var->m_v)));
            ASR::Variable_t* variable = ASR::down_cast<ASR::Variable_t>(ASRUtils::symbol_get_past_external(var->m_v));
            variable->m_type_declaration = struct_sym;
            return;
        } 
        case ASR::exprType::StructInstanceMember: {
            ASR::StructInstanceMember_t* struct_instance_member = ASR::down_cast<ASR::StructInstanceMember_t>(expression);
            ASR::Variable_t* variable = ASR::down_cast<ASR::Variable_t>(struct_instance_member->m_m);
            variable->m_type_declaration = struct_sym;
            return;
        } 
        case ASR::exprType::ArrayItem: {
            ASR::ArrayItem_t* array_item = ASR::down_cast<ASR::ArrayItem_t>(expression);
            return set_struct_sym_to_struct_expr(array_item->m_v, struct_sym);
        }
        case ASR::exprType::ArraySection: {
            ASR::ArraySection_t* array_section = ASR::down_cast<ASR::ArraySection_t>(expression);
            return set_struct_sym_to_struct_expr(array_section->m_v, struct_sym);
        }
        case ASR::exprType::FunctionCall: {
            ASR::FunctionCall_t* func_call = ASR::down_cast<ASR::FunctionCall_t>(expression);
            // `func_call->m_dt` will be non-null for Struct expressions
            LCOMPILERS_ASSERT(func_call->m_dt != nullptr);
            return set_struct_sym_to_struct_expr(func_call->m_dt, struct_sym);
        }
        case ASR::exprType::StructConstant: {
            ASR::StructConstant_t* struct_constant = ASR::down_cast<ASR::StructConstant_t>(expression);
            struct_constant->m_dt_sym = struct_sym;
            return;
        }
        default: {
            throw LCompilersException("set_struct_sym_to_struct_expr() not implemented for "
                                + std::to_string(expression->type));
        }
    }

}

ASR::symbol_t* get_union_sym_from_union_expr(ASR::expr_t* expression)
{
    switch (expression->type) {
        case ASR::exprType::Var: {
            // The symbol m_v has to be `Variable` or 'Function' for a Union expression.
            if (ASR::is_a<ASR::Variable_t>(*ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(expression)->m_v))) {
                ASR::Variable_t* var = ASR::down_cast<ASR::Variable_t>(ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(expression)->m_v));
                return var->m_type_declaration;
            } else if (ASR::is_a<ASR::Function_t>(*ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(expression)->m_v))) {
                ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(expression)->m_v));
                if (func->m_return_var != nullptr && ASRUtils::symbol_get_past_external(ASRUtils::get_union_sym_from_union_expr(func->m_return_var))) {
                   return ASRUtils::symbol_get_past_external(ASRUtils::get_union_sym_from_union_expr(func->m_return_var));
                } else {
                    for (size_t i = 0; i < func->n_args; i++) {
                        ASR::expr_t* arg = func->m_args[i];
                        if (arg != nullptr) {
                            ASR::symbol_t* union_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_union_sym_from_union_expr(arg));
                            if (union_sym != nullptr) {
                                return union_sym;
                            }
                        }
                    }
                    return nullptr;
                }
            } else if (ASR::is_a<ASR::Union_t>(*ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(expression)->m_v))) {
                // If the Var is a Union, we return the symbol of the Union.
                return ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(expression)->m_v);
            } else {
                throw LCompilersException("Expected Var to be either Variable or Function type, but found: " +
                                    std::to_string(ASR::down_cast<ASR::Var_t>(expression)->m_v->type));
            }
        }
        case ASR::exprType::UnionInstanceMember: {
            ASR::UnionInstanceMember_t* union_instance_member = ASR::down_cast<ASR::UnionInstanceMember_t>(expression);
            ASR::Variable_t* var = ASR::down_cast<ASR::Variable_t>(ASRUtils::symbol_get_past_external(union_instance_member->m_m));
            return var->m_type_declaration;
        }
        case ASR::exprType::FunctionCall: {
            ASR::FunctionCall_t* func_call = ASR::down_cast<ASR::FunctionCall_t>(expression);
            // `func_call->m_dt` will be non-null for Union expressions
            if ( func_call->m_dt != nullptr ){
                // If `func_call->m_dt` is not null, it means that the function call
                // is returning a union type.
                return ASRUtils::symbol_get_past_external(ASRUtils::get_union_sym_from_union_expr(func_call->m_dt));
            } else {
                ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(ASRUtils::symbol_get_past_external(func_call->m_name));
                return ASRUtils::symbol_get_past_external(ASRUtils::get_union_sym_from_union_expr(func->m_return_var));
            }
        }
        case ASR::exprType::UnionConstructor: {
            ASR::UnionConstructor_t* union_constructor = ASR::down_cast<ASR::UnionConstructor_t>(expression);
            return union_constructor->m_dt_sym;
        }
        default: {
            throw LCompilersException("get_union_sym_from_union_expr() not implemented for "
                                + std::to_string(expression->type));
            return nullptr;
        }
    }
}


// Recursively fetch `ASR::Function_t` from an `ASR::expr_t` if it has `FunctionType`.
const ASR::Function_t* get_function_from_expr(ASR::expr_t* expr) {
    if (!expr) {
        throw LCompilersException("Passed `ASR::expr_t expr` is nullptr.");
    }
    if (!ASR::is_a<ASR::FunctionType_t>(*ASRUtils::extract_type(ASRUtils::expr_type(expr)))) {
        throw LCompilersException("`ttype_t` of passed `ASR::expr_t expr` is not `ASR::exprType::FunctionType`.");
    }

    switch (expr->type) {
        case ASR::exprType::Var: {
            ASR::Var_t* var = ASR::down_cast<ASR::Var_t>(expr);
            ASR::symbol_t* sym = ASRUtils::symbol_get_past_external(var->m_v);
            if (ASR::is_a<ASR::Function_t>(*sym)) {
                return ASR::down_cast<ASR::Function_t>(sym);
            } else if ( ASR::is_a<ASR::Variable_t>(*sym) ) {
                // case: procedure variables
                ASR::Variable_t* var_sym = ASR::down_cast<ASR::Variable_t>(sym);
                ASR::symbol_t* type_decl = ASRUtils::symbol_get_past_external(var_sym->m_type_declaration);
                if (type_decl != nullptr && 
                    ASR::is_a<ASR::Function_t>(*type_decl)) {
                    return ASR::down_cast<ASR::Function_t>(type_decl);
                } else {
                    throw LCompilersException("`ASR::Var_t` symbol is a `ASR::Variable_t` without type declared");
                }
            } else {
                throw LCompilersException("`ASR::Var_t` symbol is not `ASR::Function_t`, rather: " + std::to_string(sym->type));
            }
        }
        case ASR::exprType::StructInstanceMember: {
            ASR::StructInstanceMember_t* sim = ASR::down_cast<ASR::StructInstanceMember_t>(expr);
            ASR::symbol_t* sym = ASRUtils::symbol_get_past_external(sim->m_m);
            if (ASR::is_a<ASR::Function_t>(*sym)) {
                return ASR::down_cast<ASR::Function_t>(sym);
            } else if ( ASR::is_a<ASR::Variable_t>(*sym) ) {
                // case: procedure variables
                ASR::Variable_t* var_sym = ASR::down_cast<ASR::Variable_t>(sym);
                ASR::symbol_t* type_decl = ASRUtils::symbol_get_past_external(var_sym->m_type_declaration);
                if (type_decl != nullptr && 
                    ASR::is_a<ASR::Function_t>(*type_decl)) {
                    return ASR::down_cast<ASR::Function_t>(type_decl);
                } else {
                    throw LCompilersException("`ASR::Var_t` symbol is a `ASR::Variable_t` without type declared");
                }
            } else {
                throw LCompilersException("`ASR::Var_t` symbol is not `ASR::Function_t`, rather: " + std::to_string(sym->type));
            }
            return get_function_from_expr(sim->m_v);
        }
        case ASR::exprType::ArrayItem: {
            ASR::ArrayItem_t* ai = ASR::down_cast<ASR::ArrayItem_t>(expr);
            return get_function_from_expr(ai->m_v);
        }
        case ASR::exprType::ArraySection: {
            ASR::ArraySection_t* as = ASR::down_cast<ASR::ArraySection_t>(expr);
            return get_function_from_expr(as->m_v);
        }
        case ASR::exprType::FunctionCall: {
            ASR::FunctionCall_t* fc = ASR::down_cast<ASR::FunctionCall_t>(expr);
            ASR::symbol_t* sym = ASRUtils::symbol_get_past_external(fc->m_name);
            if (ASR::is_a<ASR::Function_t>(*sym)) {
                ASR::Function_t* called_func = ASR::down_cast<ASR::Function_t>(sym);
                if (called_func->m_return_var != nullptr) {
                    ASR::ttype_t* ret_type = ASRUtils::extract_type(
                        ASRUtils::expr_type(called_func->m_return_var));
                    if (ASR::is_a<ASR::FunctionType_t>(*ret_type)
                            && ASR::is_a<ASR::Var_t>(*called_func->m_return_var)) {
                        // The call returns a procedure pointer. Resolve it
                        // directly (without recursing into get_function_from_expr)
                        // to avoid an infinite loop when the return variable's
                        // type_declaration points back at `called_func` itself.
                        ASR::Var_t* rv = ASR::down_cast<ASR::Var_t>(called_func->m_return_var);
                        ASR::symbol_t* rv_sym = ASRUtils::symbol_get_past_external(rv->m_v);
                        if (ASR::is_a<ASR::Variable_t>(*rv_sym)) {
                            ASR::Variable_t* rv_var = ASR::down_cast<ASR::Variable_t>(rv_sym);
                            ASR::symbol_t* td = rv_var->m_type_declaration;
                            if (td != nullptr) {
                                td = ASRUtils::symbol_get_past_external(td);
                                if (td != nullptr
                                        && ASR::is_a<ASR::Function_t>(*td)
                                        && td != (ASR::symbol_t*)called_func) {
                                    return ASR::down_cast<ASR::Function_t>(td);
                                }
                            }
                        }
                    }
                }
                return called_func;
            } else if (ASR::is_a<ASR::Variable_t>(*sym)) {
                ASR::Variable_t* var_sym = ASR::down_cast<ASR::Variable_t>(sym);
                ASR::symbol_t* type_decl = ASRUtils::symbol_get_past_external(var_sym->m_type_declaration);
                if (type_decl != nullptr &&
                    ASR::is_a<ASR::Function_t>(*type_decl)) {
                    return ASR::down_cast<ASR::Function_t>(type_decl);
                }
            }
            return nullptr;
        }
        case ASR::exprType::PointerNullConstant: {
            // PointerNullConstant is a special case where it does not have a function
            // associated with it, so we return nullptr.
            return nullptr;
        }
        default:
            throw LCompilersException("get_function_from_expr() not implemented for "
                                + std::to_string(expr->type));
    }
}

std::vector<std::string> determine_variable_declaration_order(
         SymbolTable* symtab) {
    std::map<std::string, std::vector<std::string>> var_dep_graph;
    for( auto itr: symtab->get_scope() ) {
        if( ASR::is_a<ASR::Variable_t>(*itr.second) ) {
            std::vector<std::string> deps;
            ASR::Variable_t* var = ASR::down_cast<ASR::Variable_t>(itr.second);
            for( size_t i = 0; i < var->n_dependencies; i++ ) {
                std::string dep = var->m_dependencies[i];
                // Check if the dependent variable is present in the symtab.
                // This will help us to include only local dependencies, and we
                // assume that dependencies in the parent symtab are already declared
                // earlier.
                if (symtab->get_symbol(dep) != nullptr)
                    deps.push_back(dep);
            }
            var_dep_graph[itr.first] = deps;
        }
    }
    return ASRUtils::order_deps(var_dep_graph);
}

void extract_module_python(const ASR::TranslationUnit_t &m,
                std::vector<std::pair<std::string, ASR::Module_t*>>& children_modules,
                std::string module_name) {
    bool module_found = false;
    for (auto &a : m.m_symtab->get_scope()) {
        if( ASR::is_a<ASR::Module_t>(*a.second) ) {
            if( a.first == "__main__" ) {
                module_found = true;
                children_modules.push_back(std::make_pair(module_name,
                    ASR::down_cast<ASR::Module_t>(a.second)));
            } else {
                children_modules.push_back(std::make_pair(a.first,
                    ASR::down_cast<ASR::Module_t>(a.second)));
            }
        }
    }
    if( !module_found ) {
        throw LCompilersException("ICE: Module not found");
    }
}

void update_call_args(Allocator &al, SymbolTable *current_scope, bool implicit_interface,
        std::map<std::string, ASR::symbol_t*> changed_external_function_symbol) {
    /*
        Iterate over body of program, check if there are any subroutine calls if yes, iterate over its args
        and update the args if they are equal to the old symbol
        For example:
            function func(f)
                double precision c
                call sub2(c)
                print *, c(d)
            end function
        This function updates `sub2` to use the new symbol `c` that is now a function, not a variable.
        Along with this, it also updates the args of `sub2` to use the new symbol `c` instead of the old one.
    */

    class ArgsReplacer : public ASR::BaseExprReplacer<ArgsReplacer> {
    public:
        Allocator &al;
        ASR::symbol_t* new_sym;

        ArgsReplacer(Allocator &al_) : al(al_) {}

        void replace_Var(ASR::Var_t* x) {
            *current_expr = ASRUtils::EXPR(ASR::make_Var_t(al, x->base.base.loc, new_sym));
        }
    };

    class ArgsVisitor : public ASR::CallReplacerOnExpressionsVisitor<ArgsVisitor>
    {
        public:
        Allocator &al;
        SymbolTable* scope = current_scope;
        ArgsReplacer replacer;
        std::map<std::string, ASR::symbol_t*> &changed_external_function_symbol;
        ArgsVisitor(Allocator &al_, std::map<std::string, ASR::symbol_t*> &changed_external_function_symbol_) : al(al_), replacer(al_),
                    changed_external_function_symbol(changed_external_function_symbol_) {}

        void call_replacer_(ASR::symbol_t* new_sym) {
            replacer.current_expr = current_expr;
            replacer.new_sym = new_sym;
            replacer.replace_expr(*current_expr);
        }

        ASR::symbol_t* fetch_sym(ASR::symbol_t* arg_sym_underlying) {
            ASR::symbol_t* sym = nullptr;
            if (ASR::is_a<ASR::Variable_t>(*arg_sym_underlying)) {
                ASR::Variable_t* arg_variable = ASR::down_cast<ASR::Variable_t>(arg_sym_underlying);
                std::string arg_variable_name = std::string(arg_variable->m_name);
                sym = arg_variable->m_parent_symtab->get_symbol(arg_variable_name);
            } else if (ASR::is_a<ASR::Function_t>(*arg_sym_underlying)) {
                ASR::Function_t* arg_function = ASR::down_cast<ASR::Function_t>(arg_sym_underlying);
                std::string arg_function_name = std::string(arg_function->m_name);
                sym = arg_function->m_symtab->parent->get_symbol(arg_function_name);
            }
            return sym;
        }

        void handle_Var(ASR::expr_t* arg_expr, ASR::expr_t** expr_to_replace) {
            if (arg_expr && ASR::is_a<ASR::Var_t>(*arg_expr)) {
                ASR::Var_t* arg_var = ASR::down_cast<ASR::Var_t>(arg_expr);
                ASR::symbol_t* arg_sym = arg_var->m_v;
                ASR::symbol_t* arg_sym_underlying = ASRUtils::symbol_get_past_external(arg_sym);
                ASR::symbol_t* sym = fetch_sym(arg_sym_underlying);
                if (sym != arg_sym_underlying) {
                    ASR::expr_t** current_expr_copy = current_expr;
                    current_expr = const_cast<ASR::expr_t**>((expr_to_replace));
                    this->call_replacer_(sym);
                    current_expr = current_expr_copy;
                }
            }
        }


        void visit_SubroutineCall(const ASR::SubroutineCall_t& x) {
            ASR::SubroutineCall_t* subrout_call = (ASR::SubroutineCall_t*)(&x);
            for (size_t j = 0; j < subrout_call->n_args; j++) {
                ASR::call_arg_t arg = subrout_call->m_args[j];
                ASR::expr_t* arg_expr = arg.m_value;
                handle_Var(arg_expr, &(subrout_call->m_args[j].m_value));
            }
        }

        void visit_FunctionCall(const ASR::FunctionCall_t& x) {
            ASR::FunctionCall_t* func_call = (ASR::FunctionCall_t*)(&x);
            for (size_t j = 0; j < func_call->n_args; j++) {
                ASR::call_arg_t arg = func_call->m_args[j];
                ASR::expr_t* arg_expr = arg.m_value;
                handle_Var(arg_expr, &(func_call->m_args[j].m_value));
            }
        }

        void visit_Function(const ASR::Function_t& x) {
            ASR::Function_t* func = (ASR::Function_t*)(&x);
            scope = func->m_symtab;
            ASRUtils::SymbolDuplicator symbol_duplicator(al);
            std::map<std::string, ASR::symbol_t*> scope_ = scope->get_scope();
            std::vector<std::string> symbols_to_duplicate;
            for (auto it: scope_) {
                if (changed_external_function_symbol.find(it.first) != changed_external_function_symbol.end() &&
                    is_external_sym_changed(it.second, changed_external_function_symbol[it.first])) {
                    symbols_to_duplicate.push_back(it.first);
                }
            }

            for (auto it: symbols_to_duplicate) {
                scope->erase_symbol(it);
                symbol_duplicator.duplicate_symbol(changed_external_function_symbol[it], scope);
            }

            for (size_t i = 0; i < func->n_args; i++) {
                ASR::expr_t* arg_expr = func->m_args[i];
                if (ASR::is_a<ASR::Var_t>(*arg_expr)) {
                    ASR::Var_t* arg_var = ASR::down_cast<ASR::Var_t>(arg_expr);
                    ASR::symbol_t* arg_sym = arg_var->m_v;
                    ASR::symbol_t* arg_sym_underlying = ASRUtils::symbol_get_past_external(arg_sym);
                    ASR::symbol_t* sym = fetch_sym(arg_sym_underlying);
                    if (sym != arg_sym_underlying) {
                        ASR::expr_t** current_expr_copy = current_expr;
                        current_expr = const_cast<ASR::expr_t**>(&(func->m_args[i]));
                        this->call_replacer_(sym);
                        current_expr = current_expr_copy;
                    }
                }
            }
            scope = func->m_symtab;
            for (auto &it: scope->get_scope()) {
                visit_symbol(*it.second);
            }
            scope = func->m_symtab;
            for (size_t i = 0; i < func->n_body; i++) {
                visit_stmt(*func->m_body[i]);
            }
            scope = func->m_symtab;
        }
    };

    if (implicit_interface) {
        ArgsVisitor v(al, changed_external_function_symbol);
        SymbolTable *tu_symtab = ASRUtils::get_tu_symtab(current_scope);
        ASR::asr_t* asr_ = tu_symtab->asr_owner;
        ASR::TranslationUnit_t* tu = ASR::down_cast2<ASR::TranslationUnit_t>(asr_);
        v.visit_TranslationUnit(*tu);
    }
}

ASR::Module_t* extract_module(const ASR::TranslationUnit_t &m) {
    LCOMPILERS_ASSERT(m.m_symtab->get_scope().size()== 1);
    for (auto &a : m.m_symtab->get_scope()) {
        LCOMPILERS_ASSERT(ASR::is_a<ASR::Module_t>(*a.second));
        return ASR::down_cast<ASR::Module_t>(a.second);
    }
    throw LCompilersException("ICE: Module not found");
}

void fix_translation_unit(Allocator &al, ASR::TranslationUnit_t *tu, SymbolTable *symtab, bool run_verify) {
    fix_external_symbols(*tu, *symtab);
    PassUtils::UpdateDependenciesVisitor v(al);
    v.visit_TranslationUnit(*tu);
    if (run_verify) {
#if defined(WITH_LFORTRAN_ASSERT)
        diag::Diagnostics diagnostics;
        if (!asr_verify(*tu, true, diagnostics)) {
            std::cerr << diagnostics.render2();
            throw LCompilersException("Verify failed");
        };
#endif
    }
}

ASR::Module_t* load_module(Allocator &al, SymbolTable *symtab,
                            const std::string &module_name,
                            const Location &loc, bool intrinsic,
                            std::set<std::string> &loaded_submodules,
                            LCompilers::PassOptions& pass_options,
                            bool run_verify,
                            const std::function<void (const std::string &, const Location &)> err,
                            LCompilers::LocationManager &lm,
                            bool generate_object_code,
                            bool load_submodules) {
    LCOMPILERS_ASSERT(symtab);
    if (symtab->get_symbol(module_name) != nullptr) {
        ASR::symbol_t *m = symtab->get_symbol(module_name);
        if (ASR::is_a<ASR::Module_t>(*m)) {
            return ASR::down_cast<ASR::Module_t>(m);
        } else {
            err("The symbol '" + module_name + "' is not a module", loc);
        }
    }
    LCOMPILERS_ASSERT(symtab->parent == nullptr);
    ASR::TranslationUnit_t* mod1 = nullptr;
    Result<ASR::TranslationUnit_t*, ErrorMessage> res
        = find_and_load_module(al, module_name, *symtab, intrinsic, pass_options, lm);
    std::string error_message = "Module '" + module_name + "' not declared in the current source and the modfile was not found";
    if (res.ok) {
        mod1 = res.result;
    } else {
        error_message = res.error.message;
        if (!intrinsic) {
            // Module not found as a regular module. Try intrinsic module
            if (module_name == "iso_c_binding"
                ||module_name == "iso_fortran_env"
                ||module_name == "ieee_arithmetic") {
                Result<ASR::TranslationUnit_t*, ErrorMessage> res
                    = find_and_load_module(al, "lfortran_intrinsic_" + module_name,
                        *symtab, true, pass_options, lm);
                if (res.ok) {
                    mod1 = res.result;
                } else {
                    error_message = res.error.message;
                }
            }
        }
    }
    if (mod1 == nullptr) {
        err(error_message, loc);
    }
    ASR::Module_t *mod2 = extract_module(*mod1);
    symtab->add_symbol(module_name, (ASR::symbol_t*)mod2);
    mod2->m_symtab->parent = symtab;
    mod2->m_loaded_from_mod = true;
    if ( generate_object_code && !startswith(mod2->m_name, "lfortran_intrinsic") ) {
        mod2->m_symtab->mark_all_variables_external(al);
    }
    LCOMPILERS_ASSERT(symtab->resolve_symbol(module_name));

    // Create a temporary TranslationUnit just for fixing the symbols
    ASR::asr_t *orig_asr_owner = symtab->asr_owner;
    ASR::TranslationUnit_t *tu
        = ASR::down_cast2<ASR::TranslationUnit_t>(ASR::make_TranslationUnit_t(al, loc,
            symtab, nullptr, 0));

    // Load any dependent modules recursively
    bool rerun = true;
    while (rerun) {
        rerun = false;
        std::vector<std::string> modules_list
            = determine_module_dependencies(*tu);
        for (auto &item : modules_list) {
            if (symtab->get_symbol(item)
                    == nullptr) {
                // A module that was loaded requires to load another
                // module

                // This is not very robust, we should store that information
                // in the ASR itself, or encode in the name in a robust way,
                // such as using `module_name@intrinsic`:
                bool is_intrinsic = startswith(item, "lfortran_intrinsic");
                ASR::TranslationUnit_t *mod1 = nullptr;
                Result<ASR::TranslationUnit_t*, ErrorMessage> res
                    = find_and_load_module(al, item, *symtab, is_intrinsic, pass_options, lm);
                std::string error_message = "Module '" + item + "' modfile was not found";
                if (res.ok) {
                    mod1 = res.result;
                } else {
                    error_message =  res.error.message;
                    if (!is_intrinsic) {
                        // Module not found as a regular module. Try intrinsic module
                        if (item == "iso_c_binding"
                            ||item == "iso_fortran_env") {
                            Result<ASR::TranslationUnit_t*, ErrorMessage> res
                                = find_and_load_module(al, "lfortran_intrinsic_" + item,
                                *symtab, true, pass_options, lm);
                            if (res.ok) {
                                mod1 = res.result;
                            } else {
                                error_message =  res.error.message;
                            }
                        }
                    }
                }

                if (mod1 == nullptr) {
                    err(error_message, loc);
                }
                ASR::Module_t *mod2 = extract_module(*mod1);
                symtab->add_symbol(item, (ASR::symbol_t*)mod2);
                mod2->m_symtab->parent = symtab;
                mod2->m_loaded_from_mod = true;
                if ( generate_object_code && !startswith(mod2->m_name, "lfortran_intrinsic") ) {
                    mod2->m_symtab->mark_all_variables_external(al);
                }
                rerun = true;
            }
        }
    }

    if (load_submodules) {
        load_dependent_submodules(al, symtab, mod2, loc,
                                  loaded_submodules, pass_options,
                                  run_verify, err, lm);
    }

    // Check that all modules are included in ASR now
    std::vector<std::string> modules_list
        = determine_module_dependencies(*tu);
    for (auto &item : modules_list) {
        if (symtab->get_symbol(item) == nullptr) {
            err("ICE: Module '" + item + "' modfile was not found, but should have", loc);
        }
    }

    // Fix all external symbols and update dependencies
    fix_translation_unit(al, tu, symtab, run_verify);
    symtab->asr_owner = orig_asr_owner;

    return mod2;
}

void load_dependent_submodules(Allocator &al, SymbolTable *symtab,
                               ASR::Module_t* mod, const Location &loc,
                               std::set<std::string> &loaded_submodules,
                               LCompilers::PassOptions& pass_options,
                               bool run_verify,
                               const std::function<void (const std::string &, const Location &)> err,
                               LCompilers::LocationManager &lm) {
    if (startswith(std::string(mod->m_name), "lfortran_intrinsic")) {
        return ;
    }

    if (loaded_submodules.count(std::string(mod->m_name))) {
        return ;
    }
    loaded_submodules.insert(std::string(mod->m_name));

    for (size_t i=0;i<mod->n_dependencies;i++) {
        std::string dep_name = std::string(mod->m_dependencies[i]);
        ASR::symbol_t *dep_sym = symtab->get_symbol(dep_name);
        if (dep_sym == nullptr) {
            bool is_intrinsic = startswith(dep_name, "lfortran_intrinsic");
            load_module(al, symtab, dep_name, loc, is_intrinsic, loaded_submodules,
                        pass_options, run_verify, err, lm, false, true);
            dep_sym = symtab->get_symbol(dep_name);
        }
        if (dep_sym == nullptr) {
            continue;
        }
        if (!ASR::is_a<ASR::Module_t>(*dep_sym)) {
            err("The symbol '" + dep_name + "' must be a module", loc);
            continue;
        }
        ASR::Module_t* dep_mod = ASR::down_cast<ASR::Module_t>(dep_sym);
        load_dependent_submodules(al, symtab, dep_mod, loc,
                                  loaded_submodules, pass_options,
                                  run_verify, err, lm);
    }

    if (mod->m_has_submodules) {
        std::vector<ASR::Module_t*> pending;
        pending.push_back(mod);
        while (!pending.empty()) {
            ASR::Module_t* current = pending.back();
            pending.pop_back();
            std::vector<ASR::TranslationUnit_t*> submods;
            Result<std::vector<ASR::TranslationUnit_t*>, ErrorMessage> res
                = ASRUtils::find_and_load_submodules(al, std::string(current->m_name), *symtab, pass_options, lm);
            if (res.ok) {
                submods = res.result;
            } else {
                std::string error_message = res.error.message;
                err(error_message, loc);
            }
            for (size_t i=0;i<submods.size();i++) {
                ASR::Module_t *submod = ASRUtils::extract_module(*submods[i]);
                if (symtab->get_symbol(std::string(submod->m_name)) == nullptr) {
                    symtab->add_symbol(std::string(submod->m_name), (ASR::symbol_t*)submod);
                    submod->m_symtab->parent = symtab;
                    submod->m_loaded_from_mod = true;
                    pending.push_back(submod);
                }
            }
        }
    }

    // Create a temporary TranslationUnit just for fixing the symbols
    ASR::asr_t *orig_asr_owner = symtab->asr_owner;
    ASR::TranslationUnit_t *tu
        = ASR::down_cast2<ASR::TranslationUnit_t>(ASR::make_TranslationUnit_t(al, loc,
            symtab, nullptr, 0));

    // Keeps track of loaded dependent modules whose submodules are not yet loaded
    std::vector<ASR::Module_t*> dependent_modules_with_not_yet_loaded_submodules;

    // Load any dependent modules recursively
    bool rerun = true;
    while (rerun) {
        rerun = false;
        std::vector<std::string> modules_list
            = determine_module_dependencies(*tu);
        for (auto &item : modules_list) {
            if (symtab->get_symbol(item)
                    == nullptr) {
                bool is_intrinsic = startswith(item, "lfortran_intrinsic");
                ASR::TranslationUnit_t *mod1 = nullptr;
                Result<ASR::TranslationUnit_t*, ErrorMessage> res
                    = find_and_load_module(al, item, *symtab, is_intrinsic, pass_options, lm);
                std::string error_message = "Module '" + item + "' modfile was not found";
                if (res.ok) {
                    mod1 = res.result;
                } else {
                    error_message =  res.error.message;
                    if (!is_intrinsic) {
                        // Module not found as a regular module. Try intrinsic module
                        if (item == "iso_c_binding"
                            ||item == "iso_fortran_env") {
                            Result<ASR::TranslationUnit_t*, ErrorMessage> res
                                = find_and_load_module(al, "lfortran_intrinsic_" + item,
                                *symtab, true, pass_options, lm);
                            if (res.ok) {
                                mod1 = res.result;
                            } else {
                                error_message =  res.error.message;
                            }
                        }
                    }
                }

                if (mod1 == nullptr) {
                    err(error_message, loc);
                }
                ASR::Module_t *mod2 = extract_module(*mod1);
                symtab->add_symbol(item, (ASR::symbol_t*)mod2);
                mod2->m_symtab->parent = symtab;
                mod2->m_loaded_from_mod = true;
                dependent_modules_with_not_yet_loaded_submodules.push_back(mod2);
                rerun = true;
            }
        }
    }

    // Load all the submodules of dependent modules recursively
    for (size_t i=0;i<dependent_modules_with_not_yet_loaded_submodules.size();i++) {
        load_dependent_submodules(al, symtab, dependent_modules_with_not_yet_loaded_submodules[i],
                                  loc, loaded_submodules, pass_options, run_verify, err, lm);
    }

    symtab->asr_owner = orig_asr_owner;
}

ASR::asr_t* make_Assignment_t_util(Allocator &al, const Location &a_loc,
    ASR::expr_t* a_target, ASR::expr_t* a_value,
    ASR::stmt_t* a_overloaded, bool a_realloc_lhs, bool a_move) {
    bool is_allocatable = ASRUtils::is_allocatable(a_target);
    if ( !is_allocatable && ASR::is_a<ASR::ArrayPhysicalCast_t>(*a_target) ) {
        is_allocatable = ASRUtils::is_allocatable(
            ASRUtils::get_past_array_physical_cast(a_target));
    }
    if ( ASR::is_a<ASR::StructInstanceMember_t>(*a_target) ) {
        ASR::StructInstanceMember_t* a_target_struct = ASR::down_cast<ASR::StructInstanceMember_t>(a_target);
        is_allocatable |= ASRUtils::is_allocatable(a_target_struct->m_v);
    }
    a_realloc_lhs = a_realloc_lhs && is_allocatable;
    return ASR::make_Assignment_t(al, a_loc, a_target, a_value,
        a_overloaded, a_realloc_lhs, a_move);
}

void set_intrinsic(ASR::symbol_t* sym) {
    switch( sym->type ) {
        case ASR::symbolType::Module: {
            ASR::Module_t* module_sym = ASR::down_cast<ASR::Module_t>(sym);
            module_sym->m_intrinsic = true;
            for( auto& itr: module_sym->m_symtab->get_scope() ) {
                set_intrinsic(itr.second);
            }
            break;
        }
        case ASR::symbolType::Function: {
            ASR::Function_t* function_sym = ASR::down_cast<ASR::Function_t>(sym);
            ASR::FunctionType_t* func_sym_type = ASR::down_cast<ASR::FunctionType_t>(function_sym->m_function_signature);
            func_sym_type->m_abi = ASR::abiType::Intrinsic;
            break;
        }
        case ASR::symbolType::Struct: {
            ASR::Struct_t* derived_type_sym = ASR::down_cast<ASR::Struct_t>(sym);
            derived_type_sym->m_abi = ASR::abiType::Intrinsic;
            break;
        }
        case ASR::symbolType::Variable: {
            ASR::Variable_t* derived_type_sym = ASR::down_cast<ASR::Variable_t>(sym);
            derived_type_sym->m_abi = ASR::abiType::Intrinsic;
            break;
        }
        default: {
            break;
        }
    }
}

void set_intrinsic(ASR::TranslationUnit_t* trans_unit) {
    for( auto& itr: trans_unit->m_symtab->get_scope() ) {
        set_intrinsic(itr.second);
    }
}

Result<ASR::TranslationUnit_t*, ErrorMessage> find_and_load_module(Allocator &al, const std::string &msym,
                                                SymbolTable &symtab, bool intrinsic,
                                                LCompilers::PassOptions& pass_options,
                                                LCompilers::LocationManager &lm) {
    std::filesystem::path runtime_library_dir { pass_options.runtime_library_dir };
    std::filesystem::path filename {msym + ".mod"};
    std::vector<std::filesystem::path> mod_files_dirs;

    mod_files_dirs.push_back( runtime_library_dir );
    mod_files_dirs.push_back( pass_options.mod_files_dir );
    mod_files_dirs.insert(mod_files_dirs.end(),
                          pass_options.include_dirs.begin(),
                          pass_options.include_dirs.end());

    bool found_empty_mod = false;
    for (auto path : mod_files_dirs) {
        std::string modfile;
        std::filesystem::path full_path = path / filename;
        if (read_file(full_path.string(), modfile)) {
            if (modfile.empty()) {
                found_empty_mod = true;
                continue;
            }
            Result<ASR::TranslationUnit_t*, ErrorMessage> res = load_modfile(al, modfile, false, symtab, lm);
            if (res.ok) {
                ASR::TranslationUnit_t* asr = res.result;
                if (intrinsic) {
                    set_intrinsic(asr);
                }
                return asr;
            } else {
                return res.error;
            }
        }
    }

    if (found_empty_mod) {
        std::string smod_suffix = "@" + msym + ".smod";
        for (auto &path : mod_files_dirs) {
            std::filesystem::path search_dir = path.empty() ? std::filesystem::path(".") : path;
            if (!std::filesystem::is_directory(search_dir)) continue;
            for (auto &entry : std::filesystem::directory_iterator(search_dir)) {
                std::string fname = entry.path().filename().string();
                if (fname.size() > smod_suffix.size() &&
                    fname.compare(fname.size() - smod_suffix.size(),
                                  smod_suffix.size(), smod_suffix) == 0) {
                    std::string smodfile;
                    if (read_file(entry.path().string(), smodfile) && !smodfile.empty()) {
                        Result<ASR::TranslationUnit_t*, ErrorMessage> res = load_modfile(al, smodfile, false, symtab, lm);
                        if (res.ok) {
                            ASR::TranslationUnit_t* asr = res.result;
                            if (intrinsic) {
                                set_intrinsic(asr);
                            }
                            return asr;
                        } else {
                            return res.error;
                        }
                    }
                }
            }
        }
    }

    return ErrorMessage("Module '" + msym + "' modfile was not found");
}

Result<std::vector<ASR::TranslationUnit_t*>, ErrorMessage> find_and_load_submodules(Allocator &al, const std::string &parent_module_name,
                                                            SymbolTable &symtab,
                                                            LCompilers::PassOptions &pass_options,
                                                            LCompilers::LocationManager &lm) {
    std::vector<ASR::TranslationUnit_t*> submodules_collector;
    std::filesystem::path runtime_library_dir { pass_options.runtime_library_dir };
    std::vector<std::filesystem::path> mod_files_dirs;

    mod_files_dirs.push_back(runtime_library_dir);
    mod_files_dirs.push_back(pass_options.mod_files_dir);
    mod_files_dirs.insert(mod_files_dirs.end(),
                          pass_options.include_dirs.begin(),
                          pass_options.include_dirs.end());

    std::string smod_prefix = parent_module_name + "@";
    for (auto &path : mod_files_dirs) {
        if (path.empty()) path = ".";
        if (!std::filesystem::is_directory(path)) continue;
        for (auto &file : std::filesystem::directory_iterator(path)) {
            std::string submod_filename = file.path().filename().string();
            if (startswith(submod_filename, smod_prefix) && endswith(submod_filename, ".smod")) {
                std::string submodfile;
                if (read_file(file.path().string(), submodfile) && !submodfile.empty()) {
                    Result<ASR::TranslationUnit_t*, ErrorMessage> sub_res = load_modfile(al, submodfile, false, symtab, lm);
                    if (sub_res.ok) {
                        ASR::TranslationUnit_t* asr = sub_res.result;
                        submodules_collector.push_back(asr);
                    } else {
                        return sub_res.error;
                    }
                }
            }
        }
    }

    return submodules_collector;
}

ASR::asr_t* getStructInstanceMember_t(Allocator& al, const Location& loc,
                            ASR::asr_t* v_var, ASR::symbol_t *v,
                            ASR::symbol_t* member, SymbolTable* current_scope) {
    member = ASRUtils::symbol_get_past_external(member);
    if (ASR::is_a<ASR::Struct_t>(*member)) {
        ASR::Struct_t* member_variable = ASR::down_cast<ASR::Struct_t>(member);
        ASR::symbol_t *mem_es = nullptr;
        std::string mem_name = "1_" + std::string(member_variable->m_name) +
            "_" + std::string(ASRUtils::symbol_name(member));
        if (current_scope->resolve_symbol(mem_name)) {
            mem_es = current_scope->resolve_symbol(mem_name);
        } else {
            mem_es = ASR::down_cast<ASR::symbol_t>(ASR::make_ExternalSymbol_t(al,
                member->base.loc, current_scope, s2c(al, mem_name), member,
                ASRUtils::symbol_name(ASRUtils::get_asr_owner(member)),
                nullptr, 0, member_variable->m_name, ASR::accessType::Public));
            current_scope->add_symbol(mem_name, mem_es);
        }
        ASR::ttype_t* member_type = ASRUtils::make_StructType_t_util(al,
            member_variable->base.base.loc, mem_es, true);
        return ASR::make_StructInstanceMember_t(al, loc, ASRUtils::EXPR(v_var),
            mem_es, member_type, nullptr);
    } else {
        LCOMPILERS_ASSERT(ASR::is_a<ASR::Variable_t>(*member));
        ASR::Variable_t* member_variable = ASR::down_cast<ASR::Variable_t>(member);
        ASR::ttype_t* member_type = ASRUtils::type_get_past_pointer(
            ASRUtils::type_get_past_allocatable(member_variable->m_type));
        ASR::ttype_t* member_type_ = ASRUtils::type_get_past_array(member_type);
        ASR::dimension_t* m_dims = nullptr;
        size_t n_dims = ASRUtils::extract_dimensions_from_ttype(member_type, m_dims);

        if( ASR::is_a<ASR::Allocatable_t>(*member_variable->m_type) ) {
            member_type = ASRUtils::TYPE(ASRUtils::make_Allocatable_t_util(al,
            member_variable->base.base.loc, member_type));
        } else if( ASR::is_a<ASR::Pointer_t>(*member_variable->m_type) ) {
            member_type = ASRUtils::TYPE(ASR::make_Pointer_t(al,
            member_variable->base.base.loc, member_type));
        }

        if (ASR::is_a<ASR::ArrayItem_t>(*ASRUtils::EXPR(v_var))) {
            ASR::ArrayItem_t *t = ASR::down_cast<ASR::ArrayItem_t>(ASRUtils::EXPR(v_var));
            if (ASRUtils::is_array_indexed_with_array_indices(t->m_args, t->n_args)) {
                n_dims = ASRUtils::extract_dimensions_from_ttype(
                    ASRUtils::type_get_past_allocatable_pointer(
                        ASRUtils::expr_type(ASRUtils::EXPR(v_var))), m_dims);
                member_type = ASRUtils::make_Array_t_util(al, loc, member_type_, m_dims, n_dims);
            }
        }

        ASR::symbol_t* member_ext = ASRUtils::import_struct_instance_member(al, member, current_scope);
        ASR::expr_t* value = nullptr;
        v = ASRUtils::symbol_get_past_external(v);
        if (v != nullptr &&
            ASR::down_cast<ASR::Variable_t>(v)->m_storage == ASR::storage_typeType::Parameter) {
            if (member_variable->m_symbolic_value != nullptr) {
                value = expr_value(member_variable->m_symbolic_value);
            }
            // Check for compile time value in StructConstant
            ASR::Variable_t *v_variable_s = ASR::down_cast<ASR::Variable_t>(v);
            if (v_variable_s->m_value != nullptr && ASR::is_a<ASR::StructConstant_t>(*v_variable_s->m_value)) {
                ASR::Struct_t *struct_s = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(v_variable_s->m_type_declaration));
                std::string mem_name = ASRUtils::symbol_name(member);
                // Find the index i of the member in the Struct symbol and set value to ith argument of StructConstant
                size_t i = 0;
                for (i = 0; i < struct_s->n_members; i++) {
                    if (struct_s->m_members[i] == mem_name) {
                        break;
                    }
                }

                ASR::StructConstant_t *stc = ASR::down_cast<ASR::StructConstant_t>(v_variable_s->m_value);
                value = stc->m_args[i].m_value;
            }
        }
        return ASR::make_StructInstanceMember_t(al, loc, ASRUtils::EXPR(v_var),
            member_ext, member_type, value);
    }
}

bool use_overloaded(ASR::expr_t* left, ASR::expr_t* right,
    ASR::binopType op, std::string& intrinsic_op_name,
    SymbolTable* curr_scope, ASR::asr_t*& asr,
    Allocator &al, const Location& loc,
    SetChar& current_function_dependencies,
    SetChar& current_module_dependencies,
    const std::function<void (const std::string &, const Location &)> err) {
    ASR::ttype_t *left_type = ASRUtils::expr_type(left);
    ASR::ttype_t *right_type = ASRUtils::expr_type(right);
    ASR::Struct_t *left_struct = nullptr, *right_struct = nullptr;

    if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(left_type))) {
        left_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(left)));
    }
    if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(right_type))) {
        right_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(right)));
    }
    bool found = false;
    bool found_on_right = false;
    bool left_has_op = is_op_overloaded(op, intrinsic_op_name, curr_scope, left_struct);
    bool right_has_op = right_struct != nullptr &&
                        is_op_overloaded(op, intrinsic_op_name, curr_scope, right_struct);
    if (!left_has_op && right_has_op) {
        found_on_right = true;
    }
    ASR::Struct_t* op_struct = found_on_right ? right_struct : left_struct;
    (void)op_struct;
    if (left_has_op || right_has_op) {
        ASR::symbol_t* sym = curr_scope->resolve_symbol(intrinsic_op_name);
        ASR::symbol_t* orig_sym = ASRUtils::symbol_get_past_external(sym);
        auto find_struct_op_sym = [&](ASR::Struct_t* s) -> ASR::symbol_t* {
            ASR::symbol_t* result = nullptr;
            ASR::Struct_t* temp_struct = s;
            while (temp_struct) {
                if (temp_struct->m_symtab->get_symbol(intrinsic_op_name) != nullptr) {
                    result = temp_struct->m_symtab->get_symbol(intrinsic_op_name);
                    break;
                } else if (temp_struct->m_parent) {
                    temp_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(temp_struct->m_parent));
                } else {
                    temp_struct = nullptr;
                }
            }
            return result;
        };
        ASR::symbol_t* orig_sym2 = nullptr;
        ASR::symbol_t* orig_sym2_right = nullptr;
        if (left_has_op && left_struct != nullptr) {
            orig_sym2 = find_struct_op_sym(left_struct);
        }
        if (right_has_op && right_struct != nullptr) {
            orig_sym2_right = find_struct_op_sym(right_struct);
        }
        Vec<ASR::symbol_t*> op_overloading_procs; op_overloading_procs.reserve(al, 1);
        size_t n_interface_proc = 0;
        size_t n_left_struct_end = 0;
        // functions that are declared by interface operator(+)
        if (orig_sym) {
            ASR::CustomOperator_t* gen_proc = ASR::down_cast<ASR::CustomOperator_t>(orig_sym);
            for( size_t i = 0; i < gen_proc->n_procs; i++ ) {
                op_overloading_procs.push_back(al, gen_proc->m_procs[i]);
            }
            n_interface_proc = gen_proc->n_procs;
        }
        // functions that are type-bound on the left operand's type
        if (orig_sym2) {
            ASR::CustomOperator_t* gen_proc = ASR::down_cast<ASR::CustomOperator_t>(orig_sym2);
            for( size_t i = 0; i < gen_proc->n_procs; i++ ) {
                op_overloading_procs.push_back(al, gen_proc->m_procs[i]);
            }
        }
        n_left_struct_end = op_overloading_procs.size();
        // functions that are type-bound on the right operand's type
        if (orig_sym2_right) {
            ASR::CustomOperator_t* gen_proc = ASR::down_cast<ASR::CustomOperator_t>(orig_sym2_right);
            for( size_t i = 0; i < gen_proc->n_procs; i++ ) {
                op_overloading_procs.push_back(al, gen_proc->m_procs[i]);
            }
        }
        for( size_t i = 0; i < op_overloading_procs.size() && !found; i++ ) {
            bool proc_from_right_struct = (i >= n_left_struct_end);
            ASR::Struct_t* proc_op_struct = proc_from_right_struct ? right_struct : left_struct;
            // For the call-site bookkeeping below, treat as found_on_right
            // when the procedure is type-bound on the right operand or when
            // there is no left-side operator at all.
            bool iter_found_on_right = proc_from_right_struct || found_on_right;
            ASR::symbol_t* proc;
            ASR::symbol_t* orig_proc = ASRUtils::symbol_get_past_external(op_overloading_procs[i]);
            if ( ASR::is_a<ASR::StructMethodDeclaration_t>(*op_overloading_procs[i]) ) {
                ASR::StructMethodDeclaration_t* cp = ASR::down_cast<ASR::StructMethodDeclaration_t>(op_overloading_procs[i]);
                if (proc_op_struct != nullptr && cp->m_parent_symtab->get_counter() != proc_op_struct->m_symtab->get_counter()) {
                    ASR::Struct_t* temp_struct = proc_op_struct;
                    while (temp_struct->m_parent) {
                        if (temp_struct->m_symtab->get_symbol(cp->m_name) != nullptr) {
                            cp = ASR::down_cast<ASR::StructMethodDeclaration_t>(temp_struct->m_symtab->get_symbol(cp->m_name));
                            orig_proc = temp_struct->m_symtab->get_symbol(cp->m_name);
                            break;
                        }
                        temp_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(temp_struct->m_parent));
                    }
                }
                proc =  ASRUtils::symbol_get_past_external(cp->m_proc);
            } else {
                proc = ASRUtils::symbol_get_past_external(op_overloading_procs[i]);
            }
            if( (void*) proc == (void*) curr_scope->asr_owner ) {
                continue ;
            }
            switch(proc->type) {
                case ASR::symbolType::Function: {
                    ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(proc);
                    bool is_elemental = ASRUtils::is_elemental(proc);
                    std::string matched_func_name = "";
                    if( func->n_args == 2 ) {
                        ASR::ttype_t* left_arg_type = ASRUtils::expr_type(func->m_args[0]);
                        ASR::ttype_t* right_arg_type = ASRUtils::expr_type(func->m_args[1]);
                        bool not_matching = (!ASRUtils::is_allocatable(left_type) && ASRUtils::is_allocatable(left_arg_type)) ||
                                            (!ASRUtils::is_allocatable(right_type) && ASRUtils::is_allocatable(right_arg_type)) ||
                                            (!ASRUtils::is_pointer(left_type) && ASRUtils::is_pointer(left_arg_type)) ||
                                            (!ASRUtils::is_pointer(right_type) && ASRUtils::is_pointer(right_arg_type));
                                            
                        ASR::ttype_t* left_type2 = ASRUtils::type_get_past_allocatable_pointer(left_type);
                        ASR::ttype_t* left_arg_type2 = ASRUtils::type_get_past_allocatable_pointer(left_arg_type);
                        ASR::ttype_t* right_type2 = ASRUtils::type_get_past_allocatable_pointer(right_type);
                        ASR::ttype_t* right_arg_type2 = ASRUtils::type_get_past_allocatable_pointer(right_arg_type);

                        not_matching = not_matching || (!is_elemental &&
                                       ((left_arg_type2->type != left_type2->type &&
                                         !ASRUtils::is_unlimited_polymorphic_type(left_arg_type2)) ||
                                        (right_arg_type2->type != right_type2->type &&
                                         !ASRUtils::is_unlimited_polymorphic_type(right_arg_type2))));

                        left_type2 = ASRUtils::type_get_past_array(left_type2);
                        left_arg_type2 = ASRUtils::type_get_past_array(left_arg_type2);
                        right_type2 = ASRUtils::type_get_past_array(right_type2);
                        right_arg_type2 = ASRUtils::type_get_past_array(right_arg_type2);

                        if( !not_matching && ((left_arg_type2->type == left_type2->type ||
                                                ASRUtils::is_unlimited_polymorphic_type(left_arg_type2)) &&
                                               (right_arg_type2->type == right_type2->type ||
                                                ASRUtils::is_unlimited_polymorphic_type(right_arg_type2))) ) {
                            // Check StructType compatibility independently for each side
                            if (ASR::is_a<ASR::StructType_t>(*left_type2) &&
                                ASR::is_a<ASR::StructType_t>(*left_arg_type2)) {
                                ASR::Struct_t* left_arg_sym = ASR::down_cast<ASR::Struct_t>(
                                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(func->m_args[0])));
                                if (!can_pass_derviedtype_arg_to_parameter(left_struct, left_arg_sym /*param*/)) {
                                    break;
                                }
                            }
                            if (ASR::is_a<ASR::StructType_t>(*right_type2) &&
                                ASR::is_a<ASR::StructType_t>(*right_arg_type2)) {
                                ASR::Struct_t* right_arg_sym = ASR::down_cast<ASR::Struct_t>(
                                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(func->m_args[1])));
                                if (!can_pass_derviedtype_arg_to_parameter(right_struct, right_arg_sym /*param*/)) {
                                    break;
                                }
                            }
                            found = true;
                            Vec<ASR::call_arg_t> a_args;
                            a_args.reserve(al, 1);
                            ASR::call_arg_t left_call_arg, right_call_arg;
                            ASR::expr_t* self_arg = nullptr;
                            if (i < n_interface_proc) {
                                left_call_arg.loc = left->base.loc, left_call_arg.m_value = left;
                                a_args.push_back(al, left_call_arg);
                                right_call_arg.loc = right->base.loc, right_call_arg.m_value = right;
                                a_args.push_back(al, right_call_arg);
                            } else if (iter_found_on_right) {
                                // Operator is a type-bound procedure on the right
                                // operand's type. Include both operands as regular
                                // call arguments (not as self_arg) so that argument
                                // order matches the function's formal parameters.
                                left_call_arg.loc = left->base.loc, left_call_arg.m_value = left;
                                a_args.push_back(al, left_call_arg);
                                right_call_arg.loc = right->base.loc, right_call_arg.m_value = right;
                                a_args.push_back(al, right_call_arg);
                            } else {
                                right_call_arg.loc = right->base.loc, right_call_arg.m_value = right;
                                a_args.push_back(al, right_call_arg);
                            }
                            std::string func_name = to_lower(func->m_name);
                            if (i >= n_interface_proc && !iter_found_on_right) {
                                ASR::symbol_t* mem_ext = import_class_procedure(al, loc, orig_proc, curr_scope);
                                matched_func_name = ASRUtils::symbol_name(mem_ext);
                                self_arg = left;
                            } else if( curr_scope->resolve_symbol(func_name) ) {
                                matched_func_name = func_name;
                            } else {
                                std::string mangled_name = func_name + "@" + intrinsic_op_name;
                                matched_func_name = mangled_name;
                            }
                            ASR::symbol_t* a_name = curr_scope->resolve_symbol(matched_func_name);
                            if( a_name == nullptr ) {
                                if (i >= n_interface_proc && !iter_found_on_right) {
                                    err("Unable to resolve matched function for operator overloading, " + matched_func_name, loc);
                                } else {
                                    a_name = ASR::down_cast<ASR::symbol_t>(ASR::make_ExternalSymbol_t(
                                                al, loc, curr_scope, s2c(al, matched_func_name), proc,
                                                ASRUtils::symbol_name(ASRUtils::get_asr_owner(proc)),
                                                nullptr, 0, func->m_name, ASR::accessType::Public));
                                    curr_scope->add_symbol(matched_func_name, a_name);
                                }
                            }
                            ASR::ttype_t *return_type = nullptr;
                            if( ASRUtils::get_FunctionType(func)->m_elemental &&
                                func->n_args >= 1 &&
                                ASRUtils::is_array(ASRUtils::expr_type(a_args[0].m_value)) ) {
                                ASR::dimension_t* array_dims;
                                size_t array_n_dims = ASRUtils::extract_dimensions_from_ttype(
                                ASRUtils::expr_type(a_args[0].m_value), array_dims);
                                Vec<ASR::dimension_t> new_dims;
                                new_dims.from_pointer_n_copy(al, array_dims, array_n_dims);
                                return_type = ASRUtils::duplicate_type(al,
                                                ASRUtils::get_FunctionType(func)->m_return_var_type,
                                                &new_dims);
                            } else {
                                return_type = ASRUtils::expr_type(func->m_return_var);
                            }
                            if (ASRUtils::symbol_parent_symtab(a_name)->get_counter() != curr_scope->get_counter()) {
                                ADD_ASR_DEPENDENCIES_WITH_NAME(curr_scope, a_name, current_function_dependencies, s2c(al, matched_func_name));
                            }
                            ASRUtils::insert_module_dependency(a_name, al, current_module_dependencies);
                            ASRUtils::set_absent_optional_arguments_to_null(a_args, func, al, self_arg);
                            ASR::call_arg_t* call_args1 = a_args.p;
                            size_t n_call_args1 = a_args.n;
                            ASRUtils::insert_self_arg(al, a_name, call_args1, n_call_args1, self_arg);
                            {
                                ASR::FunctionType_t* ftype = ASRUtils::get_FunctionType(func);
                                if (ftype->m_return_var_type) {
                                    return_type = ASRUtils::duplicate_type(al, ftype->m_return_var_type);
                                    ASR::dimension_t* ret_dims = nullptr;
                                    size_t n_ret_dims = ASRUtils::extract_dimensions_from_ttype(return_type, ret_dims);
                                    if (n_ret_dims > 0) {
                                       // Keep whichever 'r' initialization is currently there, for example:
                                       ReplaceFunctionParamWithArg r(al, a_args.p, a_args.n); // OR call_args1, n_call_args1

                                       // 1. Initialize the caller's duplicator OUTSIDE the loop
                                       ASRUtils::ExprStmtDuplicator caller_dup(al);
                                       caller_dup.allow_procedure_calls = true;
                                       caller_dup.success = true;

                                       for (size_t di = 0; di < n_ret_dims; di++) {
                                          if (ret_dims[di].m_length) {
                                          // 2. Extract and safely duplicate m_length
                                          ASR::expr_t* replaced_length = r.replace_FunctionParam_with_arg(ret_dims[di].m_length);
                                          ret_dims[di].m_length = (replaced_length == ret_dims[di].m_length) ? caller_dup.duplicate_expr(replaced_length) : replaced_length;
                                           }
        
                                          if (ret_dims[di].m_start) {
                                          // 3. Extract and safely duplicate m_start
                                          ASR::expr_t* replaced_start = r.replace_FunctionParam_with_arg(ret_dims[di].m_start);
                                          ret_dims[di].m_start = (replaced_start == ret_dims[di].m_start) ? caller_dup.duplicate_expr(replaced_start) : replaced_start;
                                           }
                                        }
                                    }
                                }
                            }
                            asr = ASRUtils::make_FunctionCall_t_util(al, loc, a_name, sym,
                                                            call_args1, n_call_args1,
                                                            return_type,
                                                            nullptr, self_arg
                                                            );
                        }
                    }
                    break;
                }
                default: {
                    err("While overloading binary operators only functions can be used",
                                        proc->base.loc);
                }
            }
        }
    }
    return found;
}

void process_overloaded_unary_minus_function(ASR::symbol_t* proc, ASR::expr_t* operand,
    ASR::ttype_t* operand_type, bool& found, Allocator& al, SymbolTable* curr_scope,
    const Location& loc, SetChar& current_function_dependencies,
    SetChar& current_module_dependencies, ASR::asr_t*& asr,
    const std::function<void (const std::string &, const Location &)> /*err*/) {
    ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(proc);
    std::string matched_func_name = "";
    if( func->n_args == 1 ) {
        ASR::ttype_t* operand_arg_type = ASRUtils::expr_type(func->m_args[0]);
        if (ASRUtils::check_equal_type(operand_arg_type, operand_type, func->m_args[0], operand)) {
            found = true;
            Vec<ASR::call_arg_t> a_args;
            a_args.reserve(al, 1);
            ASR::call_arg_t operand_call_arg;
            operand_call_arg.loc = operand->base.loc;
            operand_call_arg.m_value = operand;
            a_args.push_back(al, operand_call_arg);
            std::string func_name = to_lower(func->m_name);
            if( curr_scope->resolve_symbol(func_name) ) {
                matched_func_name = func_name;
            } else {
                std::string mangled_name = func_name + "@~sub";
                matched_func_name = mangled_name;
            }
            ASR::symbol_t* a_name = curr_scope->resolve_symbol(matched_func_name);
            if( a_name == nullptr ) {
                a_name = ASR::down_cast<ASR::symbol_t>(ASR::make_ExternalSymbol_t(
                            al, loc, curr_scope, s2c(al, matched_func_name), proc,
                            ASRUtils::symbol_name(ASRUtils::get_asr_owner(proc)),
                            nullptr, 0, func->m_name, ASR::accessType::Public));
                curr_scope->add_symbol(matched_func_name, a_name);
            }
            ASR::ttype_t *return_type = nullptr;
            if( ASRUtils::get_FunctionType(func)->m_elemental &&
                func->n_args >= 1 &&
                ASRUtils::is_array(ASRUtils::expr_type(a_args[0].m_value)) ) {
                ASR::dimension_t* array_dims;
                size_t array_n_dims = ASRUtils::extract_dimensions_from_ttype(
                ASRUtils::expr_type(a_args[0].m_value), array_dims);
                Vec<ASR::dimension_t> new_dims;
                new_dims.from_pointer_n_copy(al, array_dims, array_n_dims);
                return_type = ASRUtils::duplicate_type(al,
                                ASRUtils::get_FunctionType(func)->m_return_var_type,
                                &new_dims);
            } else {
                return_type = ASRUtils::expr_type(func->m_return_var);
                bool is_array = ASRUtils::is_array(return_type);
                ASR::dimension_t* m_dims = nullptr;
                size_t n_dims = ASRUtils::extract_dimensions_from_ttype(return_type, m_dims);
                return_type = ASRUtils::type_get_past_array(return_type);
                if( ASR::is_a<ASR::StructType_t>(*return_type) ) {
                    return_type = ASRUtils::duplicate_type(al, return_type);
                    if( is_array ) {
                        return_type = ASRUtils::make_Array_t_util(
                            al, loc, return_type, m_dims, n_dims);
                    }
                }
            }
            if (ASRUtils::symbol_parent_symtab(a_name)->get_counter() != curr_scope->get_counter()) {
                ADD_ASR_DEPENDENCIES_WITH_NAME(curr_scope, a_name, current_function_dependencies, s2c(al, matched_func_name));
            }
            ASRUtils::insert_module_dependency(a_name, al, current_module_dependencies);
            ASRUtils::set_absent_optional_arguments_to_null(a_args, func, al);
            {
                ASR::FunctionType_t* ftype = ASRUtils::get_FunctionType(func);
                if (ftype->m_return_var_type) {
                    return_type = ASRUtils::duplicate_type(al, ftype->m_return_var_type);
                    ASR::dimension_t* ret_dims = nullptr;
                    size_t n_ret_dims = ASRUtils::extract_dimensions_from_ttype(return_type, ret_dims);
                    if (n_ret_dims > 0) {
                       // Keep whichever 'r' initialization is currently there, for example:
                       ReplaceFunctionParamWithArg r(al, a_args.p, a_args.n); // OR call_args1, n_call_args1

                       // 1. Initialize the caller's duplicator OUTSIDE the loop
                       ASRUtils::ExprStmtDuplicator caller_dup(al);
                       caller_dup.allow_procedure_calls = true;
                       caller_dup.success = true;

                       for (size_t di = 0; di < n_ret_dims; di++) {
                           if (ret_dims[di].m_length) {
                              // 2. Extract and safely duplicate m_length
                              ASR::expr_t* replaced_length = r.replace_FunctionParam_with_arg(ret_dims[di].m_length);
                              ret_dims[di].m_length = (replaced_length == ret_dims[di].m_length) ? caller_dup.duplicate_expr(replaced_length) : replaced_length;
                            }
        
                            if (ret_dims[di].m_start) {
                               // 3. Extract and safely duplicate m_start
                               ASR::expr_t* replaced_start = r.replace_FunctionParam_with_arg(ret_dims[di].m_start);
                               ret_dims[di].m_start = (replaced_start == ret_dims[di].m_start) ? caller_dup.duplicate_expr(replaced_start) : replaced_start;
                            }
                        }
                    }
                }
            }
            ASR::symbol_t* original_name = a_name;
            asr = ASRUtils::make_FunctionCall_t_util(al, loc, a_name, original_name,
                                            a_args.p, 1,
                                            return_type,
                                            nullptr, nullptr
                                            );
        }
    }
}

bool use_overloaded_unary_minus(ASR::expr_t* operand,
    SymbolTable* curr_scope, ASR::asr_t*& asr,
    Allocator &al, const Location& loc, SetChar& current_function_dependencies,
    SetChar& current_module_dependencies,
    const std::function<void (const std::string &, const Location &)> err) {
    ASR::ttype_t *operand_type = ASRUtils::type_get_past_allocatable_pointer(
        ASRUtils::expr_type(operand));
    ASR::symbol_t* sym = curr_scope->resolve_symbol("~sub");

    if (!sym) {
        if (ASR::is_a<ASR::StructType_t>(*operand_type)) {
            ASR::symbol_t* struct_t_sym = ASRUtils::symbol_get_past_external(
                ASRUtils::get_struct_sym_from_struct_expr(operand));
            if (ASR::is_a<ASR::Struct_t>(*struct_t_sym)) {
                ASR::Struct_t* struct_type_t = ASR::down_cast<ASR::Struct_t>(struct_t_sym);
                sym = struct_type_t->m_symtab->resolve_symbol("~sub");
                while (sym == nullptr && struct_type_t->m_parent != nullptr) {
                    struct_type_t = ASR::down_cast<ASR::Struct_t>(
                        ASRUtils::symbol_get_past_external(struct_type_t->m_parent));
                    sym = struct_type_t->m_symtab->resolve_symbol("~sub");
                }
                if (sym == nullptr) {
                    return false;
                }
                sym = ASR::down_cast<ASR::symbol_t>(
                    ASR::make_ExternalSymbol_t(al,
                                               loc,
                                               curr_scope,
                                               s2c(al, "~sub"),
                                               sym,
                                               struct_type_t->m_name,
                                               nullptr,
                                               0,
                                               s2c(al, "~sub"),
                                               ASR::accessType::Public));
                curr_scope->add_symbol("~sub", sym);
            } else {
                LCOMPILERS_ASSERT(false);
            }
        }
    }

    bool found = false;
    ASR::symbol_t* orig_sym = ASRUtils::symbol_get_past_external(sym);
    ASR::CustomOperator_t* gen_proc = ASR::down_cast<ASR::CustomOperator_t>(orig_sym);
    for( size_t i = 0; i < gen_proc->n_procs && !found; i++ ) {
        ASR::symbol_t* proc = ASRUtils::symbol_get_past_external(gen_proc->m_procs[i]);
        switch(proc->type) {
            case ASR::symbolType::Function: {
                process_overloaded_unary_minus_function(proc, operand, operand_type,
                    found, al, curr_scope, loc, current_function_dependencies,
                    current_module_dependencies, asr, err);
                break;
            }
            case ASR::symbolType::StructMethodDeclaration: {
                ASR::StructMethodDeclaration_t* class_procedure_t = ASR::down_cast<ASR::StructMethodDeclaration_t>(proc);
                process_overloaded_unary_minus_function(class_procedure_t->m_proc,
                    operand, operand_type, found, al, curr_scope, loc,
                    current_function_dependencies, current_module_dependencies, asr, err);
                break;
            }
            default: {
                err("While overloading binary operators only functions can be used",
                                    proc->base.loc);
            }
        }
    }
    if (!found && ASR::is_a<ASR::StructType_t>(*operand_type) &&
            !ASRUtils::is_class_type(operand_type)) {
        ASR::symbol_t* struct_t_sym = ASRUtils::symbol_get_past_external(
            ASRUtils::get_struct_sym_from_struct_expr(operand));
        if (ASR::is_a<ASR::Struct_t>(*struct_t_sym)) {
            ASR::Struct_t* struct_type_t = ASR::down_cast<ASR::Struct_t>(struct_t_sym);
            ASR::symbol_t* struct_sub = struct_type_t->m_symtab->resolve_symbol("~sub");
            while (struct_sub == nullptr && struct_type_t->m_parent != nullptr) {
                struct_type_t = ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(struct_type_t->m_parent));
                struct_sub = struct_type_t->m_symtab->resolve_symbol("~sub");
            }
            if (struct_sub != nullptr) {
                ASR::symbol_t* struct_orig = ASRUtils::symbol_get_past_external(struct_sub);
                if (ASR::is_a<ASR::CustomOperator_t>(*struct_orig)) {
                    ASR::CustomOperator_t* struct_gen = ASR::down_cast<ASR::CustomOperator_t>(struct_orig);
                    for (size_t i = 0; i < struct_gen->n_procs && !found; i++) {
                        ASR::symbol_t* proc = struct_gen->m_procs[i];
                        switch(proc->type) {
                            case ASR::symbolType::Function: {
                                process_overloaded_unary_minus_function(proc, operand, operand_type,
                                    found, al, curr_scope, loc, current_function_dependencies,
                                    current_module_dependencies, asr, err);
                                break;
                            }
                            case ASR::symbolType::StructMethodDeclaration: {
                                ASR::StructMethodDeclaration_t* class_procedure_t =
                                    ASR::down_cast<ASR::StructMethodDeclaration_t>(proc);
                                process_overloaded_unary_minus_function(class_procedure_t->m_proc,
                                    operand, operand_type, found, al, curr_scope, loc,
                                    current_function_dependencies, current_module_dependencies, asr, err);
                                break;
                            }
                            default:
                                break;
                        }
                    }
                }
            }
        }
    }
    return found;
}

bool is_op_overloaded(ASR::binopType op, std::string& intrinsic_op_name,
                      SymbolTable* curr_scope, ASR::Struct_t* left_struct) {
    bool result = true;
    switch(op) {
        case ASR::binopType::Add: {
            if(intrinsic_op_name != "~add") {
                result = false;
            }
            break;
        }
        case ASR::binopType::Sub: {
            if(intrinsic_op_name != "~sub") {
                result = false;
            }
            break;
        }
        case ASR::binopType::Mul: {
            if(intrinsic_op_name != "~mul") {
                result = false;
            }
            break;
        }
        case ASR::binopType::Div: {
            if(intrinsic_op_name != "~div") {
                result = false;
            }
            break;
        }
        case ASR::binopType::Pow: {
            if(intrinsic_op_name != "~pow") {
                result = false;
            }
            break;
        }
        default: {
            throw LCompilersException("Binary operator '" + ASRUtils::binop_to_str_python(op) + "' not supported yet");
        }
    }

    if( result && curr_scope->resolve_symbol(intrinsic_op_name) == nullptr ) {
        if ( left_struct != nullptr ) {
            ASR::Struct_t* temp_struct = left_struct;
            result = false;
            while (temp_struct) {
                if (temp_struct->m_symtab->resolve_symbol(intrinsic_op_name) != nullptr) {
                    result = true;
                    break;
                } else if (temp_struct->m_parent) {
                    temp_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(temp_struct->m_parent));
                } else {
                    temp_struct = nullptr;
                }
            }
        } else {
            result = false;
        }
    }
    return result;
}

void process_overloaded_assignment_function(ASR::symbol_t* proc, ASR::expr_t* target, ASR::expr_t* value,
    ASR::ttype_t* target_type, ASR::ttype_t* value_type, bool& found, Allocator& al, const Location& target_loc,
    const Location& value_loc, SymbolTable* curr_scope, SetChar& current_function_dependencies,
    SetChar& current_module_dependencies, ASR::asr_t*& asr, ASR::symbol_t* sym, const Location& loc, ASR::expr_t* expr_dt,
    const std::function<void (const std::string &, const Location &)> err, char* pass_arg=nullptr) {
    ASR::Function_t* subrout = ASR::down_cast<ASR::Function_t>(proc);
    std::string matched_subrout_name = "";
    if( subrout->n_args == 2 ) {
        ASR::ttype_t* target_arg_type = ASRUtils::expr_type(subrout->m_args[0]);
        ASR::ttype_t* value_arg_type = ASRUtils::expr_type(subrout->m_args[1]);
        // F2023 10.2.1.4 / 15.5.2.4: non-elemental defined assignment requires
        // matching ranks of actuals and dummies. types_equal peels arrays, so
        // enforce ranks explicitly unless the procedure is elemental.
        bool is_elemental = ASRUtils::is_elemental(proc);
        if (!is_elemental) {
            int target_rank = ASRUtils::extract_n_dims_from_ttype(target_type);
            int value_rank = ASRUtils::extract_n_dims_from_ttype(value_type);
            int arg0_rank = ASRUtils::extract_n_dims_from_ttype(target_arg_type);
            int arg1_rank = ASRUtils::extract_n_dims_from_ttype(value_arg_type);
            if (target_rank != arg0_rank || value_rank != arg1_rank) {
                return;
            }
        }
        if( ASRUtils::types_equal(target_arg_type, target_type, subrout->m_args[0], target) &&
            ASRUtils::types_equal(value_arg_type, value_type, subrout->m_args[1], value) ) {
            std::string arg0_name = ASRUtils::symbol_name(ASR::down_cast<ASR::Var_t>(subrout->m_args[0])->m_v);
            std::string arg1_name = ASRUtils::symbol_name(ASR::down_cast<ASR::Var_t>(subrout->m_args[1])->m_v);
            if( pass_arg != nullptr ) {
                std::string pass_arg_str = std::string(pass_arg);
                if( arg0_name != pass_arg_str && arg1_name != pass_arg_str ) {
                    err(pass_arg_str + " argument is not present in " + std::string(subrout->m_name),
                        proc->base.loc);
                }
                if( (arg0_name == pass_arg_str && target != expr_dt) ) {
                    err(std::string(subrout->m_name) + " is not a procedure of " +
                        ASRUtils::type_to_str_fortran_expr(target_type, target),
                        loc);
                }
                if( (arg1_name == pass_arg_str && value != expr_dt) ) {
                    err(std::string(subrout->m_name) + " is not a procedure of " +
                        ASRUtils::type_to_str_fortran_expr(value_type, value),
                        loc);
                }
            }
            found = true;
            Vec<ASR::call_arg_t> a_args;
            a_args.reserve(al, 2);
            ASR::call_arg_t target_arg, value_arg;
            target_arg.loc = target_loc, target_arg.m_value = target;
            a_args.push_back(al, target_arg);
            value_arg.loc = value_loc, value_arg.m_value = value;
            a_args.push_back(al, value_arg);
            std::string subrout_name = to_lower(subrout->m_name);
            ASR::symbol_t* resolved = curr_scope->resolve_symbol(subrout_name);
            if( resolved && ASRUtils::symbol_get_past_external(resolved) == proc ) {
                matched_subrout_name = subrout_name;
            } else {
                std::string mangled_name = subrout_name + "@~assign";
                matched_subrout_name = mangled_name;
            }
            ASR::symbol_t *a_name = curr_scope->get_symbol(matched_subrout_name);
            if( a_name == nullptr ) {
                a_name = ASR::down_cast<ASR::symbol_t>(ASR::make_ExternalSymbol_t(
                            al, loc, curr_scope, s2c(al, matched_subrout_name), proc,
                            ASRUtils::symbol_name(ASRUtils::get_asr_owner(proc)),
                            nullptr, 0, subrout->m_name, ASR::accessType::Public));
                curr_scope->add_symbol(matched_subrout_name, a_name);
            }
            if (ASRUtils::symbol_parent_symtab(a_name)->get_counter() != curr_scope->get_counter()) {
                ADD_ASR_DEPENDENCIES_WITH_NAME(curr_scope, a_name, current_function_dependencies, s2c(al, matched_subrout_name));
            }
            ASRUtils::insert_module_dependency(a_name, al, current_module_dependencies);
            ASRUtils::set_absent_optional_arguments_to_null(a_args, subrout, al);
            ASR::symbol_t* a_original_name = ASR::is_a<ASR::ExternalSymbol_t>(*a_name) ? a_name : sym;
            asr = ASRUtils::make_SubroutineCall_t_util(al, loc, a_name, a_original_name,
                                            a_args.p, 2, nullptr, nullptr, false);
        }
    }
}

ASR::symbol_t* resolve_struct_assign_symbol(ASR::Struct_t* s) {
    while (s != nullptr) {
        ASR::symbol_t* result = s->m_symtab->resolve_symbol("~assign");
        if (result != nullptr) {
            return result;
        }
        if (s->m_parent == nullptr) {
            break;
        }
        s = ASR::down_cast<ASR::Struct_t>(
            ASRUtils::symbol_get_past_external(s->m_parent));
    }
    return nullptr;
}

ASR::symbol_t* resolve_struct_assign_symbol(ASR::expr_t* expression) {
    ASR::symbol_t* struct_sym = ASRUtils::get_struct_sym_from_struct_expr(expression);
    if (struct_sym == nullptr) {
        return nullptr;
    }
    return resolve_struct_assign_symbol(ASR::down_cast<ASR::Struct_t>(
        ASRUtils::symbol_get_past_external(struct_sym)));
}

bool use_overloaded_assignment(ASR::expr_t* target, ASR::expr_t* value,
                               SymbolTable* curr_scope, ASR::asr_t*& asr,
                               Allocator &al, const Location& loc,
                               SetChar& current_function_dependencies,
                               SetChar& current_module_dependencies,
                               const std::function<void (const std::string &, const Location &)> err) {
    ASR::ttype_t *target_type = ASRUtils::type_get_past_allocatable(ASRUtils::expr_type(target));
    ASR::ttype_t *value_type = ASRUtils::type_get_past_allocatable(ASRUtils::expr_type(value));
    bool found = false;
    ASR::symbol_t* sym = curr_scope->resolve_symbol("~assign");
    ASR::expr_t* expr_dt = nullptr;
    if (!sym) {
        // extract_type peels array/pointer/allocatable so both scalar and
        // array-of-struct assignments share one lookup path.
        ASR::ttype_t* target_elem = ASRUtils::extract_type(target_type);
        ASR::ttype_t* value_elem = ASRUtils::extract_type(value_type);
        if (ASR::is_a<ASR::StructType_t>(*target_elem)) {
            sym = resolve_struct_assign_symbol(target);
            if (sym) {
                expr_dt = target;
            }
        } else if (ASR::is_a<ASR::StructType_t>(*value_elem)) {
            sym = resolve_struct_assign_symbol(value);
            if (sym) {
                expr_dt = value;
            }
        }
    }
    if (sym) {
        ASR::symbol_t* orig_sym = ASRUtils::symbol_get_past_external(sym);
        ASR::CustomOperator_t* gen_proc = ASR::down_cast<ASR::CustomOperator_t>(orig_sym);
        for( size_t i = 0; i < gen_proc->n_procs && !found; i++ ) {
            ASR::symbol_t* proc = ASRUtils::symbol_get_past_external(gen_proc->m_procs[i]);
            switch( proc->type ) {
                case ASR::symbolType::Function: {
                    process_overloaded_assignment_function(proc, target, value, target_type,
                        value_type, found, al, target->base.loc, value->base.loc, curr_scope,
                        current_function_dependencies, current_module_dependencies, asr, sym,
                        loc, expr_dt, err);
                    break;
                }
                case ASR::symbolType::StructMethodDeclaration: {
                    ASR::StructMethodDeclaration_t* class_proc = ASR::down_cast<ASR::StructMethodDeclaration_t>(proc);
                    ASR::symbol_t* proc_func = class_proc->m_proc;
                    process_overloaded_assignment_function(proc_func, target, value, target_type,
                        value_type, found, al, target->base.loc, value->base.loc, curr_scope,
                        current_function_dependencies, current_module_dependencies, asr, proc_func, loc,
                        expr_dt, err, class_proc->m_self_argument);
                    if (found && expr_dt && ASRUtils::is_class_type(
                            ASRUtils::type_get_past_allocatable(ASRUtils::expr_type(expr_dt)))) {
                        // For polymorphic (class) types, create a type-bound call
                        // referencing the StructMethodDeclaration with m_dt set,
                        // so codegen can use vtable dispatch.
                        std::string method_name = std::string(class_proc->m_name) + "@~assign";
                        ASR::symbol_t* ext_sym = curr_scope->get_symbol(method_name);
                        if (ext_sym == nullptr) {
                            ext_sym = ASR::down_cast<ASR::symbol_t>(ASR::make_ExternalSymbol_t(
                                al, loc, curr_scope, s2c(al, method_name), proc,
                                ASRUtils::symbol_name(ASRUtils::get_asr_owner(proc)),
                                nullptr, 0, class_proc->m_name, ASR::accessType::Public));
                            curr_scope->add_symbol(method_name, ext_sym);
                        }
                        if (ASRUtils::symbol_parent_symtab(ext_sym)->get_counter() != curr_scope->get_counter()) {
                            ADD_ASR_DEPENDENCIES_WITH_NAME(curr_scope, ext_sym, current_function_dependencies, s2c(al, method_name));
                        }
                        ASRUtils::insert_module_dependency(ext_sym, al, current_module_dependencies);

                        ASR::expr_t* non_pass_arg = (expr_dt == target) ? value : target;
                        Vec<ASR::call_arg_t> a_args;
                        a_args.reserve(al, 1);
                        ASR::call_arg_t arg;
                        arg.loc = non_pass_arg->base.loc;
                        arg.m_value = non_pass_arg;
                        a_args.push_back(al, arg);

                        ASR::call_arg_t* call_args = a_args.p;
                        size_t n_call_args = 1;
                        ASRUtils::insert_self_arg(al, ext_sym, call_args, n_call_args, expr_dt);
                        asr = ASRUtils::make_SubroutineCall_t_util(al, loc, ext_sym, ext_sym,
                            call_args, n_call_args, expr_dt, nullptr, false);
                    }
                    break;
                }
                default: {
                    err("Only functions and class procedures can be used for generic assignment statement, found " + std::to_string(proc->type), loc);
                }
            }
        }
    }
    return found;
}

void process_overloaded_read_write_function(std::string &read_write, ASR::symbol_t* proc, Vec<ASR::expr_t*> &args,
    ASR::ttype_t* arg_type, bool& found, Allocator& al, const Location& arg_loc,
    SymbolTable* curr_scope, SetChar& current_function_dependencies,
    SetChar& current_module_dependencies, ASR::asr_t*& asr, ASR::symbol_t* sym, const Location& loc, ASR::expr_t* expr_dt,
    const std::function<void (const std::string &, const Location &)> err, char* pass_arg=nullptr) {
    ASR::Function_t* subrout = ASR::down_cast<ASR::Function_t>(proc);
    std::string matched_subrout_name = "";
    ASR::ttype_t* func_arg_type = ASRUtils::expr_type(subrout->m_args[0]);
    if( ASRUtils::types_equal(func_arg_type, arg_type, subrout->m_args[0], args[0]) ) {
        std::string arg0_name = ASRUtils::symbol_name(ASR::down_cast<ASR::Var_t>(subrout->m_args[0])->m_v);
        if( pass_arg != nullptr ) {
            std::string pass_arg_str = std::string(pass_arg);
            if( (arg0_name == pass_arg_str && args[0] != expr_dt) ) {
                err(std::string(subrout->m_name) + " is not a procedure of " +
                    ASRUtils::type_to_str_fortran_expr(arg_type, args[0]),
                    loc);
            }
        }
        found = true;
        Vec<ASR::call_arg_t> a_args;
        a_args.reserve(al, 1);
        for (size_t i = 0; i < args.size(); i++) {
            if (i == 0 && (expr_dt != nullptr)) {
                continue;
            }
            ASR::call_arg_t arg;
            arg.loc = arg_loc;
            arg.m_value = args[i];
            a_args.push_back(al, arg);
        }
        std::string subrout_name = to_lower(subrout->m_name);
        ASR::symbol_t* resolved = curr_scope->resolve_symbol(subrout_name);
        if( resolved && ASRUtils::symbol_get_past_external(resolved) == proc ) {
            matched_subrout_name = subrout_name;
        } else {
            std::string mangled_name = subrout_name + "@" + read_write;
            matched_subrout_name = mangled_name;
        }
        ASR::symbol_t *a_name = curr_scope->resolve_symbol(matched_subrout_name);
        if (expr_dt != nullptr) {
            ASR::Struct_t* temp_struct = ASR::down_cast<ASR::Struct_t>(
                ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(expr_dt)));
            a_name = ASRUtils::import_class_procedure(al, loc,
                temp_struct->m_symtab->get_symbol(subrout_name), curr_scope);
        }
        if( a_name == nullptr ) {
            a_name = ASR::down_cast<ASR::symbol_t>(ASR::make_ExternalSymbol_t(
                        al, loc, curr_scope, s2c(al, matched_subrout_name), proc,
                        ASRUtils::symbol_name(ASRUtils::get_asr_owner(proc)),
                        nullptr, 0, subrout->m_name, ASR::accessType::Public));
            curr_scope->add_symbol(matched_subrout_name, a_name);
        }
        if (ASRUtils::symbol_parent_symtab(a_name)->get_counter() != curr_scope->get_counter()) {
            ADD_ASR_DEPENDENCIES_WITH_NAME(curr_scope, a_name, current_function_dependencies, s2c(al, matched_subrout_name));
        }
        ASRUtils::insert_module_dependency(a_name, al, current_module_dependencies);
        ASRUtils::set_absent_optional_arguments_to_null(a_args, subrout, al, expr_dt);
        ASR::symbol_t* a_original_name = ASR::is_a<ASR::ExternalSymbol_t>(*a_name) ? a_name : sym;
        ASR::call_arg_t* call_args = a_args.p;
        size_t n_call_args = a_args.n;
        ASRUtils::insert_self_arg(al, a_name, call_args, n_call_args, expr_dt);
        asr = ASRUtils::make_SubroutineCall_t_util(al, loc, a_name, a_original_name,
                                        call_args, n_call_args, expr_dt, nullptr, false);
    }
}

bool use_overloaded_file_read_write(std::string &read_write, Vec<ASR::expr_t*> args,
                               SymbolTable* curr_scope, ASR::asr_t*& asr,
                               Allocator &al, const Location& loc,
                               SetChar& current_function_dependencies,
                               SetChar& current_module_dependencies,
                               const std::function<void (const std::string &, const Location &)> err) {
    ASR::ttype_t *arg_type = ASRUtils::type_get_past_allocatable(ASRUtils::expr_type(args[0]));
    bool found = false;
    ASR::symbol_t* sym = curr_scope->resolve_symbol(read_write);
    ASR::expr_t* expr_dt = nullptr;
    if( sym == nullptr ) {
        if( ASR::is_a<ASR::StructType_t>(*arg_type) ) {
            ASR::Struct_t* arg_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(args[0])));
            sym = arg_struct->m_symtab->resolve_symbol(read_write);
            expr_dt = args[0];
        }
    }
    if (sym) {
        ASR::symbol_t* orig_sym = ASRUtils::symbol_get_past_external(sym);
        auto process_proc_list = [&](ASR::symbol_t** procs, size_t n_procs) {
            for( size_t i = 0; i < n_procs && !found; i++ ) {
                ASR::symbol_t* proc = ASRUtils::symbol_get_past_external(procs[i]);
                switch( proc->type ) {
                    case ASR::symbolType::Function: {
                        process_overloaded_read_write_function(read_write, proc, args, arg_type,
                            found, al, args[0]->base.loc, curr_scope,
                            current_function_dependencies, current_module_dependencies, asr, sym,
                            loc, expr_dt, err);
                        break;
                    }
                    case ASR::symbolType::StructMethodDeclaration: {
                        ASR::StructMethodDeclaration_t* class_proc = ASR::down_cast<ASR::StructMethodDeclaration_t>(proc);
                        ASR::symbol_t* proc_func = ASR::down_cast<ASR::StructMethodDeclaration_t>(proc)->m_proc;
                        process_overloaded_read_write_function(read_write, proc_func, args, arg_type,
                            found, al, args[0]->base.loc, curr_scope,
                            current_function_dependencies, current_module_dependencies, asr, proc_func, loc,
                            expr_dt, err, class_proc->m_self_argument);
                        break;
                    }
                    default: {
                        err("Only functions and class procedures can be used for generic read/write statement, found " + std::to_string(proc->type), loc);
                    }
                }
            }
        };
        if (ASR::is_a<ASR::CustomOperator_t>(*orig_sym)) {
            ASR::CustomOperator_t* gen_proc = ASR::down_cast<ASR::CustomOperator_t>(orig_sym);
            process_proc_list(gen_proc->m_procs, gen_proc->n_procs);
        } else if (ASR::is_a<ASR::GenericProcedure_t>(*orig_sym)) {
            ASR::GenericProcedure_t* gen_proc = ASR::down_cast<ASR::GenericProcedure_t>(orig_sym);
            process_proc_list(gen_proc->m_procs, gen_proc->n_procs);
        } else {
            err("Only generic procedures can be used for overloaded read/write statement, found " + std::to_string(orig_sym->type), loc);
        }
    }
    return found;
}

bool use_overloaded(ASR::expr_t* left, ASR::expr_t* right,
                    ASR::cmpopType op, std::string& intrinsic_op_name,
                    SymbolTable* curr_scope, ASR::asr_t*& asr,
                    Allocator &al, const Location& loc,
                    SetChar& current_function_dependencies,
                    SetChar& current_module_dependencies,
                    const std::function<void (const std::string &, const Location &)> err) {
    ASR::ttype_t *left_type = ASRUtils::expr_type(left);
    ASR::ttype_t *right_type = ASRUtils::expr_type(right);
    ASR::Struct_t *left_struct = nullptr, *right_struct = nullptr;

    if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(left_type))) {
        left_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(left)));
    }
    if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(right_type))) {
        right_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(right)));
    }
    bool found = false;
    // Try left_struct first; if not found, try right_struct (for pass(rhs) operators)
    ASR::Struct_t* active_struct = left_struct;
    bool pass_right = false;
    bool op_overloaded_found = is_op_overloaded(op, intrinsic_op_name, curr_scope, left_struct);
    if (!op_overloaded_found && right_struct != nullptr) {
        op_overloaded_found = is_op_overloaded(op, intrinsic_op_name, curr_scope, right_struct);
        if (op_overloaded_found) {
            active_struct = right_struct;
            pass_right = true;
        }
    }
    if (op_overloaded_found) {
        ASR::symbol_t* sym = curr_scope->resolve_symbol(intrinsic_op_name);
        ASR::symbol_t* orig_sym = ASRUtils::symbol_get_past_external(sym);
        ASR::symbol_t* orig_sym2 = nullptr;
        if ( active_struct != nullptr) {
            ASR::Struct_t* temp_struct = active_struct;
            while (temp_struct) {
                if (temp_struct->m_symtab->get_symbol(intrinsic_op_name) != nullptr) {
                    orig_sym2 = temp_struct->m_symtab->get_symbol(intrinsic_op_name);
                    break;
                } else if (temp_struct->m_parent) {
                    temp_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(temp_struct->m_parent));
                } else {
                    temp_struct = nullptr;
                }
            }
        }
        // We need to compare all functions available (Class Procedures + interface declared)
        Vec<ASR::symbol_t*> op_overloading_procs; op_overloading_procs.reserve(al, 1);
        size_t n_interface_proc = 0;
        // functions that are declared by interface operator(==)
        if (orig_sym) {
            ASR::CustomOperator_t* gen_proc = ASR::down_cast<ASR::CustomOperator_t>(orig_sym);
            for( size_t i = 0; i < gen_proc->n_procs; i++ ) {
                op_overloading_procs.push_back(al, gen_proc->m_procs[i]);
            }
            n_interface_proc = gen_proc->n_procs;
        }
        // functions that are class procedure
        if (orig_sym2) {
            ASR::CustomOperator_t* gen_proc = ASR::down_cast<ASR::CustomOperator_t>(orig_sym2);
            for( size_t i = 0; i < gen_proc->n_procs; i++ ) {
                op_overloading_procs.push_back(al, gen_proc->m_procs[i]);
            }
        }
        for( size_t i = 0; i < op_overloading_procs.size() && !found; i++ ) {
            ASR::symbol_t* proc;
            ASR::symbol_t* orig_proc = ASRUtils::symbol_get_past_external(op_overloading_procs[i]);
            if ( ASR::is_a<ASR::StructMethodDeclaration_t>(*op_overloading_procs[i]) ) {
                ASR::StructMethodDeclaration_t* cp = ASR::down_cast<ASR::StructMethodDeclaration_t>(op_overloading_procs[i]);
                if (cp->m_parent_symtab->get_counter() != active_struct->m_symtab->get_counter()) {
                    // It may be overided in the derived class
                    ASR::Struct_t* temp_struct = active_struct;
                    while (temp_struct->m_parent) {
                        if (temp_struct->m_symtab->get_symbol(cp->m_name) != nullptr) {
                            cp = ASR::down_cast<ASR::StructMethodDeclaration_t>(temp_struct->m_symtab->get_symbol(cp->m_name));
                            orig_proc = temp_struct->m_symtab->get_symbol(cp->m_name);
                            break;
                        }
                        temp_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(temp_struct->m_parent));
                    }
                }
                proc =  ASRUtils::symbol_get_past_external(cp->m_proc);
            } else {
                proc = ASRUtils::symbol_get_past_external(op_overloading_procs[i]);
            }
            switch(proc->type) {
                case ASR::symbolType::Function: {
                    ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(proc);
                    bool is_elemental = ASRUtils::is_elemental(proc);
                    std::string matched_func_name = "";
                    if( func->n_args == 2 ) {
                        ASR::ttype_t* left_arg_type = ASRUtils::expr_type(func->m_args[0]);
                        ASR::ttype_t* right_arg_type = ASRUtils::expr_type(func->m_args[1]);
                        bool not_matching = (!ASRUtils::is_allocatable(left_type) && ASRUtils::is_allocatable(left_arg_type)) ||
                                            (!ASRUtils::is_allocatable(right_type) && ASRUtils::is_allocatable(right_arg_type)) ||
                                            (!ASRUtils::is_pointer(left_type) && ASRUtils::is_pointer(left_arg_type)) ||
                                            (!ASRUtils::is_pointer(right_type) && ASRUtils::is_pointer(right_arg_type));
                                            
                        ASR::ttype_t* left_type2 = ASRUtils::type_get_past_allocatable_pointer(left_type);
                        ASR::ttype_t* left_arg_type2 = ASRUtils::type_get_past_allocatable_pointer(left_arg_type);
                        ASR::ttype_t* right_type2 = ASRUtils::type_get_past_allocatable_pointer(right_type);
                        ASR::ttype_t* right_arg_type2 = ASRUtils::type_get_past_allocatable_pointer(right_arg_type);

                        // Check for array type
                        not_matching = not_matching || (!is_elemental &&
                                       ((left_arg_type2->type != left_type2->type &&
                                         !ASRUtils::is_unlimited_polymorphic_type(left_arg_type2)) ||
                                        (right_arg_type2->type != right_type2->type &&
                                         !ASRUtils::is_unlimited_polymorphic_type(right_arg_type2))));

                        // Get element type and compare
                        left_type2 = ASRUtils::type_get_past_array(left_type2);
                        left_arg_type2 = ASRUtils::type_get_past_array(left_arg_type2);
                        right_type2 = ASRUtils::type_get_past_array(right_type2);
                        right_arg_type2 = ASRUtils::type_get_past_array(right_arg_type2);

                        if( !not_matching && ((left_arg_type2->type == left_type2->type ||
                                                ASRUtils::is_unlimited_polymorphic_type(left_arg_type2)) &&
                                               (right_arg_type2->type == right_type2->type ||
                                                ASRUtils::is_unlimited_polymorphic_type(right_arg_type2))) ) {
                            // Check StructType compatibility independently for each side
                            if (ASR::is_a<ASR::StructType_t>(*left_type2) &&
                                ASR::is_a<ASR::StructType_t>(*left_arg_type2)) {
                                ASR::Struct_t* left_arg_sym = ASR::down_cast<ASR::Struct_t>(
                                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(func->m_args[0])));
                                if (!can_pass_derviedtype_arg_to_parameter(left_struct, left_arg_sym)) {
                                    break;
                                }
                            }
                            if (ASR::is_a<ASR::StructType_t>(*right_type2) &&
                                ASR::is_a<ASR::StructType_t>(*right_arg_type2)) {
                                ASR::Struct_t* right_arg_sym = ASR::down_cast<ASR::Struct_t>(
                                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(func->m_args[1])));
                                if (!can_pass_derviedtype_arg_to_parameter(right_struct, right_arg_sym)) {
                                    break;
                                }
                            }
                            found = true;
                            Vec<ASR::call_arg_t> a_args;
                            a_args.reserve(al, 1);
                            ASR::call_arg_t left_call_arg, right_call_arg;
                            ASR::expr_t* self_arg = nullptr;
                            if (i < n_interface_proc || pass_right) {
                                // For interface procs or pass(rhs) class procs,
                                // both args go in a_args in proper order
                                left_call_arg.loc = left->base.loc, left_call_arg.m_value = left;
                                a_args.push_back(al, left_call_arg);
                                right_call_arg.loc = right->base.loc, right_call_arg.m_value = right;
                                a_args.push_back(al, right_call_arg);
                            } else {
                                // pass(lhs) or default: self_arg is left, right is the regular argument
                                right_call_arg.loc = right->base.loc, right_call_arg.m_value = right;
                                a_args.push_back(al, right_call_arg);
                            }
                            std::string func_name = to_lower(func->m_name);
                            if (i >= n_interface_proc && !pass_right) {
                                ASR::symbol_t* mem_ext = import_class_procedure(al, loc, orig_proc, curr_scope);
                                matched_func_name = ASRUtils::symbol_name(mem_ext);
                                self_arg = left;
                            } else if( curr_scope->resolve_symbol(func_name) ) {
                                matched_func_name = func_name;
                            } else {
                                std::string mangled_name = func_name + "@" + intrinsic_op_name;
                                matched_func_name = mangled_name;
                            }
                            ASR::symbol_t* a_name = curr_scope->resolve_symbol(matched_func_name);
                            if( a_name == nullptr ) {
                                if (i >= n_interface_proc && !pass_right) {
                                    err("Unable to resolve matched function for operator overloading, " + matched_func_name, loc);
                                } else {
                                    a_name = ASR::down_cast<ASR::symbol_t>(ASR::make_ExternalSymbol_t(
                                                al, loc, curr_scope, s2c(al, matched_func_name), proc,
                                                ASRUtils::symbol_name(ASRUtils::get_asr_owner(proc)),
                                                nullptr, 0, func->m_name, ASR::accessType::Public));
                                    curr_scope->add_symbol(matched_func_name, a_name);
                                }
                            }
                            ASR::ttype_t *return_type = nullptr;
                            if( ASRUtils::get_FunctionType(func)->m_elemental &&
                                func->n_args >= 1 &&
                                ASRUtils::is_array(ASRUtils::expr_type(a_args[0].m_value)) ) {
                                ASR::dimension_t* array_dims;
                                size_t array_n_dims = ASRUtils::extract_dimensions_from_ttype(
                                ASRUtils::expr_type(a_args[0].m_value), array_dims);
                                Vec<ASR::dimension_t> new_dims;
                                new_dims.from_pointer_n_copy(al, array_dims, array_n_dims);
                                return_type = ASRUtils::duplicate_type(al,
                                                ASRUtils::get_FunctionType(func)->m_return_var_type,
                                                &new_dims);
                            }
                            if (ASRUtils::symbol_parent_symtab(a_name)->get_counter() != curr_scope->get_counter()) {
                                ADD_ASR_DEPENDENCIES_WITH_NAME(curr_scope, a_name, current_function_dependencies, s2c(al, matched_func_name));
                            }
                            ASRUtils::insert_module_dependency(a_name, al, current_module_dependencies);
                            ASRUtils::set_absent_optional_arguments_to_null(a_args, func, al, self_arg);
                            ASR::call_arg_t* call_args2 = a_args.p;
                            size_t n_call_args2 = a_args.n;
                            ASRUtils::insert_self_arg(al, a_name, call_args2, n_call_args2, self_arg);
                            if (return_type == nullptr) {
                                return_type = ASRUtils::EXPR2VAR(func->m_return_var)->m_type;
                                if (ASR::is_a<ASR::Array_t>(*return_type)) {
                                    ASR::Array_t* arr_t = ASR::down_cast<ASR::Array_t>(return_type);
                                    ASRUtils::ExprStmtDuplicator expr_duplicator(al);
                                    expr_duplicator.allow_procedure_calls = true;
                                    Vec<ASR::call_arg_t> oa_vec;
                                    oa_vec.from_pointer_n_copy(al, call_args2, n_call_args2);
                                    ASRUtils::ReplaceArgVisitor arg_replacer(al, curr_scope, func,
                                        oa_vec, current_function_dependencies, current_module_dependencies);
                                    Vec<ASR::dimension_t> new_dims;
                                    new_dims.reserve(al, arr_t->n_dims);
                                    for (size_t di = 0; di < arr_t->n_dims; di++) {
                                        ASR::dimension_t new_dim;
                                        new_dim.loc = arr_t->m_dims[di].loc;
                                        new_dim.m_start = nullptr;
                                        new_dim.m_length = nullptr;
                                        if (arr_t->m_dims[di].m_start) {
                                            expr_duplicator.success = true;
                                            ASR::expr_t* sc = expr_duplicator.duplicate_expr(arr_t->m_dims[di].m_start);
                                            LCOMPILERS_ASSERT(expr_duplicator.success);
                                            arg_replacer.current_expr = &sc;
                                            arg_replacer.replace_expr(sc);
                                            new_dim.m_start = sc;
                                        }
                                        if (arr_t->m_dims[di].m_length) {
                                            expr_duplicator.success = true;
                                            ASR::expr_t* lc = expr_duplicator.duplicate_expr(arr_t->m_dims[di].m_length);
                                            LCOMPILERS_ASSERT(expr_duplicator.success);
                                            arg_replacer.current_expr = &lc;
                                            arg_replacer.replace_expr(lc);
                                            new_dim.m_length = lc;
                                        }
                                        new_dims.push_back(al, new_dim);
                                    }
                                    bool for_type = true;
                                    for (size_t k = 0; k < new_dims.size(); k++) {
                                        if (new_dims[k].m_length && ASR::is_a<ASR::ArraySize_t>(*new_dims[k].m_length)) {
                                            ASR::ArraySize_t* as_t = ASR::down_cast<ASR::ArraySize_t>(new_dims[k].m_length);
                                            if ((ASR::is_a<ASR::FunctionCall_t>(*as_t->m_v) && ASRUtils::is_allocatable(as_t->m_v)) ||
                                                ASR::is_a<ASR::IntrinsicArrayFunction_t>(*as_t->m_v)) {
                                                for_type = false;
                                                break;
                                            }
                                        }
                                    }
                                    return_type = ASRUtils::make_Array_t_util(
                                        al, loc, arr_t->m_type, new_dims.p, new_dims.size(),
                                        ASR::abiType::Source, false,
                                        ASR::array_physical_typeType::DescriptorArray,
                                        false, false, for_type);
                                }
                            }
                            asr = ASRUtils::make_FunctionCall_t_util(al, loc, a_name, sym,
                                                            call_args2, n_call_args2,
                                                            return_type,
                                                            nullptr, self_arg
                                                            );
                        }
                    }
                    break;
                }
                default: {
                    err("While overloading binary operators only functions can be used",
                                        proc->base.loc);
                }
            }
        }
    }
    return found;
}

bool use_overloaded(ASR::expr_t* left, ASR::expr_t* right,
                            ASR::logicalbinopType op, std::string& intrinsic_op_name,
                            SymbolTable* curr_scope, ASR::asr_t*& asr,
                            Allocator& al, const Location& loc,
                            SetChar& current_function_dependencies,
                            SetChar& current_module_dependencies,
                            const std::function<void(const std::string&, const Location&)> err) {
    ASR::ttype_t* left_type = ASRUtils::expr_type(left);
    ASR::ttype_t* right_type = ASRUtils::expr_type(right);
    ASR::Struct_t* left_struct = nullptr;
    if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(left_type))) {
        left_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(left))));
    }
    bool found = false;
    if (is_op_overloaded(op, intrinsic_op_name, curr_scope, left_struct)) {
        ASR::symbol_t* sym = curr_scope->resolve_symbol(intrinsic_op_name);
        ASR::symbol_t* orig_sym = ASRUtils::symbol_get_past_external(sym);
        if (left_struct != nullptr && orig_sym == nullptr) {
            orig_sym = left_struct->m_symtab->resolve_symbol(intrinsic_op_name);
        }
        ASR::CustomOperator_t* gen_proc = ASR::down_cast<ASR::CustomOperator_t>(orig_sym);
        for (size_t i = 0; i < gen_proc->n_procs && !found; i++) {
            ASR::symbol_t* proc;
            if (ASR::is_a<ASR::StructMethodDeclaration_t>(*gen_proc->m_procs[i])) {
                proc = ASRUtils::symbol_get_past_external(
                    ASR::down_cast<ASR::StructMethodDeclaration_t>(gen_proc->m_procs[i])->m_proc);
            } else {
                proc = ASRUtils::symbol_get_past_external(gen_proc->m_procs[i]);
            }
            switch (proc->type) {
                case ASR::symbolType::Function: {
                    ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(proc);
                    bool is_elemental = ASRUtils::is_elemental(proc);
                    std::string matched_func_name = "";
                    if (func->n_args == 2) {
                        ASR::ttype_t* left_arg_type = ASRUtils::expr_type(func->m_args[0]);
                        ASR::ttype_t* right_arg_type = ASRUtils::expr_type(func->m_args[1]);
                        bool not_matching = (!ASRUtils::is_allocatable(left_type) && ASRUtils::is_allocatable(left_arg_type)) ||
                                            (!ASRUtils::is_allocatable(right_type) && ASRUtils::is_allocatable(right_arg_type)) ||
                                            (!ASRUtils::is_pointer(left_type) && ASRUtils::is_pointer(left_arg_type)) ||
                                            (!ASRUtils::is_pointer(right_type) && ASRUtils::is_pointer(right_arg_type));

                        ASR::ttype_t* left_type2 = ASRUtils::type_get_past_allocatable_pointer(left_type);
                        ASR::ttype_t* left_arg_type2 = ASRUtils::type_get_past_allocatable_pointer(left_arg_type);
                        ASR::ttype_t* right_type2 = ASRUtils::type_get_past_allocatable_pointer(right_type);
                        ASR::ttype_t* right_arg_type2 = ASRUtils::type_get_past_allocatable_pointer(right_arg_type);

                        // Check for array type
                        not_matching = not_matching || (!is_elemental &&
                                       ((left_arg_type2->type != left_type2->type &&
                                         !ASRUtils::is_unlimited_polymorphic_type(left_arg_type2)) ||
                                        (right_arg_type2->type != right_type2->type &&
                                         !ASRUtils::is_unlimited_polymorphic_type(right_arg_type2))));

                        // Get element type and compare
                        left_type2 = ASRUtils::type_get_past_array(left_type2);
                        left_arg_type2 = ASRUtils::type_get_past_array(left_arg_type2);
                        right_type2 = ASRUtils::type_get_past_array(right_type2);
                        right_arg_type2 = ASRUtils::type_get_past_array(right_arg_type2);

                        if ( !not_matching && ((left_arg_type2->type == left_type2->type ||
                                                ASRUtils::is_unlimited_polymorphic_type(left_arg_type2)) &&
                                               (right_arg_type2->type == right_type2->type ||
                                                ASRUtils::is_unlimited_polymorphic_type(right_arg_type2))) ) {
                            // Check StructType compatibility independently for each side
                            if (ASR::is_a<ASR::StructType_t>(*left_type2)
                                && ASR::is_a<ASR::StructType_t>(*left_arg_type2)) {
                                ASR::Struct_t* left_sym = ASR::down_cast<ASR::Struct_t>(
                                    ASRUtils::symbol_get_past_external(
                                        ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(left))));
                                ASR::Struct_t* left_arg_sym = ASR::down_cast<ASR::Struct_t>(
                                    ASRUtils::symbol_get_past_external(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(func->m_args[0]))));
                                if (!can_pass_derviedtype_arg_to_parameter(left_sym, left_arg_sym)) {
                                    break;
                                }
                            }
                            if (ASR::is_a<ASR::StructType_t>(*right_type2)
                                && ASR::is_a<ASR::StructType_t>(*right_arg_type2)) {
                                ASR::Struct_t* right_sym = ASR::down_cast<ASR::Struct_t>(
                                    ASRUtils::symbol_get_past_external(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(right))));
                                ASR::Struct_t* right_arg_sym = ASR::down_cast<ASR::Struct_t>(
                                    ASRUtils::symbol_get_past_external(
                                        ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(func->m_args[1]))));
                                if (!can_pass_derviedtype_arg_to_parameter(right_sym, right_arg_sym)) {
                                    break;
                                }
                            }
                            found = true;
                            Vec<ASR::call_arg_t> a_args;
                            a_args.reserve(al, 2);
                            a_args.push_back(al, ASR::call_arg_t{ left->base.loc, left });
                            a_args.push_back(al, ASR::call_arg_t{ right->base.loc, right });
                            std::string func_name = to_lower(func->m_name);
                            if (curr_scope->resolve_symbol(func_name)) {
                                matched_func_name = func_name;
                            } else {
                                matched_func_name = func_name + "@" + intrinsic_op_name;
                            }
                            ASR::symbol_t* a_name = curr_scope->resolve_symbol(matched_func_name);
                            if (a_name == nullptr) {
                                err("Unable to resolve matched function for overloaded logical operator: "
                                        + matched_func_name,
                                    loc);
                            }
                            ASR::ttype_t* return_type = nullptr;
                            if (ASRUtils::get_FunctionType(func)->m_elemental && func->n_args >= 1
                                && ASRUtils::is_array(ASRUtils::expr_type(a_args[0].m_value))) {
                                ASR::dimension_t* array_dims;
                                size_t array_n_dims = ASRUtils::extract_dimensions_from_ttype(
                                    ASRUtils::expr_type(a_args[0].m_value), array_dims);
                                Vec<ASR::dimension_t> new_dims;
                                new_dims.from_pointer_n_copy(al, array_dims, array_n_dims);
                                return_type = ASRUtils::duplicate_type(
                                    al,
                                    ASRUtils::get_FunctionType(func)->m_return_var_type,
                                    &new_dims);
                            } else {
                                return_type = ASRUtils::expr_type(func->m_return_var);
                            }
                            if (ASRUtils::symbol_parent_symtab(a_name)->get_counter()
                                != curr_scope->get_counter()) {
                                ADD_ASR_DEPENDENCIES_WITH_NAME(curr_scope,
                                                               a_name,
                                                               current_function_dependencies,
                                                               s2c(al, matched_func_name));
                            }
                            ASRUtils::insert_module_dependency(
                                a_name, al, current_module_dependencies);
                            ASRUtils::set_absent_optional_arguments_to_null(a_args, func, al);
                            {
                                ASR::FunctionType_t* ftype = ASRUtils::get_FunctionType(func);
                                if (ftype->m_return_var_type) {
                                    return_type = ASRUtils::duplicate_type(al, ftype->m_return_var_type);
                                    ASR::dimension_t* ret_dims = nullptr;
                                    size_t n_ret_dims = ASRUtils::extract_dimensions_from_ttype(return_type, ret_dims);
                                     if (n_ret_dims > 0) {
                                        // Keep whichever 'r' initialization is currently there, for example:
                                        ReplaceFunctionParamWithArg r(al, a_args.p, a_args.n); // OR call_args1, n_call_args1

                                        // 1. Initialize the caller's duplicator OUTSIDE the loop
                                        ASRUtils::ExprStmtDuplicator caller_dup(al);
                                        caller_dup.allow_procedure_calls = true;
                                        caller_dup.success = true;

                                     for (size_t di = 0; di < n_ret_dims; di++) {
                                         if (ret_dims[di].m_length) {
                                            // 2. Extract and safely duplicate m_length
                                            ASR::expr_t* replaced_length = r.replace_FunctionParam_with_arg(ret_dims[di].m_length);
                                            ret_dims[di].m_length = (replaced_length == ret_dims[di].m_length) ? caller_dup.duplicate_expr(replaced_length) : replaced_length;
                                         }
        
                                         if (ret_dims[di].m_start) {
                                            // 3. Extract and safely duplicate m_start
                                            ASR::expr_t* replaced_start = r.replace_FunctionParam_with_arg(ret_dims[di].m_start);
                                            ret_dims[di].m_start = (replaced_start == ret_dims[di].m_start) ? caller_dup.duplicate_expr(replaced_start) : replaced_start;
                                         }
                                        }
                                     }
                                }
                            }
                            asr = ASRUtils::make_FunctionCall_t_util(
                                al, loc, a_name, sym, a_args.p, 2, return_type, nullptr, nullptr);
                        }
                    }
                    break;
                }
                default: {
                    err("Only functions can be used to overload logical binary operators",
                        proc->base.loc);
                }
            }
        }
    }
    return found;
}

bool is_op_overloaded(ASR::cmpopType op, std::string& intrinsic_op_name,
                      SymbolTable* curr_scope, ASR::Struct_t *left_struct) {
    bool result = true;
    switch(op) {
        case ASR::cmpopType::Eq: {
            if(intrinsic_op_name != "~eq") {
                result = false;
            }
            break;
        }
        case ASR::cmpopType::NotEq: {
            if(intrinsic_op_name != "~noteq") {
                result = false;
            }
            break;
        }
        case ASR::cmpopType::Lt: {
            if(intrinsic_op_name != "~lt") {
                result = false;
            }
            break;
        }
        case ASR::cmpopType::LtE: {
            if(intrinsic_op_name != "~lte") {
                result = false;
            }
            break;
        }
        case ASR::cmpopType::Gt: {
            if(intrinsic_op_name != "~gt") {
                result = false;
            }
            break;
        }
        case ASR::cmpopType::GtE: {
            if(intrinsic_op_name != "~gte") {
                result = false;
            }
            break;
        }
    }
    if( result && curr_scope->resolve_symbol(intrinsic_op_name) == nullptr ) {
        bool is_op_in_scope = false;
        ASR::Struct_t* temp_struct = left_struct;
        while (temp_struct) {
            // Try to resolve intrinsic_op_name till its last ancestor
            if (temp_struct->m_symtab->get_symbol(
                intrinsic_op_name) != nullptr) {
                is_op_in_scope = true;
                break;
            } else if (temp_struct->m_parent) {
                temp_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(temp_struct->m_parent));
            } else {
                temp_struct = nullptr;
            }
        }
        if (is_op_in_scope) {
            result = true;
        } else {
            result = false;
        }
    }

    return result;
}

bool is_op_overloaded(ASR::logicalbinopType op, std::string& intrinsic_op_name,
                      SymbolTable* curr_scope, ASR::Struct_t* left_struct) {
    bool result = true;
    switch (op) {
        case ASR::logicalbinopType::And: {
            if (intrinsic_op_name != "~and") {
                result = false;
            }
            break;
        }
        case ASR::logicalbinopType::Or: {
            if (intrinsic_op_name != "~or") {
                result = false;
            }
            break;
        }
        case ASR::logicalbinopType::Xor: {
            if (intrinsic_op_name != "~xor") {
                result = false;
            }
            break;
        }
        case ASR::logicalbinopType::NEqv: {
            if (intrinsic_op_name != "~neqv") {
                result = false;
            }
            break;
        }
        case ASR::logicalbinopType::Eqv: {
            if (intrinsic_op_name != "~eqv") {
                result = false;
            }
            break;
        }
        default: {
            result = false;
            break;
        }
    }

    if (result && curr_scope->resolve_symbol(intrinsic_op_name) == nullptr) {
        if (left_struct != nullptr) {
            ASR::Struct_t* temp_struct = left_struct;
            result = false;
            while (temp_struct) {
                if (temp_struct->m_symtab->resolve_symbol(intrinsic_op_name) != nullptr) {
                    result = true;
                    break;
                } else if (temp_struct->m_parent) {
                    temp_struct = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(temp_struct->m_parent));
                } else {
                    temp_struct = nullptr;
                }
            }
        } else {
            result = false;
        }
    }

    return result;
}

template <typename T>
bool argument_types_match(const Vec<ASR::call_arg_t>& args,
        const T &sub) {
    if (args.size() <= sub.n_args) {
        size_t i;
        for (i = 0; i < args.size(); i++) {
            ASR::symbol_t* sub_arg_sym = symbol_get_past_external(
                ASR::down_cast<ASR::Var_t>(sub.m_args[i])->m_v
            );
            if (ASR::is_a<ASR::Variable_t>(*sub_arg_sym)) {
                ASR::Variable_t* v = ASR::down_cast<ASR::Variable_t>(sub_arg_sym);
                if (args[i].m_value == nullptr &&
                    v->m_presence == ASR::presenceType::Optional) {
                    // If it's optional and argument is empty
                    // continue to next argument.
                    continue;
                }
                if (args[i].m_value == nullptr) {
                    // Required argument is not provided — overload doesn't match.
                    return false;
                }
                ASR::ttype_t *arg1 = ASRUtils::expr_type(args[i].m_value);
                ASR::ttype_t *arg2 = v->m_type;

                // Check if formal argument is an implicit interface procedure (e.g. procedure() :: f)
                // If it is, it accepts any explicit interface procedure as actual argument.
                if (v->m_type_declaration && ASR::is_a<ASR::FunctionType_t>(*arg1)) {
                    ASR::symbol_t* type_decl = ASRUtils::symbol_get_past_external(v->m_type_declaration);
                    if (ASR::is_a<ASR::Function_t>(*type_decl)) {
                        std::string decl_name = ASR::down_cast<ASR::Function_t>(type_decl)->m_name;
                        if (decl_name.find("__") == 0 && decl_name.find("_iface_implicit") != std::string::npos) {
                            continue;
                        }
                    }
                }

                // Check if actual argument is an implicit interface procedure (e.g. external :: f)
                // If it is, it is compatible with any explicit interface formal argument.
                // Similarly, if formal argument is an implicit interface, it accepts an explicit interface actual argument.
                if (ASR::is_a<ASR::FunctionType_t>(*arg1) && ASR::is_a<ASR::FunctionType_t>(*arg2)) {
                    ASR::FunctionType_t* arg1_func_type = ASR::down_cast<ASR::FunctionType_t>(arg1);
                    ASR::FunctionType_t* arg2_func_type = ASR::down_cast<ASR::FunctionType_t>(arg2);
                    if ((arg1_func_type->n_arg_types == 0 && arg1_func_type->m_deftype == ASR::deftypeType::Interface) ||
                        (arg2_func_type->n_arg_types == 0 && arg2_func_type->m_deftype == ASR::deftypeType::Interface)) {
                        continue;
                    }
                }

                ASR::symbol_t* s1 = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(args[i].m_value));
                ASR::symbol_t* s2 = nullptr;
                ASR::ttype_t* arg2_ext = ASRUtils::extract_type(arg2);
                bool is_elemental = ASRUtils::get_FunctionType(sub)->m_elemental;
                if (ASR::is_a<ASR::StructType_t>(*arg2_ext)) {
                    if ((ASRUtils::is_allocatable(arg2) && !ASRUtils::is_allocatable(arg1)) ||
                            (ASRUtils::is_array(arg2) && !ASRUtils::is_array(arg1) && !ASRUtils::is_assumed_rank_array(arg2)) ||
                            (!is_elemental && !ASRUtils::is_array(arg2) && ASRUtils::is_array(arg1))) {
                        return false;
                    }
                    s2 = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(sub.m_args[i]));
                }
                if (s1 && s2) {
                    bool is_arg_class = ASRUtils::is_class_type(arg2_ext);
                    if (!is_arg_class) {     // if argument is c_struct we need exact matching types
                        if (s1 != s2) {
                            return false;
                        }
                    } else if (!ASRUtils::check_class_assignment_compatibility(s2, s1)) {
                        return false;
                    }
                    if (!is_elemental) {
                        int rank1 = ASRUtils::extract_n_dims_from_ttype(arg1);
                        int rank2 = ASRUtils::extract_n_dims_from_ttype(arg2);
                        if (rank1 != rank2
                                && !ASRUtils::is_assumed_rank_array(arg2)) {
                            return false;
                        }
                    }
                } else {
                    if (!types_equal(arg1, arg2, args[i].m_value, sub.m_args[i], !ASRUtils::get_FunctionType(sub)->m_elemental)) {
                        return false;
                    }
                }
            } else if (ASR::is_a<ASR::Function_t>(*sub_arg_sym)) {
                if (args[i].m_value == nullptr) {
                    // Required procedure argument is not provided — overload doesn't match.
                    return false;
                }
                ASR::Function_t* f = ASR::down_cast<ASR::Function_t>(sub_arg_sym);

                ASR::ttype_t *arg1 = ASRUtils::expr_type(args[i].m_value);
                ASR::ttype_t *arg2 = f->m_function_signature;

                // Check if actual argument is an implicit interface procedure (e.g. external :: f)
                // If it is, it is compatible with any explicit interface formal argument.
                if (ASR::is_a<ASR::FunctionType_t>(*arg1) && ASR::is_a<ASR::FunctionType_t>(*arg2)) {
                    ASR::FunctionType_t* arg1_func_type = ASR::down_cast<ASR::FunctionType_t>(arg1);
                    if (arg1_func_type->n_arg_types == 0 && arg1_func_type->m_deftype == ASR::deftypeType::Interface) {
                        continue;
                    }
                }

                Allocator al(512);
                if (!types_equal(arg1, arg2, args[i].m_value,
                    ASRUtils::get_expr_from_sym(al, &f->base), false)) {
                    return false;
                }
            }
        }
        for( ; i < sub.n_args; i++ ) {
            ASR::symbol_t* sub_arg_sym = symbol_get_past_external(
                ASR::down_cast<ASR::Var_t>(sub.m_args[i])->m_v);
            if (ASR::is_a<ASR::Variable_t>(*sub_arg_sym)) {
                ASR::Variable_t* v = ASR::down_cast<ASR::Variable_t>(sub_arg_sym);
                if (v->m_presence != ASR::presenceType::Optional) {
                    return false;
                }
            } else if (ASR::is_a<ASR::Function_t>(*sub_arg_sym)) {
                return false;
            }
        }
        return true;
    } else {
        return false;
    }
}

bool select_func_subrout(const ASR::symbol_t* proc, const Vec<ASR::call_arg_t>& args,
                         Location& loc, const std::function<void (const std::string &, const Location &)> err) {
    bool result = false;
    proc = ASRUtils::symbol_get_past_external(proc);
    if (ASR::is_a<ASR::StructMethodDeclaration_t>(*proc)) {
        proc = ASRUtils::symbol_get_past_external(
            ASR::down_cast<ASR::StructMethodDeclaration_t>(proc)->m_proc);
    }
    if (ASR::is_a<ASR::Function_t>(*proc)) {
        ASR::Function_t *fn
            = ASR::down_cast<ASR::Function_t>(proc);
        if (argument_types_match(args, *fn)) {
            result = true;
        }
    } else {
        err("Only Subroutine and Function supported in generic procedure", loc);
    }
    return result;
}

ASR::asr_t* symbol_resolve_external_generic_procedure_without_eval(
            const Location &loc,
            ASR::symbol_t *v, Vec<ASR::call_arg_t>& args,
            SymbolTable* current_scope, Allocator& al,
            const std::function<void (const std::string &, const Location &)> err) {
    ASR::ExternalSymbol_t *p = ASR::down_cast<ASR::ExternalSymbol_t>(v);
    ASR::symbol_t *f2 = ASR::down_cast<ASR::ExternalSymbol_t>(v)->m_external;
    ASR::GenericProcedure_t *g = ASR::down_cast<ASR::GenericProcedure_t>(f2);
    int idx = select_generic_procedure(args, *g, loc, err);
    if (idx < 0) {
        return nullptr;
    }
    ASR::symbol_t *final_sym = g->m_procs[idx];
    ASR::symbol_t *final_sym_unwrapped = ASRUtils::symbol_get_past_external(final_sym);
    if (ASR::is_a<ASR::StructMethodDeclaration_t>(*final_sym_unwrapped)) {
        // Import class procedures into the current scope so call symbols stay local.
        final_sym = ASRUtils::import_class_procedure(al, loc, final_sym_unwrapped, current_scope);
    }

    if (ASRUtils::symbol_parent_symtab(final_sym)->get_counter() != current_scope->get_counter()) {
        // Create a local ExternalSymbol for non-local specific procedures.
        ASR::symbol_t *final_sym_past_ext = ASRUtils::symbol_get_past_external(final_sym);
        std::string local_sym = std::string(p->m_name) + "@"
            + ASRUtils::symbol_name(final_sym_past_ext);
        if (current_scope->get_symbol(local_sym) == nullptr) {
            ASR::symbol_t *owner_sym = ASRUtils::get_asr_owner(final_sym_past_ext);
            Str name;
            name.from_str(al, local_sym);
            ASR::asr_t *sub = ASR::make_ExternalSymbol_t(
                al, g->base.base.loc,
                current_scope,
                name.c_str(al),
                final_sym_past_ext,
                s2c(al, ASRUtils::symbol_name(owner_sym)), nullptr, 0,
                s2c(al, ASRUtils::symbol_name(final_sym_past_ext)),
                ASR::accessType::Private
            );
            current_scope->add_symbol(local_sym, ASR::down_cast<ASR::symbol_t>(sub));
        }
        final_sym = current_scope->get_symbol(local_sym);
    }

    ASR::symbol_t *called_proc = ASRUtils::symbol_get_past_external(final_sym);
    if (ASR::is_a<ASR::StructMethodDeclaration_t>(*called_proc)) {
        called_proc = ASRUtils::symbol_get_past_external(
            ASR::down_cast<ASR::StructMethodDeclaration_t>(called_proc)->m_proc);
    }
    if (!ASR::is_a<ASR::Function_t>(*called_proc)) {
        err("Only Subroutine and Function supported in generic procedure", loc);
        return nullptr;
    }

    ASR::Function_t *func = ASR::down_cast<ASR::Function_t>(called_proc);
    bool is_subroutine = func->m_return_var == nullptr;
    ASR::ttype_t *return_type = nullptr;
    if (func->m_return_var) {
        if( ASRUtils::get_FunctionType(func)->m_elemental &&
            func->n_args >= 1 &&
            ASRUtils::is_array(ASRUtils::expr_type(args[0].m_value)) ) {
            ASR::dimension_t* array_dims;
            size_t array_n_dims = ASRUtils::extract_dimensions_from_ttype(
            ASRUtils::expr_type(args[0].m_value), array_dims);
            Vec<ASR::dimension_t> new_dims;
            new_dims.from_pointer_n_copy(al, array_dims, array_n_dims);
            return_type = ASRUtils::duplicate_type(al,
                            ASRUtils::get_FunctionType(func)->m_return_var_type,
                            &new_dims);
        } else {
            return_type = ASRUtils::EXPR2VAR(func->m_return_var)->m_type;
        }
    }
    // ASRUtils::insert_module_dependency(v, al, current_module_dependencies);
    if( is_subroutine ) {
        ASRUtils::set_absent_optional_arguments_to_null(args, func, al);
        return ASRUtils::make_SubroutineCall_t_util(al, loc, final_sym,
                                        v, args.p, args.size(),
                                        nullptr, nullptr, false);
    } else {
        ASRUtils::set_absent_optional_arguments_to_null(args, func, al);
        return ASRUtils::make_FunctionCall_t_util(al, loc, final_sym,
                                        v, args.p, args.size(),
                                        return_type,
                                        nullptr, nullptr
                                        );
    }
}

ASR::asr_t* make_Cast_t_value(Allocator &al, const Location &a_loc,
            ASR::expr_t* a_arg, ASR::cast_kindType a_kind, ASR::ttype_t* a_type) {

    ASR::expr_t* value = nullptr;

    if (ASRUtils::expr_value(a_arg)) {
        // calculate value
        if (a_kind == ASR::cast_kindType::RealToInteger) {
            ASR::RealConstant_t* rc = ASR::down_cast<ASR::RealConstant_t>(
                        ASRUtils::expr_value(a_arg));
            // kind=16 m_r is a pointer-encoded payload, not a double — skip
            // compile-time folding and let the runtime fptosi handle it.
            if (ASRUtils::extract_kind_from_ttype_t(rc->m_type) != 16) {
                int64_t v = rc->m_r;
                value = ASR::down_cast<ASR::expr_t>(
                        ASR::make_IntegerConstant_t(al, a_loc, v, a_type));
            }
        } else if (a_kind == ASR::cast_kindType::RealToReal) {
            ASR::RealConstant_t* rc = ASR::down_cast<ASR::RealConstant_t>(
                       ASRUtils::expr_value(a_arg));
            int src_kind = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(a_arg));
            int dest_kind = ASRUtils::extract_kind_from_ttype_t(a_type);
            // Skip compile-time folding when either side is real(16): m_r is
            // a pointer payload for kind=16 (see real_constant_get_r16_bytes),
            // and folding through double would silently truncate. Runtime
            // cast handles it.
            if (src_kind != 16 && dest_kind != 16) {
                value = ASR::down_cast<ASR::expr_t>(
                        ASR::make_RealConstant_t(al, a_loc, rc->m_r, a_type));
            }
        } else if (a_kind == ASR::cast_kindType::RealToComplex) {
            ASR::RealConstant_t* rc = ASR::down_cast<ASR::RealConstant_t>(
                                  ASRUtils::expr_value(a_arg));
            if (ASRUtils::extract_kind_from_ttype_t(rc->m_type) != 16) {
                value = ASR::down_cast<ASR::expr_t>(ASR::make_ComplexConstant_t(
                            al, a_loc, rc->m_r, 0, a_type));
            }
        } else if (a_kind == ASR::cast_kindType::IntegerToReal) {
            // TODO: Clashes with the pow functions
            int64_t int_value = ASR::down_cast<ASR::IntegerConstant_t>(ASRUtils::expr_value(a_arg))->m_n;
            int dest_kind = ASRUtils::extract_kind_from_ttype_t(a_type);
            if (dest_kind != 16) {
                value = ASR::down_cast<ASR::expr_t>(ASR::make_RealConstant_t(al, a_loc, (double)int_value, a_type));
            } else {
                // int -> real(16) is always exact for int64; build the binary128
                // bytes directly on the arena and pack them into m_r.
                uint8_t* bytes = (uint8_t*)al.alloc(16);
                lf_float128 v = lf_f128_from_int64(int_value);
                std::memcpy(bytes, v.bytes, 16);
                double m_r = ASRUtils::real_constant_pack_r16(bytes);
                value = ASR::down_cast<ASR::expr_t>(
                    ASR::make_RealConstant_t(al, a_loc, m_r, a_type));
            }
        } else if (a_kind == ASR::cast_kindType::IntegerToComplex) {
            int64_t int_value = ASR::down_cast<ASR::IntegerConstant_t>(
                                ASRUtils::expr_value(a_arg))->m_n;
            value = ASR::down_cast<ASR::expr_t>(ASR::make_ComplexConstant_t(al, a_loc,
                        (double)int_value, 0, a_type));
        } else if (a_kind == ASR::cast_kindType::IntegerToInteger) {
            int64_t int_value = ASR::down_cast<ASR::IntegerConstant_t>(
                                ASRUtils::expr_value(a_arg))->m_n;
            value = ASR::down_cast<ASR::expr_t>(ASR::make_IntegerConstant_t(al, a_loc, int_value, a_type));
        } else if (a_kind == ASR::cast_kindType::IntegerToUnsignedInteger) {
            int64_t int_value = ASR::down_cast<ASR::IntegerConstant_t>(
                                ASRUtils::expr_value(a_arg))->m_n;
            value = ASR::down_cast<ASR::expr_t>(ASR::make_UnsignedIntegerConstant_t(al, a_loc, int_value, a_type));
        } else if (a_kind == ASR::cast_kindType::UnsignedIntegerToInteger) {
            int64_t int_value = ASR::down_cast<ASR::UnsignedIntegerConstant_t>(
                                ASRUtils::expr_value(a_arg))->m_n;
            value = ASR::down_cast<ASR::expr_t>(ASR::make_IntegerConstant_t(al, a_loc, int_value, a_type));
        } else if (a_kind == ASR::cast_kindType::UnsignedIntegerToUnsignedInteger) {
            int64_t int_value = ASR::down_cast<ASR::UnsignedIntegerConstant_t>(
                                ASRUtils::expr_value(a_arg))->m_n;
            value = ASR::down_cast<ASR::expr_t>(ASR::make_UnsignedIntegerConstant_t(al, a_loc, int_value, a_type));
        } else if (a_kind == ASR::cast_kindType::IntegerToLogical) {
            int64_t int_value = ASR::down_cast<ASR::IntegerConstant_t>(
                                ASRUtils::expr_value(a_arg))->m_n;
            value = ASR::down_cast<ASR::expr_t>(ASR::make_LogicalConstant_t(al, a_loc, int_value, a_type));
        } else if (a_kind == ASR::cast_kindType::ComplexToComplex) {
            ASR::ComplexConstant_t* value_complex = ASR::down_cast<ASR::ComplexConstant_t>(
                        ASRUtils::expr_value(a_arg));
            double real = value_complex->m_re;
            double imag = value_complex->m_im;
            value = ASR::down_cast<ASR::expr_t>(
                    ASR::make_ComplexConstant_t(al, a_loc, real, imag, a_type));
        } else if (a_kind == ASR::cast_kindType::ComplexToReal) {
            ASR::ComplexConstant_t* value_complex = ASR::down_cast<ASR::ComplexConstant_t>(
                        ASRUtils::expr_value(a_arg));
            double real = value_complex->m_re;
            value = ASR::down_cast<ASR::expr_t>(
                    ASR::make_RealConstant_t(al, a_loc, real, a_type));
        } else if (a_kind == ASR::cast_kindType::IntegerToSymbolicExpression) {
            Vec<ASR::expr_t*> args;
            args.reserve(al, 1);
            args.push_back(al, a_arg);
            LCompilers::ASRUtils::create_intrinsic_function create_function =
                LCompilers::ASRUtils::IntrinsicElementalFunctionRegistry::get_create_function("SymbolicInteger");
            diag::Diagnostics diag;
            value = ASR::down_cast<ASR::expr_t>(create_function(al, a_loc, args, diag));
        }
    }

    return ASR::make_Cast_t(al, a_loc, a_arg, a_kind, a_type, value, nullptr);
}

ASR::symbol_t* import_class_procedure(Allocator &al, const Location& loc,
        ASR::symbol_t* original_sym, SymbolTable *current_scope) {
    if (original_sym && ASR::is_a<ASR::ExternalSymbol_t>(*original_sym)) {
        ASR::symbol_t* original_sym_past_ext = ASRUtils::symbol_get_past_external(original_sym);
        if (ASR::is_a<ASR::StructMethodDeclaration_t>(*original_sym_past_ext) ||
            (ASR::is_a<ASR::Variable_t>(*original_sym_past_ext) &&
             ASR::is_a<ASR::FunctionType_t>(*ASRUtils::type_get_past_pointer(ASRUtils::symbol_type(original_sym_past_ext))))) {
            original_sym = original_sym_past_ext;
        }
    }
    if( original_sym && (ASR::is_a<ASR::StructMethodDeclaration_t>(*original_sym) ||
        (ASR::is_a<ASR::Variable_t>(*original_sym) &&
         ASR::is_a<ASR::FunctionType_t>(*ASRUtils::type_get_past_pointer(ASRUtils::symbol_type(original_sym))))) ) {
        std::string class_proc_name;
        if (ASR::is_a<ASR::StructMethodDeclaration_t>(*original_sym)) {
            class_proc_name = std::string(ASR::down_cast<ASR::StructMethodDeclaration_t>(original_sym)->m_name);
        } else {
            class_proc_name = ASRUtils::symbol_name(original_sym);
        }
        if( original_sym != current_scope->resolve_symbol(class_proc_name) ) {
            std::string struct_name = ASRUtils::symbol_name(ASRUtils::get_asr_owner(original_sym));
            std::string imported_proc_name = "1_" + struct_name + "_" + class_proc_name;
            if( current_scope->resolve_symbol(imported_proc_name) == nullptr ) {
                ASR::symbol_t* module_sym = ASRUtils::get_asr_owner(original_sym);
                std::string module_name = ASRUtils::symbol_name(module_sym);
                if( current_scope->resolve_symbol(module_name) == nullptr ) {
                    std::string imported_module_name = "1_" + module_name;
                    if( current_scope->resolve_symbol(imported_module_name) == nullptr ) {
                        LCOMPILERS_ASSERT(ASR::is_a<ASR::Module_t>(
                            *ASRUtils::get_asr_owner(module_sym)));
                        ASR::symbol_t* imported_module = ASR::down_cast<ASR::symbol_t>(
                            ASR::make_ExternalSymbol_t(
                                al, loc, current_scope, s2c(al, imported_module_name),
                                module_sym, ASRUtils::symbol_name(ASRUtils::get_asr_owner(module_sym)),
                                nullptr, 0, s2c(al, module_name), ASR::accessType::Public
                            )
                        );
                        current_scope->add_symbol(imported_module_name, imported_module);
                    }
                    module_name = imported_module_name;
                }
                // Use the owning module's name as module_name and encode
                // the struct path in scope_names. This avoids ambiguity
                // during deserialization when a struct and a top-level module
                // share the same name.
                std::string ext_module_name = module_name;
                char** scope_names_arr = nullptr;
                size_t n_scope_names = 0;
                ASR::symbol_t* struct_owner = ASRUtils::get_asr_owner(
                    ASRUtils::symbol_get_past_external(module_sym));
                if (struct_owner != nullptr && ASR::is_a<ASR::Module_t>(*struct_owner)) {
                    ext_module_name = ASRUtils::symbol_name(struct_owner);
                    scope_names_arr = al.allocate<char*>(1);
                    scope_names_arr[0] = s2c(al, struct_name);
                    n_scope_names = 1;
                }
                ASR::symbol_t* imported_sym = ASR::down_cast<ASR::symbol_t>(
                    ASR::make_ExternalSymbol_t(
                        al, loc, current_scope, s2c(al, imported_proc_name),
                        original_sym, s2c(al, ext_module_name), scope_names_arr, n_scope_names,
                        ASRUtils::symbol_name(original_sym), ASR::accessType::Public
                    )
                );
                current_scope->add_symbol(imported_proc_name, imported_sym);
                original_sym = imported_sym;
            } else {
                original_sym = current_scope->resolve_symbol(imported_proc_name);
            }
        }
    }
    return original_sym;
}

ASR::asr_t* make_Binop_util(Allocator &al, const Location& loc, ASR::binopType binop,
                        ASR::expr_t* lexpr, ASR::expr_t* rexpr, ASR::ttype_t* ttype) {
    ASRUtils::make_ArrayBroadcast_t_util(al, loc, lexpr, rexpr);
    switch (ttype->type) {
        case ASR::ttypeType::Real: {
            return ASR::make_RealBinOp_t(al, loc, lexpr, binop, rexpr,
                ASRUtils::duplicate_type(al, ttype), nullptr);
        }
        case ASR::ttypeType::Integer: {
            return ASR::make_IntegerBinOp_t(al, loc, lexpr, binop, rexpr,
                ASRUtils::duplicate_type(al, ttype), nullptr);
        }
        case ASR::ttypeType::Complex: {
            return ASR::make_ComplexBinOp_t(al, loc, lexpr, binop, rexpr,
                ASRUtils::duplicate_type(al, ttype), nullptr);
        }
        default:
            throw LCompilersException("Not implemented " + ASRUtils::type_to_str_python_expr(ttype, lexpr));
    }
}

ASR::asr_t* make_Cmpop_util(Allocator &al, const Location& loc, ASR::cmpopType cmpop,
                        ASR::expr_t* lexpr, ASR::expr_t* rexpr, ASR::ttype_t* ttype) {
    ASR::ttype_t* expr_type = ASRUtils::TYPE(ASR::make_Logical_t(al, loc, 4));
    switch (ttype->type) {
        case ASR::ttypeType::Real: {
            return ASR::make_RealCompare_t(al, loc, lexpr, cmpop, rexpr, expr_type, nullptr);
        }
        case ASR::ttypeType::Integer: {
            return ASR::make_IntegerCompare_t(al, loc, lexpr, cmpop, rexpr, expr_type, nullptr);
        }
        case ASR::ttypeType::Complex: {
            return ASR::make_ComplexCompare_t(al, loc, lexpr, cmpop, rexpr, expr_type, nullptr);
        }
        case ASR::ttypeType::String: {
            return ASR::make_StringCompare_t(al, loc, lexpr, cmpop, rexpr, expr_type, nullptr);
        }
        default:
            throw LCompilersException("Not implemented " + ASRUtils::type_to_str_python_expr(ttype, lexpr));
    }
}

void mark_modules_as_external(const LCompilers::ASR::TranslationUnit_t &u)
{
    Allocator al(4*1024);
    for (auto &item : u.m_symtab->get_scope()) {
        if (LCompilers::ASR::is_a<LCompilers::ASR::Module_t>(*item.second)) {
            LCompilers::ASR::Module_t *m = LCompilers::ASR::down_cast<LCompilers::ASR::Module_t>(item.second);
            m->m_symtab->mark_all_variables_external(al);
        }
    }
}

void make_ArrayBroadcast_t_util(Allocator& al, const Location& loc,
    ASR::expr_t*& expr1, ASR::expr_t*& expr2, ASR::dimension_t* expr1_mdims,
    size_t expr1_ndims) {
    ASR::ttype_t* expr1_type = ASRUtils::expr_type(expr1);
    Vec<ASR::expr_t*> shape_args;
    shape_args.reserve(al, 1);
    shape_args.push_back(al, expr1);
    bool is_value_character_array = ASRUtils::is_character(*ASRUtils::expr_type(expr2));

    Vec<ASR::dimension_t> dims;
    dims.reserve(al, 1);
    ASR::dimension_t dim;
    dim.loc = loc;
    dim.m_length = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, loc,
        expr1_ndims, ASRUtils::TYPE(ASR::make_Integer_t(al, loc, 4))));
    dim.m_start = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, loc,
        1, ASRUtils::TYPE(ASR::make_Integer_t(al, loc, 4))));
    dims.push_back(al, dim);
    ASR::ttype_t* dest_shape_type = ASRUtils::TYPE(ASR::make_Array_t(al, loc,
        ASRUtils::TYPE(ASR::make_Integer_t(al, loc, 4)), dims.p, dims.size(),
        is_value_character_array && !is_value_constant(expr2) ? ASR::array_physical_typeType::StringArraySinglePointer: ASR::array_physical_typeType::FixedSizeArray));

    ASR::expr_t* dest_shape = nullptr;
    ASR::expr_t* value = nullptr;
    ASR::ttype_t* ret_type = nullptr;
    if( ASRUtils::is_fixed_size_array(expr1_mdims, expr1_ndims) ) {
        Vec<ASR::expr_t*> lengths; lengths.reserve(al, expr1_ndims);
        for( size_t i = 0; i < expr1_ndims; i++ ) {
            lengths.push_back(al, ASRUtils::expr_value(expr1_mdims[i].m_length));
        }
        dest_shape = EXPR(ASRUtils::make_ArrayConstructor_t_util(al, loc, lengths.p,
            lengths.size(), dest_shape_type, ASR::arraystorageType::ColMajor));
        Vec<ASR::dimension_t> dims;
        dims.reserve(al, 1);
        ASR::dimension_t dim;
        dim.loc = loc;
        dim.m_length = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, loc,
            ASRUtils::get_fixed_size_of_array(expr1_mdims, expr1_ndims),
            ASRUtils::TYPE(ASR::make_Integer_t(al, loc, 4))));
        dim.m_start = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, loc,
            1, ASRUtils::TYPE(ASR::make_Integer_t(al, loc, 4))));
        dims.push_back(al, dim);

        if( ASRUtils::is_value_constant(expr2) &&
            ASRUtils::get_fixed_size_of_array(expr1_mdims, expr1_ndims) <= 256 ) {
            ASR::ttype_t* value_type = ASRUtils::TYPE(ASR::make_Array_t(al, loc,
                ASRUtils::type_get_past_array(ASRUtils::expr_type(expr2)), dims.p, dims.size(),
                is_value_character_array ? ASR::array_physical_typeType::PointerArray: ASR::array_physical_typeType::FixedSizeArray));
            Vec<ASR::expr_t*> values;
            values.reserve(al, ASRUtils::get_fixed_size_of_array(expr1_mdims, expr1_ndims));
            for( int64_t i = 0; i < ASRUtils::get_fixed_size_of_array(expr1_mdims, expr1_ndims); i++ ) {
                values.push_back(al, expr2);
            }
            value = EXPR(ASRUtils::make_ArrayConstructor_t_util(al, loc, values.p,
                values.size(), value_type, ASR::arraystorageType::ColMajor));
            if (ASR::is_a<ASR::ArrayConstructor_t>(*value) && ASRUtils::expr_value(value)) {
                value = ASRUtils::expr_value(value);
            }
            ret_type = value_type;
        }
    } else {
        dest_shape = ASRUtils::EXPR(ASR::make_IntrinsicArrayFunction_t(al, loc,
            static_cast<int64_t>(ASRUtils::IntrinsicArrayFunctions::Shape), shape_args.p,
            shape_args.size(), 0, dest_shape_type, nullptr));
    }

    if (ret_type == nullptr) {
        // TODO: Construct appropriate return type here
        // For now simply coping the type from expr1
        if (ASRUtils::is_simd_array(expr1)) {
            // TODO: Make this more general; do not check for SIMDArray
            ret_type = ASRUtils::duplicate_type(al, expr1_type);
        } else {
            ret_type = expr1_type;
        }
    }
    expr2 = ASRUtils::EXPR(ASR::make_ArrayBroadcast_t(al, loc, expr2, dest_shape, ret_type, value));

    if (ASRUtils::extract_physical_type(expr1_type) != ASRUtils::extract_physical_type(ret_type)) {
        expr2 = ASRUtils::EXPR(ASRUtils::make_ArrayPhysicalCast_t_util(al, loc, expr2,
            ASRUtils::extract_physical_type(ret_type),
            ASRUtils::extract_physical_type(expr1_type), expr1_type, nullptr));
    }
}

void make_ArrayBroadcast_t_util(Allocator& al, const Location& loc,
    ASR::expr_t*& expr1, ASR::expr_t*& expr2) {
    ASR::ttype_t* expr1_type = ASRUtils::expr_type(expr1);
    ASR::ttype_t* expr2_type = ASRUtils::expr_type(expr2);
    ASR::dimension_t *expr1_mdims = nullptr, *expr2_mdims = nullptr;
    size_t expr1_ndims = ASRUtils::extract_dimensions_from_ttype(expr1_type, expr1_mdims);
    size_t expr2_ndims = ASRUtils::extract_dimensions_from_ttype(expr2_type, expr2_mdims);
    if( expr1_ndims == expr2_ndims ) {
        // TODO: Always broadcast both the expressions
        return ;
    }

    if( expr1_ndims > expr2_ndims ) {
        if( ASR::is_a<ASR::ArrayReshape_t>(*expr2) ) {
            return ;
        }
        make_ArrayBroadcast_t_util(al, loc, expr1, expr2, expr1_mdims, expr1_ndims);
    } else {
        if( ASR::is_a<ASR::ArrayReshape_t>(*expr1) ) {
            return ;
        }
        make_ArrayBroadcast_t_util(al, loc, expr2, expr1, expr2_mdims, expr2_ndims);
    }
}

int64_t compute_trailing_zeros(int64_t number, int64_t kind) {
    int64_t trailing_zeros = 0;
    if (number == 0 && kind == 4) {
        return 32;
    } else if (number == 0 && kind == 8) {
        return 64;
    }
    while (number % 2 == 0) {
        number = number / 2;
        trailing_zeros++;
    }
    return trailing_zeros;
}

int64_t compute_leading_zeros(int64_t number, int64_t kind) {
    int64_t leading_zeros = 0;
    int64_t total_bits = 32;
    if (kind == 8) total_bits = 64;
    if (number < 0) return 0;
    while (total_bits > 0) {
        if (number%2 == 0) {
            leading_zeros++;
        } else {
            leading_zeros = 0;
        }
        number = number/2;
        total_bits--;
    }
    return leading_zeros;
}

void append_error(diag::Diagnostics& diag, const std::string& msg,
                const Location& loc) {
    diag.add(diag::Diagnostic(msg, diag::Level::Error,
        diag::Stage::Semantic, {diag::Label("", { loc })}));
}

size_t get_constant_ArrayConstant_size(ASR::ArrayConstant_t* x) {
    return ASRUtils::get_fixed_size_of_array(x->m_type);
}

void get_sliced_indices(ASR::ArraySection_t* arr_sec, std::vector<size_t> &indices) {
    for (size_t i = 0; i < arr_sec->n_args; i++) {
        if (arr_sec->m_args[i].m_step != nullptr) {
            indices.push_back(i + 1);
        }
    }
}

ASR::expr_t* get_ArrayConstant_size(Allocator& al, ASR::ArrayConstant_t* x) {
    ASR::ttype_t* int_type = ASRUtils::TYPE(ASR::make_Integer_t(al, x->base.base.loc, 4));
    return make_ConstantWithType(make_IntegerConstant_t,
            ASRUtils::get_fixed_size_of_array(x->m_type), int_type, x->base.base.loc);
}

ASR::expr_t* get_ImpliedDoLoop_size(Allocator& al, ASR::ImpliedDoLoop_t* implied_doloop) {
    const Location& loc = implied_doloop->base.base.loc;
    ASRUtils::ASRBuilder builder(al, loc);
    ASR::expr_t* start = implied_doloop->m_start;
    ASR::expr_t* end = implied_doloop->m_end;
    ASR::expr_t* d = implied_doloop->m_increment;
    ASR::expr_t* implied_doloop_size = nullptr;
    int kind = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(end));
    start = builder.i2i_t(start, ASRUtils::expr_type(end));
    if( d == nullptr ) {
        implied_doloop_size = ASRUtils::compute_length_from_start_end(al, start, end);
    } else {
        d = builder.i2i_t(d, ASRUtils::expr_type(end));
        implied_doloop_size = builder.Add(builder.Div(
            builder.Sub(end, start), d),
            make_ConstantWithKind(make_IntegerConstant_t, make_Integer_t, 1, kind, loc));
    }
    int const_elements = 0;
    ASR::expr_t* implied_doloop_size_ = nullptr;
    for( size_t i = 0; i < implied_doloop->n_values; i++ ) {
        if( ASR::is_a<ASR::ImpliedDoLoop_t>(*implied_doloop->m_values[i]) ) {
            if( implied_doloop_size_ == nullptr ) {
                implied_doloop_size_ = get_ImpliedDoLoop_size(al,
                    ASR::down_cast<ASR::ImpliedDoLoop_t>(implied_doloop->m_values[i]));
            } else {
                implied_doloop_size_ = builder.Add(get_ImpliedDoLoop_size(al,
                    ASR::down_cast<ASR::ImpliedDoLoop_t>(implied_doloop->m_values[i])),
                    implied_doloop_size_);
            }
        } else if( ASR::is_a<ASR::ArrayConstructor_t>(*implied_doloop->m_values[i]) ) {
            ASR::expr_t* ac_size = get_ArrayConstructor_size(al,
                ASR::down_cast<ASR::ArrayConstructor_t>(implied_doloop->m_values[i]));
            if( implied_doloop_size_ == nullptr ) {
                implied_doloop_size_ = ac_size;
            } else {
                implied_doloop_size_ = builder.Add(ac_size, implied_doloop_size_);
            }
        } else {
            const_elements += 1;
        }
    }

    ASR::symbol_t* loop_var_sym = nullptr;
    if (ASR::is_a<ASR::Var_t>(*implied_doloop->m_var)) {
        loop_var_sym = ASR::down_cast<ASR::Var_t>(implied_doloop->m_var)->m_v;
    }
    bool inner_depends_on_var = (loop_var_sym != nullptr &&
        implied_doloop_size_ != nullptr && d == nullptr &&
        expr_references_symbol(implied_doloop_size_, loop_var_sym));

    if (inner_depends_on_var) {
        ASR::expr_t* two = make_ConstantWithKind(
            make_IntegerConstant_t, make_Integer_t, 2, kind, loc);
        ASR::expr_t* arith_sum = builder.Div(
            builder.Mul(implied_doloop_size, builder.Add(start, end)), two);
        if (const_elements > 0) {
            ASR::expr_t* const_total = builder.Mul(
                make_ConstantWithKind(make_IntegerConstant_t,
                    make_Integer_t, const_elements, kind, loc),
                implied_doloop_size);
            implied_doloop_size = builder.Add(arith_sum, const_total);
        } else {
            implied_doloop_size = arith_sum;
        }
    } else {
        if( const_elements > 0 ) {
            if( implied_doloop_size_ == nullptr ) {
                implied_doloop_size_ = make_ConstantWithKind(make_IntegerConstant_t,
                    make_Integer_t, const_elements, kind, loc);
            } else {
                implied_doloop_size_ = builder.Add(
                    make_ConstantWithKind(make_IntegerConstant_t,
                        make_Integer_t, const_elements, kind, loc),
                    implied_doloop_size_);
            }
        }
        if( implied_doloop_size_ ) {
            implied_doloop_size = builder.Mul(implied_doloop_size_, implied_doloop_size);
        }
    }
    return implied_doloop_size;
}

ASR::expr_t* get_ArrayConstructor_size(Allocator& al, ASR::ArrayConstructor_t* x) {
    ASR::ttype_t* int_type = ASRUtils::TYPE(ASR::make_Integer_t(al, x->base.base.loc, 4));
    ASR::expr_t* array_size = nullptr;
    int64_t constant_size = 0;
    const Location& loc = x->base.base.loc;
    ASRUtils::ASRBuilder builder(al, loc);
    for( size_t i = 0; i < x->n_args; i++ ) {
        ASR::expr_t* element = x->m_args[i];
        if( ASR::is_a<ASR::ArrayConstant_t>(*element) ) {
            if( ASRUtils::is_value_constant(element) ) {
                constant_size += get_constant_ArrayConstant_size(
                    ASR::down_cast<ASR::ArrayConstant_t>(element));
            } else {
                ASR::expr_t* element_array_size = get_ArrayConstant_size(al,
                    ASR::down_cast<ASR::ArrayConstant_t>(element));
                if( array_size == nullptr ) {
                    array_size = element_array_size;
                } else {
                    array_size = builder.Add(array_size,
                                    element_array_size);
                }
            }
        } else if( ASR::is_a<ASR::ArrayConstructor_t>(*element) ) {
            ASR::expr_t* element_array_size = get_ArrayConstructor_size(al,
                ASR::down_cast<ASR::ArrayConstructor_t>(element));
            if( array_size == nullptr ) {
                array_size = element_array_size;
            } else {
                array_size = builder.Add(array_size,
                                element_array_size);
            }
        } else if( ASR::is_a<ASR::Var_t>(*element) ) {
            ASR::ttype_t* element_type = ASRUtils::type_get_past_allocatable(
                ASRUtils::expr_type(element));
            if( ASRUtils::is_array(element_type) ) {
                if( ASRUtils::is_fixed_size_array(element_type) ) {
                    ASR::dimension_t* m_dims = nullptr;
                    size_t n_dims = ASRUtils::extract_dimensions_from_ttype(element_type, m_dims);
                    constant_size += ASRUtils::get_fixed_size_of_array(m_dims, n_dims);
                } else {
                    ASR::expr_t* element_array_size = ASRUtils::get_size(element, al);
                    if( array_size == nullptr ) {
                        array_size = element_array_size;
                    } else {
                        array_size = builder.Add(array_size,
                                        element_array_size);
                    }
                }
            } else {
                constant_size += 1;
            }
        } else if( ASR::is_a<ASR::ImpliedDoLoop_t>(*element) ) {
            ASR::expr_t* implied_doloop_size = get_ImpliedDoLoop_size(al,
                ASR::down_cast<ASR::ImpliedDoLoop_t>(element));
            if( array_size ) {
                array_size = builder.Add(implied_doloop_size, array_size);
            } else {
                array_size = implied_doloop_size;
            }
        } else if( ASR::is_a<ASR::ArraySection_t>(*element) ) {
            ASR::ArraySection_t* array_section_t = ASR::down_cast<ASR::ArraySection_t>(element);
            ASR::expr_t* array_section_size = nullptr;
            for( size_t j = 0; j < array_section_t->n_args; j++ ) {
                ASR::expr_t* start = array_section_t->m_args[j].m_left;
                ASR::expr_t* end = array_section_t->m_args[j].m_right;
                ASR::expr_t* d = array_section_t->m_args[j].m_step;
                if( d == nullptr ) {
                    continue;
                }
                ASR::expr_t* dim_size = builder.Add(builder.Div(
                    builder.Sub(end, start), d),
                    make_ConstantWithKind(make_IntegerConstant_t, make_Integer_t, 1, 4, loc));
                if( array_section_size == nullptr ) {
                    array_section_size = dim_size;
                } else {
                    array_section_size = builder.Mul(array_section_size, dim_size);
                }
            }
            if( array_size == nullptr ) {
                array_size = array_section_size;
            } else {
                array_size = builder.Add(array_section_size, array_size);
            }
        } else {
            ASR::ttype_t* element_type = ASRUtils::type_get_past_allocatable(
                ASRUtils::type_get_past_pointer(ASRUtils::expr_type(element)));
            if( ASRUtils::is_array(element_type) ) {
                if( ASRUtils::is_fixed_size_array(element_type) ) {
                    ASR::dimension_t* m_dims = nullptr;
                    size_t n_dims = ASRUtils::extract_dimensions_from_ttype(element_type, m_dims);
                    constant_size += ASRUtils::get_fixed_size_of_array(m_dims, n_dims);
                } else {
                    ASR::expr_t* element_array_size = ASRUtils::get_size(element, al);
                    if( array_size == nullptr ) {
                        array_size = element_array_size;
                    } else {
                        array_size = builder.Add(array_size, element_array_size);
                    }
                }
            } else {
                constant_size += 1;
            }
        }
    }
    ASR::expr_t* constant_size_asr = nullptr;
    if (constant_size == 0 && array_size == nullptr) {
        constant_size = ASRUtils::get_fixed_size_of_array(x->m_type);
    }
    if( constant_size > 0 ) {
        constant_size_asr = make_ConstantWithType(make_IntegerConstant_t,
                                constant_size, int_type, x->base.base.loc);
        if( array_size == nullptr ) {
            return constant_size_asr;
        }
    }
    if( constant_size_asr ) {
        array_size = builder.Add(array_size, constant_size_asr);
    }

    if( array_size == nullptr ) {
        array_size = make_ConstantWithKind(make_IntegerConstant_t,
            make_Integer_t, 0, 4, x->base.base.loc);
    }
    return array_size;
}

ASR::asr_t* make_ArraySize_t_util(
    Allocator &al, const Location &a_loc, ASR::expr_t* a_v,
    ASR::expr_t* a_dim, ASR::ttype_t* a_type, ASR::expr_t* a_value,
    bool for_type) {
    int dim = -1;
    bool is_dimension_constant = (a_dim != nullptr) && ASRUtils::extract_value(
        ASRUtils::expr_value(a_dim), dim);
    ASR::ttype_t* array_func_type = nullptr;
    if( ASR::is_a<ASR::ArrayPhysicalCast_t>(*a_v) ) {
        ASR::expr_t* inner = ASR::down_cast<ASR::ArrayPhysicalCast_t>(a_v)->m_arg;
        if (ASRUtils::is_array(ASRUtils::expr_type(inner))) {
            a_v = inner;
        }
    }
    if ( ASR::is_a<ASR::IntrinsicArrayFunction_t>(*a_v) && for_type ) {
        ASR::IntrinsicArrayFunction_t* af = ASR::down_cast<ASR::IntrinsicArrayFunction_t>(a_v);
        int64_t dim_arg_index = ASRUtils::IntrinsicArrayFunctionRegistry::get_dim_arg_index(
            static_cast<ASRUtils::IntrinsicArrayFunctions>(af->m_arr_intrinsic_id));
        ASR::expr_t* af_dim = nullptr;
        if( dim_arg_index >= 1 && (size_t) dim_arg_index < af->n_args && af->m_args[dim_arg_index] != nullptr ) {
            af_dim = af->m_args[dim_arg_index];
        }
        if ( ASRUtils::is_array(af->m_type) ) {
            array_func_type = af->m_type;
        }
        for ( size_t i = 0; i < af->n_args; i++ ) {
            if ( ASRUtils::is_array(ASRUtils::expr_type(af->m_args[i])) ) {
                a_v = af->m_args[i];
                if ( ASR::is_a<ASR::ArrayPhysicalCast_t>(*a_v)) {
                    a_v = ASR::down_cast<ASR::ArrayPhysicalCast_t>(a_v)->m_arg;
                }
                break;
            }
        }

        if( af_dim != nullptr ) {
            ASRBuilder builder(al, a_loc);
            ASR::ttype_t* int32_type = ASRUtils::TYPE(ASR::make_Integer_t(al, a_loc, 4));
            if( a_dim == nullptr ) {
                size_t rank = ASRUtils::extract_n_dims_from_ttype(ASRUtils::expr_type(a_v));
                Vec<ASR::expr_t*> array_sizes; array_sizes.reserve(al, rank);
                for( size_t i = 1; i <= rank; i++ ) {
                    ASR::expr_t* i_a_dim = ASRUtils::EXPR(
                        ASR::make_IntegerConstant_t(al, a_loc, i, int32_type));
                    ASR::expr_t* i_dim_size = ASRUtils::EXPR(make_ArraySize_t_util(
                        al, a_loc, a_v, i_a_dim, a_type, nullptr, for_type));
                    array_sizes.push_back(al, i_dim_size);
                }

                rank--;
                Vec<ASR::expr_t*> merged_sizes; merged_sizes.reserve(al, rank);
                for( size_t i = 0; i < rank; i++ ) {
                    Vec<ASR::expr_t*> merge_args; merge_args.reserve(al, 3);
                    merge_args.push_back(al, array_sizes[i]);
                    merge_args.push_back(al, array_sizes[i + 1]);
                    merge_args.push_back(al, builder.Lt(builder.i32(i+1), af_dim));
                    diag::Diagnostics diag;
                    merged_sizes.push_back(al, ASRUtils::EXPR(
                        ASRUtils::Merge::create_Merge(al, a_loc, merge_args, diag)));
                }

                ASR::expr_t* size = merged_sizes[0];
                for( size_t i = 1; i < rank; i++ ) {
                    size = builder.Mul(merged_sizes[i], size);
                }

                return &(size->base);
            } else {
                ASR::expr_t *dim_size_lt = ASRUtils::EXPR(make_ArraySize_t_util(
                    al, a_loc, a_v, a_dim, a_type, nullptr, for_type));
                ASR::expr_t *dim_size_gte = ASRUtils::EXPR(make_ArraySize_t_util(
                    al, a_loc, a_v, builder.Add(a_dim, builder.i_t(1, ASRUtils::expr_type(a_dim))),
                    a_type, nullptr, for_type));
                Vec<ASR::expr_t*> merge_args; merge_args.reserve(al, 3);
                merge_args.push_back(al, dim_size_lt); merge_args.push_back(al, dim_size_gte);
                merge_args.push_back(al, builder.Lt(a_dim, af_dim));
                diag::Diagnostics diag;
                return ASRUtils::Merge::create_Merge(al, a_loc, merge_args, diag);
            }
        } else if( array_func_type != nullptr && is_dimension_constant ) {
            ASR::dimension_t* af_dims = nullptr;
            size_t af_n_dims = ASRUtils::extract_dimensions_from_ttype(
                array_func_type, af_dims);
            if( (size_t) dim >= 1 && (size_t) dim <= af_n_dims &&
                af_dims[dim - 1].m_length != nullptr ) {
                return (ASR::asr_t*) af_dims[dim - 1].m_length;
            }
        }
    } else if( ASR::is_a<ASR::FunctionCall_t>(*a_v) && for_type ) {
        ASR::FunctionCall_t* function_call = ASR::down_cast<ASR::FunctionCall_t>(a_v);
        ASR::dimension_t* m_dims = nullptr;
        size_t n_dims = ASRUtils::extract_dimensions_from_ttype(function_call->m_type, m_dims);
        if( ASRUtils::is_fixed_size_array(function_call->m_type) ) {
            if( a_dim == nullptr ) {
                return ASR::make_IntegerConstant_t(al, a_loc,
                    ASRUtils::get_fixed_size_of_array(function_call->m_type), a_type);
            } else if( is_dimension_constant ) {
                return &(m_dims[dim - 1].m_length->base);
            }
        } else {
            if( a_dim == nullptr ) {
                if( n_dims == 0 || m_dims[0].m_length == nullptr ) {
                    return ASR::make_ArraySize_t(al, a_loc, a_v, a_dim, a_type, a_value);
                }
                ASR::expr_t* result = m_dims[0].m_length;
                for( size_t i = 1; i < n_dims; i++ ) {
                    if( m_dims[i].m_length == nullptr ) {
                        return ASR::make_ArraySize_t(al, a_loc, a_v, a_dim, a_type, a_value);
                    }
                    result = ASRUtils::EXPR(ASR::make_IntegerBinOp_t(al, a_loc,
                        result, ASR::binopType::Mul, m_dims[i].m_length, a_type, nullptr));
                }
                return &(result->base);
            } else if( is_dimension_constant ) {
                if( m_dims[dim - 1].m_length == nullptr ) {
                    // For Deferred-shape allocatable return, calculate size at runtime
                    return ASR::make_ArraySize_t(al, a_loc, a_v, a_dim, a_type, a_value);
                }
                LCOMPILERS_ASSERT(m_dims[dim - 1].m_length);
                return &(m_dims[dim - 1].m_length->base);
            }
        }
    } else if( ASR::is_a<ASR::ArrayReshape_t>(*a_v) && for_type ) {
        ASR::ArrayReshape_t* array_reshape = ASR::down_cast<ASR::ArrayReshape_t>(a_v);
        ASR::dimension_t* m_dims = nullptr;
        size_t n_dims = ASRUtils::extract_dimensions_from_ttype(array_reshape->m_type, m_dims);
        if( ASRUtils::is_fixed_size_array(array_reshape->m_type) ) {
            if( a_dim == nullptr ) {
                return ASR::make_IntegerConstant_t(al, a_loc,
                    ASRUtils::get_fixed_size_of_array(array_reshape->m_type), a_type);
            } else if( is_dimension_constant ) {
                return &(m_dims[dim - 1].m_length->base);
            }
        } else {
            if( a_dim == nullptr ) {
                if( n_dims == 0 || m_dims[0].m_length == nullptr ) {
                    return ASR::make_ArraySize_t(al, a_loc, a_v, a_dim, a_type, a_value);
                }
                ASR::expr_t* result = m_dims[0].m_length;
                for( size_t i = 1; i < n_dims; i++ ) {
                    if( m_dims[i].m_length == nullptr ) {
                        return ASR::make_ArraySize_t(al, a_loc, a_v, a_dim, a_type, a_value);
                    }
                    result = ASRUtils::EXPR(ASR::make_IntegerBinOp_t(al, a_loc,
                        result, ASR::binopType::Mul, m_dims[i].m_length, a_type, nullptr));
                }
                return &(result->base);
            } else if( is_dimension_constant ) {
                if( m_dims[dim - 1].m_length == nullptr ) {
                    return ASR::make_ArraySize_t(al, a_loc, a_v, a_dim, a_type, a_value);
                }
                LCOMPILERS_ASSERT(m_dims[dim - 1].m_length);
                return &(m_dims[dim - 1].m_length->base);
            }
        }
    } else if( ASR::is_a<ASR::IntrinsicElementalFunction_t>(*a_v) && for_type ) {
        ASR::IntrinsicElementalFunction_t* elemental = ASR::down_cast<ASR::IntrinsicElementalFunction_t>(a_v);
        for( size_t i = 0; i < elemental->n_args; i++ ) {
            if( ASRUtils::is_array(ASRUtils::expr_type(elemental->m_args[i])) ) {
                return make_ArraySize_t_util(al, a_loc, elemental->m_args[i], a_dim, a_type, a_value, for_type);
            }
        }
    }
    if( ASR::is_a<ASR::ArraySection_t>(*a_v) ) {
        ASR::ArraySection_t* array_section_t = ASR::down_cast<ASR::ArraySection_t>(a_v);
        if( a_dim == nullptr ) {
            ASR::asr_t* const1 = ASR::make_IntegerConstant_t(al, a_loc, 1, a_type);
            ASR::asr_t* size = const1;
            for( size_t i = 0; i < array_section_t->n_args; i++ ) {
                ASR::expr_t* start = array_section_t->m_args[i].m_left;
                ASR::expr_t* end = array_section_t->m_args[i].m_right;
                ASR::expr_t* d = array_section_t->m_args[i].m_step;
                if( (start == nullptr || end == nullptr || d == nullptr) &&
                    !ASRUtils::is_array(ASRUtils::expr_type(end))){
                    continue;
                }
                ASR::expr_t* plus1 = nullptr;
                // Case: A(:, iact) where iact is an array
                if( ASRUtils::is_array(ASRUtils::expr_type(end)) ) {
                    ASR::ttype_t* arr_type = ASRUtils::expr_type(end);
                    bool is_func_with_unknown_return = (ASR::is_a<ASR::FunctionCall_t>(*end) &&
                        ASRUtils::is_allocatable(ASRUtils::expr_type(end))) || ASR::is_a<ASR::IntrinsicArrayFunction_t>(*end);
                    if( ASRUtils::is_fixed_size_array(arr_type) ) {
                        int64_t arr_size = ASRUtils::get_fixed_size_of_array(arr_type);
                        plus1 = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, a_loc, arr_size, a_type));
                    } else {
                        plus1 = ASRUtils::EXPR(ASRUtils::make_ArraySize_t_util(al, end->base.loc, end,
                            nullptr, a_type, ASRUtils::expr_value(end), !is_func_with_unknown_return));
                    }
                } else {
                    start = CastingUtil::perform_casting(start, a_type, al, a_loc);
                    end = CastingUtil::perform_casting(end, a_type, al, a_loc);
                    d = CastingUtil::perform_casting(d, a_type, al, a_loc);
                    ASR::expr_t* endminusstart = ASRUtils::EXPR(ASR::make_IntegerBinOp_t(
                        al, a_loc, end, ASR::binopType::Sub, start, a_type, nullptr));
                    ASR::expr_t* byd = ASRUtils::EXPR(ASR::make_IntegerBinOp_t(
                        al, a_loc, endminusstart, ASR::binopType::Div, d, a_type, nullptr));
                    plus1 = ASRUtils::EXPR(ASR::make_IntegerBinOp_t(
                        al, a_loc, byd, ASR::binopType::Add, ASRUtils::EXPR(const1), a_type, nullptr));
                }
                size = ASR::make_IntegerBinOp_t(al, a_loc, ASRUtils::EXPR(size),
                    ASR::binopType::Mul, plus1, a_type, nullptr);
            }
            // ArraySize should not be negative
            ASR::expr_t* const0 = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, a_loc, 0, a_type));
            ASRBuilder builder(al, a_loc);
            size = (ASR::asr_t *)builder.Max(const0, ASRUtils::EXPR(size));
            return size;
        } else if( is_dimension_constant ) {
            ASR::asr_t* const1 = ASR::make_IntegerConstant_t(al, a_loc, 1, a_type);

            // ArraySection arguments include scalar indices as well. For
            // ArraySize(section, dim=k), `k` is over the resulting section
            // rank, so we must skip scalar-indexed dimensions when mapping
            // to section args.
            ASR::array_index_t* section_arg = nullptr;
            int64_t logical_dim = 0;
            for (size_t i = 0; i < array_section_t->n_args; i++) {
                ASR::expr_t* i_start = array_section_t->m_args[i].m_left;
                ASR::expr_t* i_end = array_section_t->m_args[i].m_right;
                ASR::expr_t* i_step = array_section_t->m_args[i].m_step;
                bool is_scalar_index = (i_start == nullptr) && (i_step == nullptr)
                    && !ASRUtils::is_array(ASRUtils::expr_type(i_end));
                if (is_scalar_index) {
                    continue;
                }
                logical_dim += 1;
                if (logical_dim == dim) {
                    section_arg = &array_section_t->m_args[i];
                    break;
                }
            }

            if (section_arg == nullptr) {
                return const1;
            }

            ASR::expr_t* start = section_arg->m_left;
            ASR::expr_t* end = section_arg->m_right;
            ASR::expr_t* d = section_arg->m_step;

            // Case: A(:, iact) where iact is an array and dim = 2
            if( ASRUtils::is_array(ASRUtils::expr_type(end)) ) {
                bool is_func_with_unknown_return = (ASR::is_a<ASR::FunctionCall_t>(*end) &&
                        ASRUtils::is_allocatable(ASRUtils::expr_type(end))) || ASR::is_a<ASR::IntrinsicArrayFunction_t>(*end);
                ASR::ttype_t* arr_type = ASRUtils::expr_type(end);
                if( ASRUtils::is_fixed_size_array(arr_type) ) {
                    int64_t arr_size = ASRUtils::get_fixed_size_of_array(arr_type);
                    return ASR::make_IntegerConstant_t(al, a_loc, arr_size, a_type);
                } else {
                    return ASRUtils::make_ArraySize_t_util(al, end->base.loc, end,
                        nullptr, a_type, ASRUtils::expr_value(end), !is_func_with_unknown_return);
                }
            }

            if( start == nullptr && d == nullptr ) {
                return const1;
            }
            start = CastingUtil::perform_casting(start, a_type, al, a_loc);
            end = CastingUtil::perform_casting(end, a_type, al, a_loc);
            d = CastingUtil::perform_casting(d, a_type, al, a_loc);
            ASR::expr_t* endminusstart = ASRUtils::EXPR(ASR::make_IntegerBinOp_t(
                al, a_loc, end, ASR::binopType::Sub, start, a_type, nullptr));
            ASR::expr_t* byd = ASRUtils::EXPR(ASR::make_IntegerBinOp_t(
                al, a_loc, endminusstart, ASR::binopType::Div, d, a_type, nullptr));
            return ASR::make_IntegerBinOp_t(al, a_loc, byd, ASR::binopType::Add,
                ASRUtils::EXPR(const1), a_type, nullptr);
        }
    }
    if( ASR::is_a<ASR::ArrayItem_t>(*a_v) ) {
        ASR::ArrayItem_t* array_item_t = ASR::down_cast<ASR::ArrayItem_t>(a_v);
        LCOMPILERS_ASSERT(ASRUtils::is_array(array_item_t->m_type));
        if( for_type ) {
            LCOMPILERS_ASSERT(!ASRUtils::is_allocatable(array_item_t->m_type) &&
                              !ASRUtils::is_pointer(array_item_t->m_type));
        }
        if( a_dim == nullptr ) {
            ASR::asr_t* const1 = ASR::make_IntegerConstant_t(al, a_loc, 1, a_type);
            ASR::asr_t* size = const1;
            for( size_t i = 0; i < array_item_t->n_args; i++ ) {
                ASR::expr_t* end = ASRUtils::EXPR(make_ArraySize_t_util(al, a_loc,
                    array_item_t->m_args[i].m_right, a_dim, a_type, nullptr, for_type));
                size = ASR::make_IntegerBinOp_t(al, a_loc, ASRUtils::EXPR(size),
                    ASR::binopType::Mul, end, a_type, nullptr);
            }
            return size;
        } else if( is_dimension_constant ) {
            return make_ArraySize_t_util(al, a_loc,
                array_item_t->m_args[dim].m_right,
                nullptr, a_type, nullptr, for_type);
        }
    }
    if( is_binop_expr(a_v) && for_type ) {
        ASR::expr_t* lhs = extract_member_from_binop(a_v, 0);
        ASR::expr_t* rhs = extract_member_from_binop(a_v, 1);
        // ArrayBroadcast wraps a scalar broadcast to the shape of the other
        // operand; take the size from the non-broadcast side.
        if( ASR::is_a<ASR::ArrayBroadcast_t>(*rhs) ) {
            return make_ArraySize_t_util(al, a_loc, lhs, a_dim, a_type, a_value, for_type);
        } else if( ASR::is_a<ASR::ArrayBroadcast_t>(*lhs) ) {
            return make_ArraySize_t_util(al, a_loc, rhs, a_dim, a_type, a_value, for_type);
        } else if( ASR::is_a<ASR::Var_t>(*rhs) ||
                   ASR::is_a<ASR::ArraySection_t>(*rhs) ) {
            return make_ArraySize_t_util(al, a_loc, rhs, a_dim, a_type, a_value, for_type);
        } else {
            return make_ArraySize_t_util(al, a_loc, lhs, a_dim, a_type, a_value, for_type);
        }
    } else if( is_unaryop_expr(a_v) && for_type ) {
        return make_ArraySize_t_util(al, a_loc, extract_member_from_unaryop(a_v), a_dim, a_type, a_value, for_type);
    } else if( ASR::is_a<ASR::Cast_t>(*a_v) && for_type ) {
        // Cast preserves array shape; recurse into its argument.
        return make_ArraySize_t_util(al, a_loc,
            ASR::down_cast<ASR::Cast_t>(a_v)->m_arg, a_dim, a_type, a_value, for_type);
    } else if( ASR::is_a<ASR::ArrayConstructor_t>(*a_v) && for_type ) {
        ASR::ArrayConstructor_t* array_constructor = ASR::down_cast<ASR::ArrayConstructor_t>(a_v);
        return &(get_ArrayConstructor_size(al, array_constructor)->base);
    } else {
        ASR::dimension_t* m_dims = nullptr;
        size_t n_dims = 0;
        if (array_func_type != nullptr) n_dims = ASRUtils::extract_dimensions_from_ttype(array_func_type, m_dims);
        else n_dims = ASRUtils::extract_dimensions_from_ttype(ASRUtils::expr_type(a_v), m_dims);
        bool is_dimension_dependent_only_on_arguments_ = is_dimension_dependent_only_on_arguments(m_dims, n_dims);

        bool compute_size = (is_dimension_dependent_only_on_arguments_ &&
            (is_dimension_constant || a_dim == nullptr));
        if( compute_size && for_type ) {
            ASR::dimension_t* m_dims = nullptr;
            if (array_func_type != nullptr) n_dims = ASRUtils::extract_dimensions_from_ttype(array_func_type, m_dims);
            else n_dims = ASRUtils::extract_dimensions_from_ttype(ASRUtils::expr_type(a_v), m_dims);
            if( a_dim == nullptr ) {
                ASR::asr_t* size = ASR::make_IntegerConstant_t(al, a_loc, 1, a_type);
                for( size_t i = 0; i < n_dims; i++ ) {
                    size = ASR::make_IntegerBinOp_t(al, a_loc, ASRUtils::EXPR(size),
                        ASR::binopType::Mul, m_dims[i].m_length, a_type, nullptr);
                }
                return size;
            } else if( is_dimension_constant ) {
                return (ASR::asr_t*) m_dims[dim - 1].m_length;
            }
        }
    }


    if( for_type ) {
        LCOMPILERS_ASSERT_MSG(
            ASR::is_a<ASR::Var_t>(*a_v) ||
            ASR::is_a<ASR::StructInstanceMember_t>(*a_v) ||
            ASR::is_a<ASR::FunctionParam_t>(*a_v),
            "Found ASR::exprType::" + std::to_string(a_v->type));
    }

    return ASR::make_ArraySize_t(al, a_loc, a_v, a_dim, a_type, a_value);
}

ASR::ttype_t* make_StructType_t_util(Allocator& al,
                                     Location loc,
                                     ASR::symbol_t* derived_type_sym,
                                     bool is_cstruct)
{
    ASR::Struct_t* derived_type = ASR::down_cast<ASR::Struct_t>(
        ASRUtils::symbol_get_past_external(derived_type_sym));

    if ( derived_type->m_struct_signature != nullptr ) {
        ASR::StructType_t* struct_type = ASR::down_cast<ASR::StructType_t>(derived_type->m_struct_signature);
        // if ( struct_type->m_is_cstruct == is_cstruct ) {
        //     // return already constructed struct type
        //     return derived_type->m_struct_signature;
        // }

        if ( struct_type->m_is_cstruct != is_cstruct ) {
            /*
                This if for the cases where a struct type is declared as class(XX) in Fortran
                For the example given below, in subroutine method, `list` is used as a class type

                module list_module

                type :: list
                    type(list), pointer :: child => null()
                ! contains
                !    procedure :: method
                end type list

                contains

                subroutine method(self)
                    class(list), intent(inout) :: self
                    print *, 'associated: ', associated(self%child)
                end subroutine method

                end module list_module
            */
            ASR::StructType_t* new_struct_type = ASR::down_cast<ASR::StructType_t>(ASRUtils::duplicate_type(al, (ASR::ttype_t*) struct_type));
            new_struct_type->m_is_cstruct = is_cstruct;
            return (ASR::ttype_t*) new_struct_type;
        }
        return derived_type->m_struct_signature;
    }

    std::string derived_type_name = derived_type->m_name;

    Vec<ASR::ttype_t*> member_types;
    member_types.reserve(al, derived_type->n_members);

    for (size_t i = 0; i < derived_type->n_members; i++) {
        std::string member_name = derived_type->m_members[i];
        ASR::symbol_t* sym = derived_type->m_symtab->get_symbol(member_name);
        if (sym && ASR::is_a<ASR::Variable_t>(*sym)) {
            ASR::Variable_t* var = ASR::down_cast<ASR::Variable_t>(
                ASRUtils::symbol_get_past_external(sym));
            if (ASRUtils::symbol_get_past_external(derived_type_sym) == ASRUtils::symbol_get_past_external(var->m_type_declaration)) {
                // this is self referential, so we can directly take it
                ASR::StructType_t* stype = ASR::down_cast<ASR::StructType_t>(ASRUtils::extract_type(var->m_type));
                return ASRUtils::TYPE(
                    ASR::make_StructType_t(al, loc, stype->m_data_member_types,
                                           stype->n_data_member_types,
                                           nullptr,
                                           0,
                                           is_cstruct == false ? false : stype->m_is_cstruct,
                                           stype->m_is_unlimited_polymorphic
                                           )
                );
            }
            member_types.push_back(al, var->m_type);
        }
    }
    return ASRUtils::TYPE(
        ASR::make_StructType_t(al,
                               loc,
                               member_types.p,
                               member_types.n,
                               nullptr,
                               0,
                               is_cstruct,
                               (derived_type_name == "~unlimited_polymorphic_type" || derived_type_name == "~assumed_type") ? true : false));
}

ASR::expr_t* get_compile_time_array_size(Allocator& al, ASR::ttype_t* array_type){
    LCOMPILERS_ASSERT(ASR::is_a<ASR::Array_t>(*
        ASRUtils::type_get_past_allocatable_pointer(array_type)));
    int64_t array_size = ASRUtils::get_fixed_size_of_array(array_type);
    if(array_size != -1){
            return ASRUtils::EXPR(
                ASR::make_IntegerConstant_t(al, array_type->base.loc, array_size,
                ASRUtils::TYPE(ASR::make_Integer_t(al, array_type->base.loc, 8))));
    }
    return nullptr;
}

template<typename T>
ASR::expr_t* get_binop_size_var(ASR::expr_t* x) {
    ASR::expr_t* left = get_expr_size_expr(ASR::down_cast<T>(x)->m_left, true);
    ASR::expr_t* right = get_expr_size_expr(ASR::down_cast<T>(x)->m_right, true);
    if (left && ASRUtils::is_array(ASRUtils::expr_type(left))) {
        return get_expr_size_expr(left, true);
    } else if (right && ASRUtils::is_array(ASRUtils::expr_type(right))) {
        return get_expr_size_expr(right, true);
    }
    return nullptr;
}

// Get past expressions to get the expr which will be used to calculate ArraySize.
// This should only return one of Var, ArrayPhysicalCast, StructInstanceMember, BitCast, or ArrayConstant.
// This should never return nullptr for regular calls, nullptr is only used for traversing binop to find an array.
ASR::expr_t* get_expr_size_expr(ASR::expr_t* x, bool inside_binop /* = false*/) {
    if (ASR::is_a<ASR::Var_t>(*x) ||
        ASR::is_a<ASR::ArrayPhysicalCast_t>(*x) ||
        ASR::is_a<ASR::BitCast_t>(*x) ||
        ASR::is_a<ASR::ArrayConstant_t>(*x) ||
        ASR::is_a<ASR::ArrayConstructor_t>(*x) ||
        ASR::is_a<ASR::ArraySection_t>(*x)) {
        return x;
    }

    if (ASR::is_a<ASR::IntegerBinOp_t>(*x)) {
        return get_binop_size_var<ASR::IntegerBinOp_t>(x);
    } else if (ASR::is_a<ASR::RealBinOp_t>(*x)) {
        return get_binop_size_var<ASR::RealBinOp_t>(x);
    } else if (ASR::is_a<ASR::ComplexBinOp_t>(*x)) {
        return get_binop_size_var<ASR::ComplexBinOp_t>(x);
    } else if (ASR::is_a<ASR::LogicalBinOp_t>(*x)) {
        return get_binop_size_var<ASR::LogicalBinOp_t>(x);
    } else if (ASR::is_a<ASR::IntegerCompare_t>(*x)) {
        return get_binop_size_var<ASR::IntegerCompare_t>(x);
    } else if (ASR::is_a<ASR::RealCompare_t>(*x)) {
        return get_binop_size_var<ASR::RealCompare_t>(x);
    } else if (ASR::is_a<ASR::ComplexCompare_t>(*x)) {
        return get_binop_size_var<ASR::ComplexCompare_t>(x);
    } else if (ASR::is_a<ASR::StringCompare_t>(*x)) {
        return get_binop_size_var<ASR::StringCompare_t>(x);
    } else if (ASR::is_a<ASR::OverloadedCompare_t>(*x)) {
        return get_binop_size_var<ASR::OverloadedCompare_t>(x);
    } else if (ASR::is_a<ASR::StringConcat_t>(*x)) {
        return get_binop_size_var<ASR::StringConcat_t>(x);
    } else if (ASR::is_a<ASR::IntegerUnaryMinus_t>(*x)) {
        ASR::expr_t* arg = ASR::down_cast<ASR::IntegerUnaryMinus_t>(x)->m_arg;
        if (ASRUtils::is_array(ASRUtils::expr_type(arg))) {
            return get_expr_size_expr(arg);
        }
    } else if (ASR::is_a<ASR::RealUnaryMinus_t>(*x)) {
        ASR::expr_t* arg = ASR::down_cast<ASR::RealUnaryMinus_t>(x)->m_arg;
        if (ASRUtils::is_array(ASRUtils::expr_type(arg))) {
            return get_expr_size_expr(arg);
        }
    } else if (ASR::is_a<ASR::Cast_t>(*x)) {
        ASR::expr_t* arg = ASR::down_cast<ASR::Cast_t>(x)->m_arg;
        if (ASRUtils::is_array(ASRUtils::expr_type(arg))) {
            return get_expr_size_expr(arg);
        }
    } else if (ASR::is_a<ASR::LogicalNot_t>(*x)) {
        ASR::expr_t* arg = ASR::down_cast<ASR::LogicalNot_t>(x)->m_arg;
        if (ASRUtils::is_array(ASRUtils::expr_type(arg))) {
            return get_expr_size_expr(arg);
        }
    } else if (ASR::is_a<ASR::IntrinsicElementalFunction_t>(*x)) {
        ASR::IntrinsicElementalFunction_t* elemental_f = ASR::down_cast<ASR::IntrinsicElementalFunction_t>(x);

        // If any argument is an array other arguments must be the same shape
        for (size_t i = 0; i < elemental_f->n_args; i++) {
            if (ASRUtils::is_array(ASRUtils::expr_type(elemental_f->m_args[i]))) {
                return get_expr_size_expr(elemental_f->m_args[i]);
            }
        }
    } else if (ASR::is_a<ASR::FunctionCall_t>(*x)) {
        ASR::FunctionCall_t* func_call = ASR::down_cast<ASR::FunctionCall_t>(x);
        if (ASRUtils::is_elemental(func_call->m_name)) {
            // If any argument is an array other arguments must be the same shape
            for (size_t i = 0; i < func_call->n_args; i++) {
                if (ASRUtils::is_array(ASRUtils::expr_type(func_call->m_args[i].m_value))) {
                    return get_expr_size_expr(func_call->m_args[i].m_value);
                }
            }
            // m_dt is also an argument
            if (func_call->m_dt && ASRUtils::is_array(ASRUtils::expr_type(func_call->m_dt))) {
                return get_expr_size_expr(func_call->m_dt);
            }
        }
    } else if (ASR::is_a<ASR::ComplexRe_t>(*x)) {
        ASR::expr_t* arg = ASR::down_cast<ASR::ComplexRe_t>(x)->m_arg;
        if (ASRUtils::is_array(ASRUtils::expr_type(arg))) {
            return get_expr_size_expr(arg);
        }
    } else if (ASR::is_a<ASR::ComplexIm_t>(*x)) {
        ASR::expr_t* arg = ASR::down_cast<ASR::ComplexIm_t>(x)->m_arg;
        if (ASRUtils::is_array(ASRUtils::expr_type(arg))) {
            return get_expr_size_expr(arg);
        }
    } else if (ASR::is_a<ASR::StructInstanceMember_t>(*x)) {
        ASR::StructInstanceMember_t* sim = ASR::down_cast<ASR::StructInstanceMember_t>(x);
        if (ASRUtils::is_array(ASRUtils::expr_type(sim->m_v))) {
            return get_expr_size_expr(sim->m_v);
        }
    }

    if (ASR::is_a<ASR::StructInstanceMember_t>(*x)) {
        return x;
    } else if (inside_binop) {
        return nullptr;
    } else {
        return nullptr;
    }
}


//Initialize pointer to zero so that it can be initialized in first call to get_instance
ASRUtils::LabelGenerator* ASRUtils::LabelGenerator::label_generator = nullptr;

} // namespace ASRUtils


} // namespace LCompilers
