#include "libasr/assert.h"
#include "libasr/string_utils.h"
#include <iostream>
#include <llvm/IR/Value.h>
#include <memory>
#include <set>
#include <unordered_map>
#include <utility>

#include <llvm/ADT/STLExtras.h>
#include <llvm/Analysis/Passes.h>
#include <llvm/ExecutionEngine/ExecutionEngine.h>
#include <llvm/ExecutionEngine/GenericValue.h>
#include <llvm/ExecutionEngine/MCJIT.h>
#include <llvm/IR/Argument.h>
#include <llvm/IR/Attributes.h>
#include <llvm/IR/BasicBlock.h>
#include <llvm/IR/Constants.h>
#include <llvm/IR/DerivedTypes.h>
#include <llvm/IR/Function.h>
#include <llvm/IR/IRBuilder.h>
#include <llvm/IR/Instructions.h>
#include <llvm/IR/Intrinsics.h>
#include <llvm/IR/LegacyPassManager.h>
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/Module.h>
#include <llvm/IR/Type.h>
#include <llvm/Support/Casting.h>
#include <llvm/Support/ManagedStatic.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Support/raw_ostream.h>
#if LLVM_VERSION_MAJOR >= 14
#   include <llvm/MC/TargetRegistry.h>
#else
#   include <llvm/Support/TargetRegistry.h>
#endif
#if LLVM_VERSION_MAJOR >= 18
#   include <llvm/TargetParser/Host.h>
#else
#   include <llvm/Support/Host.h>
#endif
#include <llvm/ADT/APFloat.h>
#include <llvm/ADT/STLExtras.h>
#include <llvm/IR/Verifier.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Target/TargetMachine.h>
#include <llvm/Transforms/Scalar.h>
#include <llvm/Transforms/Utils/ModuleUtils.h>
#include <llvm/ExecutionEngine/ObjectCache.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/Path.h>
#include <llvm/IR/DIBuilder.h>
#if LLVM_VERSION_MAJOR < 18
#   include <llvm/Transforms/Vectorize.h>
#endif

#include <libasr/asr.h>
#include <libasr/containers.h>
#include <libasr/codegen/asr_to_llvm.h>
#include <libasr/pass/pass_manager.h>
#include <libasr/exception.h>
#include <libasr/asr_utils.h>
#include <libasr/codegen/llvm_utils.h>
#include <libasr/codegen/llvm_array_utils.h>
#include <libasr/pass/intrinsic_function_registry.h>
#include <libasr/codegen/llvm_compat.h>
#include <libasr/codegen/asr_to_metal.h>
#include <libasr/codegen/asr_to_cuda.h>
#include <libasr/codegen/gpu_utils.h>
namespace LCompilers {

using ASR::is_a;
using ASR::down_cast;
using ASR::down_cast2;

using ASRUtils::expr_type;
using ASRUtils::symbol_get_past_external;
using ASRUtils::EXPR2VAR;
using ASRUtils::EXPR2FUN;
using ASRUtils::intent_local;
using ASRUtils::intent_return_var;
using ASRUtils::determine_module_dependencies;
using ASRUtils::is_arg_dummy;
using ASRUtils::is_argument_of_type_CPtr;

// Helper functions for LLVM function name mangling
namespace {

/**
 * Check if a function is an external interface function.
 * External interface functions are functions with:
 * - Interface deftype
 * - Not intrinsic ABI
 * - Not in a module
 */
static inline bool is_external_interface_function(ASR::FunctionType_t* ftype) {
    return ftype->m_deftype == ASR::deftypeType::Interface &&
           ftype->m_abi != ASR::abiType::Intrinsic &&
           !ftype->m_module;
}

/**
 * Compute the final mangled LLVM function name.
 *
 * This function encapsulates all mangling logic in one place:
 * 1. Determines base name (BindC, external interface, or prefixed)
 * 2. Adds parent function prefix if applicable
 *
 * @param sym_name Original symbol name from ASR
 * @param ftype Function type information
 * @param compiler_options Compiler options including mangling flags
 * @param mangle_prefix Prefix to add (typically "_lfortran_")
 * @param parent_function Parent function if this is a nested function
 * @return Final mangled name to use in LLVM
 */
static std::string compute_llvm_function_name(
    const std::string& sym_name,
    ASR::FunctionType_t* ftype,
    const CompilerOptions& compiler_options,
    const std::string& mangle_prefix,
    const ASR::Function_t* parent_function
) {
    (void)compiler_options;
    bool is_external_interface = is_external_interface_function(ftype);
    bool is_bindc = (ftype->m_abi == ASR::abiType::BindC);

    std::string fn_name;

    // Determine base name based on function type
    if (is_bindc) {
        if (ftype->m_bindc_name && ftype->m_bindc_name[0] != '\0') {
            // bind(c, name='foo') — use the explicit name
            fn_name = ftype->m_bindc_name;
        } else if (!ftype->m_bindc_name) {
            // bind(c) without name= — use the Fortran name
            fn_name = sym_name;
        } else {
            // bind(c, name='') — empty name, use mangled name
            fn_name = mangle_prefix + sym_name;
        }
    } else if (is_external_interface) {
        fn_name = sym_name;
    } else {
        fn_name = mangle_prefix + sym_name;
    }

    // Add parent function prefix for nested functions
    if (parent_function != nullptr &&
        !is_external_interface &&
        ftype->m_abi != ASR::abiType::Intrinsic &&
        !is_bindc) {
        fn_name = std::string(parent_function->m_name) + "." + fn_name;
    }

    return fn_name;
}

} // anonymous namespace

class ASRToLLVMVisitor : public ASR::BaseVisitor<ASRToLLVMVisitor>
{
private:
  //! To be used by visit_StructInstanceMember.
  std::string current_der_type_name;

    //! Helpful for debugging while testing LLVM code
    void print_util(llvm::Value* v, std::string fmt_chars, std::string endline) {
        // Usage:
        // print_util(tmp, "%d", "\n") // `tmp` is an integer type to match the format specifiers
        std::vector<llvm::Value *> args;
        std::vector<std::string> fmt;
        args.push_back(v);
        fmt.push_back(fmt_chars);
        std::string fmt_str;
        for (size_t i=0; i<fmt.size(); i++) {
            fmt_str += fmt[i];
            if (i < fmt.size()-1) fmt_str += " ";
        }
        fmt_str += endline;
        llvm::Value *fmt_ptr = LCompilers::create_global_string_ptr(context, *module, *builder, fmt_str);
        std::vector<llvm::Value *> printf_args;
        printf_args.push_back(fmt_ptr);
        printf_args.insert(printf_args.end(), args.begin(), args.end());
        printf(context, *module, *builder, printf_args);
    }

    //! Helpful for debugging while testing LLVM code
    void print_util(llvm::Value* v, std::string endline="\n") {
        // Usage:
        // print_util(tmp)
        std::string buf;
        llvm::raw_string_ostream os(buf);
        v->print(os);
        std::cout << os.str() << endline;
    }

    //! Helpful for debugging while testing LLVM code
    void print_util(llvm::Type* v, std::string endline="\n") {
        // Usage:
        // print_util(tmp->getType())
        std::string buf;
        llvm::raw_string_ostream os(buf);
        v->print(os);
        std::cout << os.str() << endline;
    }

    std::string get_expr_name_for_runtime_message(ASR::expr_t* expr) {
        if (!expr) {
            return "";
        }

        if (ASR::is_a<ASR::Var_t>(*expr)) {
            ASR::Var_t* v = ASR::down_cast<ASR::Var_t>(expr);
            return ASRUtils::symbol_name(ASRUtils::symbol_get_past_external(v->m_v));
        }

        if (ASR::is_a<ASR::IntegerConstant_t>(*expr)) {
            ASR::IntegerConstant_t* i = ASR::down_cast<ASR::IntegerConstant_t>(expr);
            return std::to_string(i->m_n);
        }

        if (ASR::is_a<ASR::ArrayItem_t>(*expr)) {
            ASR::ArrayItem_t* array_item = ASR::down_cast<ASR::ArrayItem_t>(expr);
            std::string base_name = get_expr_name_for_runtime_message(array_item->m_v);
            if (base_name.empty()) {
                return "";
            }

            std::string result = base_name + "(";
            for (size_t i = 0; i < array_item->n_args; i++) {
                ASR::expr_t* idx_expr = array_item->m_args[i].m_right;
                std::string index_name = get_expr_name_for_runtime_message(idx_expr);
                if (index_name.empty()) {
                    return "";
                }
                if (i > 0) {
                    result += ", ";
                }
                result += index_name;
            }
            result += ")";
            return result;
        }

        if (ASR::is_a<ASR::StructInstanceMember_t>(*expr)) {
            ASR::StructInstanceMember_t* sim = ASR::down_cast<ASR::StructInstanceMember_t>(expr);
            std::string base_name = get_expr_name_for_runtime_message(sim->m_v);
            std::string member_name = ASRUtils::symbol_name(ASRUtils::symbol_get_past_external(sim->m_m));
            if (base_name.empty()) {
                return "";
            }
            return base_name + "%" + member_name;
        }

        return "";
    }

    void generate_unallocated_array_runtime_error(llvm::Value* is_not_allocated,
                                                  ASR::expr_t* target_expr) {
        std::string target_expr_name = get_expr_name_for_runtime_message(target_expr);
        if (!target_expr_name.empty()) {
            llvm::Value* target_expr_name_llvm = LCompilers::create_global_string_ptr(
                context, *module, *builder, target_expr_name);
            llvm_utils->generate_runtime_error(
                is_not_allocated,
                "Array '%s' is not allocated. Allocate it manually or use the '--realloc-lhs-arrays' option to allocate it automatically.",
                {LLVMUtils::RuntimeLabel("not allocated here", {target_expr->base.loc}, {})},
                infile,
                location_manager,
                target_expr_name_llvm);
        } else {
            llvm_utils->generate_runtime_error(
                is_not_allocated,
                "Array is not allocated. Allocate it manually or use the '--realloc-lhs-arrays' option to allocate it automatically.",
                {LLVMUtils::RuntimeLabel("not allocated here", {target_expr->base.loc}, {})},
                infile,
                location_manager);
        }
    }

    llvm::Value* allocate_class_wrapper_storage(ASR::expr_t* target_expr,
                                                llvm::Type* target_llvm_type,
                                                llvm::Value* wrapper_size) {
        if (ASR::is_a<ASR::Var_t>(*target_expr)) {
            ASR::symbol_t* target_sym = ASRUtils::symbol_get_past_external(
                ASR::down_cast<ASR::Var_t>(target_expr)->m_v);
            if (ASR::is_a<ASR::Variable_t>(*target_sym)) {
                ASR::Variable_t* target_var = ASR::down_cast<ASR::Variable_t>(target_sym);
                if (target_var->m_storage == ASR::storage_typeType::Save) {
                    std::string wrapper_name = "__lfortran_save_class_wrapper_"
                        + std::to_string(get_hash((ASR::asr_t*)target_sym));
                    llvm::GlobalVariable* wrapper_g = module->getGlobalVariable(
                        wrapper_name, true);
                    if (!wrapper_g) {
                        wrapper_g = new llvm::GlobalVariable(
                            *module,
                            target_llvm_type,
                            false,
                            llvm::GlobalValue::InternalLinkage,
                            llvm::Constant::getNullValue(target_llvm_type),
                            wrapper_name);
                    }
                    return wrapper_g;
                }
            }
        }

        llvm::Value* wrapper_ptr = LLVMArrUtils::lfortran_malloc(
            context, *module, *builder, wrapper_size);
        builder->CreateMemSet(wrapper_ptr, llvm::ConstantInt::get(context, llvm::APInt(8, 0)),
            wrapper_size, llvm::MaybeAlign());
        return builder->CreateBitCast(wrapper_ptr, target_llvm_type->getPointerTo());
    }

public:
    diag::Diagnostics &diag;
    llvm::LLVMContext &context;
    std::unique_ptr<llvm::Module> module;
    std::unique_ptr<llvm::IRBuilder<>> builder;
    std::string infile;
    Allocator &al;

    llvm::Value *tmp;
    llvm::BasicBlock *proc_return;
    std::string mangle_prefix;
    bool prototype_only;
    llvm::StructType *complex_type_4, *complex_type_8;
    llvm::StructType *complex_type_4_ptr, *complex_type_8_ptr;
    llvm::Type* string_descriptor;
    llvm::PointerType *character_type;
    llvm::PointerType *list_type;
    std::vector<std::string> struct_type_stack;

    std::unordered_map<std::uint32_t, std::unordered_map<std::string, llvm::Type*>> arr_arg_type_cache;

    std::map<std::string, std::pair<llvm::Type*, llvm::Type*>> fname2arg_type;

    // Maps for containing information regarding derived types
    std::map<std::string, llvm::StructType*> name2dertype, name2dercontext;
    std::map<std::string, std::string> dertype2parent;
    std::map<std::string, std::map<std::string, int>> name2memidx;

    std::map<uint64_t, llvm::Value*> llvm_symtab; // llvm_symtab_value
    std::map<std::pair<uint64_t, SymbolTable*>, llvm::Value*> llvm_symtab_deep_copy;
    std::map<uint64_t, llvm::Function*> llvm_symtab_fn;
    std::map<std::string, uint64_t> llvm_symtab_fn_names;
    std::map<uint64_t, llvm::Value*> llvm_symtab_fn_arg;
    std::map<uint64_t, llvm::BasicBlock*> llvm_goto_targets;
    std::unordered_map<const ASR::symbol_t*, llvm::BasicBlock*> symbol_to_returnBlock; /// Get Symbol's Return Block -- Used for Finalization. See LLVMFinalize
    std::set<uint32_t> global_string_allocated;
    const ASR::Function_t *parent_function = nullptr;

    std::vector<llvm::BasicBlock*> loop_head; /* For saving the head of a loop,
        so that we can jump to the head of the loop when we reach a cycle */
    std::vector<std::string> loop_head_names;
    std::vector<llvm::BasicBlock*> loop_or_block_end; /* For saving the end of a block,
        so that we can jump to the end of the block when we reach an exit */
    std::vector<std::string> loop_or_block_end_names;

    struct FlatCopyback {
        llvm::Value* flat_buf;
        llvm::Value* orig_struct_ptr;
        llvm::Type* struct_llvm_type;
        struct Member { uint64_t flat_off; uint64_t size; int member_idx; bool is_char; };
        std::vector<Member> layout;
    };
    std::vector<FlatCopyback> pending_flat_copybacks;

    int64_t ptr_loads;
    bool lookup_enum_value_for_nonints;
    bool is_assignment_target;
    int64_t global_array_count;
    int64_t global_deep_count;

    CompilerOptions &compiler_options;
    LocationManager &location_manager;

    // For handling debug information
    std::unique_ptr<llvm::DIBuilder> DBuilder;
    llvm::DICompileUnit *debug_CU;
    llvm::DIScope *debug_current_scope;
    llvm::DIFile *debug_Unit;

    std::map<ASR::symbol_t*, std::map<SymbolTable*, llvm::Value*>> type2vtab;
    std::map<ASR::symbol_t*, std::map<SymbolTable*, std::vector<llvm::Value*>>> class2vtab;
    std::map<ASR::symbol_t*, llvm::Type*> type2vtabtype;
    std::map<ASR::symbol_t*, int> type2vtabid;
    std::map<ASR::symbol_t*, std::map<std::string, int64_t>> vtabtype2procidx;
    std::map<ASR::symbol_t*, std::map<std::string, int64_t>> struct_vtab_function_offset;



    llvm::Value* current_sret_arg;
    llvm::Value* current_decimal_mode = nullptr;
    llvm::Value* current_sign_mode = nullptr;
    llvm::Value* current_round_mode = nullptr;

    SymbolTable* current_scope;
    std::unique_ptr<LLVMUtils> llvm_utils;
    LLVMFinalize llvm_symtab_finalizer;
    std::unique_ptr<LLVMList> list_api;
    std::unique_ptr<LLVMStruct> struct_api;
    std::unique_ptr<LLVMTuple> tuple_api;
    std::unique_ptr<LLVMDictInterface> dict_api_lp;
    std::unique_ptr<LLVMDictInterface> dict_api_sc;
    std::unique_ptr<LLVMSetInterface> set_api_lp;
    std::unique_ptr<LLVMSetInterface> set_api_sc;
    std::unique_ptr<LLVMArrUtils::Descriptor> arr_descr;
    Vec<llvm::Value*> strings_to_be_deallocated;
    Vec<llvm::Value*> heap_fixed_size_arrays;  // Heap-allocated large fixed-size arrays for cleanup
    bool in_block_context = false;  // Flag to track if we're inside a BLOCK construct
    // Tracks allocatable/pointer array args in bind(C) functions that need
    // stride conversion from element-based back to byte-based on exit.
    struct BindCStrideExit {
        llvm::Value* desc_ptr_ptr;   // alloca storing the descriptor pointer
        llvm::Type* desc_array_type; // LLVM type of the descriptor
        uint64_t el_size;            // element size in bytes
        int n_dims;                  // number of dimensions
        llvm::Value* original_cfi_ptr; // original CFI descriptor pointer (for writeback)
        llvm::Type* el_type;         // element LLVM type
        int8_t type_code;            // CFI type code
        int cfi_attr;                // CFI attribute
    };
    std::vector<BindCStrideExit> bindc_stride_exits;
    struct CharConsolidationWriteback {
        llvm::Value* original_descs_i8;
        llvm::Value* consolidated_desc;
        llvm::Value* n_elems_i64;
    };
    std::vector<CharConsolidationWriteback> pending_char_writebacks;
    struct to_be_allocated_array{ // struct to hold details for the initializing pointer_to_array_type later inside main function.
        ASR::expr_t* expr;
        llvm::Constant* pointer_to_array_type;
        llvm::Type* array_type;
        LCompilers::ASR::ttype_t* var_type;
        size_t n_dims;
    };
    std::vector<to_be_allocated_array> allocatable_array_details;
    std::vector<std::pair<ASR::symbol_t*, llvm::Value*>> allocatable_struct_array_members_details;
    struct variable_inital_value { /* Saves information for variables that need to be initialized once. To be initialized in `program`*/
        ASR::Variable_t* v;
        llvm::Value* target_var; // Corresponds to variable `v` in llvm IR.
    };
    std::vector<variable_inital_value> variable_inital_value_vec; /* Saves information for variables that need to be initialized once. To be initialized in `program`*/

    // Pool of allocas for call arguments, keyed by LLVM type.
    // This avoids creating a new alloca for every expression argument at every
    // call site, which caused excessive stack usage (e.g., 217x more than
    // GFortran for LAPACK's ctfsm function). Instead, we reuse allocas of the
    // same type across different calls.
    std::map<llvm::Type*, std::vector<llvm::AllocaInst*>> call_arg_alloca_pool;
    std::map<llvm::Type*, size_t> call_arg_alloca_idx;
    int convert_call_args_depth = 0;

    // Track list-type call_arg_value allocas that need their data freed
    // at scope exit. Maps alloca -> element type_code for free_data_using_type.
    std::map<llvm::Value*, std::string> list_call_arg_to_finalize;
    // Get or create an alloca for a call argument of the given type.
    // Reuses allocas from the pool when possible.
    llvm::AllocaInst* get_call_arg_alloca(llvm::Type* type) {
        size_t& idx = call_arg_alloca_idx[type];
        std::vector<llvm::AllocaInst*>& pool = call_arg_alloca_pool[type];
        if (idx < pool.size()) {
            return pool[idx++];
        }
        // Need to create a new alloca
        llvm::AllocaInst* alloca = llvm_utils->CreateAlloca(type, nullptr, "call_arg_value");
        // Zero-initialize struct allocas (e.g. list, tuple) at entry block
        // so null-guarded free is safe on the very first use in a loop.
        if (type->isStructTy()) {
            llvm::BasicBlock &entry_block = builder->GetInsertBlock()->getParent()->getEntryBlock();
            llvm::IRBuilder<> entry_builder(context);
            entry_builder.SetInsertPoint(&entry_block, std::next(alloca->getIterator()));
            entry_builder.CreateStore(llvm::ConstantAggregateZero::get(type), alloca);
        }

        pool.push_back(alloca);
        idx++;
        return alloca;
    }

    // Reset alloca pool indices at the start of processing call arguments.
    // This allows reuse of allocas across different call sites.
    void reset_call_arg_alloca_pool() {
        for (auto& kv : call_arg_alloca_idx) {
            kv.second = 0;
        }
    }

    // Free data buffers of list-type call_arg_value allocas at scope exit.
    void finalize_list_call_arg_allocas() {
        for (auto& kv : list_call_arg_to_finalize) {
            list_api->free_data_using_type(kv.second, kv.first, module.get());
            llvm::AllocaInst* alloca_inst = llvm::cast<llvm::AllocaInst>(kv.first);
            llvm::Type* alloc_type = alloca_inst->getAllocatedType();
            builder->CreateStore(llvm::ConstantAggregateZero::get(alloc_type), kv.first);
        }
        list_call_arg_to_finalize.clear();
    }

    // Materialize a BindC + value argument so it is ready to be passed to the
    // callee. For struct-valued arguments the caller already holds a pointer
    // (because LLVM's function signature uses a pointer even with bind(C) +
    // value), so we only bitcast it to match the callee's expected pointer
    // type. For all other types we store the value into a fresh alloca and
    // then bitcast the alloca pointer to the callee's parameter type if
    // layouts differ (e.g. %complex_4* vs [2 x float]*).
    void pass_bindc_value_arg(ASR::Variable_t* orig_arg, ASR::ttype_t* arg_type) {
        llvm::Type *orig_llvm_type =
            llvm_utils->get_type_from_ttype_t_util(
                ASRUtils::EXPR(ASR::make_Var_t(
                    al, orig_arg->base.base.loc,
                    &orig_arg->base)),
                orig_arg->m_type, module.get());
        if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(arg_type))) {
            if (orig_llvm_type->getPointerTo() != tmp->getType()) {
                tmp = builder->CreateBitCast(tmp, orig_llvm_type->getPointerTo());
            }
            return;
        }
        llvm::Type *target_type = tmp->getType();
        llvm::AllocaInst *target = get_call_arg_alloca(target_type);
        builder->CreateStore(tmp, target);
        if (orig_llvm_type != target_type) {
            tmp = builder->CreateBitCast(target, orig_llvm_type->getPointerTo());
        } else {
            tmp = target;
        }
    }

    ASRToLLVMVisitor(Allocator &al, llvm::LLVMContext &context, std::string infile,
        CompilerOptions &compiler_options_, diag::Diagnostics &diagnostics, LocationManager &lm) :
    diag{diagnostics},
    context(context),
    builder(std::make_unique<llvm::IRBuilder<>>(context)),
    infile{infile},
    al{al},
    prototype_only(false),
    ptr_loads(2),
    lookup_enum_value_for_nonints(false),
    is_assignment_target(false),
    global_array_count(0),
    global_deep_count(0),
    compiler_options(compiler_options_),
    location_manager{lm},
    current_sret_arg(nullptr),
    current_scope(nullptr),
    llvm_utils(std::make_unique<LLVMUtils>(context, builder.get(),
        current_der_type_name, name2dertype, name2dercontext, struct_type_stack,
        dertype2parent, name2memidx, compiler_options, arr_arg_type_cache,
        fname2arg_type, llvm_symtab)),
    llvm_symtab_finalizer(*this, llvm_utils, builder, al, llvm_symtab_fn),
    list_api(std::make_unique<LLVMList>(context, llvm_utils.get(), builder.get())),
    struct_api(std::make_unique<LLVMStruct>(context, llvm_utils.get(), builder.get(), llvm_symtab_fn,
                [this](ASR::Struct_t* s, llvm::Value* v, ASR::ttype_t* t, bool flag) {
                        allocate_array_members_of_struct(s, v, t, flag);
                }, llvm_symtab_finalizer)),
    tuple_api(std::make_unique<LLVMTuple>(context, llvm_utils.get(), builder.get())),
    dict_api_lp(std::make_unique<LLVMDictOptimizedLinearProbing>(context, llvm_utils.get(), builder.get())),
    dict_api_sc(std::make_unique<LLVMDictSeparateChaining>(context, llvm_utils.get(), builder.get())),
    set_api_lp(std::make_unique<LLVMSetLinearProbing>(context, llvm_utils.get(), builder.get())),
    set_api_sc(std::make_unique<LLVMSetSeparateChaining>(context, llvm_utils.get(), builder.get())),
    arr_descr(LLVMArrUtils::Descriptor::get_descriptor(context,
              builder.get(), llvm_utils.get(),
              LLVMArrUtils::DESCR_TYPE::_SimpleCMODescriptor, compiler_options_))
    {
        LLVM::set_memory_debug(compiler_options.detect_leaks);
        llvm_utils->tuple_api = tuple_api.get();
        llvm_utils->list_api = list_api.get();
        llvm_utils->struct_api = struct_api.get();
        llvm_utils->dict_api = nullptr;
        llvm_utils->set_api = nullptr;
        llvm_utils->arr_api = arr_descr.get();
        llvm_utils->dict_api_lp = dict_api_lp.get();
        llvm_utils->dict_api_sc = dict_api_sc.get();
        llvm_utils->set_api_lp = set_api_lp.get();
        llvm_utils->set_api_sc = set_api_sc.get();
        llvm_utils->dim_descr_type_ = arr_descr->get_dimension_descriptor_type();
        strings_to_be_deallocated.reserve(al, 1);
        heap_fixed_size_arrays.reserve(al, 1);

        if (compiler_options.po.fast && !compiler_options.po.no_fast_math) {
            llvm::FastMathFlags fmf;
            fmf.setFast();
            builder->setFastMathFlags(fmf);
        }
    }

    #define load_non_array_non_character_pointers(expr, type, llvm_value) if( ASR::is_a<ASR::StructInstanceMember_t>(*expr) && \
        !ASRUtils::is_array(type) && \
        LLVM::is_llvm_pointer(*type) && \
        !ASRUtils::is_character(*type) ) { \
        llvm::Type *llvm_type = llvm_utils->get_type_from_ttype_t_util(expr, \
            ASRUtils::extract_type(type), module.get()); \
        llvm_value = llvm_utils->CreateLoad2(llvm_type, llvm_value); \
    } \

    // Inserts a new block `bb` using the current builder
    // and terminates the previous block if it is not already terminated
    void start_new_block(llvm::BasicBlock *bb) {
        llvm::BasicBlock *last_bb = builder->GetInsertBlock();
        llvm::Function *fn = last_bb->getParent();
        llvm::Instruction *block_terminator = last_bb->getTerminator();
        if (block_terminator == nullptr) {
            // The previous block is not terminated --- terminate it by jumping
            // to our new block
            builder->CreateBr(bb);
        }
        if (bb->getParent() == nullptr) {
            llvm_fn_insert_bb(fn, bb);
        } else {
            LCOMPILERS_ASSERT(bb->getParent() == fn);
        }
        builder->SetInsertPoint(bb);
    }

    // For ONE-wrapper lazy init: visits the target ArrayItem's array
    // expression, computes array size, allocates a contiguous data buffer,
    // and stores it in the ONE-wrapper. Returns the actual_wrapper pointer
    // (so the caller can set the vptr).
    llvm::Value* init_upoly_array_element_buffer(
            ASR::ArrayItem_t* target_ai,
            llvm::Value* elem_size,
            llvm::Type* target_llvm_type) {
        ASR::ttype_t* target_arr_type = ASRUtils::expr_type(target_ai->m_v);

        // Visit the array expression to get the descriptor
        llvm::Value* saved_tmp = tmp;
        int64_t saved_ptr_loads = ptr_loads;
        ptr_loads = LLVM::is_llvm_pointer(*target_arr_type);
        if (ASRUtils::is_unlimited_polymorphic_type(target_arr_type) && ptr_loads > 0) {
            ptr_loads--;
        }
        this->visit_expr_wrapper(target_ai->m_v, true);
        ptr_loads = saved_ptr_loads;
        llvm::Value* target_arr = tmp;
        tmp = saved_tmp;

        llvm::Type* arr_desc_type = arr_descr->get_array_type(
            target_ai->m_v,
            ASRUtils::type_get_past_allocatable_pointer(target_arr_type),
            llvm_utils->get_el_type(target_ai->m_v,
                ASRUtils::extract_type(target_arr_type), module.get()),
            false);
        llvm::Value* num_elems = arr_descr->get_array_size(
            arr_desc_type, target_arr, nullptr, 4);
        llvm::Value* num_elems_64 = builder->CreateSExtOrTrunc(
            num_elems, llvm::Type::getInt64Ty(context));

        // Allocate zero-initialized contiguous data buffer
        llvm::Value* total_bytes = builder->CreateMul(num_elems_64, elem_size);
        llvm::Value* data_buf = llvm_utils->allocate_zeroed_bytes(total_bytes);

        // Store data in the ACTUAL ONE-wrapper (descriptor.field0)
        llvm::Value* wrapper_ptr_ptr = arr_descr->get_pointer_to_data(
            target_ai->m_v, target_arr_type, target_arr, module.get());
        llvm::Value* actual_wrapper = llvm_utils->CreateLoad2(
            target_llvm_type->getPointerTo(), wrapper_ptr_ptr);
        builder->CreateStore(data_buf,
            llvm_utils->create_gep2(target_llvm_type, actual_wrapper, 1));
        return actual_wrapper;
    }

    void predeclare_goto_targets(llvm::Function *fn, ASR::stmt_t **body, size_t n) {
        for (size_t i = 0; i < n; i++) {
            if (body[i]->type != ASR::stmtType::GoToTarget) continue;
            ASR::GoToTarget_t *gt = ASR::down_cast<ASR::GoToTarget_t>(body[i]);
            if (llvm_goto_targets.find(gt->m_id) != llvm_goto_targets.end()) continue;
            llvm::BasicBlock *bb = llvm::BasicBlock::Create(context, "goto_target");
            llvm_fn_insert_bb(fn, bb);
            llvm_goto_targets[gt->m_id] = bb;
        }
    }

    template <typename Cond, typename Body>
    void create_loop(char *name, Cond condition, Body loop_body) {

        std::string loop_name;
        if (name) {
            loop_name = std::string(name);
        } else {
            loop_name = "loop";
        }

        std::string loophead_name = loop_name + ".head";
        std::string loopbody_name = loop_name + ".body";
        std::string loopend_name = loop_name + ".end";

        llvm::BasicBlock *loophead = llvm::BasicBlock::Create(context, loophead_name);
        llvm::BasicBlock *loopbody = llvm::BasicBlock::Create(context, loopbody_name);
        llvm::BasicBlock *loopend = llvm::BasicBlock::Create(context, loopend_name);

        loop_head.push_back(loophead);
        loop_head_names.push_back(loophead_name);
        loop_or_block_end.push_back(loopend);
        loop_or_block_end_names.push_back(loopend_name);

        // head
        start_new_block(loophead); {
            llvm::Value* cond = llvm_utils->to_i1(condition());
            builder->CreateCondBr(cond, loopbody, loopend);
        }

        // body
        start_new_block(loopbody); {
            loop_body();
            builder->CreateBr(loophead);
        }

        // end
        loop_head.pop_back();
        loop_head_names.pop_back();
        loop_or_block_end.pop_back();
        loop_or_block_end_names.pop_back();
        start_new_block(loopend);
    }

    void get_type_debug_info(ASR::ttype_t* t, std::string &type_name,
            uint32_t &type_size, uint32_t &type_encoding) {
        ASR::ttype_t* t_ = ASRUtils::extract_type(t);

        type_size = ASRUtils::extract_kind_from_ttype_t(t_)*8;
        switch( t_->type ) {
            case ASR::ttypeType::Integer: {
                type_name = "integer";
                type_encoding = llvm::dwarf::DW_ATE_signed;
                break;
            }
            case ASR::ttypeType::UnsignedInteger: {
                type_name = "integer";
                type_encoding = llvm::dwarf::DW_ATE_unsigned;
                break;
            }
            case ASR::ttypeType::Logical: {
                type_name = "boolean";
                type_encoding = llvm::dwarf::DW_ATE_boolean;
                break;
            }
            case ASR::ttypeType::Real: {
                if( type_size == 32 ) {
                    type_name = "float";
                } else if( type_size == 64 ) {
                    type_name = "double";
                }
                type_encoding = llvm::dwarf::DW_ATE_float;
                break;
            }
            case ASR::ttypeType::Complex: {
                type_name = "complex";
                type_size = 2 * ASRUtils::extract_kind_from_ttype_t(t_) * 8;
                type_encoding = llvm::dwarf::DW_ATE_complex_float;
                break;
            }
            case ASR::ttypeType::String: {
                type_name = "character";
                type_encoding = llvm::dwarf::DW_ATE_unsigned_char;
                break;
            }
            case ASR::ttypeType::CPtr: {
                type_name = "c_ptr";
                type_size = 64;
                type_encoding = llvm::dwarf::DW_ATE_address;
                break;
            }
            default : {
                type_name = "non-specified-debug-type-SIZE-set-to-ZERO";
                type_size = 0;
                type_encoding = llvm::dwarf::DW_ATE_unsigned;
                break;
            }
        }
    }

    void debug_get_line_column(const uint32_t &loc_first,
            uint32_t &line, uint32_t &column) {
        LocationManager lm;
        LocationManager::FileLocations fl;
        fl.in_filename = infile;
        lm.files.push_back(fl);
        std::string input;
        if (!read_file(infile, input)) {
            throw CodeGenError("File '" + infile + "' cannot be opened.");
        }
        lm.init_simple(input);
        lm.file_ends.push_back(input.size());
        lm.pos_to_linecol(lm.output_to_input_pos(loc_first, false),
            line, column, fl.in_filename);
    }

    template <typename T>
    void debug_emit_loc(const T &x) {
        Location loc = x.base.base.loc;
        uint32_t line, column;
        debug_get_line_column(loc.first, line, column);
        builder->SetCurrentDebugLocation(
            llvm::DILocation::get(debug_current_scope->getContext(),
                line, column, debug_current_scope));
    }

    template <typename T>
    void debug_emit_function(const T &x, llvm::DISubprogram *&SP) {
        debug_Unit = DBuilder->createFile(
            debug_CU->getFilename(),
            debug_CU->getDirectory());
        llvm::DIScope *FContext = debug_Unit;
        uint32_t line, column;
        debug_get_line_column(x.base.base.loc.first, line, column);
        std::string fn_debug_name = x.m_name;
        llvm::DIBasicType *return_type_info = nullptr;
        if constexpr (std::is_same_v<T, ASR::Function_t>){
            if(x.m_return_var != nullptr) {
                std::string type_name; uint32_t type_size, type_encoding;
                get_type_debug_info(ASRUtils::expr_type(x.m_return_var),
                    type_name, type_size, type_encoding);
                return_type_info = DBuilder->createBasicType(type_name,
                    type_size, type_encoding);
            }
        } else if constexpr (std::is_same_v<T, ASR::Program_t>) {
            return_type_info = DBuilder->createBasicType("integer", 32,
                llvm::dwarf::DW_ATE_signed);
        }
        llvm::DISubroutineType *return_type = DBuilder->createSubroutineType(
            DBuilder->getOrCreateTypeArray(return_type_info));
        SP = DBuilder->createFunction(
            FContext, fn_debug_name, llvm::StringRef(), debug_Unit,
            line, return_type, 0, // TODO: ScopeLine
            llvm::DINode::FlagPrototyped,
#if LLVM_VERSION_MAJOR >= 8
            llvm::DISubprogram::SPFlagDefinition);
#else
            // LLVM 7: SPFlag enum doesn't exist, use DIDescriptor::FlagPrototyped
            llvm::DINode::FlagZero);
#endif
        debug_current_scope = SP;
    }

    inline bool verify_dimensions_t(ASR::dimension_t* m_dims, int n_dims) {
        if( n_dims <= 0 ) {
            return false;
        }
        bool is_ok = true;
        for( int r = 0; r < n_dims; r++ ) {
            if( m_dims[r].m_length == nullptr ) {
                is_ok = false;
                break;
            }
        }
        return is_ok;
    }

    void fill_array_details(llvm::Type* arr_ty, llvm::Value* arr, llvm::Type* llvm_data_type,
                            ASR::dimension_t* m_dims, int n_dims, bool is_data_only=false,
                            bool reserve_data_memory=true) {
        std::vector<std::pair<llvm::Value*, llvm::Value*>> llvm_dims;
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 2;
        for( int r = 0; r < n_dims; r++ ) {
            ASR::dimension_t m_dim = m_dims[r];
            LCOMPILERS_ASSERT(m_dim.m_start != nullptr);
            // Use visit_expr_wrapper with load_ref=true so that ArrayItem and
            // similar reference expressions are loaded with the correct
            // element width (e.g. i32) before being widened to the descriptor's
            // index type. Plain visit_expr can leave them as raw pointers,
            // which downstream code would then load with the wrong type.
            visit_expr_wrapper(m_dim.m_start, true);
            llvm::Value* start = tmp;
            LCOMPILERS_ASSERT(m_dim.m_length != nullptr);
            load_array_size_deep_copy(m_dim.m_length);
            llvm::Value* end = tmp;
            llvm_dims.push_back(std::make_pair(start, end));
        }
        ptr_loads = ptr_loads_copy;
        if( is_data_only ) {
            if( !ASRUtils::is_fixed_size_array(m_dims, n_dims) ) {
                // Use the descriptor's index type for consistent bit width (i32 or i64)
                llvm::Type* index_type = arr_descr->get_index_type();
                unsigned index_bit_width = index_type->getIntegerBitWidth();
                llvm::Value* const_1 = llvm::ConstantInt::get(context, llvm::APInt(index_bit_width, 1));
                llvm::Value* prod = const_1;
                for( int r = 0; r < n_dims; r++ ) {
                    llvm::Value* dim_size = llvm_dims[r].second;
                    // Ensure dim_size matches index_type
                    dim_size = builder->CreateSExtOrTrunc(dim_size, index_type);
                    prod = builder->CreateMul(prod, dim_size);
                }
                llvm::Value* arr_first = nullptr;
                if( !compiler_options.stack_arrays ) {
                    llvm::DataLayout data_layout(module->getDataLayout());
                    uint64_t size = data_layout.getTypeAllocSize(llvm_data_type);
                    prod = builder->CreateMul(prod,
                        llvm::ConstantInt::get(context, llvm::APInt(index_bit_width, size)));
                    llvm::Value* arr_first_i8 = LLVMArrUtils::lfortran_malloc(
                        context, *module, *builder, prod);
                    arr_first = builder->CreateBitCast(
                        arr_first_i8, llvm_data_type->getPointerTo());
                } else {
                    arr_first = llvm_utils->CreateAlloca(*builder, llvm_data_type, prod);
                    builder->CreateStore(arr_first, arr);
                }
            }
        } else {
            arr_descr->fill_array_details(arr_ty, arr, llvm_data_type, n_dims,
                llvm_dims, module.get(), reserve_data_memory);
        }
    }

    /*
        This function fills the descriptor
        (pointer to the first element, offset and descriptor of each dimension)
        of the array which are allocated memory in heap.
    */
    inline void fill_malloc_array_details(llvm::Value* arr, llvm::Type* arr_type, llvm::Type* llvm_data_type,
                                          ASR::ttype_t* asr_type, ASR::symbol_t* const variable_declaration,
                                          ASR::dimension_t* m_dims, int n_dims,
                                          ASR::expr_t* string_len_to_allocate,
                                          ASR::symbol_t* allocated_subclass=nullptr,
                                          bool realloc=false,
                                          ASR::ttype_t* alloc_type=nullptr) {
        std::vector<std::pair<llvm::Value*, llvm::Value*>> llvm_dims;
        int ptr_loads_copy = ptr_loads;
        ptr_loads = 2;
        for( int r = 0; r < n_dims; r++ ) {
            ASR::dimension_t m_dim = m_dims[r];
            visit_expr_wrapper(m_dim.m_start, true);
            llvm::Value* start = tmp;
            visit_expr_wrapper(m_dim.m_length, true);
            llvm::Value* end = tmp;
            llvm_dims.push_back(std::make_pair(start, end));
        }
        llvm::Value* string_len{};
        if(string_len_to_allocate){
            visit_expr_wrapper(string_len_to_allocate, true);
            string_len = tmp;
            tmp = nullptr;
        }
        ptr_loads = ptr_loads_copy;
        arr_descr->fill_malloc_array_details(arr, arr_type, llvm_data_type,
            asr_type, n_dims, llvm_dims, string_len, variable_declaration, module.get(), 
            allocated_subclass, realloc, alloc_type);
    }


    std::pair<llvm::Value*, llvm::Value*> get_string_data_and_length(ASR::expr_t* str_expr){
        LCOMPILERS_ASSERT(ASR::is_a<ASR::String_t>(*
            ASRUtils::extract_type(ASRUtils::expr_type(str_expr))))

        // Evaluate Expression
        ASR::String_t* str = ASR::down_cast<ASR::String_t>(
            ASRUtils::extract_type(expr_type(str_expr)));
        this->visit_expr_load_wrapper(str_expr, 0);

        // For bind(C)/SEQUENCE struct character members, the LLVM type is
        // [len x i8]* even though the ASR physical type says DescriptorString.
        ASR::string_physical_typeType phys_type = str->m_physical_type;
        int64_t bindc_inline_len = 0;
        if (phys_type == ASR::DescriptorString && tmp->getType()->isPointerTy()) {
            // Check if this is a non-pointer character member of a bind(C)/SEQUENCE struct
            if (ASR::is_a<ASR::StructInstanceMember_t>(*str_expr)) {
                ASR::StructInstanceMember_t* sim =
                    ASR::down_cast<ASR::StructInstanceMember_t>(str_expr);
                ASR::symbol_t* member_sym = ASRUtils::symbol_get_past_external(sim->m_m);
                ASR::Variable_t* member_var = ASR::down_cast<ASR::Variable_t>(member_sym);
                ASR::symbol_t* struct_sym = ASR::down_cast<ASR::symbol_t>(
                    member_var->m_parent_symtab->asr_owner);
                struct_sym = ASRUtils::symbol_get_past_external(struct_sym);
                if (ASR::is_a<ASR::Struct_t>(*struct_sym) &&
                    (ASR::down_cast<ASR::Struct_t>(struct_sym)->m_abi == ASR::abiType::BindC ||
                     ASR::down_cast<ASR::Struct_t>(struct_sym)->m_is_sequence)) {
                    ASR::ttype_t* mem_type = sim->m_type;
                    if (!ASR::is_a<ASR::Pointer_t>(*mem_type) &&
                        !ASR::is_a<ASR::Allocatable_t>(*mem_type)) {
                        phys_type = ASR::CChar;
                        bindc_inline_len = 1;
                        if (str->m_len) {
                            ASRUtils::extract_value(str->m_len, bindc_inline_len);
                        }
                        if (bindc_inline_len < 1) bindc_inline_len = 1;
                    }
                }
            }
        }

        std::pair<llvm::Value*, llvm::Value*> data_and_length;
        if (bindc_inline_len > 0) {
            // tmp is a pointer to inline [len x i8]; bitcast to i8* (first byte).
            data_and_length.first = builder->CreateBitCast(
                tmp, character_type);
            data_and_length.second = llvm::ConstantInt::get(
                context, llvm::APInt(64, bindc_inline_len));
            return data_and_length;
        }
        switch (phys_type)
        {
            case ASR::DescriptorString:{
                if (!tmp->getType()->isPointerTy()) {
                    llvm::Value* tmp_ptr = llvm_utils->CreateAlloca(
                        *builder, tmp->getType(), nullptr, "str_value");
                    builder->CreateStore(tmp, tmp_ptr);
                    tmp = tmp_ptr;
                }
                //Set data
                data_and_length.first = builder->CreateLoad(
                    character_type,
                    llvm_utils->create_gep2(string_descriptor, tmp, 0));

                // Set length (Length could be explicit OR implicit)
                if(str->m_len && ASR::is_a<ASR::IntegerConstant_t>(*str->m_len)){ // Explicit-Constant Length
                    ASR::IntegerConstant_t* len = ASR::down_cast<ASR::IntegerConstant_t>(str->m_len);
                    llvm::Value* len_value = llvm::ConstantInt::get(context, llvm::APInt(64, len->m_n));
                    data_and_length.second = len_value;
                } else { // Implicit Length
                    data_and_length.second = builder->CreateLoad(
                        llvm::Type::getInt64Ty(context),
                        llvm_utils->create_gep2(string_descriptor, tmp, 1));
                }
                break;
            }
            case ASR::CChar:{
                if (!tmp->getType()->isPointerTy()) {
                    llvm::Value* tmp_ptr = llvm_utils->CreateAlloca(
                        *builder, tmp->getType(), nullptr, "char_value");
                    builder->CreateStore(tmp, tmp_ptr);
                    tmp = tmp_ptr;
                }
                // tmp is i8* (pointer to the character byte)
                data_and_length.first = tmp;
                data_and_length.second = llvm::ConstantInt::get(context, llvm::APInt(64, 1));
                break;
            }
            default:
                throw LCompilersException("Unsupported string physical type.");
        }
        return data_and_length;
    }





    /*
        * Returns length of the string in an array of strings.
    */
    llvm::Value* get_string_length_in_array(ASR::expr_t* expr, llvm::Value* str){
        LCOMPILERS_ASSERT(llvm_utils->is_proper_array_of_strings_llvm_var(expr_type(expr), str))
        ASR::String_t* str_type = ASRUtils::get_string_type(expr_type(expr));
        switch(ASRUtils::extract_physical_type(expr_type(expr))){
            case ASR::DescriptorArray : {
                switch(str_type->m_physical_type){
                    case ASR::DescriptorString : {
                        llvm::Value* temp{};
                        temp = arr_descr->get_pointer_to_data(expr, ASRUtils::expr_type(expr), str, module.get());
                        temp = builder->CreateLoad(
                            llvm_utils->get_el_type(expr, ASRUtils::extract_type(expr_type(expr)), module.get())->getPointerTo(),
                            temp);
                        return builder->CreateLoad(llvm::Type::getInt64Ty(context),
                                llvm_utils->create_gep2(string_descriptor, temp, 1));
                    }
                    default:
                        throw LCompilersException("Unhandled string physicalType");
                }
            }
            case ASR::PointerArray:{
                switch(str_type->m_physical_type){
                    case ASR::DescriptorString : {
                        return builder->CreateLoad(llvm::Type::getInt64Ty(context),
                                llvm_utils->create_gep2(string_descriptor, str, 1));
                    }
                    default:
                        throw LCompilersException("Unhandled string physicalType");

                }
            default:
                throw LCompilersException("Unhandled Array Physical type");
            }

        }
    }


    /*
     * Returns length of the string (int64)
     * It handles string and array of strings.
    */
    llvm::Value* get_string_length(ASR::expr_t* str_expr){
        ASR::ttype_t* exp_type = ASRUtils::expr_type(str_expr);
        LCOMPILERS_ASSERT(ASR::is_a<ASR::String_t>(*
            ASRUtils::extract_type(exp_type)))

        ASR::String_t* str = ASR::down_cast<ASR::String_t>(
            ASRUtils::extract_type(exp_type));


        switch (str->m_physical_type)
        {
            case ASR::DescriptorString:{
                // Set length Length could be explicit
                if(str->m_len && ASR::is_a<ASR::IntegerConstant_t>(*str->m_len)){ // Explicit-Constant Length
                    ASR::IntegerConstant_t* len = ASR::down_cast<ASR::IntegerConstant_t>(str->m_len);
                    llvm::Value* len_value = llvm::ConstantInt::get(context, llvm::APInt(64, len->m_n));
                    return len_value;
                } else { // Implicit Length
                    if(ASRUtils::is_array(exp_type)){
                        visit_expr_load_wrapper(str_expr, ASRUtils::is_allocatable_or_pointer(exp_type) ? 1 : 0);
                        return get_string_length_in_array(str_expr, tmp);
                    } else {
                        // Handling Logic for ArrayItem accessing empty array
                        // In this case, return length from array descriptors
                        if (ASR::is_a<ASR::ArrayItem_t>(*str_expr)) {
                            ASR::expr_t* arr_expr = ASR::down_cast<ASR::ArrayItem_t>(str_expr)->m_v;
                            ASR::ttype_t* arr_type_asr = ASRUtils::expr_type(arr_expr);
                            if (ASRUtils::is_array(arr_type_asr)) {
                                ASR::Array_t* arr_t = ASR::down_cast<ASR::Array_t>(
                                    ASRUtils::type_get_past_allocatable_pointer(arr_type_asr));
                                if (arr_t->m_physical_type == ASR::array_physical_typeType::DescriptorArray) {
                                    visit_expr_load_wrapper(arr_expr, 0);
                                    llvm::Value* arr_desc = tmp;
                                    llvm::Type* arr_type_llvm = llvm_utils->get_type_from_ttype_t_util(arr_expr,
                                        ASRUtils::type_get_past_allocatable_pointer(arr_type_asr), module.get());
                                    if (ASRUtils::is_allocatable_or_pointer(arr_type_asr)) {
                                        arr_desc = llvm_utils->CreateLoad2(arr_type_llvm->getPointerTo(), arr_desc);
                                    }
                                    llvm::Value* is_empty = builder->CreateICmpEQ(
                                        arr_descr->get_array_size(arr_type_llvm, arr_desc, nullptr, 4),
                                        llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, 0)));

                                    llvm::Value* len_result = llvm_utils->CreateAlloca(llvm::Type::getInt64Ty(context), nullptr, "");
                                    llvm_utils->create_if_else(is_empty, [&]() {
                                        llvm::Value* str_desc = builder->CreateLoad(string_descriptor->getPointerTo(),
                                            llvm_utils->create_gep2(arr_type_llvm, arr_desc, 0));
                                        llvm::Value* len_val = builder->CreateLoad(llvm::Type::getInt64Ty(context),
                                            llvm_utils->create_gep2(string_descriptor, str_desc, 1));
                                        builder->CreateStore(len_val, len_result);
                                    }, [&]() {
                                        visit_expr_load_wrapper(str_expr, 0);
                                        llvm::Value* len_val = builder->CreateLoad(llvm::Type::getInt64Ty(context),
                                            llvm_utils->create_gep2(string_descriptor, tmp, 1));
                                        builder->CreateStore(len_val, len_result);
                                    });
                                    return llvm_utils->CreateLoad2(llvm::Type::getInt64Ty(context), len_result);
                                }
                            }
                        }
                        // Accessing Non-array string or non-empty ArrayItem
                        visit_expr_load_wrapper(str_expr, 0);
                        LCOMPILERS_ASSERT(llvm_utils->is_proper_string_llvm_variable(str, tmp))
                        return builder->CreateLoad(
                            llvm::Type::getInt64Ty(context),
                            llvm_utils->create_gep2(string_descriptor, tmp, 1));
                    }
                }
            }
            case ASR::CChar:
                return llvm::ConstantInt::get(context, llvm::APInt(64, 1));
            default:
                throw LCompilersException("Unsupported string physical type.");
        }

    }

    llvm::Value* get_string_data(ASR::expr_t* str_expr){
        LCOMPILERS_ASSERT(ASR::is_a<ASR::String_t>(*
            ASRUtils::extract_type(ASRUtils::expr_type(str_expr))))

        // Evaluate Expression
        ASR::String_t* str = ASR::down_cast<ASR::String_t>(
            ASRUtils::extract_type(expr_type(str_expr)));
        int ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        visit_expr(*str_expr);
        ptr_loads = ptr_loads_copy;
        switch (str->m_physical_type)
        {
            case ASR::DescriptorString:{
                return builder->CreateLoad(
                    character_type,
                    llvm_utils->create_gep2(string_descriptor, tmp, 0));
            }
            case ASR::CChar:{
                llvm::Value* char_ptr = builder->CreateAlloca(llvm::Type::getInt8Ty(context));
                return builder->CreateStore(tmp, char_ptr);
            }
            default:
                throw LCompilersException("Unsupported string physical type.");
        }
        return nullptr;
    }

    void setup_string_length(llvm::Value* str,
        ASR::String_t* str_type, ASR::expr_t* len){
        LCOMPILERS_ASSERT(str)
        switch(str_type->m_len_kind){
            case ASR::ExpressionLength:{
                LCOMPILERS_ASSERT(len);
                visit_expr_load_wrapper(len, 2 - !LLVM::is_llvm_pointer(*ASRUtils::expr_type(len)), true);
                if (ASRUtils::is_allocatable_or_pointer(ASRUtils::expr_type(len)) &&
                    (ASR::is_a<ASR::StructInstanceMember_t>(*len) ||
                     ASR::is_a<ASR::ArrayItem_t>(*len) ||
                     ASR::is_a<ASR::ArraySection_t>(*len))) {
                    llvm::Type* t = llvm_utils->get_type_from_ttype_t_util(
                        len, ASRUtils::type_get_past_allocatable_pointer(ASRUtils::expr_type(len)), module.get());
                    tmp = llvm_utils->CreateLoad2(t, tmp);
                }
                tmp = llvm_utils->convert_kind(tmp, llvm::Type::getInt64Ty(context));
                llvm::Value* len_ptr = llvm_utils->get_string_length(str_type, str, true);
                builder->CreateStore(tmp, len_ptr);
                tmp = nullptr;
                break;
            }
            case ASR::AssumedLength:
                // Assumed-length strings inherit their length from the caller.
                break;
            case ASR::DeferredLength:
                // Do nothing, deferred length strings doesn't have information to set it up with.
                break;
            default:
                throw LCompilersException("Unhandled string length kind");
                break;
        }
    }
    /*
        Setup a string variable declaration.
        Sets up string's information (data and length)
        based on ASR::String node details.
        - Everything is set to its inital value at first (nullptr, 0).
        - Length is set, if not deferred.
        - Memory gets allocated, if not allocatable.
    */
    void setup_string(llvm::Value* str, ASR::ttype_t* type){
        if(ASRUtils::is_descriptorString(type)){
            builder->CreateStore(llvm::Constant::getNullValue(string_descriptor), str);
            ASR::String_t *t = down_cast<ASR::String_t>(ASRUtils::extract_type(type));
            setup_string_length(str, t, t->m_len);
            // Handle Memory
            if(!ASRUtils::is_allocatable_or_pointer(type) &&
               t->m_len_kind != ASR::string_length_kindType::AssumedLength){
                llvm_utils->set_string_memory_on_heap(t->m_physical_type, str, llvm_utils->get_string_length(t, str), t->m_kind);
            }
        } else {
            throw LCompilersException("Unhandled string physicalType");
        }
    }

    /*
        *Creates the string (based on stringPhysicalType) used for arrays of strings.
        *This sets the initial state of the array's string.
        - Creates a single string (based on the physical type).
        - If not allocatable, set data member with (array_size * string_length)
        - If not deferred length, Set length member.
        It doesn't handle the array itself, only the string data it contains.
    */
    llvm::Value* create_and_setup_string_for_array(ASR::ttype_t* var_type, llvm::Value* array_size, bool stack_allocation=false, std::string name = "") {
        LCOMPILERS_ASSERT(ASRUtils::is_array(var_type) && ASRUtils::is_character(*var_type))
        ASR::String_t* str = ASR::down_cast<ASR::String_t>(ASRUtils::extract_type(var_type));

        if(str->m_physical_type == ASR::DescriptorString){
            llvm::Value* str_desc = llvm_utils->create_empty_string_descriptor(name); // StringDesc with initial state.
            // Setup Length
            setup_string_length(str_desc, str, str->m_len);
            // Setup memory
            if(!ASRUtils::is_allocatable_or_pointer(var_type) /*Length is Fixed*/){
                LCOMPILERS_ASSERT(str->m_len_kind == ASR::ExpressionLength)
                LCOMPILERS_ASSERT( array_size )
                LCOMPILERS_ASSERT( str->m_len )

                tmp = nullptr;
                visit_expr_load_wrapper(str->m_len, 1);
                llvm::Value* str_len = tmp;

                // Arrays are allocated on heap by default.
                stack_allocation ?
                    llvm_utils->set_array_of_strings_memory_on_stack(str, str_desc, str_len, array_size)
                    :llvm_utils->set_array_of_strings_memory_on_heap(str, str_desc, str_len, array_size);
            }
            return str_desc;
        } else {
            throw LCompilersException("Unhandled string physicalType");
        }
        return nullptr;
    }
    /*
    * Dispatches the required function from runtime library to
    * perform the specified binary operation.
    *
    * @param left_arg llvm::Value* The left argument of the binary operator.
    * @param right_arg llvm::Value* The right argument of the binary operator.
    * @param runtime_func_name std::string The name of the function to be dispatched
    *                                      from runtime library.
    * @returns llvm::Value* The result of the operation.
    *
    * Note
    * ====
    *
    * Internally the call to this function gets transformed into a runtime call:
    * void _lfortran_complex_add(complex* a, complex* b, complex *result)
    *
    * As of now the following values for func_name are supported,
    *
    * _lfortran_complex_add
    * _lfortran_complex_sub
    * _lfortran_complex_div
    * _lfortran_complex_mul
    */
    // Convert a complex value from vector type (e.g. <2 x float>) to struct type
    // (e.g. {float, float}) if needed. BindC functions return complex as vectors
    // on non-macOS ARM platforms, but we use struct types internally.
    llvm::Value* convert_complex_vector_to_struct(llvm::Value* val, llvm::Type* complex_type) {
        llvm::Type* val_type = val->getType();
        if (!llvm::isa<FIXED_VECTOR_TYPE>(val_type)) {
            return val;
        }
        llvm::Value* result = llvm::UndefValue::get(complex_type);
        llvm::Value* idx0 = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0);
        llvm::Value* idx1 = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 1);
        llvm::Value* re = builder->CreateExtractElement(val, idx0);
        llvm::Value* im = builder->CreateExtractElement(val, idx1);
        result = builder->CreateInsertValue(result, re, {0});
        result = builder->CreateInsertValue(result, im, {1});
        return result;
    }

    llvm::Value* lfortran_complex_bin_op(llvm::Value* left_arg, llvm::Value* right_arg,
                                         std::string runtime_func_name,
                                         llvm::Type* complex_type=nullptr)
    {
        if( complex_type == nullptr ) {
            complex_type = complex_type_4;
        }

        // Convert vector types to struct types if needed (BindC ABI compatibility)
        left_arg = convert_complex_vector_to_struct(left_arg, complex_type);
        right_arg = convert_complex_vector_to_struct(right_arg, complex_type);

        // Extract real and imaginary parts: complex is {float, float} or {double, double}
        llvm::Value* left_real = builder->CreateExtractValue(left_arg, {0});
        llvm::Value* left_imag = builder->CreateExtractValue(left_arg, {1});
        llvm::Value* right_real = builder->CreateExtractValue(right_arg, {0});
        llvm::Value* right_imag = builder->CreateExtractValue(right_arg, {1});

        llvm::Value* result_real = nullptr;
        llvm::Value* result_imag = nullptr;

        // Inline arithmetic for add, sub, mul, div; fallback to runtime for pow
        if (runtime_func_name == "_lfortran_complex_add_32" ||
            runtime_func_name == "_lfortran_complex_add_64") {
            // (a+bi) + (c+di) = (a+c) + (b+d)i
            result_real = builder->CreateFAdd(left_real, right_real);
            result_imag = builder->CreateFAdd(left_imag, right_imag);
        } else if (runtime_func_name == "_lfortran_complex_sub_32" ||
                   runtime_func_name == "_lfortran_complex_sub_64") {
            // (a+bi) - (c+di) = (a-c) + (b-d)i
            result_real = builder->CreateFSub(left_real, right_real);
            result_imag = builder->CreateFSub(left_imag, right_imag);
        } else if (runtime_func_name == "_lfortran_complex_mul_32" ||
                   runtime_func_name == "_lfortran_complex_mul_64") {
            // (a+bi) * (c+di) = (ac-bd) + (ad+bc)i
            llvm::Value* ac = builder->CreateFMul(left_real, right_real);
            llvm::Value* bd = builder->CreateFMul(left_imag, right_imag);
            llvm::Value* ad = builder->CreateFMul(left_real, right_imag);
            llvm::Value* bc = builder->CreateFMul(left_imag, right_real);
            result_real = builder->CreateFSub(ac, bd);
            result_imag = builder->CreateFAdd(ad, bc);
        } else if (runtime_func_name == "_lfortran_complex_div_32" ||
                   runtime_func_name == "_lfortran_complex_div_64") {
            auto create_fabs = [&](llvm::Value* value) -> llvm::Value* {
#if LLVM_VERSION_MAJOR >= 12
                return builder->CreateUnaryIntrinsic(llvm::Intrinsic::fabs, value);
#elif LLVM_VERSION_MAJOR >= 8
                return builder->CreateIntrinsic(llvm::Intrinsic::fabs,
                    {value->getType()}, {value});
#else
                llvm::Function *fn = llvm::Intrinsic::getDeclaration(module.get(),
                    llvm::Intrinsic::fabs, {value->getType()});
                return builder->CreateCall(fn, {value});
#endif
            };

            llvm::Value* abs_right_real = create_fabs(right_real);
            llvm::Value* abs_right_imag = create_fabs(right_imag);
            llvm::Value* cond = builder->CreateFCmpOGE(
                abs_right_real, abs_right_imag);

            llvm::Function* parent_fn = builder->GetInsertBlock()->getParent();
            llvm::BasicBlock* r_ge_s_bb = llvm::BasicBlock::Create(
                context, "complex_div_r_ge_s", parent_fn);
            llvm::BasicBlock* r_lt_s_bb = llvm::BasicBlock::Create(
                context, "complex_div_r_lt_s", parent_fn);
            llvm::BasicBlock* div_cont_bb = llvm::BasicBlock::Create(
                context, "complex_div_cont", parent_fn);

            builder->CreateCondBr(cond, r_ge_s_bb, r_lt_s_bb);

            builder->SetInsertPoint(r_ge_s_bb);
            llvm::Value* ratio_r_ge = builder->CreateFDiv(
                right_imag, right_real);
            llvm::Value* denom_r_ge = builder->CreateFAdd(right_real,
                builder->CreateFMul(right_imag, ratio_r_ge));
            llvm::Value* num_real_r_ge = builder->CreateFAdd(left_real,
                builder->CreateFMul(left_imag, ratio_r_ge));
            llvm::Value* num_imag_r_ge = builder->CreateFSub(left_imag,
                builder->CreateFMul(left_real, ratio_r_ge));
            llvm::Value* res_real_r_ge = builder->CreateFDiv(
                num_real_r_ge, denom_r_ge);
            llvm::Value* res_imag_r_ge = builder->CreateFDiv(
                num_imag_r_ge, denom_r_ge);
            builder->CreateBr(div_cont_bb);

            builder->SetInsertPoint(r_lt_s_bb);
            llvm::Value* ratio_r_lt = builder->CreateFDiv(
                right_real, right_imag);
            llvm::Value* denom_r_lt = builder->CreateFAdd(right_imag,
                builder->CreateFMul(right_real, ratio_r_lt));
            llvm::Value* num_real_r_lt = builder->CreateFAdd(
                builder->CreateFMul(left_real, ratio_r_lt), left_imag);
            llvm::Value* num_imag_r_lt = builder->CreateFSub(
                builder->CreateFMul(left_imag, ratio_r_lt), left_real);
            llvm::Value* res_real_r_lt = builder->CreateFDiv(
                num_real_r_lt, denom_r_lt);
            llvm::Value* res_imag_r_lt = builder->CreateFDiv(
                num_imag_r_lt, denom_r_lt);
            builder->CreateBr(div_cont_bb);

            builder->SetInsertPoint(div_cont_bb);
            llvm::PHINode* result_real_phi = builder->CreatePHI(
                left_real->getType(), 2);
            result_real_phi->addIncoming(res_real_r_ge, r_ge_s_bb);
            result_real_phi->addIncoming(res_real_r_lt, r_lt_s_bb);
            llvm::PHINode* result_imag_phi = builder->CreatePHI(
                left_real->getType(), 2);
            result_imag_phi->addIncoming(res_imag_r_ge, r_ge_s_bb);
            result_imag_phi->addIncoming(res_imag_r_lt, r_lt_s_bb);
            result_real = result_real_phi;
            result_imag = result_imag_phi;
        } else {
            // Fallback to runtime call for pow and any other operations
            llvm::Function *fn = module->getFunction(runtime_func_name);
            if (!fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context), {
                            complex_type->getPointerTo(),
                            complex_type->getPointerTo(),
                            complex_type->getPointerTo()
                        }, false);
                fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, runtime_func_name, module.get());
            }
            llvm::AllocaInst *pleft_arg = llvm_utils->CreateAlloca(complex_type);
            builder->CreateStore(left_arg, pleft_arg);
            llvm::AllocaInst *pright_arg = llvm_utils->CreateAlloca(complex_type);
            builder->CreateStore(right_arg, pright_arg);
            llvm::AllocaInst *presult = llvm_utils->CreateAlloca(complex_type);
            std::vector<llvm::Value*> args = {pleft_arg, pright_arg, presult};
            builder->CreateCall(fn, args);
            return llvm_utils->CreateLoad2(complex_type, presult);
        }

        // Construct result complex value
        llvm::Value* result = llvm::UndefValue::get(complex_type);
        result = builder->CreateInsertValue(result, result_real, {0});
        result = builder->CreateInsertValue(result, result_imag, {1});
        return result;
    }


    llvm::Value* lfortran_strConcat(
        llvm::Value* left_arg, llvm::Value* left_len,
        llvm::Value* right_arg, llvm::Value* right_len)
    {
        std::string runtime_func_name = "_lfortran_strcat_alloc";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    character_type, {
                        character_type,
                        character_type, llvm::Type::getInt64Ty(context),
                        character_type, llvm::Type::getInt64Ty(context)
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        llvm::Value* allocator = llvm_utils->get_allocator(module.get());
        std::vector<llvm::Value*> args = {allocator, left_arg, left_len, right_arg, right_len};
        return builder->CreateCall(fn, args);
    }

    llvm::Value* lfortran_str_cmp(
        llvm::Value* left_arg, llvm::Value* left_arg_len,
        llvm::Value* right_arg, llvm::Value* right_arg_len)
    {
        std::string runtime_func_name = "str_compare";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if(!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getInt32Ty(context), {
                        character_type, llvm::Type::getInt64Ty(context),
                        character_type, llvm::Type::getInt64Ty(context)
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        std::vector<llvm::Value*> args = {left_arg, left_arg_len, right_arg, right_arg_len};
        return builder->CreateCall(fn, args);
    }

    llvm::Value* lfortran_strrepeat(llvm::Value* left_arg, llvm::Value* right_arg)
    {
        std::string runtime_func_name = "_lfortran_strrepeat_alloc";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        character_type,
                        character_type->getPointerTo(),
                        llvm::Type::getInt32Ty(context),
                        character_type->getPointerTo()
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        llvm::Value* allocator = llvm_utils->get_allocator(module.get());
        llvm::AllocaInst *pleft_arg = llvm_utils->CreateAlloca(*builder, character_type);
        builder->CreateStore(left_arg, pleft_arg);
        llvm::AllocaInst *presult = llvm_utils->CreateAlloca(*builder, character_type);
        std::vector<llvm::Value*> args = {allocator, pleft_arg, right_arg, presult};
        builder->CreateCall(fn, args);
        return llvm_utils->CreateLoad2(character_type, presult);
    }

    llvm::Value* lfortran_str_len(llvm::Type* type, llvm::Value* str, bool use_descriptor=false)
    {
        if (use_descriptor) {
            str = llvm_utils->CreateLoad2(character_type->getPointerTo(), arr_descr->get_pointer_to_data(type, str));
        }
        std::string runtime_func_name = "_lfortran_str_len";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getInt64Ty(context), {
                        character_type
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        return builder->CreateCall(fn, {str});
    }

    llvm::Value* lfortran_str_to_int(llvm::Value* str)
    {
        std::string runtime_func_name = "_lfortran_str_to_int";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getInt32Ty(context), {
                        character_type->getPointerTo()
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        return builder->CreateCall(fn, {str});
    }

    llvm::Value* lfortran_str_ord(llvm::Value* str)
    {
        std::string runtime_func_name = "_lfortran_str_ord";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getInt32Ty(context), {
                        character_type->getPointerTo()
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        return builder->CreateCall(fn, {str});
    }

    llvm::Value* lfortran_str_chr(llvm::Value* str)
    {
        std::string runtime_func_name = "_lfortran_str_chr_alloc";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    character_type, {
                        character_type,
                        llvm::Type::getInt8Ty(context)
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        llvm::Value* allocator = llvm_utils->get_allocator(module.get());
        return builder->CreateCall(fn, {allocator, str});
    }

    llvm::Value* lfortran_str_item(llvm::Value* str, llvm::Value* str_len, llvm::Value* idx1)
    {
        std::string runtime_func_name = "_lfortran_str_item_alloc";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    character_type,
                    {
                        character_type,
                        character_type /*str*/,
                        llvm::Type::getInt64Ty(context) /*str_len*/,
                        llvm::Type::getInt64Ty(context) /*idx*/
                    },
                    false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        llvm::Value* allocator = llvm_utils->get_allocator(module.get());
        idx1 = builder->CreateSExt(idx1, llvm::Type::getInt64Ty(context));
        return builder->CreateCall(fn, {allocator, str, str_len, idx1});
    }

    llvm::Value* lfortran_str_slice(
        llvm::Value* str, llvm::Value* str_len,
        llvm::Value* idx1, llvm::Value* idx2,
        llvm::Value* step, llvm::Value* left_present, llvm::Value* right_present)
    {
        std::string runtime_func_name = "_lfortran_str_slice_alloc";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    character_type, {
                        character_type,
                        character_type, llvm::Type::getInt64Ty(context),
                        llvm::Type::getInt64Ty(context),
                        llvm::Type::getInt64Ty(context), llvm::Type::getInt64Ty(context),
                        llvm::Type::getInt1Ty(context), llvm::Type::getInt1Ty(context)
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        llvm::Value* allocator = llvm_utils->get_allocator(module.get());
        // Make sure they're int64 integers
        idx1 = llvm_utils->convert_kind(idx1, llvm::Type::getInt64Ty(context));
        idx2 = llvm_utils->convert_kind(idx2, llvm::Type::getInt64Ty(context));
        step = llvm_utils->convert_kind(step, llvm::Type::getInt64Ty(context));
        return builder->CreateCall(fn, {allocator, str, str_len, idx1, idx2, step, left_present, right_present});
    }

    // Specific For Fortran Strings
    llvm::Value* lfortran_str_slice_fortran(
        llvm::Value* str,
        llvm::Value* start, llvm::Value* end){
        std::string runtime_func_name = "_lfortran_str_slice_fortran_alloc";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    character_type, {
                        character_type,
                        character_type,
                        llvm::Type::getInt64Ty(context), // Start
                        llvm::Type::getInt64Ty(context) // End
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        llvm::Value* allocator = llvm_utils->get_allocator(module.get());
        // Make sure they're int64 integers
        start = llvm_utils->convert_kind(start, llvm::Type::getInt64Ty(context));
        end = llvm_utils->convert_kind(end, llvm::Type::getInt64Ty(context));
        return builder->CreateCall(fn, {allocator, str, start, end});
    }



    llvm::Value* lfortran_type_to_str(llvm::Value* arg, llvm::Type* value_type, std::string type, int value_kind) {
        std::string func_name = "_lfortran_" + type + "_to_str" + std::to_string(value_kind) + "_alloc";
         llvm::Function *fn = module->getFunction(func_name);
         if(!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                 character_type, {
                     character_type,
                     value_type
                 }, false);
            fn = llvm::Function::Create(function_type,
                     llvm::Function::ExternalLinkage, func_name, module.get());
         }
         llvm::Value* allocator = llvm_utils->get_allocator(module.get());
         llvm::Value* res = builder->CreateCall(fn, {allocator, arg});
         return res;
    }

    llvm::Value* lfortran_dynamic_cast(llvm::Value* static_ptr,
                                       llvm::Value* dst_type,
                                       bool match_exact_type)
    {
        static_ptr = builder->CreateBitCast(static_ptr, llvm_utils->i8_ptr);
        dst_type = builder->CreateBitCast(dst_type, llvm_utils->i8_ptr);

        std::string runtime_func_name = "__lfortran_dynamic_cast";
        llvm::Function* fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType* function_type = llvm::FunctionType::get(
                llvm_utils->i8_ptr,
                { llvm_utils->i8_ptr, llvm_utils->i8_ptr, llvm::Type::getInt1Ty(context) },
                false);
            fn = llvm::Function::Create(
                function_type, llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        std::vector<llvm::Value*> args
            = { static_ptr,
                dst_type,
                match_exact_type ? llvm::ConstantInt::getTrue(context)
                                 : llvm::ConstantInt::getFalse(context) };
        return builder->CreateCall(fn, args);
    }

    // This function is called as:
    // float complex_re(complex a)
    // And it extracts the real part of the complex number
    llvm::Value *complex_re(llvm::Value *c, llvm::Type* complex_type=nullptr) {
        if( complex_type == nullptr ) {
            complex_type = complex_type_4;
        }
        llvm::Value *val = c;
        llvm::Type *val_type = c->getType();
        if (val_type->isPointerTy()) {
            val = llvm_utils->CreateLoad2(complex_type, c);
            val_type = complex_type;
        }
        if (llvm::isa<FIXED_VECTOR_TYPE>(val_type)) {
            llvm::Value* idx0 = llvm::ConstantInt::get(
                llvm::Type::getInt32Ty(context), 0);
            return builder->CreateExtractElement(val, idx0);
        }
        return builder->CreateExtractValue(val, {0});
    }

    llvm::Value *complex_im(llvm::Value *c, llvm::Type* complex_type=nullptr) {
        if( complex_type == nullptr ) {
            complex_type = complex_type_4;
        }
        llvm::Value *val = c;
        llvm::Type *val_type = c->getType();
        if (val_type->isPointerTy()) {
            val = llvm_utils->CreateLoad2(complex_type, c);
            val_type = complex_type;
        }
        if (llvm::isa<FIXED_VECTOR_TYPE>(val_type)) {
            llvm::Value* idx1 = llvm::ConstantInt::get(
                llvm::Type::getInt32Ty(context), 1);
            return builder->CreateExtractElement(val, idx1);
        }
        return builder->CreateExtractValue(val, {1});
    }

    llvm::Value *complex_from_floats(llvm::Value *re, llvm::Value *im,
                                     llvm::Type* complex_type=nullptr) {
        if( complex_type == nullptr ) {
            complex_type = complex_type_4;
        }
        if (llvm::isa<FIXED_VECTOR_TYPE>(complex_type)) {
            llvm::Value* result = llvm::UndefValue::get(complex_type);
            llvm::Value* idx0 = llvm::ConstantInt::get(
                llvm::Type::getInt32Ty(context), 0);
            llvm::Value* idx1 = llvm::ConstantInt::get(
                llvm::Type::getInt32Ty(context), 1);
            result = builder->CreateInsertElement(result, re, idx0);
            result = builder->CreateInsertElement(result, im, idx1);
            return result;
        }
        llvm::Value* result = llvm::UndefValue::get(complex_type);
        result = builder->CreateInsertValue(result, re, {0});
        result = builder->CreateInsertValue(result, im, {1});
        return result;
    }

    llvm::Value *nested_struct_rd(std::vector<llvm::Value*> vals,
            llvm::StructType* rd) {
        llvm::AllocaInst *pres = llvm_utils->CreateAlloca(*builder, rd);
        llvm::Value *pim = llvm_utils->CreateGEP2(rd, pres, vals);
        llvm::Type* elem_type = rd->getStructElementType(vals.size() - 1);
        return llvm_utils->CreateLoad2(elem_type, pim);
    }

    /**
     * @brief This function generates the
     * @detail This is converted to
     *
     *     float lfortran_KEY(float *x)
     *
     *   Where KEY can be any of the supported intrinsics; this is then
     *   transformed into a runtime call:
     *
     *     void _lfortran_KEY(float x, float *result)
     */
    llvm::Value* lfortran_intrinsic(llvm::Function *fn, llvm::Value* pa, int a_kind)
    {
        llvm::Type *presult_type = llvm_utils->getFPType(a_kind);
        llvm::AllocaInst *presult = llvm_utils->CreateAlloca(*builder, presult_type);
        llvm::Value* a = llvm_utils->CreateLoad2(presult_type, pa);
        std::vector<llvm::Value*> args = {a, presult};
        builder->CreateCall(fn, args);
        return llvm_utils->CreateLoad2(presult_type, presult);
    }

    llvm::Type* get_llvm_struct_data_type(ASR::Struct_t* st, bool is_pointer) {
        std::string struct_name = (std::string)st->m_name;
        if (ASRUtils::is_unlimited_polymorphic_type(st)) {
            if (is_pointer) {
                return llvm_utils->getClassType(st)->getPointerTo();
            } else {
                return llvm_utils->getClassType(st);
            }
        } else {
            return llvm_utils->getStructType(st, module.get(), is_pointer);
        }
    }

    void visit_TranslationUnit(const ASR::TranslationUnit_t &x) {
        module = std::make_unique<llvm::Module>("LFortran", context);
        // Set host target DataLayout so that getTypeAllocSize() returns
        // correct sizes (respecting alignment) during IR generation.
        {
            std::string target_triple = llvm::sys::getDefaultTargetTriple();
            std::string Error;
#if LLVM_VERSION_MAJOR >= 21
            const llvm::Target *target =
                llvm::TargetRegistry::lookupTarget(llvm::Triple(target_triple), Error);
#else
            const llvm::Target *target =
                llvm::TargetRegistry::lookupTarget(target_triple, Error);
#endif
            if (target) {
                llvm::TargetOptions opt;
                auto TM = target->createTargetMachine(
#if LLVM_VERSION_MAJOR >= 21
                    llvm::Triple(target_triple),
#else
                    target_triple,
#endif
                    "generic", "", opt, {});
                if (TM) {
                    module->setDataLayout(TM->createDataLayout());
                    delete TM;
                } else {
                    module->setDataLayout("");
                }
            } else {
                module->setDataLayout("");
            }
        }
        llvm_utils->set_module(module.get());

        if (compiler_options.emit_debug_info) {
            DBuilder = std::make_unique<llvm::DIBuilder>(*module);
            debug_CU = DBuilder->createCompileUnit(
                llvm::dwarf::DW_LANG_C, DBuilder->createFile(infile, "."),
                "LPython Compiler", false, "", 0);
        }

        // All loose statements must be converted to a function, so the items
        // must be empty:
        LCOMPILERS_ASSERT(x.n_items == 0);

        // Define LLVM types that we might need
        // Complex type is represented as an identified struct in LLVM
        // %complex = type { float, float }
        complex_type_4 = llvm_utils->complex_type_4;
        complex_type_8 = llvm_utils->complex_type_8;
        complex_type_4_ptr = llvm_utils->complex_type_4_ptr;
        complex_type_8_ptr = llvm_utils->complex_type_8_ptr;
        character_type = llvm_utils->character_type;
        string_descriptor = llvm_utils->string_descriptor;
        list_type = llvm::Type::getInt8Ty(context)->getPointerTo();

        llvm::Type* bound_arg = static_cast<llvm::Type*>(arr_descr->get_dimension_descriptor_type(true));
        fname2arg_type["lbound"] = std::make_pair(bound_arg, bound_arg->getPointerTo());
        fname2arg_type["ubound"] = std::make_pair(bound_arg, bound_arg->getPointerTo());

        // Process Variables first:
        for (auto &item : x.m_symtab->get_scope()) {
            if (is_a<ASR::Variable_t>(*item.second) ||
                is_a<ASR::Enum_t>(*item.second)) {
                visit_symbol(*item.second);
            }
        }

        prototype_only = false;
        for (auto &item : x.m_symtab->get_scope()) {
            if (is_a<ASR::Module_t>(*item.second) &&
                item.first.find("lfortran_intrinsic_optimization") != std::string::npos) {
                ASR::Module_t* mod = ASR::down_cast<ASR::Module_t>(item.second);
                for( auto &moditem: mod->m_symtab->get_scope() ) {
                    ASR::symbol_t* sym = ASRUtils::symbol_get_past_external(moditem.second);
                    if (is_a<ASR::Function_t>(*sym)) {
                        visit_Function(*ASR::down_cast<ASR::Function_t>(sym));
                    }
                }
            }
        }

        prototype_only = true;
        // Generate function prototypes
        for (auto &item : x.m_symtab->get_scope()) {
            if (is_a<ASR::Function_t>(*item.second)) {
                visit_Function(*ASR::down_cast<ASR::Function_t>(item.second));
            }
        }
        prototype_only = false;

        // TODO: handle dependencies across modules and main program

        // Then do all the modules in the right order
        std::vector<std::string> build_order
            = determine_module_dependencies(x);
        for (auto &item : build_order) {
            if (!item.compare("_lcompilers_mlir_gpu_offloading")) continue;
            ASR::symbol_t *mod = x.m_symtab->get_symbol(item);
            if (mod == nullptr) continue;
            visit_symbol(*mod);
        }

        // Then do all the procedures
        for (auto &item : x.m_symtab->get_scope()) {
            if( ASR::is_a<ASR::Function_t>(*item.second) ) {
                visit_symbol(*item.second);
            }
        }

        // Then the main program
        for (auto &item : x.m_symtab->get_scope()) {
            if (is_a<ASR::Program_t>(*item.second)) {
                visit_symbol(*item.second);
            }
        }

        // Register coarray per-TU init functions in @llvm.global_ctors
        // so saved coarray allocations run before main() in separate compilation
        for (auto &item : x.m_symtab->get_scope()) {
            if (is_a<ASR::Function_t>(*item.second)) {
                std::string name = item.first;
                if (name.find("__lfortran_coarray_init") == 0) {
                    llvm::Function *init_fn = module->getFunction(name);
                    if (init_fn) {
                        llvm::appendToGlobalCtors(*module, init_fn, 65535);
                    }
                }
            }
        }

        LCOMPILERS_ASSERT_MSG(llvm_utils->stringFormat_return.all_clean(),
                        "`_lcompilers_string_format_fortran()` Return Not Freed");
    }

    template <typename T>
    void visit_AllocateUtil(const T& x, ASR::expr_t* m_stat, bool realloc, ASR::expr_t* m_source = nullptr) {
        llvm::Value* stat_target = nullptr;
        if (m_stat) {
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(m_stat, false);
            ptr_loads = ptr_loads_copy;
            stat_target = tmp;

            ASR::ttype_t* stat_type = ASRUtils::expr_type(m_stat);
            int kind = ASRUtils::extract_kind_from_ttype_t(stat_type);
            llvm::Type* stat_llvm_type = llvm::Type::getIntNTy(context, 8*kind);
            llvm::Value* stat_zero = llvm::ConstantInt::get(stat_llvm_type, 0);
            builder->CreateStore(stat_zero, stat_target);
        }
        for( size_t i = 0; i < x.n_args; i++ ) {
            ASR::alloc_arg_t curr_arg = x.m_args[i];
            ASR::expr_t* tmp_expr = x.m_args[i].m_a;
            LCOMPILERS_ASSERT(
                ASRUtils::is_allocatable(tmp_expr)||
                ASRUtils::is_pointer(expr_type(tmp_expr)));
            visit_expr_load_wrapper(tmp_expr, 0);
            llvm::Value* x_arr = tmp;
            ASR::ttype_t* curr_arg_m_a_type = ASRUtils::type_get_past_pointer(
                ASRUtils::type_get_past_allocatable(
                ASRUtils::expr_type(tmp_expr)));
            size_t n_dims = ASRUtils::extract_n_dims_from_ttype(curr_arg_m_a_type);
            if (n_dims == 0 && curr_arg.n_dims > 0) {
                n_dims = curr_arg.n_dims;
            }
            curr_arg_m_a_type = ASRUtils::type_get_past_array(curr_arg_m_a_type);
            if( n_dims == 0 ) {
                if (ASRUtils::is_character(*curr_arg_m_a_type)) {
                    ASR::String_t* str = ASR::down_cast<ASR::String_t>(ASRUtils::extract_type(ASRUtils::expr_type(tmp_expr)));
                    llvm::Value* amount_to_allocate{};
                    if(curr_arg.m_len_expr){ // Visit desired length to be allocated.
                        visit_expr_load_wrapper(curr_arg.m_len_expr, 1, true);
                        amount_to_allocate = tmp;
                    } else {
                        amount_to_allocate = nullptr;
                    }
                    llvm_utils->allocate_allocatable_string(str, x_arr, amount_to_allocate);
                } else if(ASR::is_a<ASR::Integer_t>(*curr_arg_m_a_type) ||
                          ASR::is_a<ASR::Real_t>(*curr_arg_m_a_type) ||
                          ASR::is_a<ASR::Complex_t>(*curr_arg_m_a_type) ||
                          ASR::is_a<ASR::Logical_t>(*curr_arg_m_a_type)) {
                    llvm::Type* llvm_arg_type = llvm_utils->get_type_from_ttype_t_util(curr_arg.m_a, curr_arg_m_a_type, module.get());
                    auto do_scalar_alloc = [&]() {
                        llvm::Value* malloc_size = SizeOfTypeUtil(curr_arg.m_a, curr_arg_m_a_type, llvm_utils->getIntType(4),
                        ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4)));
                        llvm::Value* malloc_ptr = LLVMArrUtils::lfortran_malloc(
                            context, *module, *builder, malloc_size);
                        builder->CreateMemSet(malloc_ptr, llvm::ConstantInt::get(context, llvm::APInt(8, 0)), malloc_size, llvm::MaybeAlign());
                        builder->CreateStore(builder->CreateBitCast(
                            malloc_ptr, llvm_arg_type->getPointerTo()), x_arr);
                    };
                    if (!realloc && compiler_options.po.bounds_checking) {
                        llvm::Value* current_ptr = llvm_utils->CreateLoad2(llvm_arg_type->getPointerTo(), x_arr);
                        llvm::Value* is_allocated = builder->CreateICmpNE(
                            builder->CreatePtrToInt(current_ptr, llvm::Type::getInt64Ty(context)),
                            builder->CreatePtrToInt(llvm::ConstantPointerNull::get(llvm_arg_type->getPointerTo()),
                                llvm::Type::getInt64Ty(context)));
                        std::string var_name = "";
                        if (ASR::is_a<ASR::Var_t>(*tmp_expr)) {
                            ASR::symbol_t* sym = ASRUtils::symbol_get_past_external(
                                ASR::down_cast<ASR::Var_t>(tmp_expr)->m_v);
                            var_name = ASRUtils::symbol_name(sym);
                        }
                        if (m_stat) {
                            llvm_utils->create_if_else(
                                is_allocated,
                                [&]() {
                                    llvm::Type* stat_llvm_type = llvm::Type::getIntNTy(context, 8*ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(m_stat)));
                                    builder->CreateStore(llvm::ConstantInt::get(stat_llvm_type, 1), stat_target);
                                },
                                [&]() {
                                    do_scalar_alloc();
                                });
                            continue;
                        }
                        llvm::Value* var_name_llvm = LCompilers::create_global_string_ptr(context, *module, *builder, var_name);
                        llvm_utils->generate_runtime_error(is_allocated,
                            "Attempting to allocate already allocated variable '%s'",
                            {LLVMUtils::RuntimeLabel("Cannot allocate '%s' because it is already allocated", {tmp_expr->base.loc}, {var_name_llvm})},
                            infile,
                            location_manager,
                            var_name_llvm);
                    }
                    do_scalar_alloc();
                } else if (ASR::is_a<ASR::StructType_t>(*curr_arg_m_a_type)) {
                    ASR::ttype_t* source_expr_type = nullptr;
                    ASR::ttype_t* source_underlying_type = nullptr;
                    if (m_source) {
                        source_expr_type = ASRUtils::expr_type(m_source);
                        ASR::ttype_t* source_no_alloc = ASRUtils::type_get_past_allocatable(source_expr_type);
                        source_underlying_type = ASRUtils::type_get_past_pointer(source_no_alloc);
                    }
                    bool m_source_is_class = source_underlying_type && ASRUtils::is_class_type(source_underlying_type);

                    if (m_source && m_source_is_class) {
                        llvm::Value* target_addr = x_arr;

                        int64_t saved_ptr_loads = ptr_loads;
                        ptr_loads = 0;
                        this->visit_expr(*m_source);
                        llvm::Value* source_handle = tmp;
                        ptr_loads = saved_ptr_loads;
                        tmp = nullptr;

                        ASR::ttype_t* source_type = source_expr_type;
                        llvm::Type* source_wrapper_type = llvm_utils->get_type_from_ttype_t_util(
                            m_source, ASRUtils::extract_type(source_type), module.get());

                        if (source_type && LLVM::is_llvm_pointer(*source_type)) {
                            source_handle = llvm_utils->CreateLoad2(
                                source_wrapper_type->getPointerTo(), source_handle);
                        }

                        llvm::Value* is_source_not_allocated = builder->CreateICmpEQ(
                            builder->CreatePtrToInt(source_handle, llvm::Type::getInt64Ty(context)),
                            llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0));
                        llvm_utils->generate_runtime_error(is_source_not_allocated,
                            "allocate with source= requires an allocated/associated polymorphic source expression",
                            {LLVMUtils::RuntimeLabel(
                                "source expression is not allocated/associated",
                                {m_source->base.loc}, {})},
                            infile,
                            location_manager);

                        llvm::FunctionType* alloc_fn_type = llvm::FunctionType::get(
                            llvm::Type::getVoidTy(context), { llvm_utils->i8_ptr->getPointerTo() }, false);
                        llvm::PointerType* alloc_fn_ptr_type = llvm::PointerType::get(alloc_fn_type, 0);
                        llvm::PointerType* alloc_fn_ptr_ptr_type = llvm::PointerType::get(alloc_fn_ptr_type, 0);
                        llvm::PointerType* alloc_fn_ptr_ptr_ptr_type = llvm::PointerType::get(alloc_fn_ptr_ptr_type, 0);

                        llvm::Value* vtable_alloc_ptr = builder->CreateBitCast(
                            source_handle, alloc_fn_ptr_ptr_ptr_type);
                        vtable_alloc_ptr = llvm_utils->CreateLoad2(alloc_fn_ptr_ptr_type, vtable_alloc_ptr);
                        llvm::Value* alloc_fn_slot = llvm_utils->create_ptr_gep2(
                            alloc_fn_ptr_type, vtable_alloc_ptr, 1);
                        alloc_fn_slot = llvm_utils->CreateLoad2(alloc_fn_ptr_type, alloc_fn_slot);

                        llvm::Value* target_addr_as_i8pp = builder->CreateBitCast(
                            target_addr, llvm_utils->i8_ptr->getPointerTo());
                        builder->CreateCall(alloc_fn_type, alloc_fn_slot, { target_addr_as_i8pp });

                        llvm::Value* target_struct_i8 = llvm_utils->CreateLoad2(
                            llvm_utils->i8_ptr, target_addr_as_i8pp);
                        llvm::Type* target_struct_type = llvm_utils->get_type_from_ttype_t_util(
                            curr_arg.m_a, curr_arg_m_a_type, module.get());
                        llvm::Value* target_struct = builder->CreateBitCast(
                            target_struct_i8, target_struct_type->getPointerTo());

                        // Only deepcopy for source=, not for mold=
                        // mold= allocates with type but doesn't copy data
                        if (!curr_arg.m_type) {
                            llvm_utils->deepcopy(m_source, source_handle, target_struct,
                                ASRUtils::type_get_past_allocatable(ASRUtils::expr_type(m_source)),
                                curr_arg_m_a_type, module.get());
                        }

                        continue;
                    }

                    if (m_source && !m_source_is_class && (ASRUtils::is_allocatable(m_source) || ASRUtils::is_pointer(ASRUtils::expr_type(m_source)))) {
                        int64_t saved_ptr_loads = ptr_loads;
                        ptr_loads = 1;
                        this->visit_expr(*m_source);
                        llvm::Value* source_ptr = tmp;
                        ptr_loads = saved_ptr_loads;
                        tmp = nullptr;

                        llvm::Value* is_source_not_allocated = builder->CreateICmpEQ(
                            builder->CreatePtrToInt(source_ptr, llvm::Type::getInt64Ty(context)),
                            llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0));
                        llvm_utils->generate_runtime_error(is_source_not_allocated,
                            "allocate with source= requires an allocated/associated source expression",
                            {LLVMUtils::RuntimeLabel(
                                "source expression is not allocated/associated",
                                {m_source->base.loc}, {})},
                            infile,
                            location_manager);
                    }

                    if (ASR::down_cast<ASR::StructType_t>(curr_arg_m_a_type)->m_is_cstruct) {
                        llvm::Value* malloc_size = SizeOfTypeUtil(curr_arg.m_a, curr_arg_m_a_type, llvm_utils->getIntType(4),
                        ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4)));
                        llvm::Value* malloc_ptr = LLVMArrUtils::lfortran_malloc(
                            context, *module, *builder, malloc_size);
                        builder->CreateMemSet(malloc_ptr, llvm::ConstantInt::get(context, llvm::APInt(8, 0)), malloc_size, llvm::MaybeAlign());
                        llvm::Type* llvm_arg_type = llvm_utils->get_type_from_ttype_t_util(curr_arg.m_a, curr_arg_m_a_type, module.get());
                        llvm::Value* bitcasted_malloc_ptr = builder->CreateBitCast(
                                            malloc_ptr, llvm_arg_type->getPointerTo());
                        builder->CreateStore(bitcasted_malloc_ptr, x_arr);

                        x_arr = llvm_utils->CreateLoad2(llvm_arg_type->getPointerTo(), x_arr);

                        allocate_array_members_of_struct(
                            ASR::down_cast<ASR::Struct_t>(
                                ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(tmp_expr))),
                            x_arr,
                            curr_arg_m_a_type);
                        if (m_source && !m_source_is_class && !curr_arg.m_type) {
                            int64_t ptr_loads_copy = ptr_loads;
                            if (ASRUtils::is_allocatable(m_source) || ASRUtils::is_pointer(ASRUtils::expr_type(m_source))) {
                                ptr_loads = 1;
                            } else {
                                ptr_loads = 0;
                            }
                            this->visit_expr(*m_source);
                            ptr_loads = ptr_loads_copy;

                            llvm_utils->deepcopy(m_source, tmp, x_arr, ASRUtils::expr_type(m_source), curr_arg_m_a_type, module.get());
                        }
                    } else {
                        ASR::ttype_t* dest_asr_type = curr_arg.m_type;
                        ASR::symbol_t* dest_class_sym = nullptr;
                        llvm::Value* malloc_size = nullptr;
                        ASR::Struct_t* src_struct_sym = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(
                            ASRUtils::get_struct_sym_from_struct_expr(curr_arg.m_a)));
                        llvm::Type* src_struct_type = get_llvm_struct_data_type(src_struct_sym, true);
                        bool is_arg_unlimited_poly = ASRUtils::is_unlimited_polymorphic_type(&src_struct_sym->base);
                        // If no type specified then use curr_arg_m_a_type as default
                        if (m_source && !m_source_is_class) {
                            dest_asr_type = ASRUtils::type_get_past_allocatable(ASRUtils::expr_type(m_source));
                            dest_class_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(m_source));
                            malloc_size = SizeOfTypeUtil(m_source, dest_asr_type, llvm_utils->getIntType(4),
                                ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4)));
                        } else if (curr_arg.m_sym_subclass == nullptr) {
                            dest_class_sym = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(curr_arg.m_a));
                            dest_asr_type = ASRUtils::make_StructType_t_util(al, curr_arg_m_a_type->base.loc, dest_class_sym, true);
                            malloc_size = SizeOfTypeUtil(curr_arg.m_a, dest_asr_type, llvm_utils->getIntType(4),
                                ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4)));
                        } else {
                            llvm::Type* llvm_type = nullptr;
                            dest_class_sym = ASRUtils::symbol_get_past_external(curr_arg.m_sym_subclass);
                            ASR::Struct_t* dest_struct = ASR::down_cast<ASR::Struct_t>(dest_class_sym);
                            llvm_type = llvm_utils->getStructType(dest_struct, module.get());
                            llvm::DataLayout data_layout(module->getDataLayout());
                            int64_t type_size = data_layout.getTypeAllocSize(llvm_type);
                            malloc_size = llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(
                                ASRUtils::extract_kind_from_ttype_t(ASRUtils::TYPE(
                                    ASR::make_Integer_t(al, x.base.base.loc, 4))) * 8, type_size));
                        }

                        llvm::Value* malloc_ptr = LLVMArrUtils::lfortran_malloc(
                            context, *module, *builder, malloc_size);
                        builder->CreateMemSet(malloc_ptr, llvm::ConstantInt::get(context, llvm::APInt(8, 0)),
                                    malloc_size, llvm::MaybeAlign());
                        if(ASRUtils::is_string_only(dest_asr_type)) { // String type has a state to be cloned.
                            // NOTE : dest_asr_type == source_type (They both refelect the type the class will be)
                            this->visit_expr_load_wrapper(m_source, 0);
                            auto* const dest__source_t = llvm_utils->get_type_from_ttype_t_util(m_source, dest_asr_type, module.get());
                            llvm_utils->clone_string_state(
                                        builder->CreateBitCast(malloc_ptr, dest__source_t->getPointerTo()),
                                        tmp /*m_source*/,
                                        ASRUtils::get_string_type(dest_asr_type));
                            tmp = nullptr;
                        }

                        if (is_arg_unlimited_poly && !dest_class_sym) {
                            // This is the case of an unlimited polymorphic type being allocated an intrinsic type.
                            struct_api->create_vtab_for_intrinsic_type(dest_asr_type, 
                                ASRUtils::extract_kind_from_ttype_t(dest_asr_type), module.get());
                        }

                        llvm::Value* bitcasted_malloc_ptr = malloc_ptr;
                        if (ASRUtils::is_class_type(ASRUtils::extract_type(ASRUtils::expr_type(tmp_expr)))) {
                            llvm::Type* src_struct_type_ = llvm_utils->get_type_from_ttype_t_util(
                                ASRUtils::expr_type(tmp_expr), &src_struct_sym->base, module.get());
                            // Allocate class wrapper first
                            llvm::Value* wrapper_size = SizeOfTypeUtil(curr_arg.m_a, curr_arg_m_a_type,
                                llvm_utils->getIntType(4), ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4)));
                            llvm::Value* wrapper_ptr = LLVMArrUtils::lfortran_malloc(
                                context, *module, *builder, wrapper_size);
                            builder->CreateMemSet(wrapper_ptr, llvm::ConstantInt::get(context, llvm::APInt(8, 0)),
                                wrapper_size, llvm::MaybeAlign());
                            wrapper_ptr = builder->CreateBitCast(wrapper_ptr, src_struct_type_);
                            builder->CreateStore(wrapper_ptr, x_arr);
                            
                            // Now allocate its data
                            x_arr = llvm_utils->CreateLoad2(src_struct_type_, x_arr);
                            x_arr = llvm_utils->create_gep2(llvm_utils->get_type_from_ttype_t_util(
                                ASRUtils::extract_type(curr_arg_m_a_type), &src_struct_sym->base, module.get()), x_arr, 1);
                            if (!is_arg_unlimited_poly) {
                                bitcasted_malloc_ptr = builder->CreateBitCast(bitcasted_malloc_ptr, 
                                    llvm_utils->getStructType(src_struct_sym, module.get(), true));
                            }
                            builder->CreateStore(bitcasted_malloc_ptr, x_arr);
                            bitcasted_malloc_ptr = wrapper_ptr;
                        } else {
                            bitcasted_malloc_ptr = builder->CreateBitCast(malloc_ptr, src_struct_type);
                            builder->CreateStore(bitcasted_malloc_ptr, x_arr);
                        }

                        // Initialize members
                        llvm::Type* dest_type = nullptr;
                        if (curr_arg.m_sym_subclass) {
                            dest_type = llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(dest_class_sym), module.get());
                        } else if (m_source && !m_source_is_class) {
                            dest_type = llvm_utils->get_type_from_ttype_t_util(m_source, dest_asr_type, module.get());
                        } else {
                            dest_type = llvm_utils->get_type_from_ttype_t_util(curr_arg.m_a, dest_asr_type, module.get());
                        }
                        
                        if (ASRUtils::is_unlimited_polymorphic_type(&src_struct_sym->base)) {
                            x_arr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, x_arr);
                        } else {
                            x_arr = llvm_utils->CreateLoad2(src_struct_type, x_arr);
                        }
                        x_arr = builder->CreateBitCast(x_arr, dest_type->getPointerTo());

                        if (ASRUtils::is_class_type(ASRUtils::extract_type(curr_arg_m_a_type))) {
                            // Store vptr after allocation
                            if (is_arg_unlimited_poly && curr_arg.m_type
                                    && !ASR::is_a<ASR::StructType_t>(*curr_arg.m_type)) {
                                // Unlimited poly with intrinsic type-spec
                                struct_api->store_intrinsic_type_vptr(
                                    curr_arg.m_type,
                                    ASRUtils::extract_kind_from_ttype_t(curr_arg.m_type),
                                    bitcasted_malloc_ptr, module.get());
                            } else if (dest_class_sym) {
                                struct_api->store_class_vptr(ASRUtils::symbol_get_past_external(dest_class_sym), bitcasted_malloc_ptr, module.get());
                            } else if (ASRUtils::is_unlimited_polymorphic_type(&src_struct_sym->base)
                                   && !dest_class_sym) {
                                struct_api->store_intrinsic_type_vptr(
                                    dest_asr_type,
                                    ASRUtils::extract_kind_from_ttype_t(dest_asr_type),
                                    bitcasted_malloc_ptr, module.get());
                            } else {
                                struct_api->store_class_vptr(&src_struct_sym->base, bitcasted_malloc_ptr, module.get());
                            }
                        }

                        // For unlimited poly with character type-spec, initialize the
                        // string descriptor in the data buffer with proper length and data.
                        if (is_arg_unlimited_poly && curr_arg.m_type
                                && ASR::is_a<ASR::String_t>(*curr_arg.m_type)
                                && curr_arg.m_len_expr) {
                            int saved_ptr = ptr_loads;
                            ptr_loads = 2;
                            visit_expr_wrapper(curr_arg.m_len_expr, true);
                            ptr_loads = saved_ptr;
                            llvm::Value* str_len = builder->CreateSExtOrTrunc(
                                tmp, llvm::Type::getInt64Ty(context));
                            llvm::Value* char_data = LLVM::lfortran_malloc(
                                context, *module, *builder, str_len);
                            builder->CreateMemSet(char_data,
                                llvm::ConstantInt::get(context, llvm::APInt(8, 32)),
                                str_len, llvm::MaybeAlign());
                            llvm::Value* str_desc = builder->CreateBitCast(
                                malloc_ptr, llvm_utils->string_descriptor->getPointerTo());
                            builder->CreateStore(char_data,
                                llvm_utils->create_gep2(llvm_utils->string_descriptor, str_desc, 0));
                            builder->CreateStore(str_len,
                                llvm_utils->create_gep2(llvm_utils->string_descriptor, str_desc, 1));
                        }

                        if (ASR::is_a<ASR::StructType_t>(*dest_asr_type)) {
                            allocate_array_members_of_struct(ASR::down_cast<ASR::Struct_t>(dest_class_sym), x_arr, ASRUtils::symbol_type(dest_class_sym));
                        }
                        if (m_source && !m_source_is_class && !curr_arg.m_type) {
                            llvm::Value* src = nullptr; {
                                const auto load = (ASRUtils::is_allocatable(m_source) || ASRUtils::is_pointer(ASRUtils::expr_type(m_source))) && !ASRUtils::is_string_only(ASRUtils::expr_type(m_source)) ? 1 : 0;
                                this->visit_expr_load_wrapper(m_source, load);
                                src = tmp;
                                tmp = nullptr;
                            }
                            llvm::Value* dest = x_arr;
                            if (!ASRUtils::is_value_constant(m_source) && 
                                !ASRUtils::is_struct(*ASRUtils::expr_type(m_source)) &&
                                !ASRUtils::is_string_only(ASRUtils::expr_type(m_source))) {
                                src = llvm_utils->CreateLoad2(dest_type, src);
                            }
                            llvm_utils->deepcopy(m_source, src, dest, dest_asr_type, dest_asr_type, module.get());
                        }
                    }
                }
            } else {
                ASR::ttype_t* asr_data_type = ASRUtils::duplicate_type_without_dims(al,
                    curr_arg_m_a_type, curr_arg_m_a_type->base.loc);
                // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                llvm::Type* llvm_data_type = llvm_utils->get_el_type(curr_arg.m_a, asr_data_type, module.get());
                llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(tmp_expr,
                    ASRUtils::type_get_past_pointer(ASRUtils::type_get_past_allocatable(
                        ASRUtils::expr_type(tmp_expr))), module.get());
                llvm::Value* ptr_val = x_arr;

                if (x_arr && x_arr->getType() == nullptr) {
                    ptr_val = llvm::ConstantPointerNull::get(character_type);
                }
                llvm::Value* is_allocated = nullptr;
                std::string var_name = "";
                if (m_stat) {
                    handle_allocated(tmp_expr);
                    is_allocated = tmp;
                } else if (!realloc && compiler_options.po.bounds_checking && x_arr && x_arr->getType() != nullptr) {
                    llvm::Value* desc_ptr = llvm_utils->CreateLoad2(type->getPointerTo(), x_arr);
                    is_allocated = arr_descr->get_is_allocated_flag(desc_ptr, tmp_expr);
                    if (ASR::is_a<ASR::Var_t>(*tmp_expr)) {
                        ASR::symbol_t* sym = ASRUtils::symbol_get_past_external(
                            ASR::down_cast<ASR::Var_t>(tmp_expr)->m_v);
                        var_name = ASRUtils::symbol_name(sym);
                    }
                }
                if (!realloc && compiler_options.po.bounds_checking && is_allocated != nullptr && !m_stat) {
                    llvm::Value* var_name_llvm = LCompilers::create_global_string_ptr(context, *module, *builder, var_name);
                    llvm_utils->generate_runtime_error(is_allocated,
                        "Attempting to allocate already allocated variable '%s'",
                        {LLVMUtils::RuntimeLabel("Cannot allocate '%s' because it is already allocated", {tmp_expr->base.loc}, {var_name_llvm})},
                        infile,
                        location_manager,
                        var_name_llvm);
                }
                auto do_array_alloc = [&]() {
                    ASR::ttype_t* array_type = ASRUtils::type_get_past_allocatable_pointer(
                        ASRUtils::expr_type(tmp_expr));
                    // Only generate descriptor allocation check for struct members or in strict bounds checking mode
                    // For regular allocatable variables, the descriptor is always properly initialized on declaration
                    // For struct/derived type members, the descriptor may be NULL and needs initialization
                    bool is_struct_member = ASR::is_a<ASR::StructInstanceMember_t>(*tmp_expr);
                    bool is_dummy_arg = false;
                    if (ASR::is_a<ASR::Var_t>(*tmp_expr)) {
                        ASR::symbol_t* sym = ASRUtils::symbol_get_past_external(
                            ASR::down_cast<ASR::Var_t>(tmp_expr)->m_v);
                        if (ASR::is_a<ASR::Variable_t>(*sym)) {
                            ASR::Variable_t* v = ASR::down_cast<ASR::Variable_t>(sym);
                            is_dummy_arg = ASRUtils::is_arg_dummy(v->m_intent);
                        }
                    }
                    if (is_struct_member || is_dummy_arg || compiler_options.po.strict_bounds_checking) {
                        llvm_utils->create_if_else(
                            builder->CreateICmpEQ(
                                builder->CreatePtrToInt(llvm_utils->CreateLoad2(type->getPointerTo(), (x_arr && x_arr->getType() != nullptr) ? x_arr : ptr_val), llvm::Type::getInt64Ty(context)),
                                builder->CreatePtrToInt(
                                    llvm::ConstantPointerNull::get((x_arr && x_arr->getType() != nullptr) ? x_arr->getType()->getPointerTo() : ptr_val->getType()->getPointerTo()),
                                    llvm::Type::getInt64Ty(context))),
                            [&]() {
                                llvm::Value* ptr_;

                                if (is_struct_member || is_dummy_arg) {
                                    ptr_ = arr_descr->allocate_descriptor_on_heap(type);
                                    if (ASRUtils::is_array(array_type) && ASRUtils::is_character(*array_type)) {
                                        llvm::Type* llvm_str_desc_type = llvm_utils->get_type_from_ttype_t_util(
                                            tmp_expr, ASRUtils::extract_type(array_type), module.get());
                                        llvm::Value* str_desc = llvm_utils->allocate_string_descriptor_on_heap(
                                            llvm_str_desc_type);
                                        builder->CreateStore(str_desc, arr_descr->get_pointer_to_data(type, ptr_));
                                    }
                                } else {
                                    ptr_ = arr_descr->create_descriptor_alloca(type);
                                    arr_descr->fill_dimension_descriptor(type, ptr_, n_dims);
                                }

                                LLVM::CreateStore(
                                    *builder, ptr_, (x_arr && x_arr->getType() != nullptr) ? x_arr : ptr_val);
                            },
                            []() {});
                    }
                    if (ASRUtils::is_array(array_type) && ASRUtils::is_character(*array_type)) {
                        llvm::Value* desc_ptr = llvm_utils->CreateLoad2(
                            type->getPointerTo(), (x_arr && x_arr->getType() != nullptr) ? x_arr : ptr_val);
                        llvm::Type* llvm_str_desc_type = llvm_utils->get_type_from_ttype_t_util(
                            tmp_expr, ASRUtils::extract_type(array_type), module.get());
                        llvm_utils->ensure_string_descriptor_on_heap(type, desc_ptr, llvm_str_desc_type);
                    }
                    // Check if this is a mold-based allocation for unlimited polymorphic arrays.
                    // When the mold is also unlimited polymorphic (class(*)),
                    // we need to allocate a zeroed wrapper first, then patch it
                    // with type info from the mold at runtime.
                    bool is_mold_unlimited_poly = m_source && curr_arg.m_type
                        && ASR::is_a<ASR::StructType_t>(*curr_arg.m_type)
                        && ASR::down_cast<ASR::StructType_t>(curr_arg.m_type)->m_is_unlimited_polymorphic
                        && ASRUtils::is_unlimited_polymorphic_type(
                            ASRUtils::extract_type(ASRUtils::expr_type(tmp_expr)));
                    // For the mold case, pass nullptr as alloc_type so
                    // fill_malloc_array_details creates a zeroed wrapper.
                    ASR::ttype_t* effective_alloc_type = is_mold_unlimited_poly
                        ? nullptr : curr_arg.m_type;
                    ASR::expr_t* string_len_to_allocate = curr_arg.m_len_expr;
                    if (!string_len_to_allocate && ASRUtils::is_array(array_type) && ASRUtils::is_character(*array_type)) {
                        ASR::String_t* str_type = ASRUtils::get_string_type(ASRUtils::type_get_past_array(array_type));
                        string_len_to_allocate = str_type->m_len;
                    }
                    fill_malloc_array_details((x_arr && x_arr->getType() != nullptr) ? x_arr : ptr_val,
                                            type, llvm_data_type,
                                            expr_type(x.m_args[i].m_a),
                                            ASRUtils::get_struct_sym_from_struct_expr(x.m_args[i].m_a),
                                            curr_arg.m_dims, curr_arg.n_dims, string_len_to_allocate,
                                            curr_arg.m_sym_subclass, realloc, effective_alloc_type);
                    if (is_mold_unlimited_poly) {
                        ASR::symbol_t* class_sym = ASRUtils::symbol_get_past_external(
                            ASRUtils::get_struct_sym_from_struct_expr(x.m_args[i].m_a));
                        llvm::Type* class_type = llvm_utils->getClassType(
                            ASR::down_cast<ASR::Struct_t>(class_sym));

                        // Load the array descriptor and get the wrapper
                        llvm::Value* arr_val = (x_arr && x_arr->getType() != nullptr) ? x_arr : ptr_val;
                        llvm::Value* desc = llvm_utils->CreateLoad2(type->getPointerTo(), arr_val);
                        llvm::Value* wrapper = llvm_utils->CreateLoad2(
                            class_type->getPointerTo(),
                            llvm_utils->create_gep2(type, desc, 0));

                        // Visit the mold expression to get mold's wrapper pointer
                        int saved = ptr_loads;
                        ptr_loads = 0;
                        this->visit_expr(*m_source);
                        llvm::Value* mold_wrapper = llvm_utils->CreateLoad2(
                            class_type->getPointerTo(), tmp);
                        tmp = nullptr;
                        ptr_loads = saved;

                        // Compute total number of elements from dimensions
                        llvm::Value* num_elements = llvm::ConstantInt::get(
                            llvm::Type::getInt64Ty(context), 1);
                        for (int d = 0; d < (int)curr_arg.n_dims; d++) {
                            int saved2 = ptr_loads;
                            ptr_loads = 2;
                            visit_expr_wrapper(curr_arg.m_dims[d].m_length, true);
                            ptr_loads = saved2;
                            llvm::Value* dim_size = builder->CreateSExtOrTrunc(
                                tmp, llvm::Type::getInt64Ty(context));
                            num_elements = builder->CreateMul(num_elements, dim_size);
                        }

                        llvm_utils->init_mold_upoly_array_data(
                            wrapper, mold_wrapper, class_type, num_elements);
                    }
                    if( ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(ASRUtils::expr_type(tmp_expr)))
                        && !ASRUtils::is_unlimited_polymorphic_type(tmp_expr) ) {
                        llvm::Value* x_arr_ = llvm_utils->CreateLoad2(type->getPointerTo(), x_arr);
                        allocate_array_members_of_struct_arrays(tmp_expr, x_arr_,
                            ASRUtils::expr_type(tmp_expr));
                    }
                };
                if (m_stat && !realloc && is_allocated != nullptr) {
                    llvm_utils->create_if_else(
                        is_allocated,
                        [&]() {
                            llvm::Type* stat_llvm_type = llvm::Type::getIntNTy(context, 8*ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(m_stat)));
                            builder->CreateStore(llvm::ConstantInt::get(stat_llvm_type, 1), stat_target);
                        },
                        [&]() {
                            do_array_alloc();
                        });
                } else {
                    do_array_alloc();
                }
            }
        }
    }

    void visit_Allocate(const ASR::Allocate_t& x) {
        visit_AllocateUtil(x, x.m_stat, false, x.m_source);
    }

    void visit_ReAlloc(const ASR::ReAlloc_t& x) {
        LCOMPILERS_ASSERT(x.n_args == 1);
        ASR::ttype_t* realloc_arg_type = ASRUtils::expr_type(x.m_args[0].m_a);
        ASR::ttype_t* realloc_storage_type =
            ASRUtils::type_get_past_allocatable_pointer(realloc_arg_type);
        auto struct_requires_pre_realloc_finalize = [](ASR::Struct_t* st) -> bool {
            while (st != nullptr) {
                for (size_t i = 0; i < st->n_members; i++) {
                    ASR::symbol_t* member_sym = st->m_symtab->get_symbol(st->m_members[i]);
                    if (!member_sym || !ASR::is_a<ASR::Variable_t>(*member_sym)) {
                        continue;
                    }
                    ASR::Variable_t* member_var = ASR::down_cast<ASR::Variable_t>(member_sym);
                    ASR::ttype_t* member_type = member_var->m_type;

                    if (ASRUtils::is_allocatable(member_type)) {
                        return true;
                    }

                    ASR::ttype_t* member_base_type = ASRUtils::type_get_past_array(
                        ASRUtils::type_get_past_allocatable_pointer(member_type));
                    if (ASR::is_a<ASR::String_t>(*member_base_type)) {
                        return true;
                    }
                }

                if (st->m_parent == nullptr) {
                    break;
                }
                ASR::symbol_t* parent_sym = ASRUtils::symbol_get_past_external(st->m_parent);
                if (!ASR::is_a<ASR::Struct_t>(*parent_sym)) {
                    break;
                }
                st = ASR::down_cast<ASR::Struct_t>(parent_sym);
            }
            return false;
        };
        ASR::Struct_t* realloc_struct_sym = nullptr;
        if (ASR::is_a<ASR::Array_t>(*realloc_storage_type) &&
                ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(realloc_storage_type))) {
            ASR::symbol_t* struct_sym = ASRUtils::get_struct_sym_from_struct_expr(x.m_args[0].m_a);
            if (struct_sym) {
                struct_sym = ASRUtils::symbol_get_past_external(struct_sym);
                if (ASR::is_a<ASR::Struct_t>(*struct_sym)) {
                    ASR::Struct_t* candidate_struct = ASR::down_cast<ASR::Struct_t>(struct_sym);
                    if (struct_requires_pre_realloc_finalize(candidate_struct)) {
                        realloc_struct_sym = candidate_struct;
                    }
                }
            }
        }
        handle_allocated(x.m_args[0].m_a);
        llvm::Value* is_allocated = tmp;
        llvm::Type* index_type = arr_descr->get_index_type();
        unsigned index_bit_width = index_type->getIntegerBitWidth();
        int index_kind = index_bit_width / 8;
        llvm::Value* size = llvm::ConstantInt::get(
            index_type, llvm::APInt(index_bit_width, 1));
        int64_t ptr_loads_copy = ptr_loads;
        for( size_t i = 0; i < x.m_args[0].n_dims; i++ ) {
            ptr_loads = 2 - !LLVM::is_llvm_pointer(*
                ASRUtils::expr_type(x.m_args[0].m_dims[i].m_length));
            this->visit_expr_wrapper(x.m_args[0].m_dims[i].m_length, true);
            tmp = builder->CreateSExtOrTrunc(tmp, index_type);
            size = builder->CreateMul(size, tmp);
        }
        ptr_loads = ptr_loads_copy;
        llvm::Value* arg_array_size = nullptr;
        // Avoid touching the descriptor if the array is not allocated
        {
            llvm::Type* index_type = arr_descr->get_index_type();
            llvm::Value* size_var = llvm_utils->CreateAlloca(index_type, nullptr, "arg_array_size_tmp");
            builder->CreateStore(llvm::ConstantInt::get(index_type, 0), size_var);
            llvm_utils->create_if_else(is_allocated, [&]() {
                visit_ArraySizeUtil(x.m_args[0].m_a,
                    ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, index_kind)));
                builder->CreateStore(tmp, size_var);
            }, [](){});
            arg_array_size = llvm_utils->CreateLoad2(index_type, size_var);
        }
        llvm::Value* realloc_condition = builder->CreateOr(
            builder->CreateNot(is_allocated), builder->CreateAnd(
                is_allocated, builder->CreateICmpNE(size, arg_array_size)));
        // Add more condition if it's array of characters (check lengths)
        if(ASRUtils::is_character(*expr_type(x.m_args[0].m_a))){
            llvm::Value* current_str_len = get_string_length(x.m_args[0].m_a);
            LCOMPILERS_ASSERT(current_str_len->getType() == llvm::Type::getInt64Ty(context));
            /* Set `desired_str_len` */
            llvm::Value* desired_str_len {};
            if(x.m_args[0].m_len_expr){ // Provided length in allocation statment.
                visit_expr(*(x.m_args[0].m_len_expr));
                desired_str_len = llvm_utils->convert_kind(tmp, llvm::Type::getInt64Ty(context));
                tmp = nullptr;
            } else { // array's string length is fixed (compile-time or run-time value) -- No need to be provided within the allocation statement.
                LCOMPILERS_ASSERT_MSG(  ASRUtils::get_string_type(x.m_args[0].m_a)->m_len_kind != ASR::DeferredLength,
                                        "Deferred length strings must have length provided when allocating")
                llvm::Value* array {};
                { visit_expr_load_wrapper(x.m_args[0].m_a, 1);   array = tmp;    tmp = nullptr; } // Visit + Set
                desired_str_len = llvm_utils->get_stringArray_length(ASRUtils::expr_type(x.m_args[0].m_a), array);
            }
            realloc_condition = builder->CreateOr(realloc_condition,
                builder->CreateICmpNE(current_str_len, desired_str_len));
        }
        llvm_utils->create_if_else(realloc_condition, [=]() {
            if (realloc_struct_sym != nullptr) {
                llvm_utils->create_if_else(is_allocated, [=]() {
                    int64_t saved_ptr_loads = ptr_loads;
                    ptr_loads = 1;
                    visit_expr_load_wrapper(x.m_args[0].m_a, 1, true);
                    llvm::Value* desc_ptr = tmp;
                    ptr_loads = saved_ptr_loads;
                    llvm_symtab_finalizer.finalize_before_deallocate(
                        desc_ptr, realloc_arg_type, realloc_struct_sym, false);
                }, [](){});
            }
            visit_AllocateUtil(x, nullptr, true);
        }, [](){});
    }

    void visit_Nullify(const ASR::Nullify_t& x) {
        for( size_t i = 0; i < x.n_vars; i++ ) {
            ASR::symbol_t* tmp_sym;
            llvm::Value *target;
            if (ASR::is_a<ASR::StructInstanceMember_t>(*x.m_vars[i])) {
                tmp_sym = ASR::down_cast<ASR::StructInstanceMember_t>(x.m_vars[i])->m_m;
                this->visit_expr(*x.m_vars[i]);
                target = tmp;
            } else if (ASR::is_a<ASR::Var_t>(*x.m_vars[i])) {
                tmp_sym = ASR::down_cast<ASR::Var_t>(x.m_vars[i])->m_v;
                tmp_sym = ASRUtils::symbol_get_past_external(tmp_sym);
                std::uint32_t h = get_hash((ASR::asr_t*)tmp_sym);
                target = llvm_symtab[h];
            } else {
                throw CodeGenError("Only StructInstanceMember and Variable are supported Nullify type");
            }
            ASR::ttype_t* const sym_type = ASRUtils::symbol_type(tmp_sym);

            llvm::Type* tp = llvm_utils->get_type_from_ttype_t_util(x.m_vars[i],
                ASRUtils::type_get_past_pointer(
                ASRUtils::type_get_past_allocatable(
                sym_type)), module.get());

            llvm::Type* dest_type = tp->getPointerTo();
            if (ASR::is_a<ASR::FunctionType_t>(*ASRUtils::type_get_past_pointer(ASRUtils::symbol_type(tmp_sym)))) {
                // functions are pointers in LLVM, so we do not need to get the pointer to it
                // Class type target is already pointer, so we dont need to get the pointer for dest
                dest_type = tp;
            }


            if(ASRUtils::is_array(ASRUtils::expr_type(x.m_vars[i]))){
                llvm::Type* x_m_vars_type = llvm_utils->get_type_from_ttype_t_util(x.m_vars[i],
                    ASRUtils::expr_type(x.m_vars[i]), module.get());
                llvm::Value* target_ = llvm_utils->CreateLoad2(x_m_vars_type, target);
                llvm::Value* data_ptr = arr_descr->get_pointer_to_data(x.m_vars[i], ASRUtils::expr_type(x.m_vars[i]), target_ , module.get());
                builder->CreateStore(
                    llvm::ConstantPointerNull::get(llvm_utils->get_el_type(x.m_vars[i],
                        ASRUtils::extract_type(ASRUtils::expr_type(x.m_vars[i])), module.get())->getPointerTo())
                    , data_ptr);
            } else if(ASRUtils::is_string_only(ASRUtils::expr_type(x.m_vars[i]))) {
                llvm::Value* np = llvm::ConstantPointerNull::get(llvm::Type::getInt8Ty(context)->getPointerTo());
                llvm::Value* data_target = llvm_utils->create_gep2(llvm_utils->get_type_from_ttype_t_util(x.m_vars[i],
                    ASRUtils::expr_type(x.m_vars[i]), module.get()), target, 0);
                llvm::Value* len = llvm_utils->create_gep2(llvm_utils->get_type_from_ttype_t_util(x.m_vars[i],
                    ASRUtils::expr_type(x.m_vars[i]), module.get()), target, 1);
                builder->CreateStore(llvm::ConstantInt::get(context, llvm::APInt(64, 0)), len);
                builder->CreateStore(np, data_target);
            } else if(ASRUtils::is_class_type(ASRUtils::type_get_past_allocatable_pointer(sym_type))) {
                llvm::Value* const wrapper_ptr = llvm_utils->CreateLoad2(dest_type, target);
                llvm_utils->lfortran_free_nocheck(wrapper_ptr);
                llvm::Value* const np = llvm::ConstantPointerNull::get(llvm::cast<llvm::PointerType>(dest_type));
                builder->CreateStore(np, target);
            } else {
                llvm::Value* np = builder->CreateIntToPtr(
                    llvm::ConstantInt::get(context, llvm::APInt(32, 0)), dest_type);
                builder->CreateStore(np, target);
            }
        }
    }

    inline void call_lfortran_free(llvm::Function* fn, llvm::Type* tmp_typ,  llvm::Type* llvm_data_type) {
        llvm::Value* arr = llvm_utils->CreateLoad2(llvm_data_type->getPointerTo(), arr_descr->get_pointer_to_data(tmp_typ, tmp));
        llvm::Value* allocator = llvm_utils->get_allocator(module.get());
        std::vector<llvm::Value*> args = { allocator, builder->CreateBitCast(arr, character_type) };
        builder->CreateCall(fn, args);
        arr_descr->reset_is_allocated_flag(tmp_typ, tmp, llvm_data_type);
    }

    llvm::Function* _Deallocate() {
        std::string func_name = "_lfortran_free_alloc";
        llvm::Function *free_fn = module->getFunction(func_name);
        if (!free_fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        character_type,
                        character_type
                    }, false);
            free_fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, func_name, module.get());
        }
        return free_fn;
    }

    llvm::Function* _Allocate() {
        std::string func_name = "_lfortran_allocate_string";
        llvm::Function *alloc_fun = module->getFunction(func_name);
        if (!alloc_fun) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        character_type->getPointerTo(),
                        llvm::Type::getInt64Ty(context),
                        llvm::Type::getInt64Ty(context)->getPointerTo(),
                        llvm::Type::getInt64Ty(context)->getPointerTo()
                    }, false);
            alloc_fun = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, func_name, module.get());
        }
        return alloc_fun;
    }

    template <typename T>
    void visit_Deallocate(const T& x) {
        if (compiler_options.emit_debug_info) debug_emit_loc(x);
        llvm::Function* free_fn = llvm_utils->_Deallocate();
        llvm::Value* allocator = llvm_utils->get_allocator(module.get());
        for( size_t i = 0; i < x.n_vars; i++ ) {
            ASR::expr_t* tmp_expr = x.m_vars[i];
            ASR::symbol_t* curr_obj = nullptr;
            ASR::abiType abt = ASR::abiType::Source;
            if( ASR::is_a<ASR::Var_t>(*tmp_expr) ) {
                const ASR::Var_t* tmp_var = ASR::down_cast<ASR::Var_t>(tmp_expr);
                curr_obj = tmp_var->m_v;
                ASR::Variable_t *v = ASR::down_cast<ASR::Variable_t>(
                                    symbol_get_past_external(curr_obj));
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = 0;
                if (!ASRUtils::is_class_type(ASRUtils::extract_type(v->m_type))) {
                    fetch_var(v);
                } else {
                    uint32_t h = get_hash((ASR::asr_t*)v);
                    tmp = llvm_symtab[h];
                }
                ptr_loads = ptr_loads_copy;
                abt = v->m_abi;
            } else if (ASR::is_a<ASR::StructInstanceMember_t>(*tmp_expr)) {
                ASR::StructInstanceMember_t* sm = ASR::down_cast<ASR::StructInstanceMember_t>(tmp_expr);
                ASR::ttype_t* caller_type = ASRUtils::type_get_past_allocatable(
                        ASRUtils::expr_type(sm->m_v));
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = 1;
                this->visit_expr_wrapper(sm->m_v, ASRUtils::is_class_type(ASRUtils::extract_type(caller_type)));
                ptr_loads = ptr_loads_copy;
                llvm::Value* dt = tmp;
                ASR::symbol_t *struct_sym = nullptr;
                llvm::Type* dt_type = llvm_utils->getStructType(
                    ASR::down_cast<ASR::Struct_t>(
                        ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(sm->m_v))),
                    module.get());
                struct_sym = ASRUtils::symbol_get_past_external(
                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(sm->m_v)));
                if (ASRUtils::is_class_type(ASRUtils::extract_type(caller_type))) {
                    llvm::Type* dt_type_poly = llvm_utils->get_type_from_ttype_t_util(sm->m_v,
                        ASRUtils::type_get_past_pointer(ASRUtils::type_get_past_allocatable(caller_type)),
                        module.get());
                    if (!dt->getType()->isPointerTy()) {
                        llvm::Value* dt_val = dt;
                        llvm::Value* alloca_tmp = builder->CreateAlloca(dt_val->getType());
                        builder->CreateStore(dt_val, alloca_tmp);
                        dt = alloca_tmp;
                    }
                    llvm::Value* dt_ptr = llvm_utils->create_gep2(dt_type_poly, dt, 1);
                    dt = llvm_utils->CreateLoad2(dt_type->getPointerTo(), dt_ptr);
                } else if (ASR::is_a<ASR::StructInstanceMember_t>(*sm->m_v) &&
                           ASRUtils::is_allocatable_or_pointer(ASRUtils::expr_type(sm->m_v))) {
                    dt = llvm_utils->CreateLoad2(dt_type->getPointerTo(), dt);
                }

                std::string curr_struct = get_type_key(struct_sym);
                std::string member_name = ASRUtils::symbol_name(ASRUtils::symbol_get_past_external(sm->m_m));
                while( name2memidx[curr_struct].find(member_name) == name2memidx[curr_struct].end() ) {
                    if( dertype2parent.find(curr_struct) == dertype2parent.end() ) {
                        throw CodeGenError(curr_struct + " doesn't have any member named " + member_name,
                                            x.base.base.loc);
                    }
                    dt = llvm_utils->create_gep2(name2dertype[curr_struct], dt, 0);
                    curr_struct = dertype2parent[curr_struct];
                }
                int dt_idx = 0;
                dt_idx = name2memidx[curr_struct][member_name];
                std::vector<llvm::Value*> idx_vars = {llvm::ConstantInt::get(context, llvm::APInt(32, 0)),
                    llvm::ConstantInt::get(context, llvm::APInt(32, dt_idx))};
                if (dt->getType() != name2dertype[curr_struct]->getPointerTo()) {
                    llvm::Value* dt_val = dt;
                    llvm::Value* alloca_tmp = builder->CreateAlloca(name2dertype[curr_struct]);
                    builder->CreateStore(dt_val, alloca_tmp);
                    dt = alloca_tmp;
                }
                LCOMPILERS_ASSERT(dt->getType()->isPointerTy());
                llvm::Value* dt_1 = builder->CreateGEP(name2dertype[curr_struct], dt, idx_vars);
                tmp = dt_1;
            } else {
                throw CodeGenError("Cannot deallocate variables in expression " +
                                    ASRUtils::type_to_str_python_expr(ASRUtils::expr_type(tmp_expr), tmp_expr),
                                    tmp_expr->base.loc);
            }
            ASR::ttype_t *cur_type = ASRUtils::expr_type(tmp_expr);
            ASR::ttype_t *cur_type_past = ASRUtils::type_get_past_allocatable_pointer(cur_type);
            bool in_struct = ASR::is_a<ASR::StructInstanceMember_t>(*tmp_expr);
            ASR::Struct_t* struct_sym = nullptr;
            if (cur_type_past->type == ASR::StructType ||
                (cur_type_past->type == ASR::Array &&
                 ASR::down_cast<ASR::Array_t>(cur_type_past)->m_type->type == ASR::StructType)) {
                ASR::symbol_t* sym = ASRUtils::symbol_get_past_external(
                    ASRUtils::get_struct_sym_from_struct_expr(tmp_expr));
                struct_sym = ASR::down_cast<ASR::Struct_t>(sym);
            }
            int dims = ASRUtils::extract_n_dims_from_ttype(cur_type);
            if(ASRUtils::is_character(*cur_type)) { // Handle Strings (array of strings or just string)
                tmp = LLVM::is_llvm_pointer(*cur_type) ?
                    builder->CreateLoad(llvm_utils->get_type_from_ttype_t_util(tmp_expr, cur_type, module.get()), tmp)
                    : tmp ;
                ASR::String_t* str_type = ASR::down_cast<ASR::String_t>(
                    ASRUtils::extract_type(cur_type));
                if (abt == ASR::abiType::BindC && ASRUtils::is_allocatable(cur_type) &&
                    str_type->m_physical_type == ASR::string_physical_typeType::DescriptorString) {
                    tmp = llvm_utils->CreateLoad2(
                        llvm_utils->string_descriptor->getPointerTo(), tmp);
                }
                llvm_utils->free_strings(tmp_expr, tmp);
            } else {
                if (dims == 0 && !ASRUtils::is_assumed_rank_array(cur_type)) {
                    llvm::Type* llvm_data_type;
                    llvm::Value* tmp_ = tmp;
                    if (LLVM::is_llvm_pointer(*cur_type)) {
                        llvm::Type* typ = llvm_utils->get_type_from_ttype_t_util(tmp_expr, cur_type, module.get());
                        tmp = llvm_utils->CreateLoad2(typ, tmp);
                    }
                    if (ASRUtils::is_class_type(ASRUtils::extract_type(cur_type))) {
                        // If it is a class type, we need to get the pointer to the struct
                        llvm::Type* class_type = llvm_utils->get_type_from_ttype_t_util(
                            tmp_expr,
                            ASRUtils::type_get_past_pointer(ASRUtils::type_get_past_allocatable(cur_type)),
                            module.get());
                        llvm_data_type = class_type;
                        tmp = llvm_utils->CreateLoad2(llvm_data_type->getPointerTo(), tmp_);
                    } else {
                        llvm_data_type = llvm_utils->get_type_from_ttype_t_util(tmp_expr,
                            ASRUtils::type_get_past_array(
                                ASRUtils::type_get_past_pointer(
                                    ASRUtils::type_get_past_allocatable(cur_type))),
                            module.get(), abt);
                    }
                    llvm::Value *cond = builder->CreateICmpNE(
                        builder->CreatePtrToInt(tmp, llvm::Type::getInt64Ty(context)),
                        builder->CreatePtrToInt(
                            llvm::ConstantPointerNull::get(llvm_data_type->getPointerTo()),
                            llvm::Type::getInt64Ty(context)) );
                    llvm_utils->create_if_else(cond, [=]() {
                        // Call user-defined FINAL procedures (Fortran 2018 §7.5.6.3)
                        if (struct_sym != nullptr && struct_sym->n_member_functions > 0) {
                            for (size_t fi = 0; fi < struct_sym->n_member_functions; fi++) {
                                std::string final_proc_name = struct_sym->m_member_functions[fi];
                                ASR::symbol_t* final_sym = struct_sym->m_symtab->parent->get_symbol(final_proc_name);
                                if (final_sym) {
                                    final_sym = ASRUtils::symbol_get_past_external(final_sym);
                                    uint32_t fh = get_hash((ASR::asr_t*)final_sym);
                                    if (llvm_symtab_fn.find(fh) != llvm_symtab_fn.end()) {
                                        llvm::Function* final_fn = llvm_symtab_fn[fh];
                                        // Finalizers take type(T), not class(T). For class
                                        // variables, load the concrete data pointer (field 1)
                                        // from the class wrapper {vptr, data*}.
                                        llvm::Value* final_arg = tmp;
                                        if (ASRUtils::is_class_type(ASRUtils::extract_type(cur_type))) {
                                            llvm::Value* data_field = llvm_utils->create_gep2(llvm_data_type, tmp, 1);
                                            llvm::Type* expected_type = final_fn->getFunctionType()->getParamType(0);
                                            final_arg = llvm_utils->CreateLoad2(expected_type, data_field);
                                        }
                                        builder->CreateCall(final_fn, {final_arg});
                                    }
                                }
                            }
                        }
                        llvm_symtab_finalizer.finalize_before_deallocate(tmp, cur_type, struct_sym, in_struct);
                        // Deallocate data of class first
                        if( ASRUtils::is_pointer(cur_type) || 
                            !ASR::is_a<ASR::StructType_t>(*ASRUtils::type_get_past_allocatable_pointer(cur_type)) ) { // TODO : Clean this by coordinating with `finalize_before_deallocate()`
                            if(ASRUtils::non_unlimited_polymorphic_class(ASRUtils::type_get_past_allocatable_pointer(cur_type)) && ASRUtils::is_pointer(cur_type) ){
                                auto const inner_struct = llvm_utils->CreateLoad2(llvm_utils->getStructType(struct_sym, module.get(), true), llvm_utils->create_gep2(llvm_data_type, tmp, 1));
                                llvm_utils->lfortran_free(inner_struct);
                            }
                            llvm::AllocaInst *arg_tmp = llvm_utils->CreateAlloca(*builder, character_type);
                            builder->CreateStore(builder->CreateBitCast(tmp, character_type), arg_tmp);
                            std::vector<llvm::Value*> args = {allocator, llvm_utils->CreateLoad2(character_type, arg_tmp)};
                            builder->CreateCall(free_fn, args);
                        }
                        builder->CreateStore(
                            llvm::ConstantPointerNull::get(llvm_data_type->getPointerTo()), tmp_); // Store NULL as identifier for deallocated variable.
                    }, [](){});
                } else {
                    if( LLVM::is_llvm_pointer(*cur_type) ) {
                        llvm::Type* typ = llvm_utils->get_type_from_ttype_t_util(tmp_expr, cur_type, module.get());
                        tmp = llvm_utils->CreateLoad2(typ, tmp);
                    }
                    llvm::Type* typ = llvm_utils->get_type_from_ttype_t_util(tmp_expr,
                        ASRUtils::type_get_past_pointer(
                            ASRUtils::type_get_past_allocatable(cur_type)),
                    module.get(), abt);
                    ASR::ttype_t* element_type = ASRUtils::type_get_past_array(
                        ASRUtils::type_get_past_pointer(
                            ASRUtils::type_get_past_allocatable(cur_type)));
                    // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                    llvm::Type* llvm_data_type = llvm_utils->get_el_type(tmp_expr, element_type, module.get());
                    llvm::Value *cond = arr_descr->get_is_allocated_flag(tmp, tmp_expr);
                    llvm_utils->create_if_else(cond, [=]() {
                        llvm_symtab_finalizer.finalize_before_deallocate(tmp, cur_type, struct_sym, in_struct);

                        if (ASRUtils::non_unlimited_polymorphic_class(element_type)) {
                            // Non-unlimited polymorphic class arrays use a single
                            // wrapper {vptr, data*} stored as the descriptor's data
                            // pointer. finalize_before_deallocate already frees the
                            // per-element payloads, the consecutive struct block and
                            // the wrapper itself, so skip call_lfortran_free to avoid
                            // a double-free; just null the descriptor's data pointer.
                            arr_descr->reset_is_allocated_flag(typ, tmp, llvm_data_type);
                        } else {
                            call_lfortran_free(free_fn, typ,  llvm_data_type);
                        }
                    }, [](){});
                }
            }
        }
    }

    void visit_ImplicitDeallocate(const ASR::ImplicitDeallocate_t& x) {
        visit_Deallocate(x);
    }

    void visit_ExplicitDeallocate(const ASR::ExplicitDeallocate_t& x) {
        visit_Deallocate(x);
    }

    void visit_ListConstant(const ASR::ListConstant_t& x) {
        ASR::List_t* list_type = ASR::down_cast<ASR::List_t>(x.m_type);
        bool is_array_type_local = false, is_malloc_array_type_local = false;
        bool is_list_local = false;
        ASR::dimension_t* m_dims_local = nullptr;
        int n_dims_local = -1, a_kind_local = -1;

        llvm::Type* llvm_el_type = llvm_utils->get_type_from_ttype_t(const_cast<ASR::expr_t*>(&x.base), list_type->m_type,
                                    nullptr, ASR::storage_typeType::Default, is_array_type_local,
                                    is_malloc_array_type_local, is_list_local, m_dims_local,
                                    n_dims_local, a_kind_local, module.get());
        std::string type_code = ASRUtils::get_type_code(list_type->m_type);
        int32_t type_size = -1;
        if( ASR::is_a<ASR::String_t>(*list_type->m_type) ||
            LLVM::is_llvm_struct(list_type->m_type) ||
            ASRUtils::is_allocatable_descriptor_string(list_type->m_type) ||
            ASR::is_a<ASR::Complex_t>(*list_type->m_type) ) {
            llvm::DataLayout data_layout(module->getDataLayout());
            type_size = data_layout.getTypeAllocSize(llvm_el_type);
        } else {
            type_size = ASRUtils::extract_kind_from_ttype_t(list_type->m_type);
        }
        llvm::Type* const_list_type = list_api->get_list_type(llvm_el_type, type_code, type_size);
        llvm::Value* const_list = llvm_utils->CreateAlloca(*builder, const_list_type, nullptr, "const_list");
        list_api->list_init(type_code, const_list, module.get(), x.n_args, x.n_args);
        for( size_t i = 0; i < x.n_args; i++ ) {
            visit_expr_load_wrapper(
                x.m_args[i],
                is_argument_of_type_CPtr(x.m_args[i]) || ASRUtils::is_character(*expr_type(x.m_args[i])) ? 0 : 1);
            llvm::Value* item = tmp;
            llvm::Value* pos = llvm::ConstantInt::get(context, llvm::APInt(32, i));
            list_api->write_item(const_cast<ASR::expr_t*>(&x.base), const_list, pos, item, list_type->m_type,
                                 false, module.get());
        }
        tmp = const_list;
    }

    void visit_DictConstant(const ASR::DictConstant_t& x) {
        llvm::Type* const_dict_type = llvm_utils->get_dict_type(const_cast<ASR::expr_t*>(&x.base), x.m_type, module.get());
        llvm::Value* const_dict = llvm_utils->CreateAlloca(*builder, const_dict_type, nullptr, "const_dict");
        ASR::Dict_t* x_dict = ASR::down_cast<ASR::Dict_t>(x.m_type);
        llvm_utils->set_dict_api(x_dict);
        llvm_utils->dict_api->dict_init(x_dict, const_dict, module.get(), x.n_keys);
        int64_t ptr_loads_key = !LLVM::is_llvm_struct(x_dict->m_key_type);
        int64_t ptr_loads_value = !LLVM::is_llvm_struct(x_dict->m_value_type);
        int64_t ptr_loads_copy = ptr_loads;
        for( size_t i = 0; i < x.n_keys; i++ ) {
            ptr_loads = ptr_loads_key;
            visit_expr_wrapper(x.m_keys[i], ASRUtils::is_character(*expr_type(x.m_keys[i])) ? 0 : 1);
            llvm::Value* key = tmp;
            ptr_loads = ptr_loads_value;
            visit_expr_wrapper(x.m_values[i], ASRUtils::is_character(*expr_type(x.m_values[i])) ? 0 : 1);
            llvm::Value* value = tmp;
            llvm_utils->dict_api->write_item(const_cast<ASR::expr_t*>(&x.base), const_dict, key, value, module.get(),
                                 x_dict->m_key_type, x_dict->m_value_type);
        }
        ptr_loads = ptr_loads_copy;
        tmp = const_dict;
    }

    void visit_SetConstant(const ASR::SetConstant_t& x) {
        llvm::Type* const_set_type = llvm_utils->get_set_type(const_cast<ASR::expr_t*>(&x.base), x.m_type, module.get());
        llvm::Value* const_set = llvm_utils->CreateAlloca(*builder, const_set_type, nullptr, "const_set");
        ASR::Set_t* x_set = ASR::down_cast<ASR::Set_t>(x.m_type);
        llvm_utils->set_set_api(x_set);
        std::string el_type_code = ASRUtils::get_type_code(x_set->m_type);
        llvm_utils->set_api->set_init(el_type_code, const_set, module.get(), x.n_elements);
        int64_t ptr_loads_el = !LLVM::is_llvm_struct(x_set->m_type);
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = ptr_loads_el;
        for( size_t i = 0; i < x.n_elements; i++ ) {
            visit_expr_wrapper(x.m_elements[i], true);
            llvm::Value* element = tmp;
            llvm_utils->set_api->write_item(const_cast<ASR::expr_t*>(&x.base), const_set, element, module.get(),
                                            x_set->m_type);
        }
        ptr_loads = ptr_loads_copy;
        tmp = const_set;
    }

    void visit_TupleConstant(const ASR::TupleConstant_t& x) {
        ASR::Tuple_t* tuple_type = ASR::down_cast<ASR::Tuple_t>(x.m_type);
        std::string type_code = ASRUtils::get_type_code(tuple_type->m_type,
                                                        tuple_type->n_type);
        std::vector<llvm::Type*> llvm_el_types;
        ASR::storage_typeType m_storage = ASR::storage_typeType::Default;
        bool is_array_type = false, is_malloc_array_type = false;
        bool is_list = false;
        ASR::dimension_t* m_dims = nullptr;
        int n_dims = 0, a_kind = -1;
        for( size_t i = 0; i < tuple_type->n_type; i++ ) {
            llvm_el_types.push_back(llvm_utils->get_type_from_ttype_t(const_cast<ASR::expr_t*>(&x.base), tuple_type->m_type[i],
                nullptr, m_storage, is_array_type, is_malloc_array_type,
                is_list, m_dims, n_dims, a_kind, module.get()));
        }
        llvm::Type* const_tuple_type = tuple_api->get_tuple_type(type_code, llvm_el_types);
        llvm::Value* const_tuple = llvm_utils->CreateAlloca(*builder, const_tuple_type, nullptr, "const_tuple");
        std::vector<llvm::Value*> init_values;
        int64_t ptr_loads_copy = ptr_loads;
        for( size_t i = 0; i < x.n_elements; i++ ) {
            if(ASRUtils::is_character(*tuple_type->m_type[i])){
                ptr_loads = 0;
            } else if(!LLVM::is_llvm_struct(tuple_type->m_type[i])) {
                ptr_loads = 2;
            }
            else {
                ptr_loads = ptr_loads_copy;
            }
            this->visit_expr(*x.m_elements[i]);
            init_values.push_back(tmp);
        }
        ptr_loads = ptr_loads_copy;
        tuple_api->tuple_init(const_cast<ASR::expr_t*>(&x.base), const_tuple, init_values, tuple_type,
                              module.get());
        // tuple_init deep-copies each element value into the tuple,
        // allocating new buffers. Free the original temporary data of
        // constant/concat elements whose heap buffers are now orphaned.
        for (size_t i = 0; i < x.n_elements; i++) {
            if (ASR::is_a<ASR::ListConstant_t>(*x.m_elements[i]) ||
                ASR::is_a<ASR::TupleConstant_t>(*x.m_elements[i]) ||
                ASR::is_a<ASR::TupleConcat_t>(*x.m_elements[i]) ||
                ASR::is_a<ASR::DictConstant_t>(*x.m_elements[i]) ||
                ASR::is_a<ASR::SetConstant_t>(*x.m_elements[i])) {
                llvm_symtab_finalizer.finalize_temporary(
                    init_values[i], tuple_type->m_type[i]);
            }
        }
        tmp = const_tuple;
    }

    void visit_IntegerBitLen(const ASR::IntegerBitLen_t& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr(*x.m_a);
        llvm::Value *int_val = tmp;
        int int_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
        std::string runtime_func_name = "_lpython_bit_length" + std::to_string(int_kind);
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getInt32Ty(context), {
                        llvm_utils->getIntType(int_kind)
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        tmp = builder->CreateCall(fn, {int_val});
    }

    void visit_Ichar(const ASR::Ichar_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_wrapper(x.m_arg, true);
        llvm::Value *c = llvm_utils->get_string_data(ASRUtils::get_string_type(x.m_arg), tmp);
        int kind = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(x.m_arg));
        llvm::Type* char_type = llvm_utils->getIntType(kind);
        llvm::Value *c_ptr = builder->CreateBitCast(c, char_type->getPointerTo());
        llvm::Value *char_val = llvm_utils->CreateLoad2(char_type, c_ptr);
        int ret_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
        tmp = builder->CreateZExtOrTrunc(char_val, llvm_utils->getIntType(ret_kind));
    }

    void visit_Iachar(const ASR::Iachar_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_load_wrapper(x.m_arg, 0);
        llvm::Value *c = llvm_utils->get_string_data(ASRUtils::get_string_type(x.m_arg), tmp);
        int kind = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(x.m_arg));
        llvm::Type* char_type = llvm_utils->getIntType(kind);
        llvm::Value *c_ptr = builder->CreateBitCast(c, char_type->getPointerTo());
        llvm::Value *char_val = llvm_utils->CreateLoad2(char_type, c_ptr);
        int ret_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
        tmp = builder->CreateZExtOrTrunc(char_val, llvm_utils->getIntType(ret_kind));
    }

    void visit_RealSqrt(const ASR::RealSqrt_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr(*x.m_arg);
        if (tmp->getType()->isPointerTy()) {
            llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_arg, x.m_type, module.get());
            tmp = llvm_utils->CreateLoad2(llvm_type, tmp);
        }
        llvm::Value *c = tmp;
        int64_t kind_value = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(x.m_arg));
        std::string func_name;
        if (kind_value ==4) {
            func_name = "llvm.sqrt.f32";
        } else if (kind_value == 8) {
            func_name = "llvm.sqrt.f64";
        } else if (kind_value == 16) {
            // Don't lower to llvm.sqrt.f128: on platforms where
            // `long double` != fp128 (notably Apple ARM64), LLVM will lower
            // that intrinsic to `sqrtl`, which reads only 64 bits of the
            // 128-bit operand and silently returns a wrong result (often 0).
            // Call into our portable runtime instead. `lf_sqrtq` uses the
            // LLVM fp128 SIMD ABI on Apple ARM64 (via a naked shim) and
            // forwards to the struct-ABI `lf_f128_sqrt` implementation.
            func_name = "lf_sqrtq";
        } else {
            throw CodeGenError("sqrt for real kind " + std::to_string(kind_value) + " is not supported yet.");
        }
        llvm::Type *type = llvm_utils->getFPType(kind_value);
        llvm::Function *fn_sqrt = module->getFunction(func_name);
        if (!fn_sqrt) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    type, {type}, false);
            fn_sqrt = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, func_name,
                    module.get());
        }
        tmp = builder->CreateCall(fn_sqrt, {c});
    }

    void visit_ListAppend(const ASR::ListAppend_t& x) {
        ASR::List_t* asr_list = ASR::down_cast<ASR::List_t>(ASRUtils::expr_type(x.m_a));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_a);
        llvm::Value* plist = tmp;

        ptr_loads = !(LLVM::is_llvm_struct(asr_list->m_type) || ASRUtils::is_character(*asr_list->m_type)) ? 1 : 0;
        this->visit_expr_wrapper(x.m_ele, true);
        llvm::Value *item = tmp;
        ptr_loads = ptr_loads_copy;
        list_api->append(x.m_a, plist, item, asr_list->m_type, module.get());
        // Free the temporary ListConstant data buffer after deep-copy into
        // the list.  The append above deep-copies the element, so the
        // original temporary's data is no longer needed.
        if (ASR::is_a<ASR::ListConstant_t>(*x.m_ele) &&
                ASR::is_a<ASR::List_t>(*asr_list->m_type)) {
            ASR::List_t* ele_list_t = ASR::down_cast<ASR::List_t>(
                asr_list->m_type);
            std::string tc = ASRUtils::get_type_code(
                ele_list_t->m_type, false, false);
            list_api->free_data_using_type(tc, item, module.get());
        }
        // Free heap resources inside a temporary TupleConstant/TupleConcat
        // after the deep-copy into the list (e.g. allocatable strings in the tuple).
        if (is_owned_tuple_temporary(x.m_ele) &&
                ASR::is_a<ASR::Tuple_t>(*asr_list->m_type)) {
            llvm_symtab_finalizer.finalize_temporary(item, asr_list->m_type);
        }
    }

    void visit_UnionInstanceMember(const ASR::UnionInstanceMember_t& x) {
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_v);
        ptr_loads = ptr_loads_copy;
        llvm::Value* union_llvm = tmp;
        ASR::Variable_t* member_var = ASR::down_cast<ASR::Variable_t>(
            ASRUtils::symbol_get_past_external(x.m_m));
        ASR::ttype_t* member_type_asr = ASRUtils::get_contained_type(member_var->m_type);
        if( ASR::is_a<ASR::StructType_t>(*member_type_asr) && !ASRUtils::is_class_type(member_type_asr) ) {
            current_der_type_name = get_type_key(member_var->m_type_declaration);
        }
        member_type_asr = member_var->m_type;
        llvm::Type* member_type_llvm = llvm_utils->getMemberType(member_type_asr, member_var, module.get())->getPointerTo();
        tmp = builder->CreateBitCast(union_llvm, member_type_llvm);
        if( is_assignment_target ) {
            return ;
        }
        if( ptr_loads > 0 ) {
            llvm::Type* value_type = llvm_utils->getMemberType(member_type_asr, member_var, module.get());
            tmp = llvm_utils->CreateLoad2(value_type, tmp);
        }
    }

    void visit_ListItem(const ASR::ListItem_t& x) {
        ASR::ttype_t* el_type = ASRUtils::get_contained_type(
                                        ASRUtils::expr_type(x.m_a));
        visit_expr_load_wrapper(x.m_a, 0);
        llvm::Value* plist = tmp;

        visit_expr_load_wrapper(x.m_pos, 1, true);
        llvm::Value *pos = tmp;

        tmp = list_api->read_item_using_ttype(el_type, plist, pos, compiler_options.po.bounds_checking, module.get(),
                (LLVM::is_llvm_struct(el_type) || ptr_loads == 0));
    }

    void visit_DictItem(const ASR::DictItem_t& x) {
        ASR::Dict_t* dict_type = ASR::down_cast<ASR::Dict_t>(
                                    ASRUtils::expr_type(x.m_a));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_a);
        llvm::Value* pdict = tmp;

        ptr_loads = !(LLVM::is_llvm_struct(dict_type->m_key_type) || ASRUtils::is_character(*dict_type->m_key_type)) ? 1 : 0;
        this->visit_expr_wrapper(x.m_key, true);
        ptr_loads = ptr_loads_copy;
        llvm::Value *key = tmp;
        if (x.m_default) {
            llvm::Type *val_type = llvm_utils->get_type_from_ttype_t_util(x.m_a, dict_type->m_value_type, module.get());
            llvm::Value *def_value_ptr = llvm_utils->CreateAlloca(*builder, val_type);
            ptr_loads = !LLVM::is_llvm_struct(dict_type->m_value_type);
            this->visit_expr_wrapper(x.m_default, true);
            ptr_loads = ptr_loads_copy;
            builder->CreateStore(tmp, def_value_ptr);
            llvm_utils->set_dict_api(dict_type);
            tmp = llvm_utils->dict_api->get_item(x.m_a, pdict, key, module.get(), dict_type, def_value_ptr,
                                  LLVM::is_llvm_struct(dict_type->m_value_type) ||
                                  ASRUtils::is_allocatable_descriptor_string(dict_type->m_value_type));
        } else {
            llvm_utils->set_dict_api(dict_type);
            tmp = llvm_utils->dict_api->read_item(x.m_a, pdict, key, module.get(), dict_type,
                                    compiler_options.po.bounds_checking,
                                    LLVM::is_llvm_struct(dict_type->m_value_type) ||
                                    ASRUtils::is_allocatable_descriptor_string(dict_type->m_value_type));
        }
    }

    void visit_DictPop(const ASR::DictPop_t& x) {
        ASR::Dict_t* dict_type = ASR::down_cast<ASR::Dict_t>(
                                    ASRUtils::expr_type(x.m_a));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_a);
        llvm::Value* pdict = tmp;

        ptr_loads = !(LLVM::is_llvm_struct(dict_type->m_key_type) || ASRUtils::is_character(*dict_type->m_key_type)) ? 1 : 0;
        this->visit_expr_wrapper(x.m_key, true);
        ptr_loads = ptr_loads_copy;
        llvm::Value *key = tmp;

        llvm_utils->set_dict_api(dict_type);
        tmp = llvm_utils->dict_api->pop_item(x.m_a, pdict, key, module.get(), dict_type,
                                 LLVM::is_llvm_struct(dict_type->m_value_type));
    }

    void visit_ListLen(const ASR::ListLen_t& x) {
        if (x.m_value) {
            this->visit_expr(*x.m_value);
        } else {
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr(*x.m_arg);
            ptr_loads = ptr_loads_copy;
            llvm::Value* plist = tmp;

            std::string type_code = ASRUtils::get_type_code(
                ASRUtils::get_contained_type(ASRUtils::expr_type(x.m_arg)));
            llvm::Type* list_type = list_api->get_list_type(nullptr, type_code, 0);
            tmp = list_api->len_using_type(list_type, plist);
            if (ASR::is_a<ASR::ListConstant_t>(*x.m_arg)) {
                llvm_symtab_finalizer.finalize_temporary(
                    plist, ASRUtils::expr_type(x.m_arg));
            }
        }
    }

    void visit_ListCompare(const ASR::ListCompare_t x) {
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_left);
        llvm::Value* left = tmp;
        this->visit_expr(*x.m_right);
        llvm::Value* right = tmp;
        ptr_loads = ptr_loads_copy;

        ASR::ttype_t* int32_type = ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4));

        if(x.m_op == ASR::cmpopType::Eq || x.m_op == ASR::cmpopType::NotEq) {
            tmp = llvm_utils->is_equal_by_value(left, right, module.get(),
                    ASRUtils::expr_type(x.m_left));
            if (x.m_op == ASR::cmpopType::NotEq) {
                tmp = builder->CreateNot(tmp);
            }
        }
        else if(x.m_op == ASR::cmpopType::Lt) {
            tmp = llvm_utils->is_ineq_by_value(left, right, module.get(),
                    ASRUtils::expr_type(x.m_left), 0, int32_type);
        }
        else if(x.m_op == ASR::cmpopType::LtE) {
            tmp = llvm_utils->is_ineq_by_value(left, right, module.get(),
                    ASRUtils::expr_type(x.m_left), 1, int32_type);
        }
        else if(x.m_op == ASR::cmpopType::Gt) {
            tmp = llvm_utils->is_ineq_by_value(left, right, module.get(),
                    ASRUtils::expr_type(x.m_left), 2, int32_type);
        }
        else if(x.m_op == ASR::cmpopType::GtE) {
            tmp = llvm_utils->is_ineq_by_value(left, right, module.get(),
                    ASRUtils::expr_type(x.m_left), 3, int32_type);
        }
    }

    void visit_DictLen(const ASR::DictLen_t& x) {
        if (x.m_value) {
            this->visit_expr(*x.m_value);
            return ;
        }

        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_arg);
        ptr_loads = ptr_loads_copy;
        llvm::Value* pdict = tmp;
        ASR::Dict_t* x_dict = ASR::down_cast<ASR::Dict_t>(ASRUtils::expr_type(x.m_arg));
        llvm_utils->set_dict_api(x_dict);
        llvm::Type* dict_type = llvm_utils->get_dict_type(x.m_arg, ASRUtils::expr_type(x.m_arg), module.get());
        tmp = llvm_utils->dict_api->len(dict_type, pdict);
        if (ASR::is_a<ASR::DictConstant_t>(*x.m_arg)) {
            llvm_symtab_finalizer.finalize_temporary(
                pdict, ASRUtils::expr_type(x.m_arg));
        }
    }

    void visit_SetLen(const ASR::SetLen_t& x) {
        if (x.m_value) {
            this->visit_expr(*x.m_value);
            return ;
        }

        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_arg);
        ptr_loads = ptr_loads_copy;
        llvm::Value* pset = tmp;
        ASR::Set_t* x_set = ASR::down_cast<ASR::Set_t>(ASRUtils::expr_type(x.m_arg));
        llvm_utils->set_set_api(x_set);
        llvm::Type* set_type = llvm_utils->get_set_type(x.m_arg, ASRUtils::expr_type(x.m_arg), module.get());
        tmp = llvm_utils->set_api->len(set_type, pset);
        if (ASR::is_a<ASR::SetConstant_t>(*x.m_arg)) {
            llvm_symtab_finalizer.finalize_temporary(
                pset, ASRUtils::expr_type(x.m_arg));
        }
    }

    void visit_ListInsert(const ASR::ListInsert_t& x) {
        ASR::List_t* asr_list = ASR::down_cast<ASR::List_t>(
                                    ASRUtils::expr_type(x.m_a));

        this->visit_expr_load_wrapper(x.m_a, 0);
        llvm::Value* plist = tmp;

        this->visit_expr_load_wrapper(x.m_pos, 1, true);
        llvm::Value *pos = tmp;

        visit_expr_load_wrapper(x.m_ele,
            !(LLVM::is_llvm_struct(asr_list->m_type) || ASRUtils::is_character(*asr_list->m_type)) ? 1 : 0,
            true);
        llvm::Value *item = tmp;

        list_api->insert_item(x.m_a, plist, pos, item, asr_list->m_type, module.get());
    }

    void visit_DictInsert(const ASR::DictInsert_t& x) {
        ASR::Dict_t* dict_type = ASR::down_cast<ASR::Dict_t>(
                                    ASRUtils::expr_type(x.m_a));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_a);
        llvm::Value* pdict = tmp;

        ptr_loads = !(LLVM::is_llvm_struct(dict_type->m_value_type) || ASRUtils::is_character(*dict_type->m_value_type));
        visit_expr_load_wrapper(x.m_key,
            !(LLVM::is_llvm_struct(dict_type->m_key_type) || ASRUtils::is_character(*dict_type->m_key_type)));
        llvm::Value *key = tmp;
        visit_expr_load_wrapper(x.m_value,
            !(LLVM::is_llvm_struct(dict_type->m_value_type) || ASRUtils::is_character(*dict_type->m_value_type)) ? 1 : 0,
            true);
        llvm::Value *value = tmp;
        ptr_loads = ptr_loads_copy;

        llvm_utils->set_dict_api(dict_type);
        llvm_utils->dict_api->write_item(x.m_a, pdict, key, value, module.get(),
                             dict_type->m_key_type,
                             dict_type->m_value_type);
    }

    void visit_Expr(const ASR::Expr_t& x) {
        this->visit_expr_wrapper(x.m_expression, false);
    }

    void visit_ListRemove(const ASR::ListRemove_t& x) {
        ASR::ttype_t* asr_el_type = ASRUtils::get_contained_type(ASRUtils::expr_type(x.m_a));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_a);
        llvm::Value* plist = tmp;

        ptr_loads = !LLVM::is_llvm_struct(asr_el_type);
        this->visit_expr_wrapper(x.m_ele, true);
        ptr_loads = ptr_loads_copy;
        llvm::Value *item = tmp;
        list_api->remove(plist, item, asr_el_type, module.get());
    }

    void visit_ListCount(const ASR::ListCount_t& x) {
        ASR::ttype_t *asr_el_type = ASRUtils::get_contained_type(ASRUtils::expr_type(x.m_arg));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_arg);
        llvm::Value* plist = tmp;

        ptr_loads = !LLVM::is_llvm_struct(asr_el_type);
        this->visit_expr_wrapper(x.m_ele, true);
        ptr_loads = ptr_loads_copy;
        llvm::Value *item = tmp;
        tmp = list_api->count(plist, item, asr_el_type, module.get());
    }

    void generate_ListIndex(ASR::expr_t* m_arg, ASR::expr_t* m_ele,
            ASR::expr_t* m_start=nullptr, ASR::expr_t* m_end=nullptr) {
        ASR::ttype_t *asr_el_type = ASRUtils::get_contained_type(ASRUtils::expr_type(m_arg));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*m_arg);
        llvm::Value* plist = tmp;

        ptr_loads = !LLVM::is_llvm_struct(asr_el_type);
        this->visit_expr_wrapper(m_ele, true);
        llvm::Value *item = tmp;

        llvm::Value* start = nullptr;
        llvm::Value* end = nullptr;
        if(m_start) {
            ptr_loads = 2;
            this->visit_expr_wrapper(m_start, true);
            start = tmp;
        }
        if(m_end) {
            ptr_loads = 2;
            this->visit_expr_wrapper(m_end, true);
            end = tmp;
        }

        ptr_loads = ptr_loads_copy;
        tmp = list_api->index(plist, item, start, end, asr_el_type, module.get());
    }

    void generate_Exp(ASR::expr_t* m_arg) {
        this->visit_expr_wrapper(m_arg, true);
        llvm::Value *item = tmp;
#if LLVM_VERSION_MAJOR >= 12
        tmp = builder->CreateUnaryIntrinsic(llvm::Intrinsic::exp, item);
#elif LLVM_VERSION_MAJOR >= 8
        tmp = builder->CreateIntrinsic(llvm::Intrinsic::exp, {item->getType()}, {item});
#else
        // LLVM 7: CreateIntrinsic(ID, Args, FMFSource)
        llvm::Function *fn = llvm::Intrinsic::getDeclaration(module.get(), llvm::Intrinsic::exp, {item->getType()});
        tmp = builder->CreateCall(fn, {item});
#endif
    }

    void generate_Exp2(ASR::expr_t* m_arg) {
        this->visit_expr_wrapper(m_arg, true);
        llvm::Value *item = tmp;
#if LLVM_VERSION_MAJOR >= 12
        tmp = builder->CreateUnaryIntrinsic(llvm::Intrinsic::exp2, item);
#elif LLVM_VERSION_MAJOR >= 8
        tmp = builder->CreateIntrinsic(llvm::Intrinsic::exp2, {item->getType()}, {item});
#else
        // LLVM 7: CreateIntrinsic(ID, Args, FMFSource)
        llvm::Function *fn = llvm::Intrinsic::getDeclaration(module.get(), llvm::Intrinsic::exp2, {item->getType()});
        tmp = builder->CreateCall(fn, {item});
#endif
    }

    void generate_Expm1(ASR::expr_t* m_arg) {
        this->visit_expr_wrapper(m_arg, true);
        llvm::Value *item = tmp;
#if LLVM_VERSION_MAJOR >= 12
        llvm::Value* exp = builder->CreateUnaryIntrinsic(llvm::Intrinsic::exp, item);
#elif LLVM_VERSION_MAJOR >= 8
        llvm::Value* exp = builder->CreateIntrinsic(llvm::Intrinsic::exp, {item->getType()}, {item});
#else
        // LLVM 7: CreateIntrinsic(ID, Args, FMFSource)
        llvm::Function *fn = llvm::Intrinsic::getDeclaration(module.get(), llvm::Intrinsic::exp, {item->getType()});
        llvm::Value* exp = builder->CreateCall(fn, {item});
#endif
        llvm::Value* one = llvm::ConstantFP::get(builder->getFloatTy(), 1.0);
        tmp = builder->CreateFSub(exp, one);
    }

    void generate_Abs(ASR::expr_t* m_arg) {
        // Mirror visit_RealBinOp: if the argument has a Pointer wrapper
        // (e.g. an associate-name target), load twice so we reach the
        // underlying scalar value before applying fabs/select.
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = LLVM::is_llvm_pointer(*ASRUtils::expr_type(m_arg)) ? 2 : 1;
        this->visit_expr_wrapper(m_arg, true);
        ptr_loads = ptr_loads_copy;
        llvm::Value *item = tmp;
        load_non_array_non_character_pointers(m_arg, ASRUtils::expr_type(m_arg), item);
        llvm::Type *item_type = item->getType();
        if (item_type->isFloatingPointTy()) {
#if LLVM_VERSION_MAJOR >= 12
            tmp = builder->CreateUnaryIntrinsic(llvm::Intrinsic::fabs, item);
#elif LLVM_VERSION_MAJOR >= 8
            tmp = builder->CreateIntrinsic(llvm::Intrinsic::fabs, {item_type}, {item});
#else
            llvm::Function *fn = llvm::Intrinsic::getDeclaration(module.get(),
                llvm::Intrinsic::fabs, {item_type});
            tmp = builder->CreateCall(fn, {item});
#endif
        } else if (item_type->isIntegerTy()) {
            // abs(x) = x >= 0 ? x : -x (branchless with select)
            llvm::Value *zero = llvm::ConstantInt::get(item_type, 0);
            llvm::Value *neg = builder->CreateNeg(item);
            llvm::Value *cmp = builder->CreateICmpSGE(item, zero);
            tmp = builder->CreateSelect(cmp, item, neg);
        }
    }

    void generate_MinMax(ASR::expr_t** m_args, size_t n_args, bool is_max) {
        LCOMPILERS_ASSERT(n_args >= 2);
        this->visit_expr_wrapper(m_args[0], true);
        llvm::Value *result = tmp;
        llvm::Type *val_type = result->getType();
        for (size_t i = 1; i < n_args; i++) {
            this->visit_expr_wrapper(m_args[i], true);
            llvm::Value *arg = tmp;
            if (val_type->isFloatingPointTy()) {
                // Use ordered comparison for Fortran-compatible NaN propagation
                // If either operand is NaN, comparison returns false, preserving result
                llvm::Value *cmp = is_max
                    ? builder->CreateFCmpOGT(arg, result)
                    : builder->CreateFCmpOLT(arg, result);
                result = builder->CreateSelect(cmp, arg, result);
            } else if (val_type->isIntegerTy()) {
                llvm::Value *cmp = is_max
                    ? builder->CreateICmpSGT(result, arg)
                    : builder->CreateICmpSLT(result, arg);
                result = builder->CreateSelect(cmp, result, arg);
            }
        }
        tmp = result;
    }

    void generate_ListReverse(ASR::expr_t* m_arg) {
        ASR::ttype_t* asr_el_type = ASRUtils::get_contained_type(ASRUtils::expr_type(m_arg));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*m_arg);
        llvm::Value* plist = tmp;

        ptr_loads = !LLVM::is_llvm_struct(asr_el_type);
        ptr_loads = ptr_loads_copy;

        list_api->reverse(asr_el_type, plist, module.get());
    }

    void generate_ListPop_0(ASR::expr_t* m_arg) {
        ASR::ttype_t* asr_el_type = ASRUtils::get_contained_type(ASRUtils::expr_type(m_arg));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*m_arg);
        llvm::Value* plist = tmp;

        ptr_loads = !LLVM::is_llvm_struct(asr_el_type);
        ptr_loads = ptr_loads_copy;
        tmp = list_api->pop_last(plist, asr_el_type, module.get());
    }

    void generate_ListPop_1(ASR::expr_t* m_arg, ASR::expr_t* m_ele) {
        ASR::ttype_t* asr_el_type = ASRUtils::get_contained_type(ASRUtils::expr_type(m_arg));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*m_arg);
        llvm::Value* plist = tmp;

        ptr_loads = 2;
        this->visit_expr_wrapper(m_ele, true);
        ptr_loads = ptr_loads_copy;
        llvm::Value *pos = tmp;
        tmp = list_api->pop_position(m_arg, plist, pos, asr_el_type, module.get());
    }

    void generate_ListReserve(ASR::expr_t* m_arg, ASR::expr_t* m_ele) {
        // For now, this only handles lists
        ASR::ttype_t* asr_el_type = ASRUtils::get_contained_type(ASRUtils::expr_type(m_arg));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*m_arg);
        llvm::Value* plist = tmp;

        ptr_loads = 2;
        this->visit_expr_wrapper(m_ele, true);
        ptr_loads = ptr_loads_copy;
        llvm::Value* n = tmp;
        list_api->reserve(plist, n, asr_el_type, module.get());
    }

    void generate_DictElems(ASR::expr_t* m_arg, bool key_or_value) {
        ASR::Dict_t* dict_type = ASR::down_cast<ASR::Dict_t>(
                                    ASRUtils::expr_type(m_arg));
        ASR::ttype_t* el_type = key_or_value == 0 ?
                                    dict_type->m_key_type : dict_type->m_value_type;

        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*m_arg);
        llvm::Value* pdict = tmp;

        ptr_loads = ptr_loads_copy;

        bool is_array_type_local = false, is_malloc_array_type_local = false;
        bool is_list_local = false;
        ASR::dimension_t* m_dims_local = nullptr;
        int n_dims_local = -1, a_kind_local = -1;
        llvm::Type* llvm_el_type = llvm_utils->get_type_from_ttype_t(m_arg, el_type, nullptr,
                                    ASR::storage_typeType::Default, is_array_type_local,
                                    is_malloc_array_type_local, is_list_local, m_dims_local,
                                    n_dims_local, a_kind_local, module.get());
        std::string type_code = ASRUtils::get_type_code(el_type);
        int32_t type_size = -1;
        if( ASR::is_a<ASR::String_t>(*el_type) ||
            LLVM::is_llvm_struct(el_type) ||
            ASR::is_a<ASR::Complex_t>(*el_type) ) {
            llvm::DataLayout data_layout(module->getDataLayout());
            type_size = data_layout.getTypeAllocSize(llvm_el_type);
        } else {
            type_size = ASRUtils::extract_kind_from_ttype_t(el_type);
        }
        llvm::Type* el_list_type = list_api->get_list_type(llvm_el_type, type_code, type_size);
        llvm::Value* el_list = llvm_utils->CreateAlloca(*builder, el_list_type, nullptr, key_or_value == 0 ?
                                                    "keys_list" : "values_list");
        list_api->list_init(type_code, el_list, module.get(), 0, 0);

        llvm_utils->set_dict_api(dict_type);
        llvm_utils->dict_api->get_elements_list(m_arg, pdict, el_list, dict_type->m_key_type,
                                                dict_type->m_value_type, module.get(), key_or_value);
        tmp = el_list;
    }

    void generate_SetAdd(ASR::expr_t* m_arg, ASR::expr_t* m_ele) {
        ASR::Set_t* set_type = ASR::down_cast<ASR::Set_t>(
                                    ASRUtils::expr_type(m_arg));
        ASR::ttype_t* asr_el_type = ASRUtils::get_contained_type(ASRUtils::expr_type(m_arg));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*m_arg);
        llvm::Value* pset = tmp;

        ptr_loads = 2;
        this->visit_expr_wrapper(m_ele, true);
        ptr_loads = ptr_loads_copy;
        llvm::Value *el = tmp;
        llvm_utils->set_set_api(set_type);
        llvm_utils->set_api->write_item(m_arg, pset, el, module.get(), asr_el_type);
    }

    void generate_SetRemove(ASR::expr_t* m_arg, ASR::expr_t* m_ele) {
        ASR::Set_t* set_type = ASR::down_cast<ASR::Set_t>(
                                    ASRUtils::expr_type(m_arg));
        ASR::ttype_t* asr_el_type = ASRUtils::get_contained_type(ASRUtils::expr_type(m_arg));
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*m_arg);
        llvm::Value* pset = tmp;

        ptr_loads = 2;
        this->visit_expr_wrapper(m_ele, true);
        ptr_loads = ptr_loads_copy;
        llvm::Value *el = tmp;
        llvm_utils->set_set_api(set_type);
        llvm_utils->set_api->remove_item(pset, el, module.get(), asr_el_type);
    }

    void visit_IntrinsicElementalFunction(const ASR::IntrinsicElementalFunction_t& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        switch (static_cast<ASRUtils::IntrinsicElementalFunctions>(x.m_intrinsic_id)) {
            case ASRUtils::IntrinsicElementalFunctions::ListIndex: {
                ASR::expr_t* m_arg = x.m_args[0];
                ASR::expr_t* m_ele = x.m_args[1];
                ASR::expr_t* m_start = nullptr;
                ASR::expr_t* m_end = nullptr;
                switch (x.m_overload_id) {
                    case 0: {
                        break ;
                    }
                    case 1: {
                        m_start = x.m_args[2];
                        break ;
                    }
                    case 2: {
                        m_start = x.m_args[2];
                        m_end = x.m_args[3];
                        break ;
                    }
                    default: {
                        throw CodeGenError("list.index accepts at most four arguments",
                                            x.base.base.loc);
                    }
                }
                generate_ListIndex(m_arg, m_ele, m_start, m_end);
                break ;
            }
            case ASRUtils::IntrinsicElementalFunctions::ListReverse: {
                generate_ListReverse(x.m_args[0]);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::ListPop: {
                switch(x.m_overload_id) {
                    case 0:
                        generate_ListPop_0(x.m_args[0]);
                        break;
                    case 1:
                        generate_ListPop_1(x.m_args[0], x.m_args[1]);
                        break;
                }
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::ListReserve: {
                generate_ListReserve(x.m_args[0], x.m_args[1]);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::DictKeys: {
                generate_DictElems(x.m_args[0], 0);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::DictValues: {
                generate_DictElems(x.m_args[0], 1);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::SetAdd: {
                generate_SetAdd(x.m_args[0], x.m_args[1]);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::SetRemove: {
                generate_SetRemove(x.m_args[0], x.m_args[1]);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::Exp: {
                switch (x.m_overload_id) {
                    case 0: {
                        ASR::expr_t* m_arg = x.m_args[0];
                        generate_Exp(m_arg);
                        break ;
                    }
                    default: {
                        throw CodeGenError("exp() only accepts one argument",
                                            x.base.base.loc);
                    }
                }
                break ;
            }
            case ASRUtils::IntrinsicElementalFunctions::Exp2: {
                switch (x.m_overload_id) {
                    case 0: {
                        ASR::expr_t* m_arg = x.m_args[0];
                        generate_Exp2(m_arg);
                        break ;
                    }
                    default: {
                        throw CodeGenError("exp2() only accepts one argument",
                                            x.base.base.loc);
                    }
                }
                break ;
            }
            case ASRUtils::IntrinsicElementalFunctions::CommandArgumentCount: {
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::ThisImage: {
                // Non-coarray fallback: single image, always 1
                tmp = llvm::ConstantInt::get(context, llvm::APInt(32, 1, true));
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::NumImages: {
                // Non-coarray fallback: single image, always 1
                tmp = llvm::ConstantInt::get(context, llvm::APInt(32, 1, true));
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::Expm1: {
                switch (x.m_overload_id) {
                    case 0: {
                        ASR::expr_t* m_arg = x.m_args[0];
                        generate_Expm1(m_arg);
                        break ;
                    }
                    default: {
                        throw CodeGenError("expm1() only accepts one argument",
                                            x.base.base.loc);
                    }
                }
                break ;
            }
            case ASRUtils::IntrinsicElementalFunctions::FlipSign: {
                Vec<ASR::call_arg_t> args;
                args.reserve(al, 2);
                ASR::call_arg_t arg0_, arg1_;
                arg0_.loc = x.m_args[0]->base.loc, arg0_.m_value = x.m_args[0];
                args.push_back(al, arg0_);
                arg1_.loc = x.m_args[1]->base.loc, arg1_.m_value = x.m_args[1];
                args.push_back(al, arg1_);
                generate_flip_sign(args.p);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::FMA: {
                Vec<ASR::call_arg_t> args;
                args.reserve(al, 3);
                ASR::call_arg_t arg0_, arg1_, arg2_;
                arg0_.loc = x.m_args[0]->base.loc, arg0_.m_value = x.m_args[0];
                args.push_back(al, arg0_);
                arg1_.loc = x.m_args[1]->base.loc, arg1_.m_value = x.m_args[1];
                args.push_back(al, arg1_);
                arg2_.loc = x.m_args[2]->base.loc, arg2_.m_value = x.m_args[2];
                args.push_back(al, arg2_);
                generate_fma(args.p);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::SignFromValue: {
                Vec<ASR::call_arg_t> args;
                args.reserve(al, 2);
                ASR::call_arg_t arg0_, arg1_;
                arg0_.loc = x.m_args[0]->base.loc, arg0_.m_value = x.m_args[0];
                args.push_back(al, arg0_);
                arg1_.loc = x.m_args[1]->base.loc, arg1_.m_value = x.m_args[1];
                args.push_back(al, arg1_);
                generate_sign_from_value(args.p);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::Abs: {
                generate_Abs(x.m_args[0]);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::Max: {
                generate_MinMax(x.m_args, x.n_args, true);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::Min: {
                generate_MinMax(x.m_args, x.n_args, false);
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::SameTypeAs: {
                if (x.m_value) {
                    this->visit_expr(*x.m_value);
                    break;
                }
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr(*x.m_args[0]);
                llvm::Value* arg0 = tmp;
                this->visit_expr(*x.m_args[1]);
                llvm::Value* arg1 = tmp;
                ptr_loads = ptr_loads_copy;

                ASR::symbol_t* struct_sym0 = ASRUtils::symbol_get_past_external(
                    ASRUtils::get_struct_sym_from_struct_expr(x.m_args[0]));
                ASR::symbol_t* struct_sym1 = ASRUtils::symbol_get_past_external(
                    ASRUtils::get_struct_sym_from_struct_expr(x.m_args[1]));
                llvm::Type* class_type0 = llvm_utils->getClassType(
                    ASR::down_cast<ASR::Struct_t>(struct_sym0), false);
                llvm::Type* class_type1 = llvm_utils->getClassType(
                    ASR::down_cast<ASR::Struct_t>(struct_sym1), false);

                // For allocatable/pointer class(*), the alloca holds a pointer
                // to the polymorphic struct. Load the pointer first.
                ASR::ttype_t* arg_type0 = ASRUtils::expr_type(x.m_args[0]);
                ASR::ttype_t* arg_type1 = ASRUtils::expr_type(x.m_args[1]);
                if (ASRUtils::is_allocatable(arg_type0) ||
                    ASR::is_a<ASR::Pointer_t>(*arg_type0)) {
                    arg0 = llvm_utils->CreateLoad2(class_type0->getPointerTo(), arg0);
                }
                if (ASRUtils::is_allocatable(arg_type1) ||
                    ASR::is_a<ASR::Pointer_t>(*arg_type1)) {
                    arg1 = llvm_utils->CreateLoad2(class_type1->getPointerTo(), arg1);
                }

                // Extract field 0 (type hash or vtable pointer) from both args
                llvm::Value* id0_ptr = llvm_utils->create_gep2(class_type0, arg0, 0);
                llvm::Value* id1_ptr = llvm_utils->create_gep2(class_type1, arg1, 0);
                // Load and compare
                llvm::Type* field0_type = llvm::cast<llvm::StructType>(class_type0)->getElementType(0);
                llvm::Value* id0 = llvm_utils->CreateLoad2(field0_type, id0_ptr);
                llvm::Value* id1 = llvm_utils->CreateLoad2(field0_type, id1_ptr);
                if (field0_type->isPointerTy()) {
                    // new_classes: compare vtable pointers
                    tmp = builder->CreateICmpEQ(
                        builder->CreatePtrToInt(id0, llvm::Type::getInt64Ty(context)),
                        builder->CreatePtrToInt(id1, llvm::Type::getInt64Ty(context)));
                } else {
                    // old classes: compare integer type hashes
                    tmp = builder->CreateICmpEQ(id0, id1);
                }
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::ExtendsTypeOf: {
                if (x.m_value) {
                    this->visit_expr(*x.m_value);
                    break;
                }
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr(*x.m_args[0]);
                llvm::Value* arg0 = tmp;
                this->visit_expr(*x.m_args[1]);
                llvm::Value* arg1 = tmp;
                ptr_loads = ptr_loads_copy;

                ASR::symbol_t* struct_sym0 = ASRUtils::symbol_get_past_external(
                    ASRUtils::get_struct_sym_from_struct_expr(x.m_args[0]));
                ASR::symbol_t* struct_sym1 = ASRUtils::symbol_get_past_external(
                    ASRUtils::get_struct_sym_from_struct_expr(x.m_args[1]));
                llvm::Type* class_type0 = llvm_utils->getClassType(
                    ASR::down_cast<ASR::Struct_t>(struct_sym0), false);
                llvm::Type* class_type1 = llvm_utils->getClassType(
                    ASR::down_cast<ASR::Struct_t>(struct_sym1), false);

                // For allocatable/pointer class(*), load the pointer first.
                ASR::ttype_t* arg_type0 = ASRUtils::expr_type(x.m_args[0]);
                ASR::ttype_t* arg_type1 = ASRUtils::expr_type(x.m_args[1]);
                if (ASRUtils::is_allocatable(arg_type0) ||
                    ASR::is_a<ASR::Pointer_t>(*arg_type0)) {
                    arg0 = llvm_utils->CreateLoad2(class_type0->getPointerTo(), arg0);
                }
                if (ASRUtils::is_allocatable(arg_type1) ||
                    ASR::is_a<ASR::Pointer_t>(*arg_type1)) {
                    arg1 = llvm_utils->CreateLoad2(class_type1->getPointerTo(), arg1);
                }

                // Extract field 0 (type hash or vtable pointer)
                llvm::Value* id0_ptr = llvm_utils->create_gep2(class_type0, arg0, 0);
                llvm::Value* id1_ptr = llvm_utils->create_gep2(class_type1, arg1, 0);
                llvm::Type* field0_type = llvm::cast<llvm::StructType>(class_type0)->getElementType(0);
                llvm::Value* id0 = llvm_utils->CreateLoad2(field0_type, id0_ptr);
                llvm::Value* id1 = llvm_utils->CreateLoad2(field0_type, id1_ptr);

                if (field0_type->isPointerTy()) {
                    // new_classes: walk parent chain via TypeInfo
                    // TypeInfo layout: { i8* name, i8* size, i8* parent_typeinfo }
                    // vptr[-1] = TypeInfo pointer
                    llvm::Type* i8_ptr = llvm::Type::getInt8Ty(context)->getPointerTo();
                    llvm::Type* i64_type = llvm::Type::getInt64Ty(context);
                    llvm::StructType* type_info_type = llvm::StructType::get(
                        context, {i8_ptr, i8_ptr, i8_ptr}, false);

                    // Get TypeInfo for MOLD (arg1)
                    llvm::Value* vptr1_i8 = builder->CreateBitCast(id1, i8_ptr->getPointerTo());
                    llvm::Value* ti1_ptr_ptr = llvm_utils->create_ptr_gep2(
                        i8_ptr, vptr1_i8, llvm::ConstantInt::get(i64_type, -1, true));
                    llvm::Value* ti_mold = llvm_utils->CreateLoad2(i8_ptr, ti1_ptr_ptr);

                    // Get TypeInfo for A (arg0)
                    llvm::Value* vptr0_i8 = builder->CreateBitCast(id0, i8_ptr->getPointerTo());
                    llvm::Value* ti0_ptr_ptr = llvm_utils->create_ptr_gep2(
                        i8_ptr, vptr0_i8, llvm::ConstantInt::get(i64_type, -1, true));
                    llvm::Value* ti_a_init = llvm_utils->CreateLoad2(i8_ptr, ti0_ptr_ptr);

                    // Create loop to walk A's parent chain
                    llvm::Function* fn = builder->GetInsertBlock()->getParent();
                    llvm::BasicBlock* loop_bb = llvm::BasicBlock::Create(context, "eto.loop", fn);
                    llvm::BasicBlock* found_bb = llvm::BasicBlock::Create(context, "eto.found", fn);
                    llvm::BasicBlock* not_found_bb = llvm::BasicBlock::Create(context, "eto.notfound", fn);
                    llvm::BasicBlock* merge_bb = llvm::BasicBlock::Create(context, "eto.merge", fn);

                    builder->CreateBr(loop_bb);
                    builder->SetInsertPoint(loop_bb);
                    llvm::PHINode* ti_current = builder->CreatePHI(i8_ptr, 2, "eto.ti");
                    ti_current->addIncoming(ti_a_init, loop_bb->getSinglePredecessor());

                    // Compare current TypeInfo with MOLD's TypeInfo
                    llvm::Value* eq = builder->CreateICmpEQ(
                        builder->CreatePtrToInt(ti_current, i64_type),
                        builder->CreatePtrToInt(ti_mold, i64_type));
                    builder->CreateCondBr(eq, found_bb, not_found_bb);

                    // Not equal: check parent
                    builder->SetInsertPoint(not_found_bb);
                    llvm::Value* ti_cast = builder->CreateBitCast(
                        ti_current, type_info_type->getPointerTo());
                    llvm::Value* parent_ptr = llvm_utils->create_gep2(type_info_type, ti_cast, 2);
                    llvm::Value* parent_ti = llvm_utils->CreateLoad2(i8_ptr, parent_ptr);
                    llvm::Value* parent_null = builder->CreateICmpEQ(
                        parent_ti, llvm::ConstantPointerNull::get(
                            llvm::cast<llvm::PointerType>(i8_ptr)));
                    // If parent is null, no more ancestors -> false
                    // Otherwise, continue loop with parent
                    builder->CreateCondBr(parent_null, merge_bb, loop_bb);
                    ti_current->addIncoming(parent_ti, not_found_bb);

                    // Found: result = true
                    builder->SetInsertPoint(found_bb);
                    builder->CreateBr(merge_bb);

                    // Merge
                    builder->SetInsertPoint(merge_bb);
                    llvm::PHINode* result = builder->CreatePHI(
                        llvm::Type::getInt1Ty(context), 2, "eto.result");
                    result->addIncoming(llvm::ConstantInt::getTrue(context), found_bb);
                    result->addIncoming(llvm::ConstantInt::getFalse(context), not_found_bb);
                    tmp = result;
                } else {
                    // old classes: compare integer type hashes
                    tmp = builder->CreateICmpEQ(id0, id1);
                }
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::Present: {
                ASR::expr_t* arg_expr = x.m_args[0];
                LCOMPILERS_ASSERT(ASR::is_a<ASR::Var_t>(*arg_expr));
                ASR::Variable_t* arg_var = ASR::down_cast<ASR::Variable_t>(
                    ASR::down_cast<ASR::Var_t>(arg_expr)->m_v);
                uint32_t h = get_hash((ASR::asr_t*) arg_var);
                llvm::Value* arg_ptr = llvm_symtab[h];
                llvm::Type* ptr_type = llvm::PointerType::getUnqual(llvm::Type::getInt8Ty(context));
                llvm::Value* loaded = llvm_utils->CreateLoad2(ptr_type, arg_ptr);
                tmp = builder->CreateICmpNE(loaded,
                    llvm::ConstantPointerNull::get(
                        llvm::cast<llvm::PointerType>(ptr_type)));
                break;
            }
            case ASRUtils::IntrinsicElementalFunctions::StorageSize: {
                ASR::expr_t* arg_expr = x.m_args[0];
                ASR::ttype_t* arg_type = ASRUtils::expr_type(arg_expr);
                
                 ASR::ttype_t* base_type = ASRUtils::extract_type(arg_type);

                if (ASRUtils::is_character(*base_type)) {
                    ASR::ttype_t* int_type = ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4));
                    ASR::expr_t* str_len_expr = ASRUtils::EXPR(ASR::make_StringLen_t(
                        al, x.base.base.loc, arg_expr, int_type, nullptr));

                    this->visit_expr(*str_len_expr);
                    llvm::Value* str_len = tmp;

                    int return_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
                    llvm::Type* dest_type = llvm_utils->getIntType(return_kind);
                    
                    if (str_len->getType() != dest_type) {
                        str_len = builder->CreateIntCast(str_len, dest_type, /*isSigned=*/true);
                    }

                    llvm::Value* bits_per_byte = llvm::ConstantInt::get(dest_type, 8);
                    tmp = builder->CreateMul(str_len, bits_per_byte, "storage_size_bits");

                } else {
                    throw CodeGenError("Runtime storage_size for this type is not yet implemented", x.base.base.loc);
                }
                break;
            }
            default: {
                throw CodeGenError("Either the '" + ASRUtils::IntrinsicElementalFunctionRegistry::
                        get_intrinsic_function_name(x.m_intrinsic_id) +
                        "' intrinsic is not implemented by LLVM backend or "
                        "the compile-time value is not available", x.base.base.loc);
            }
        }
    }

    void visit_IntrinsicImpureFunction(const ASR::IntrinsicImpureFunction_t &x) {
        switch (static_cast<ASRUtils::IntrinsicImpureFunctions>(x.m_impure_intrinsic_id)) {
            case ASRUtils::IntrinsicImpureFunctions::IsIostatEnd : {
                this->visit_expr(*x.m_args[0]);
                tmp = builder->CreateICmpEQ(tmp, llvm::ConstantInt::get(context, llvm::APInt(32, -1, true)));
                break ;
            } case ASRUtils::IntrinsicImpureFunctions::IsIostatEor : {
                this->visit_expr(*x.m_args[0]);
                tmp = builder->CreateICmpEQ(tmp, llvm::ConstantInt::get(context, llvm::APInt(32, -2, true)));
                break ;
            } case ASRUtils::IntrinsicImpureFunctions::Allocated : {
                handle_allocated(x.m_args[0]);
                break ;
            } default: {
                throw CodeGenError( ASRUtils::get_impure_intrinsic_name(x.m_impure_intrinsic_id) +
                        " is not implemented by LLVM backend.", x.base.base.loc);
            }
        }
    }

    void visit_TypeInquiry(const ASR::TypeInquiry_t &x) {
        this->visit_expr(*x.m_value);
    }

    void visit_ListClear(const ASR::ListClear_t& x) {
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_a);
        llvm::Value* plist = tmp;
        ptr_loads = ptr_loads_copy;
        llvm::Type* list_type = llvm_utils->get_type_from_ttype_t_util(x.m_a, ASRUtils::expr_type(x.m_a), module.get());
        list_api->list_clear_using_type(list_type, plist);
    }

    void visit_ListRepeat(const ASR::ListRepeat_t& x) {
        this->visit_expr_wrapper(x.m_left, true);
        llvm::Value *left = tmp;
        ptr_loads = 2;      // right is int always
        this->visit_expr_wrapper(x.m_right, true);
        llvm::Value *right = tmp;

        ASR::List_t* list_type = ASR::down_cast<ASR::List_t>(x.m_type);
        bool is_array_type_local = false, is_malloc_array_type_local = false;
        bool is_list_local = false;
        ASR::dimension_t* m_dims_local = nullptr;
        int n_dims_local = -1, a_kind_local = -1;
        llvm::Type* llvm_el_type = llvm_utils->get_type_from_ttype_t(const_cast<ASR::expr_t*>(&x.base), list_type->m_type,
            nullptr, ASR::storage_typeType::Default, is_array_type_local,
            is_malloc_array_type_local, is_list_local, m_dims_local,
            n_dims_local, a_kind_local, module.get());
        std::string type_code = ASRUtils::get_type_code(list_type->m_type);
        int32_t type_size = -1;
        if( ASR::is_a<ASR::String_t>(*list_type->m_type) ||
            LLVM::is_llvm_struct(list_type->m_type) ||
            ASR::is_a<ASR::Complex_t>(*list_type->m_type) ) {
            llvm::DataLayout data_layout(module->getDataLayout());
            type_size = data_layout.getTypeAllocSize(llvm_el_type);
        } else {
            type_size = ASRUtils::extract_kind_from_ttype_t(list_type->m_type);
        }
        llvm::Type* repeat_list_type = list_api->get_list_type(llvm_el_type, type_code, type_size);
        llvm::Value* repeat_list = llvm_utils->CreateAlloca(*builder, repeat_list_type, nullptr, "repeat_list");
        llvm::Value* left_len = list_api->len_using_type(repeat_list_type, left);
        llvm::Value* capacity = builder->CreateMul(left_len, right);
        list_api->list_init(type_code, repeat_list, module.get(), capacity, capacity);
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 1;
        list_api->list_repeat_copy(list_type, repeat_list, left, right, left_len, module.get());
        ptr_loads = ptr_loads_copy;
        tmp = repeat_list;
    }

    // Returns true when `e` is a tuple expression whose LLVM alloca
    // owns deep-copied heap resources (e.g. strings) that must be freed
    // after the value has been consumed (deep-copied or compared).
    static bool is_owned_tuple_temporary(ASR::expr_t* e) {
        return ASR::is_a<ASR::TupleConstant_t>(*e)
            || ASR::is_a<ASR::TupleConcat_t>(*e);
    }

    void visit_TupleCompare(const ASR::TupleCompare_t& x) {
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_left);
        llvm::Value* left = tmp;
        this->visit_expr(*x.m_right);
        llvm::Value* right = tmp;
        ptr_loads = ptr_loads_copy;
        if(x.m_op == ASR::cmpopType::Eq || x.m_op == ASR::cmpopType::NotEq) {
            tmp = llvm_utils->is_equal_by_value(left, right, module.get(),
                    ASRUtils::expr_type(x.m_left));
            if (x.m_op == ASR::cmpopType::NotEq) {
                tmp = builder->CreateNot(tmp);
            }
        }
        else if(x.m_op == ASR::cmpopType::Lt) {
            tmp = llvm_utils->is_ineq_by_value(left, right, module.get(),
                    ASRUtils::expr_type(x.m_left), 0);
        }
        else if(x.m_op == ASR::cmpopType::LtE) {
            tmp = llvm_utils->is_ineq_by_value(left, right, module.get(),
                    ASRUtils::expr_type(x.m_left), 1);
        }
        else if(x.m_op == ASR::cmpopType::Gt) {
            tmp = llvm_utils->is_ineq_by_value(left, right, module.get(),
                    ASRUtils::expr_type(x.m_left), 2);
        }
        else if(x.m_op == ASR::cmpopType::GtE) {
            tmp = llvm_utils->is_ineq_by_value(left, right, module.get(),
                    ASRUtils::expr_type(x.m_left), 3);
        }
        // Free heap resources in temporary tuple operands after comparison
        if (is_owned_tuple_temporary(x.m_left)) {
            llvm_symtab_finalizer.finalize_temporary(
                left, ASRUtils::expr_type(x.m_left));
        }
        if (is_owned_tuple_temporary(x.m_right)) {
            llvm_symtab_finalizer.finalize_temporary(
                right, ASRUtils::expr_type(x.m_right));
        }
    }

    void visit_TupleLen(const ASR::TupleLen_t& x) {
        LCOMPILERS_ASSERT(x.m_value);
        this->visit_expr(*x.m_value);
    }

    void visit_TupleItem(const ASR::TupleItem_t& x) {
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_a);
        ptr_loads = ptr_loads_copy;
        llvm::Value* ptuple = tmp;

        this->visit_expr_wrapper(x.m_pos, true);
        llvm::Value *pos = tmp;

        llvm::Type* el_type = llvm_utils->get_type_from_ttype_t_util(x.m_a, x.m_type, llvm_utils->module);
        tmp = tuple_api->read_item_using_pos_value(el_type, ptuple, ASR::down_cast<ASR::Tuple_t>(ASRUtils::expr_type(x.m_a)), pos, 
                                                            LLVM::is_llvm_struct(x.m_type) || ASRUtils::is_allocatable_descriptor_string(x.m_type));
    }

    void visit_TupleConcat(const ASR::TupleConcat_t& x) {
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_left);
        llvm::Value* left = tmp;
        this->visit_expr(*x.m_right);
        llvm::Value* right = tmp;
        ptr_loads = ptr_loads_copy;

        ASR::Tuple_t* tuple_type_left = ASR::down_cast<ASR::Tuple_t>(ASRUtils::expr_type(x.m_left));
        std::string type_code_left = ASRUtils::get_type_code(tuple_type_left->m_type,
                                                        tuple_type_left->n_type);
        ASR::Tuple_t* tuple_type_right = ASR::down_cast<ASR::Tuple_t>(ASRUtils::expr_type(x.m_right));
        std::string type_code_right = ASRUtils::get_type_code(tuple_type_right->m_type,
                                                        tuple_type_right->n_type);
        Vec<ASR::ttype_t*> v_type;
        v_type.reserve(al, tuple_type_left->n_type + tuple_type_right->n_type);
        std::string type_code = type_code_left + type_code_right;
        std::vector<llvm::Type*> llvm_el_types;
        ASR::storage_typeType m_storage = ASR::storage_typeType::Default;
        bool is_array_type = false, is_malloc_array_type = false;
        bool is_list = false;
        ASR::dimension_t* m_dims = nullptr;
        int n_dims = 0, a_kind = -1;
        for( size_t i = 0; i < tuple_type_left->n_type; i++ ) {
            llvm_el_types.push_back(llvm_utils->get_type_from_ttype_t(x.m_left, tuple_type_left->m_type[i],
                nullptr, m_storage, is_array_type, is_malloc_array_type,
                is_list, m_dims, n_dims, a_kind, module.get()));
            v_type.push_back(al, tuple_type_left->m_type[i]);
        }
        is_array_type = false; is_malloc_array_type = false;
        is_list = false;
        m_dims = nullptr;
        n_dims = 0; a_kind = -1;
        for( size_t i = 0; i < tuple_type_right->n_type; i++ ) {
            llvm_el_types.push_back(llvm_utils->get_type_from_ttype_t(x.m_right, tuple_type_right->m_type[i],
                nullptr, m_storage, is_array_type, is_malloc_array_type,
                is_list, m_dims, n_dims, a_kind, module.get()));
            v_type.push_back(al, tuple_type_right->m_type[i]);
        }
        llvm::Type* concat_tuple_type = tuple_api->get_tuple_type(type_code, llvm_el_types);
        llvm::Value* concat_tuple = llvm_utils->CreateAlloca(*builder, concat_tuple_type, nullptr, "concat_tuple");
        ASR::Tuple_t* tuple_type = (ASR::Tuple_t*)(ASR::make_Tuple_t(
                                    al, x.base.base.loc, v_type.p, v_type.n));
        tuple_api->concat(x.m_left, left, right, tuple_type_left, tuple_type_right, concat_tuple,
                          tuple_type, module.get());
        // concat() deep-copies elements into concat_tuple. Free heap
        // resources (e.g. strings) owned by temporary operands.
        if (is_owned_tuple_temporary(x.m_left)) {
            llvm_symtab_finalizer.finalize_temporary(
                left, ASRUtils::expr_type(x.m_left));
        }
        if (is_owned_tuple_temporary(x.m_right)) {
            llvm_symtab_finalizer.finalize_temporary(
                right, ASRUtils::expr_type(x.m_right));
        }
        tmp = concat_tuple;
    }

    void visit_ArrayItem(const ASR::ArrayItem_t& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        ASR::ttype_t* x_mv_type = ASRUtils::expr_type(x.m_v);
        llvm::Value* array = nullptr;
        ASR::Variable_t *v = nullptr;
        bool is_intent_in = false;
        std::string array_name;
        if( ASR::is_a<ASR::Var_t>(*x.m_v) ) {
            v = ASRUtils::EXPR2VAR(x.m_v);
            array_name = v->m_name;
            uint32_t v_h = get_hash((ASR::asr_t*)v);
            if (v->m_intent == ASRUtils::intent_in) {
                is_intent_in = true;
            }
            array = llvm_symtab[v_h];
            if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(v->m_type))) {
                ASR::Struct_t* der_symbol = ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(v->m_type_declaration));
                current_der_type_name = get_type_key(der_symbol);
            }
        } else {
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr(*x.m_v);
            ptr_loads = ptr_loads_copy;
            array = tmp;
            if (!array->getType()->isPointerTy()) {
                llvm::Value* val = array;
                llvm::AllocaInst* alloc = llvm_utils->CreateAlloca(*builder, val->getType());
                builder->CreateStore(val, alloc);
                array = alloc;
            }
        }

        ASR::dimension_t* m_dims;
        int n_dims = ASRUtils::extract_dimensions_from_ttype(x_mv_type, m_dims);
        {
            // Array indexing:
            std::vector<llvm::Value*> indices;
            for( size_t r = 0; r < x.n_args; r++ ) {
                ASR::array_index_t curr_idx = x.m_args[r];
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = 2;
                this->visit_expr_wrapper(curr_idx.m_right, true);
                ptr_loads = ptr_loads_copy;
                indices.push_back(tmp);
            }

            ASR::ttype_t* x_mv_type_ = ASRUtils::type_get_past_allocatable(
                ASRUtils::type_get_past_pointer(x_mv_type));
            LCOMPILERS_ASSERT(ASR::is_a<ASR::Array_t>(*x_mv_type_));
            ASR::Array_t* array_t = ASR::down_cast<ASR::Array_t>(x_mv_type_);
            bool is_bindc_array = ASRUtils::expr_abi(x.m_v) == ASR::abiType::BindC;
            if ( LLVM::is_llvm_pointer(*x_mv_type) ||
               ((is_bindc_array && !ASRUtils::is_fixed_size_array(m_dims, n_dims)) &&
                ASR::is_a<ASR::StructInstanceMember_t>(*x.m_v)) ) {
                llvm::Type *array_type = llvm_utils->get_type_from_ttype_t_util(x.m_v, x_mv_type_, module.get());
                array = llvm_utils->CreateLoad2(array_type->getPointerTo(), array);
            } else if (ASR::is_a<ASR::ArraySection_t>(*x.m_v) &&
                       array_t->m_physical_type == ASR::array_physical_typeType::DescriptorArray) {
                ASR::ArraySection_t* as = ASR::down_cast<ASR::ArraySection_t>(x.m_v);
                ASR::ttype_t* base_type = ASRUtils::expr_type(as->m_v);
                if (LLVM::is_llvm_pointer(*base_type)) {
                    llvm::Type *array_type = llvm_utils->get_type_from_ttype_t_util(x.m_v, x_mv_type_, module.get());
                    array = llvm_utils->CreateLoad2(array_type->getPointerTo(), array);
                }
            } else if (array_t->m_physical_type == ASR::array_physical_typeType::PointerArray &&
                       !LLVM::is_llvm_pointer(*x_mv_type) &&
                       is_bindc_array &&
                       llvm::isa<llvm::GlobalVariable>(array)) {
                // PointerArray global without Pointer/Allocatable wrapper (e.g. bind(C) globals):
                // LLVM globals add a pointer level, so load to get data pointer
                llvm::Type *array_type = llvm_utils->get_type_from_ttype_t_util(x.m_v, x_mv_type_, module.get());
                array = llvm_utils->CreateLoad2(array_type, array);
            } else if (array_t->m_physical_type == ASR::array_physical_typeType::UnboundedPointerArray &&
                       !LLVM::is_llvm_pointer(*x_mv_type) &&
                       llvm::isa<llvm::GlobalVariable>(array)) {
                llvm::Type *array_type = llvm_utils->get_type_from_ttype_t_util(x.m_v, x_mv_type_, module.get());
                array = llvm_utils->CreateLoad2(array_type, array);
            }
            if (compiler_options.po.bounds_checking && ASRUtils::is_allocatable(x_mv_type)) {
                llvm::Value* is_allocated = arr_descr->get_is_allocated_flag(array, x.m_v);
                llvm::Value* cond = builder->CreateNot(is_allocated);
                llvm_utils->generate_runtime_error(cond,
                    "Array '%s' is indexed but not allocated.",
                    {LLVMUtils::RuntimeLabel("", {x.m_v->base.loc})},
                    infile,
                    location_manager,
                    LCompilers::create_global_string_ptr(context, *module, *builder, array_name));
            }

            Vec<llvm::Value*> llvm_diminfo;
            llvm_diminfo.reserve(al, 2 * x.n_args + 1);
            bool check_for_bounds = compiler_options.po.bounds_checking;
            if( array_t->m_physical_type == ASR::array_physical_typeType::PointerArray ||
                array_t->m_physical_type == ASR::array_physical_typeType::UnboundedPointerArray ||
                array_t->m_physical_type == ASR::array_physical_typeType::FixedSizeArray ||
                array_t->m_physical_type == ASR::array_physical_typeType::SIMDArray ||
                array_t->m_physical_type == ASR::array_physical_typeType::StringArraySinglePointer ) {
                int ptr_loads_copy = ptr_loads;
                for( size_t idim = 0; idim < x.n_args; idim++ ) {
                    llvm::Value* dim_start = nullptr;
                    if (m_dims[idim].m_start) {
                        ptr_loads = 2 - !LLVM::is_llvm_pointer(*ASRUtils::expr_type(m_dims[idim].m_start));
                        this->visit_expr_wrapper(m_dims[idim].m_start, true);
                        dim_start = tmp;
                    } else {
                        llvm::Type* idx_type = arr_descr->get_index_type();
                        unsigned idx_bits = idx_type->getIntegerBitWidth();
                        dim_start = llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 1));
                    }
                    llvm::Value* dim_size = nullptr;
                    if (m_dims[idim].m_length) {
                        ptr_loads = 2 - !LLVM::is_llvm_pointer(*ASRUtils::expr_type(m_dims[idim].m_length));
                        if (is_intent_in) {
                            this->visit_expr_wrapper(m_dims[idim].m_length, true);
                        } else {
                            load_array_size_deep_copy(m_dims[idim].m_length);
                        }
                        dim_size = tmp;
                    } else {
                        llvm::Type* idx_type = arr_descr->get_index_type();
                        unsigned idx_bits = idx_type->getIntegerBitWidth();
                        dim_size = llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 0));
                        check_for_bounds = false;
                    }
                    llvm_diminfo.push_back(al, dim_start);
                    llvm_diminfo.push_back(al, dim_size);
                }
                ptr_loads = ptr_loads_copy;
            } else if( array_t->m_physical_type == ASR::array_physical_typeType::UnboundedPointerArray ) {
                int ptr_loads_copy = ptr_loads;
                for( size_t idim = 0; idim < x.n_args; idim++ ) {
                    llvm::Value* dim_start = nullptr;
                    if (m_dims[idim].m_start) {
                        ptr_loads = 2 - !LLVM::is_llvm_pointer(*ASRUtils::expr_type(m_dims[idim].m_start));
                        this->visit_expr_wrapper(m_dims[idim].m_start, true);
                        dim_start = tmp;
                    } else {
                        llvm::Type* idx_type = arr_descr->get_index_type();
                        unsigned idx_bits = idx_type->getIntegerBitWidth();
                        dim_start = llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 1));
                    }
                    llvm::Value* dim_size = nullptr;
                    if (m_dims[idim].m_length) {
                        ptr_loads = 2 - !LLVM::is_llvm_pointer(*ASRUtils::expr_type(m_dims[idim].m_length));
                        if (is_intent_in) {
                            this->visit_expr_wrapper(m_dims[idim].m_length, true);
                        } else {
                            load_array_size_deep_copy(m_dims[idim].m_length);
                        }
                        dim_size = tmp;
                    } else {
                        // Last dimension of assumed-size array has no length
                        llvm::Type* idx_type = arr_descr->get_index_type();
                        unsigned idx_bits = idx_type->getIntegerBitWidth();
                        dim_size = llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 0));
                    }
                    llvm_diminfo.push_back(al, dim_start);
                    llvm_diminfo.push_back(al, dim_size);
                }
                ptr_loads = ptr_loads_copy;
            }
            LCOMPILERS_ASSERT(ASRUtils::extract_n_dims_from_ttype(x_mv_type) > 0);
            bool is_polymorphic = ASRUtils::is_unlimited_polymorphic_type(
                ASRUtils::extract_type(x_mv_type));
            ASR::symbol_t* selector_type_decl = ASRUtils::get_struct_sym_from_struct_expr(x.m_v);
            if (array_t->m_physical_type == ASR::array_physical_typeType::UnboundedPointerArray) {
                // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                llvm::Type* type = llvm_utils->get_el_type(x.m_v, ASRUtils::extract_type(x_mv_type), module.get());
                tmp = arr_descr->get_single_element(type, array, indices, x.n_args, ASRUtils::expr_type(x.m_v), x.m_v, location_manager,
                                                    selector_type_decl,
                                                    true,
                                                    false,
                                                    llvm_diminfo.p, is_polymorphic, nullptr,
                                                    false, false, array_name, infile);
            } else {
                llvm::Type* type;
                bool is_fixed_size = (array_t->m_physical_type == ASR::array_physical_typeType::FixedSizeArray ||
                    array_t->m_physical_type == ASR::array_physical_typeType::SIMDArray ||
                    (
                        array_t->m_physical_type == ASR::array_physical_typeType::StringArraySinglePointer &&
                        ASRUtils::is_fixed_size_array(x_mv_type)
                    )
                );
                if (is_fixed_size) {
                    type = llvm_utils->get_type_from_ttype_t_util(x.m_v, x_mv_type, module.get());
                } else {
                    // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                    type = llvm_utils->get_el_type(x.m_v, ASRUtils::extract_type(x_mv_type), module.get());
                }
                tmp = arr_descr->get_single_element(type, array, indices, x.n_args, ASRUtils::expr_type(x.m_v), x.m_v, location_manager,
                                                    selector_type_decl,
                                                    array_t->m_physical_type == ASR::array_physical_typeType::PointerArray ||
                                                    array_t->m_physical_type == ASR::array_physical_typeType::StringArraySinglePointer,
                                                    is_fixed_size, llvm_diminfo.p, is_polymorphic,
                                                    nullptr, false,
                                                    check_for_bounds, array_name, infile);
            }
        }
        if( ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(x.m_type)) && !ASRUtils::is_class_type(x.m_type) ) {
            current_der_type_name = get_type_key(
                ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_v)));
        }
    }

    void visit_ArraySection(const ASR::ArraySection_t& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_v);
        ptr_loads = ptr_loads_copy;
        llvm::Value* array = tmp;
        ASR::dimension_t* m_dims;
        [[maybe_unused]] int n_dims = ASRUtils::extract_dimensions_from_ttype(
                        ASRUtils::expr_type(x.m_v), m_dims);

        bool is_string_section = ASR::is_a<ASR::String_t>(*ASRUtils::expr_type(x.m_v)) && n_dims == 0;
        if (is_string_section) {
            // String indexing:
            if (x.n_args == 1) {
                throw CodeGenError("Only string(a:b) supported for now.", x.base.base.loc);
            }

            LCOMPILERS_ASSERT(x.m_args[0].m_left)
            LCOMPILERS_ASSERT(x.m_args[0].m_right)
            //throw CodeGenError("Only string(a:b) for a,b variables for now.", x.base.base.loc);
            // Use the "right" index for now
            this->visit_expr_wrapper(x.m_args[0].m_right, true);
            llvm::Value *idx2 = tmp;
            this->visit_expr_wrapper(x.m_args[0].m_left, true);
            llvm::Value *idx1 = tmp;
            // idx = builder->CreateSub(idx, llvm::ConstantInt::get(context, llvm::APInt(32, 1)));
            //std::vector<llvm::Value*> idx_vec = {llvm::ConstantInt::get(context, llvm::APInt(32, 0)), idx};
            // std::vector<llvm::Value*> idx_vec = {idx};
            llvm::Value *str_data, *str_len;
            std::tie(str_data, str_len) = llvm_utils->get_string_length_data(ASRUtils::get_string_type(x.m_v), array);
            // llvm::Value *p = CreateGEP(str, idx_vec);
            // TODO: Currently the string starts at the right location, but goes to the end of the original string.
            // We have to allocate a new string, copy it and add null termination.
            llvm::Value *step = llvm::ConstantInt::get(context, llvm::APInt(32, 1));
            llvm::Value *present = llvm::ConstantInt::get(context, llvm::APInt(1, 1));
            llvm::Value *p = lfortran_str_slice(str_data, str_len, idx1, idx2, step, present, present);
            tmp = llvm_utils->CreateAlloca(*builder, character_type);
            builder->CreateStore(p, tmp);
        } else {
            tmp = array;
        }
    }

    void visit_ArrayReshape(const ASR::ArrayReshape_t& x) {
        if (x.m_value) {
            this->visit_expr(*x.m_value);
            return;
        }
        int ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_array);
        llvm::Value* array = tmp;
        this->visit_expr(*x.m_shape);
        llvm::Value* shape = tmp;
        ptr_loads = ptr_loads_copy;
        ASR::ttype_t* x_m_array_type = ASRUtils::expr_type(x.m_array);
        ASR::array_physical_typeType array_physical_type = ASRUtils::extract_physical_type(x_m_array_type);
        switch( array_physical_type ) {
            case ASR::array_physical_typeType::DescriptorArray: {
                ASR::ttype_t* asr_data_type = ASRUtils::duplicate_type_without_dims(al,
                    ASRUtils::get_contained_type(x_m_array_type), x_m_array_type->base.loc);
                ASR::ttype_t* asr_shape_type = ASRUtils::get_contained_type(ASRUtils::expr_type(x.m_shape));
                // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                llvm::Type* llvm_data_type = llvm_utils->get_el_type(x.m_array, asr_data_type, module.get());
                llvm::Type* array_type = llvm_utils->get_type_from_ttype_t_util(x.m_array,
                    ASRUtils::type_get_past_allocatable_pointer(x_m_array_type), module.get());
                llvm::Type* shape_type = llvm_utils->get_type_from_ttype_t_util(x.m_shape,
                    ASRUtils::type_get_past_allocatable_pointer(asr_shape_type), module.get());
                ASR::ttype_t* asr_result_type = ASRUtils::expr_type(
                    const_cast<ASR::expr_t*>(&(x.base)));
                asr_result_type = ASRUtils::duplicate_type(al, asr_result_type, nullptr,
                    ASR::array_physical_typeType::DescriptorArray, true);
                llvm::Type* result_desc_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_array, ASRUtils::type_get_past_allocatable_pointer(asr_result_type),
                    module.get());
                llvm::Value* order = nullptr;
                if (x.m_order != nullptr) {
                    this->visit_expr(*x.m_order);
                    order = tmp;
                }

                if (LLVM::is_llvm_pointer(*x_m_array_type)) {
                    array = llvm_utils->CreateLoad2(array_type->getPointerTo(), array);
                }
                tmp = arr_descr->reshape(array_type, array, llvm_data_type, shape_type, shape, asr_shape_type, module.get(),
                    const_cast<ASR::expr_t*>(x.m_array), asr_data_type,
                    result_desc_type, order, x.m_order);
                // If the result type is PointerArray, extract the data pointer
                // from the descriptor, since the assignment expects a raw pointer.
                ASR::array_physical_typeType result_ptype = ASRUtils::extract_physical_type(
                    ASRUtils::expr_type(const_cast<ASR::expr_t*>(&(x.base))));
                if (result_ptype != ASR::array_physical_typeType::DescriptorArray) {
                    llvm::Value* data_ptr = arr_descr->get_pointer_to_data(result_desc_type, tmp);
                    tmp = llvm_utils->CreateLoad2(llvm_data_type->getPointerTo(), data_ptr);
                }
                break;
            }
            case ASR::array_physical_typeType::FixedSizeArray: {
                ASR::dimension_t* asr_dims = nullptr;
                size_t asr_n_dims = ASRUtils::extract_dimensions_from_ttype(x_m_array_type, asr_dims);
                int64_t source_size = ASRUtils::get_fixed_size_of_array(asr_dims, asr_n_dims);
                int64_t target_size = ASRUtils::get_fixed_size_of_array(x.m_type);
                if (target_size == -1) target_size = source_size;
                ASR::ttype_t* element_type = ASRUtils::type_get_past_array(
                    ASRUtils::type_get_past_allocatable(ASRUtils::type_get_past_pointer(x_m_array_type)));
                // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                llvm::Type* llvm_data_type = llvm_utils->get_el_type(x.m_array, element_type, module.get());
                llvm::DataLayout data_layout(module->getDataLayout());
                uint64_t data_size = data_layout.getTypeAllocSize(llvm_data_type);
                // Allocate flat buffer for target_size elements.
                llvm::ArrayType* flat_target_type = llvm::ArrayType::get(llvm_data_type, target_size);
                llvm::Value* target = llvm_utils->CreateAlloca(
                    flat_target_type, nullptr, "fixed_size_reshaped_array");
                llvm::Value* target_ = llvm_utils->create_gep2(flat_target_type, target, 0);

                bool is_struct_type = ASR::is_a<ASR::StructType_t>(
                    *ASRUtils::extract_type(element_type));
                llvm::Type* src_target_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_array, x_m_array_type, module.get());
                int64_t copy_size = std::min(source_size, target_size);
                if (is_struct_type) {
                    // Struct types with allocatable components need element-wise
                    // deep copy; a flat memcpy would share allocatable pointers.
                    llvm::Value* llvm_total_bytes = llvm::ConstantInt::get(
                        context, llvm::APInt(32, target_size * data_size));
                    builder->CreateMemSet(target_,
                        llvm::ConstantInt::get(context, llvm::APInt(8, 0)),
                        llvm_total_bytes, llvm::MaybeAlign());
                    for (int64_t i = 0; i < copy_size; i++) {
                        llvm::Value* src_elem = llvm_utils->create_gep2(
                            src_target_type, array, i);
                        llvm::Value* dest_elem = builder->CreateConstGEP2_32(
                            flat_target_type, target, 0, i);
                        llvm_utils->deepcopy(const_cast<ASR::expr_t*>(x.m_array),
                            src_elem, dest_elem, element_type, element_type,
                            module.get());
                    }
                } else {
                    llvm::Value* copy_bytes = llvm::ConstantInt::get(
                        context, llvm::APInt(64, copy_size * data_size));
                    builder->CreateMemCpy(target_, llvm::MaybeAlign(), array, llvm::MaybeAlign(), copy_bytes);
                    // Fill remaining elements with pad (cycling).
                    if (x.m_pad != nullptr && target_size > source_size) {
                        int64_t pad_size = ASRUtils::get_fixed_size_of_array(ASRUtils::expr_type(x.m_pad));
                        this->visit_expr(*x.m_pad);
                        // tmp is already an element pointer (i32*, real*, etc.)
                        llvm::Value* pad_base = tmp;
                        for (int64_t i = source_size; i < target_size; i++) {
                            llvm::Value* pad_elem = builder->CreateLoad(
                                llvm_data_type,
                                builder->CreateConstGEP1_32(llvm_data_type, pad_base, i % pad_size));
                            builder->CreateStore(pad_elem,
                                builder->CreateConstGEP1_32(llvm_data_type, target_, i));
                        }
                    }
                }

                if (x.m_order != nullptr) {
                    this->visit_expr(*x.m_order);
                    llvm::Value* order_base = tmp;

                    ASR::dimension_t* result_dims = nullptr;
                    size_t rank = ASRUtils::extract_dimensions_from_ttype(x.m_type, result_dims);
                    int64_t n = static_cast<int64_t>(rank);
                    ASR::ttype_t* order_elem_type = ASRUtils::type_get_past_array(
                        ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(ASRUtils::expr_type(x.m_order))));
                    llvm::Type* llvm_order_type = llvm_utils->get_el_type(x.m_order, order_elem_type, module.get());
                    llvm::Type* order_array_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_order, ASRUtils::type_get_past_allocatable_pointer(ASRUtils::expr_type(x.m_order)), module.get());

                    llvm::Value* order_data_base = order_base;
                    ASR::array_physical_typeType order_physical_type = ASRUtils::extract_physical_type(ASRUtils::expr_type(x.m_order));
                    if (order_physical_type == ASR::array_physical_typeType::DescriptorArray) {
                        order_data_base = llvm_utils->create_gep2(order_array_type, order_base, 0);
                        order_data_base = llvm_utils->CreateLoad2(llvm_order_type->getPointerTo(), order_data_base);
                    } else if (order_physical_type == ASR::array_physical_typeType::FixedSizeArray && 
                                !ASRUtils::expr_value(x.m_order)) {
                        order_data_base = llvm_utils->create_gep2(order_array_type, order_base, 0);
                    }

                    ASR::ttype_t* shape_expr_type = ASRUtils::expr_type(x.m_shape);
                    ASR::ttype_t* shape_elem_type = ASRUtils::type_get_past_array(
                        ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(shape_expr_type)));
                    llvm::Type* llvm_shape_type = llvm_utils->get_el_type(x.m_shape, shape_elem_type, module.get());
                    llvm::Type* shape_array_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_shape, shape_expr_type, module.get());

                    llvm::Value* shape_data_base = shape;
                    ASR::array_physical_typeType shape_physical_type = ASRUtils::extract_physical_type(shape_expr_type);
                    if (shape_physical_type == ASR::array_physical_typeType::DescriptorArray) {
                        shape_data_base = llvm_utils->create_gep2(shape_array_type, shape, 0);
                        shape_data_base = llvm_utils->CreateLoad2(llvm_shape_type->getPointerTo(), shape_data_base);
                    } else if (shape_physical_type == ASR::array_physical_typeType::FixedSizeArray &&
                                !ASRUtils::expr_value(x.m_shape)) {
                        shape_data_base = llvm_utils->create_gep2(shape_array_type, shape, 0);
                    }

                    std::vector<llvm::Value*> shape_values(n);
                    for (int64_t i = 0; i < n; i++) {
                        int64_t shape_i = -1;
                        bool shape_is_constant = ASRUtils::extract_value(result_dims[i].m_length, shape_i);
                        if (shape_is_constant) {
                            shape_values[i] = llvm::ConstantInt::get(context, llvm::APInt(64, shape_i));
                        } else {
                            llvm::Value* shape_i_val = llvm_utils->CreateLoad2(
                                llvm_shape_type,
                                llvm_utils->create_ptr_gep2(
                                    llvm_shape_type,
                                    shape_data_base,
                                    llvm::ConstantInt::get(context, llvm::APInt(32, i))));
                            shape_values[i] = builder->CreateSExtOrTrunc(shape_i_val, llvm::Type::getInt64Ty(context));
                        }
                    }

                    llvm::Value* source_base = llvm_utils->create_gep2(src_target_type, array, 0);

                    llvm::ArrayType* idx_arr_type = llvm::ArrayType::get(llvm::Type::getInt64Ty(context), n);
                    llvm::Value* I_arr = llvm_utils->CreateAlloca(idx_arr_type, nullptr, "reshape_I");
                    for (int64_t i = 0; i < target_size; i++) {
                        llvm::Value* temp_val = llvm::ConstantInt::get(context, llvm::APInt(64, i));
                        for (int64_t j = 0; j < n; j++) {
                            llvm::Value* ij_ptr = llvm_utils->create_gep2(
                                idx_arr_type, I_arr,
                                llvm::ConstantInt::get(context, llvm::APInt(64, j)));
                            builder->CreateStore(llvm::ConstantInt::get(context, llvm::APInt(64, 0)), ij_ptr);
                        }

                        // Decode result linear index i into result subscripts
                        // in normal Fortran column-major order.
                        for (int64_t j = 0; j < n; j++) {
                            llvm::Value* shape_dim = shape_values[j];

                            llvm::Value* d_j = builder->CreateSRem(temp_val, shape_dim);
                            temp_val = builder->CreateSDiv(temp_val, shape_dim);

                            llvm::Value* I_j_ptr = llvm_utils->create_gep2(
                                idx_arr_type, I_arr,
                                llvm::ConstantInt::get(context, llvm::APInt(64, j)));
                            builder->CreateStore(d_j, I_j_ptr);
                        }

                        // Build source linear index using ORDER permutation.
                        llvm::Value* source_index = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
                        llvm::Value* stride = llvm::ConstantInt::get(context, llvm::APInt(64, 1));
                        for (int64_t j = 0; j < n; j++) {
                            llvm::Value* order_j = llvm_utils->CreateLoad2(
                                llvm_order_type,
                                llvm_utils->create_ptr_gep2(
                                    llvm_order_type, order_data_base,
                                    llvm::ConstantInt::get(context, llvm::APInt(32, j))));
                            order_j = builder->CreateSExtOrTrunc(order_j, llvm::Type::getInt64Ty(context));
                            llvm::Value* dim = builder->CreateSub(order_j,
                                llvm::ConstantInt::get(context, llvm::APInt(64, 1)));

                            llvm::Value* I_dim_ptr = llvm_utils->create_gep2(idx_arr_type, I_arr, dim);
                            llvm::Value* I_dim = llvm_utils->CreateLoad2(llvm::Type::getInt64Ty(context), I_dim_ptr);
                            llvm::Value* term = builder->CreateMul(I_dim, stride);
                            source_index = builder->CreateAdd(source_index, term);

                            llvm::Value* shape_dim = shape_values[0];
                            for (int64_t k = 1; k < n; k++) {
                                llvm::Value* cond = builder->CreateICmpEQ(dim,
                                    llvm::ConstantInt::get(context, llvm::APInt(64, k)));
                                shape_dim = builder->CreateSelect(cond,
                                    shape_values[k],
                                    shape_dim);
                            }
                            stride = builder->CreateMul(stride, shape_dim);
                        }

                        llvm::Value* in_source = builder->CreateICmpSLT(
                            source_index,
                            llvm::ConstantInt::get(context, llvm::APInt(64, source_size)));
                        llvm::Value* safe_source_index = builder->CreateSelect(
                            in_source,
                            source_index,
                            llvm::ConstantInt::get(context, llvm::APInt(64, 0)));
                        llvm::Value* source_elem = llvm_utils->CreateLoad2(
                            llvm_data_type,
                            llvm_utils->create_ptr_gep2(llvm_data_type, source_base, safe_source_index));

                        builder->CreateStore(source_elem,
                            builder->CreateConstGEP1_32(llvm_data_type, target_, i));
                    }
                }
                tmp = target;
                break;
            }
            case ASR::PointerArray: {
                ASR::array_physical_typeType result_ptype = ASRUtils::extract_physical_type(
                    ASRUtils::expr_type(const_cast<ASR::expr_t*>(&(x.base))));
                if (result_ptype == ASR::array_physical_typeType::DescriptorArray &&
                    !ASRUtils::is_character(*x_m_array_type)) {
                    ASR::ttype_t* desc_input_type = ASRUtils::duplicate_type(al, x_m_array_type,
                        nullptr, ASR::array_physical_typeType::DescriptorArray, true);
                    tmp = array;
                    PointerToData_to_Descriptor(const_cast<ASR::expr_t*>(x.m_array),
                        desc_input_type, x_m_array_type);
                    array = tmp;

                    ASR::ttype_t* asr_data_type = ASRUtils::duplicate_type_without_dims(al,
                        ASRUtils::get_contained_type(x_m_array_type), x_m_array_type->base.loc);
                    ASR::ttype_t* asr_shape_type = ASRUtils::get_contained_type(ASRUtils::expr_type(x.m_shape));
                    llvm::Type* llvm_data_type = llvm_utils->get_el_type(x.m_array, asr_data_type, module.get());
                    llvm::Type* array_type = llvm_utils->get_type_from_ttype_t_util(x.m_array,
                        ASRUtils::type_get_past_allocatable_pointer(desc_input_type), module.get());
                    llvm::Type* shape_type = llvm_utils->get_type_from_ttype_t_util(x.m_shape,
                        ASRUtils::type_get_past_allocatable_pointer(asr_shape_type), module.get());
                    ASR::ttype_t* asr_result_type = ASRUtils::expr_type(
                        const_cast<ASR::expr_t*>(&(x.base)));
                    asr_result_type = ASRUtils::duplicate_type(al, asr_result_type, nullptr,
                        ASR::array_physical_typeType::DescriptorArray, true);
                    llvm::Type* result_desc_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_array, ASRUtils::type_get_past_allocatable_pointer(asr_result_type),
                        module.get());
                    llvm::Value* order = nullptr;
                    if (x.m_order != nullptr) {
                        this->visit_expr(*x.m_order);
                        order = tmp;
                    }
                    tmp = arr_descr->reshape(array_type, array, llvm_data_type,
                        shape_type, shape, asr_shape_type, module.get(),
                        const_cast<ASR::expr_t*>(x.m_array), asr_data_type,
                        result_desc_type, order, x.m_order);
                    break;
                }
                llvm::Type* type_of_array = llvm_utils->get_type_from_ttype_t_util(
                                            x.m_array,
                                            ASRUtils::extract_type(x_m_array_type), module.get());
                llvm::Value* llvm_arr_size = nullptr;
                if(ASRUtils::is_fixed_size_array(x_m_array_type)){
                    int64_t arr_size = ASRUtils::get_fixed_size_of_array(x_m_array_type);
                    llvm_arr_size = llvm::ConstantInt::get(context, llvm::APInt(32, arr_size));
                } else {
                    ASR::dimension_t* m_dims = nullptr;
                    int n_dims = ASRUtils::extract_dimensions_from_ttype(x_m_array_type, m_dims);
                    llvm_arr_size = llvm::ConstantInt::get(context, llvm::APInt(32, 1));
                    int ptr_loads_copy = ptr_loads;
                    ptr_loads = 2;
                    for( int i = 0; i < n_dims; i++ ) {
                        load_array_size_deep_copy(m_dims[i].m_length);
                        llvm::Value* dim_len = tmp;
                        if(dim_len->getType() != llvm::Type::getInt32Ty(context)){
                            dim_len = builder->CreateSExtOrTrunc(dim_len, llvm::Type::getInt32Ty(context));
                        }
                        llvm_arr_size = builder->CreateMul(llvm_arr_size, dim_len);
                    }
                    ptr_loads = ptr_loads_copy;
                }

                if(ASRUtils::is_character(*x_m_array_type)){
                    tmp = create_and_setup_string_for_array(x_m_array_type,
                        builder->CreateSExtOrTrunc(llvm_arr_size, llvm::Type::getInt64Ty(context)),
                        true, "StringArrayReshape");
                    builder->CreateMemCpy(
                        llvm_utils->get_stringArray_data(x_m_array_type, tmp), llvm::MaybeAlign(),
                        llvm_utils->get_stringArray_data(ASRUtils::expr_type(x.m_array), array), llvm::MaybeAlign(),
                        llvm_utils->get_stringArray_whole_size(ASRUtils::expr_type(x.m_array))
                    );
                } else {
                    llvm::Value* type_alloca = builder->CreateAlloca(type_of_array, llvm_arr_size);
                    llvm::DataLayout data_layout(module->getDataLayout());
                    uint64_t data_size = data_layout.getTypeAllocSize(type_of_array);
                    llvm::Value* total_bytes = builder->CreateMul(llvm_arr_size,
                        llvm::ConstantInt::get(context, llvm::APInt(32, data_size)));
                    builder->CreateMemCpy(
                        type_alloca, llvm::MaybeAlign(),
                        array, llvm::MaybeAlign(),
                        total_bytes);
                    tmp = type_alloca;
                }
                break;
            }
            default: {
                LCOMPILERS_ASSERT(false);
            }
        }
    }

    void visit_ArrayIsContiguous(const ASR::ArrayIsContiguous_t& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value);
            return;
        }
        ASR::ttype_t* x_m_array_type = ASRUtils::expr_type(x.m_array);
        llvm::Type* array_type = llvm_utils->get_type_from_ttype_t_util(x.m_array,
                ASRUtils::type_get_past_allocatable(ASRUtils::type_get_past_pointer(x_m_array_type)), module.get());
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 2 - // Sync: instead of 2 - , should this be ptr_loads_copy -
                    (LLVM::is_llvm_pointer(*ASRUtils::expr_type(x.m_array)));
        visit_expr_wrapper(x.m_array);
        ptr_loads = ptr_loads_copy;
        if (is_a<ASR::StructInstanceMember_t>(*x.m_array)) {
            tmp = llvm_utils->CreateLoad2(array_type->getPointerTo(), tmp);
        }
        llvm::Value* llvm_arg1 = tmp;
        ASR::array_physical_typeType physical_type = ASRUtils::extract_physical_type(x_m_array_type);
        switch( physical_type ) {
            case ASR::array_physical_typeType::DescriptorArray: {
                llvm::Value* dim_des_val = arr_descr->get_pointer_to_dimension_descriptor_array(array_type, llvm_arg1);
                llvm::Value* is_contiguous = llvm::ConstantInt::get(context, llvm::APInt(1, 1));
                ASR::dimension_t* m_dims = nullptr;
                int n_dims = ASRUtils::extract_dimensions_from_ttype(x_m_array_type, m_dims);
                unsigned index_bit_width = arr_descr->get_index_type()->getIntegerBitWidth();
                llvm::Value* expected_stride = llvm::ConstantInt::get(context, llvm::APInt(index_bit_width, 1));
                for (int i = 0; i < n_dims; i++) {
                    llvm::Value* dim_index = llvm::ConstantInt::get(context, llvm::APInt(32, i));
                    llvm::Value* dim_desc = arr_descr->get_pointer_to_dimension_descriptor(dim_des_val, dim_index);
                    llvm::Value* dim_start = arr_descr->get_lower_bound(dim_desc);
                    llvm::Value* stride = arr_descr->get_stride(dim_desc);
                    llvm::Value* is_dim_contiguous = builder->CreateICmpEQ(stride, expected_stride);
                    is_contiguous = builder->CreateAnd(is_contiguous, is_dim_contiguous);
                    llvm::Value* dim_size = arr_descr->get_upper_bound(dim_desc);
                    expected_stride = builder->CreateMul(expected_stride, builder->CreateAdd(builder->CreateSub(dim_size, dim_start), llvm::ConstantInt::get(context, llvm::APInt(index_bit_width, 1))));
                }
                // Unallocated array is contiguous
                llvm::Value* is_allocated = arr_descr->get_is_allocated_flag(llvm_arg1, x.m_array);
                tmp = builder->CreateOr(is_contiguous, builder->CreateNot(is_allocated));
                break;
            }
            case ASR::array_physical_typeType::FixedSizeArray:
            case ASR::array_physical_typeType::SIMDArray:
                tmp = llvm::ConstantInt::get(context, llvm::APInt(1, 1));
                break;
            case ASR::array_physical_typeType::PointerArray:
                tmp = llvm::ConstantInt::get(context, llvm::APInt(1, 1));
                break;
            case ASR::array_physical_typeType::StringArraySinglePointer:
                tmp = llvm::ConstantInt::get(context, llvm::APInt(1, 0));
                break;
            default:
                LCOMPILERS_ASSERT(false);
        }
    }

    void lookup_EnumValue(const ASR::EnumValue_t& x) {
        ASR::EnumType_t* enum_type = ASR::down_cast<ASR::EnumType_t>(x.m_enum_type);
        ASR::Enum_t* enum_symbol = ASR::down_cast<ASR::Enum_t>(enum_type->m_enum_type);
        uint32_t h = get_hash((ASR::asr_t*) enum_symbol);
        llvm::Value* array = llvm_symtab[h];
        llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(x.m_enum_type, enum_type->m_enum_type, module.get());
        tmp = llvm_utils->create_gep2(type, array, tmp);
        // After the first GEP, tmp points to an enum value struct
        // Get the element type of the array (which is the enum value struct type)
        llvm::ArrayType* array_type = llvm::dyn_cast<llvm::ArrayType>(type);
        llvm::Type* enum_value_struct_type = array_type->getElementType();
        llvm::Type* value_field_type = enum_value_struct_type->getStructElementType(1);
        tmp = llvm_utils->CreateLoad2(value_field_type, llvm_utils->create_gep2(enum_value_struct_type, tmp, 1));
    }

    void visit_EnumValue(const ASR::EnumValue_t& x) {
        if( x.m_value ) {
            if( ASR::is_a<ASR::Integer_t>(*x.m_type) ) {
                this->visit_expr(*x.m_value);
            } else if( ASR::is_a<ASR::EnumStaticMember_t>(*x.m_v) ) {
                ASR::EnumStaticMember_t* x_enum_member = ASR::down_cast<ASR::EnumStaticMember_t>(x.m_v);
                ASR::Variable_t* x_mv = ASR::down_cast<ASR::Variable_t>(x_enum_member->m_m);
                ASR::EnumType_t* enum_t = ASR::down_cast<ASR::EnumType_t>(x.m_enum_type);
                ASR::Enum_t* enum_type = ASR::down_cast<ASR::Enum_t>(enum_t->m_enum_type);
                for( size_t i = 0; i < enum_type->n_members; i++ ) {
                    if( std::string(enum_type->m_members[i]) == std::string(x_mv->m_name) ) {
                        tmp = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, i));
                        break ;
                    }
                }
                if( lookup_enum_value_for_nonints ) {
                    lookup_EnumValue(x);
                }
            }
            return ;
        }

        visit_expr(*x.m_v);
        if( ASR::is_a<ASR::StructInstanceMember_t>(*x.m_v) ) {
            llvm::Type* member_type = llvm_utils->get_type_from_ttype_t_util(x.m_v, x.m_type, module.get());
            tmp = llvm_utils->CreateLoad2(member_type, tmp);
        }
        if( !ASR::is_a<ASR::Integer_t>(*x.m_type) && lookup_enum_value_for_nonints ) {
            lookup_EnumValue(x);
        }
    }

    void visit_EnumName(const ASR::EnumName_t& x) {
        if( x.m_value ) {
            this->visit_expr(*x.m_value);
            return ;
        }

        visit_expr(*x.m_v);
        if( ASR::is_a<ASR::StructInstanceMember_t>(*x.m_v) ) {
            llvm::Type* member_type = llvm_utils->get_type_from_ttype_t_util(x.m_v, x.m_enum_type, module.get());
            tmp = llvm_utils->CreateLoad2(member_type, tmp);
        }
        ASR::EnumType_t* enum_t = ASR::down_cast<ASR::EnumType_t>(x.m_enum_type);
        ASR::Enum_t* enum_type = ASR::down_cast<ASR::Enum_t>(enum_t->m_enum_type);
        uint32_t h = get_hash((ASR::asr_t*) enum_type);
        llvm::Value* array = llvm_symtab[h];
        if( ASR::is_a<ASR::Integer_t>(*enum_type->m_type) ) {
            int64_t min_value = INT64_MAX;

            for( auto itr: enum_type->m_symtab->get_scope() ) {
                ASR::Variable_t* itr_var = ASR::down_cast<ASR::Variable_t>(itr.second);
                ASR::expr_t* value = ASRUtils::expr_value(itr_var->m_symbolic_value);
                int64_t value_int64 = -1;
                ASRUtils::extract_value(value, value_int64);
                min_value = std::min(value_int64, min_value);
            }
            tmp = builder->CreateSub(tmp, llvm::ConstantInt::get(tmp->getType(),
                        llvm::APInt(32, min_value, true)));
            llvm::Type* array_type = llvm_utils->get_type_from_ttype_t_util(nullptr, enum_t->m_enum_type, module.get());
            tmp = llvm_utils->create_gep2(array_type, array, tmp);
            // After the first GEP, tmp points to an enum value struct
            // Get the element type of the array (which is the enum value struct type)
            llvm::ArrayType* array_array_type = llvm::dyn_cast<llvm::ArrayType>(array_type);
            llvm::Type* enum_value_struct_type = array_array_type->getElementType();
            tmp = llvm_utils->create_gep2(enum_value_struct_type, tmp, 0);
        }
    }

    void visit_EnumConstructor(const ASR::EnumConstructor_t& x) {
        LCOMPILERS_ASSERT(x.n_args == 1);
        ASR::expr_t* m_arg = x.m_args[0];
        this->visit_expr(*m_arg);
    }

    void visit_UnionConstructor([[maybe_unused]] const ASR::UnionConstructor_t& x) {
        LCOMPILERS_ASSERT(x.n_args == 0);
    }

    llvm::Value* SizeOfTypeUtil(ASR::expr_t* expr, ASR::ttype_t* m_type, llvm::Type* output_type,
        ASR::ttype_t* output_type_asr) {
        llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(expr, m_type, module.get());
        llvm::DataLayout data_layout(module->getDataLayout());
        int64_t type_size = data_layout.getTypeAllocSize(llvm_type);
        return llvm::ConstantInt::get(output_type, llvm::APInt(
            ASRUtils::extract_kind_from_ttype_t(output_type_asr) * 8, type_size));
    }

    void visit_SizeOfType(const ASR::SizeOfType_t& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        ASR::ttype_t* arg_type = x.m_arg;
        int kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
        // For array types, compute element_size * array_count.
        // This avoids passing a nullptr expr into get_type_from_ttype_t_util
        // for array-of-struct types (where the struct symbol cannot be
        // resolved from a type alone).
        if (ASR::is_a<ASR::Array_t>(*arg_type)) {
            int64_t array_size = ASRUtils::get_fixed_size_of_array(arg_type);
            ASR::ttype_t* elem_type = ASRUtils::type_get_past_array(arg_type);
            llvm::Type* llvm_elem_type = llvm_utils->get_type_from_ttype_t_util(
                elem_type, nullptr, module.get());
            llvm::DataLayout data_layout(module->getDataLayout());
            int64_t elem_size = data_layout.getTypeAllocSize(llvm_elem_type);
            int64_t type_size = (array_size > 0) ? elem_size * array_size : elem_size;
            tmp = llvm::ConstantInt::get(llvm_utils->getIntType(kind),
                llvm::APInt(kind * 8, type_size));
        } else {
            llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(
                arg_type, nullptr, module.get());
            llvm::DataLayout data_layout(module->getDataLayout());
            int64_t type_size = data_layout.getTypeAllocSize(llvm_type);
            tmp = llvm::ConstantInt::get(llvm_utils->getIntType(kind),
                llvm::APInt(kind * 8, type_size));
        }
    }

    void visit_StructInstanceMember(const ASR::StructInstanceMember_t& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        current_der_type_name = "";
        ASR::ttype_t* x_m_v_type = ASRUtils::expr_type(x.m_v);

        // Early null check for Cast_t with allocatable class type argument
        // This must happen BEFORE visit_expr(*x.m_v) because the cast operation
        // will try to dereference the pointer, causing a crash if it's null
        if (compiler_options.po.bounds_checking && ASR::is_a<ASR::Cast_t>(*x.m_v)) {
            ASR::Cast_t* cast_expr = ASR::down_cast<ASR::Cast_t>(x.m_v);
            ASR::ttype_t* cast_arg_type = ASRUtils::expr_type(cast_expr->m_arg);
            if (ASRUtils::is_allocatable(cast_arg_type) &&
                ASRUtils::is_class_type(ASRUtils::type_get_past_allocatable(cast_arg_type))) {
                // Visit the underlying class variable to get its pointer
                int64_t ptr_loads_save = ptr_loads;
                ptr_loads = 0;
                this->visit_expr(*cast_expr->m_arg);
                ptr_loads = ptr_loads_save;
                llvm::Value* class_ptr = tmp;

                // Get the variable name for the error message
                ASR::Variable_t* var = ASRUtils::expr_to_variable_or_null(cast_expr->m_arg);
                std::string var_name = var ? var->m_name : "unknown";

                // Get the LLVM type for the allocatable class
                llvm::Type* class_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    cast_expr->m_arg, cast_arg_type, module.get());

                // Load the pointer to get the actual class instance pointer
                llvm::Value* class_instance_ptr = llvm_utils->CreateLoad2(
                    class_llvm_type->getPointerTo(), class_ptr);

                // Check if the class instance pointer is null
                llvm::Value* cond = builder->CreateICmpEQ(
                    builder->CreatePtrToInt(class_instance_ptr,
                        llvm::Type::getInt64Ty(context)),
                    builder->CreatePtrToInt(llvm::ConstantPointerNull::get(
                        llvm::cast<llvm::PointerType>(class_instance_ptr->getType())),
                        llvm::Type::getInt64Ty(context)));
                llvm::Value* var_name_llvm = LCompilers::create_global_string_ptr(context, *module, *builder, var_name);
                llvm_utils->generate_runtime_error(cond,
                        "Tried to access member of unallocated variable '%s'",
                        {LLVMUtils::RuntimeLabel("'%s' unallocated here", {cast_expr->m_arg->base.loc}, {var_name_llvm})},
                        infile,
                        location_manager,
                        var_name_llvm);
            }
        }

        int64_t ptr_loads_copy = ptr_loads;
        if( ASR::is_a<ASR::UnionInstanceMember_t>(*x.m_v) ) {
            ptr_loads = 0;
        } else {
            ptr_loads = 2 - LLVM::is_llvm_pointer(*x_m_v_type);
        }
        this->visit_expr(*x.m_v);
        ptr_loads = ptr_loads_copy;
        if (ASR::is_a<ASR::Cast_t>(*x.m_v) && LLVM::is_llvm_pointer(*x_m_v_type)) {
            llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(
                x.m_v, ASRUtils::type_get_past_allocatable_pointer(x_m_v_type), module.get());
            tmp = llvm_utils->CreateLoad2(type->getPointerTo(), tmp);
        }
        if (ASRUtils::is_class_type(ASRUtils::type_get_past_pointer(
                ASRUtils::type_get_past_allocatable(x_m_v_type))) ) {
            if (ASRUtils::is_allocatable(x_m_v_type)) {
                ASR::ttype_t* wrapped_struct_type = ASRUtils::make_StructType_t_util(al, x_m_v_type->base.loc,
                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_v)), true);
                llvm::Type* wrapper_struct_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_v, wrapped_struct_type, module.get())->getPointerTo();
                llvm::Type* x_mv_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_v, ASRUtils::extract_type(x_m_v_type), module.get());

                if (ASR::is_a<ASR::StructInstanceMember_t>(*x.m_v)) {
                    tmp = llvm_utils->CreateLoad2(x_mv_llvm_type->getPointerTo(), tmp);
                }
                if (compiler_options.po.bounds_checking) {
                    ASR::Variable_t* var = ASRUtils::expr_to_variable_or_null(x.m_v);
                    if (var) {
                        llvm::Value* var_name_llvm = LCompilers::create_global_string_ptr(context, *module, *builder, var->m_name);
                        llvm::Value* cond = builder->CreateICmpEQ(
                            builder->CreatePtrToInt(tmp,
                                llvm::Type::getInt64Ty(context)),
                            builder->CreatePtrToInt(llvm::ConstantPointerNull::get(x_mv_llvm_type->getPointerTo()),
                                llvm::Type::getInt64Ty(context)));
                        llvm_utils->generate_runtime_error(cond,
                                "Tried to access member of unallocated variable '%s'",
                                {LLVMUtils::RuntimeLabel("'%s' unallocated here", {x.m_v->base.loc}, {var_name_llvm})},
                                infile,
                                location_manager,
                                var_name_llvm);
                    }
                }
                tmp = llvm_utils->create_gep2(x_mv_llvm_type, tmp, 1);
                tmp = llvm_utils->CreateLoad2(wrapper_struct_llvm_type, tmp);
            } else {
                ASR::ttype_t* x_m_v_type_ = ASRUtils::type_get_past_allocatable(
                    ASRUtils::type_get_past_pointer(x_m_v_type));
                llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(x.m_v, x_m_v_type_, module.get());
                
                if (ASR::is_a<ASR::StructInstanceMember_t>(*x.m_v)) {
                    tmp = llvm_utils->CreateLoad2(type->getPointerTo(), tmp);
                }
                
                tmp = llvm_utils->CreateLoad2(
                    name2dertype[current_der_type_name]->getPointerTo(), llvm_utils->create_gep2(type, tmp, 1));
            }
        } else if (compiler_options.po.bounds_checking &&
                ASRUtils::is_allocatable(x_m_v_type) && ASRUtils::is_struct(*x_m_v_type)) {
            ASR::Variable_t* var = ASRUtils::expr_to_variable_or_null(x.m_v);
            if (var) {
                llvm::Type* x_mv_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_v, x_m_v_type, module.get());
                llvm::Value* var_name_llvm = LCompilers::create_global_string_ptr(context, *module, *builder, var->m_name);
                llvm::Value* cond = builder->CreateICmpEQ(
                    builder->CreatePtrToInt(tmp,
                        llvm::Type::getInt64Ty(context)),
                    builder->CreatePtrToInt(llvm::ConstantPointerNull::get(x_mv_llvm_type->getPointerTo()),
                        llvm::Type::getInt64Ty(context)));
                llvm_utils->generate_runtime_error(cond,
                        "Tried to access member of unallocated variable '%s'",
                        {LLVMUtils::RuntimeLabel("'%s' unallocated here", {x.m_v->base.loc}, {var_name_llvm})},
                        infile,
                        location_manager,
                        var_name_llvm);
            }
        }
        if (ASR::is_a<ASR::Struct_t>(*symbol_get_past_external(x.m_m))) {
            ASR::Struct_t* member_struct = down_cast<ASR::Struct_t>(symbol_get_past_external(x.m_m));
            std::string member_name = get_type_key(member_struct);
            while( dertype2parent.find(current_der_type_name) != dertype2parent.end() &&
                        (current_der_type_name != member_name)) {
                if (tmp->getType()->isPointerTy()) {
                    tmp = llvm_utils->create_gep2(name2dertype[current_der_type_name], tmp, 0);
                } else {
                    if (llvm::isa<llvm::Constant>(tmp)) {
                        tmp = llvm::cast<llvm::Constant>(tmp)->getAggregateElement(0u);
                    } else {
                        tmp = builder->CreateExtractValue(tmp, 0);
                    }
                }
                current_der_type_name = dertype2parent[current_der_type_name];
            }
            return;
        }
        ASR::Variable_t* member = down_cast<ASR::Variable_t>(symbol_get_past_external(x.m_m));
        std::string member_name = std::string(member->m_name);
        LCOMPILERS_ASSERT(current_der_type_name.size() != 0);

        llvm::Type *xtype = name2dertype[current_der_type_name];
        if (LLVM::is_llvm_pointer(*x_m_v_type) && 
            ASR::is_a<ASR::StructInstanceMember_t>(*x.m_v) &&
            !ASRUtils::is_class_type(ASRUtils::extract_type(x_m_v_type))) {
            tmp = llvm_utils->CreateLoad2(xtype->getPointerTo(), tmp);
        }

        while( name2memidx[current_der_type_name].find(member_name) == name2memidx[current_der_type_name].end() ) {
            if( dertype2parent.find(current_der_type_name) == dertype2parent.end() ) {
                throw CodeGenError(current_der_type_name + " doesn't have any member named " + member_name,
                                    x.base.base.loc);
            }
            if (tmp->getType()->isPointerTy()) {
                tmp = llvm_utils->create_gep2(name2dertype[current_der_type_name], tmp, 0);
            } else {
                if (llvm::isa<llvm::Constant>(tmp)) {
                    tmp = llvm::cast<llvm::Constant>(tmp)->getAggregateElement(0u);
                } else {
                    tmp = builder->CreateExtractValue(tmp, 0);
                }
            }
            current_der_type_name = dertype2parent[current_der_type_name];
        }
        int member_idx = 0;
        member_idx = name2memidx[current_der_type_name][member_name];

        xtype = name2dertype[current_der_type_name];
        if (tmp->getType()->isPointerTy()) {
            ASR::ttype_t* base_t = ASRUtils::expr_type(x.m_v);
            base_t = ASRUtils::type_get_past_allocatable(base_t);
            base_t = ASRUtils::type_get_past_pointer(base_t);
            if (ASRUtils::is_array(base_t)) {// If nested derived type
                ASR::ttype_t *elem_t = ASRUtils::type_get_past_array(base_t);\
                if (ASRUtils::is_struct(*elem_t)){
                    llvm::Type *array_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_v, base_t, module.get());
                    tmp = llvm_utils->create_gep2(array_type, tmp, 0);
                    base_t = elem_t;
                }
            }
            tmp = llvm_utils->create_gep2(xtype, tmp, member_idx);
        } else {
            if (llvm::isa<llvm::Constant>(tmp)) {
                tmp = llvm::cast<llvm::Constant>(tmp)->getAggregateElement(member_idx);
            } else {
                tmp = builder->CreateExtractValue(tmp, member_idx);
            }
        }
        ASR::ttype_t* member_type = ASRUtils::type_get_past_pointer(
            ASRUtils::type_get_past_allocatable(member->m_type));
        member_type = ASRUtils::type_get_past_array(member_type);
        if( ASR::is_a<ASR::StructType_t>(*member_type) ) {
            ASR::symbol_t *s_sym = member->m_type_declaration;
            current_der_type_name = get_type_key(s_sym);
            if (!ASRUtils::is_class_type(member_type)) {
                uint32_t h = get_hash((ASR::asr_t*)member);
                if( llvm_symtab.find(h) != llvm_symtab.end() ) {
                    tmp = llvm_symtab[h];
                }
            }
        }

        // COMMON block type aliasing: Fortran allows different program units to
        // view the same COMMON block storage with different types (storage
        // association). When the local variable type (x.m_type) differs from
        // the struct member type (member->m_type), we must bitcast the pointer
        // to match the local view. Details handled by the helper function.
        tmp = llvm_utils->apply_common_block_alias_cast(
            tmp,
            const_cast<ASR::expr_t*>(reinterpret_cast<const ASR::expr_t*>(&x)),
            x.m_type,
            member->m_type
        );
    }

    void visit_CoarrayRef(const ASR::CoarrayRef_t& x) {
        // We visit the base expression and ignore the coindices
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        int64_t ptr_loads_copy = ptr_loads;
        if (is_assignment_target) {
            ptr_loads = 1; // Load once: i64** alloca → i64* data pointer for the store target
            this->visit_expr_wrapper(x.m_var, false);
        } else if (ptr_loads <= 0) {
            this->visit_expr_wrapper(x.m_var, false);
        } else {
            this->visit_expr_wrapper(x.m_var, true);
        }
        ptr_loads = ptr_loads_copy;
        // In single-image mode, coindices are ignored; tmp already contains the base expression result
    }

    void visit_StructConstant(const ASR::StructConstant_t& x) {
        std::vector<llvm::Constant *> elements;
        llvm::StructType* t = llvm::cast<llvm::StructType>(
            llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(x.m_dt_sym)), module.get()));
        ASR::Struct_t* struct_
            = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(x.m_dt_sym));

        [[maybe_unused]] size_t n_members = struct_->n_members;
        if (struct_->m_parent) {
            ASR::Struct_t* parent_struct = ASR::down_cast<ASR::Struct_t>(
                ASRUtils::symbol_get_past_external(struct_->m_parent));
            while (parent_struct) {
                n_members += parent_struct->n_members;
                if (parent_struct->m_parent) {
                    parent_struct = ASR::down_cast<ASR::Struct_t>(
                        ASRUtils::symbol_get_past_external(parent_struct->m_parent));
                } else {
                    parent_struct = nullptr;
                }
            }
        }

        LCOMPILERS_ASSERT(x.n_args == n_members);
        for (size_t i = 0; i < x.n_args; ++i) {
            ASR::expr_t *value = x.m_args[i].m_value;
            llvm::Constant* initializer = nullptr;
            llvm::Type* type = nullptr;
            if (value == nullptr) {
                auto member2sym = struct_->m_symtab->get_scope();
                LCOMPILERS_ASSERT(member2sym[struct_->m_members[i]]->type == ASR::symbolType::Variable);
                ASR::Variable_t *s = ASR::down_cast<ASR::Variable_t>(member2sym[struct_->m_members[i]]);
                type = llvm_utils->get_type_from_ttype_t_util(
                    ASRUtils::EXPR(ASR::make_Var_t(al, s->base.base.loc, &s->base)),
                    s->m_type,
                    module.get());
                if (type->isArrayTy()) {
                    initializer = llvm::ConstantArray::getNullValue(type);
                } else {
                    initializer = llvm::Constant::getNullValue(type);
                }
            } else if (ASR::is_a<ASR::ArrayConstant_t>(*value)) {
                ASR::ArrayConstant_t *arr_expr = ASR::down_cast<ASR::ArrayConstant_t>(value);
                type = llvm_utils->get_type_from_ttype_t_util(value, arr_expr->m_type, module.get());
                if (type->isArrayTy()) {
                    initializer = get_const_array(value, type->getArrayElementType());
                } else if (ASRUtils::is_character(
                               *ASRUtils::type_get_past_array(ASRUtils::expr_type(value)))) {
                    llvm::GlobalVariable* gv = llvm::dyn_cast<llvm::GlobalVariable>(
                        llvm_utils->declare_constant_stringArray(al, arr_expr));
                    if (gv && gv->hasInitializer()) {
                        initializer = gv->getInitializer();
                    }
                } else {
                    throw CodeGenError("Unsupported non-array type in struct ArrayConstant initializer");
                }
            } else {
                visit_expr_wrapper(value);
                initializer = llvm::dyn_cast<llvm::Constant>(tmp);
                if (!initializer) {
                    throw CodeGenError("Non-constant value found in struct initialization");
                }
                if (ASR::is_a<ASR::StringConstant_t>(*value)) {
                    llvm::GlobalVariable* gv = llvm::dyn_cast<llvm::GlobalVariable>(initializer);
                    if (gv && gv->hasInitializer()) {
                        initializer = gv->getInitializer();
                    }
                }
            }
            elements.push_back(initializer);
        }
        tmp = llvm::ConstantStruct::get(t, elements);
        current_der_type_name = get_type_key(x.m_dt_sym);
    }

    llvm::Constant* get_const_array(ASR::expr_t *value, llvm::Type* type) {
        LCOMPILERS_ASSERT(ASR::is_a<ASR::ArrayConstant_t>(*value));
        ASR::ArrayConstant_t* arr_const = ASR::down_cast<ASR::ArrayConstant_t>(value);
        std::vector<llvm::Constant*> arr_elements;
        size_t arr_const_size = (size_t) ASRUtils::get_fixed_size_of_array(arr_const->m_type);
        arr_elements.reserve(arr_const_size);
        llvm::ArrayType* arr_type = nullptr;
        llvm::Type* elem_type = type;
        if (type->isArrayTy()) {
            arr_type = llvm::cast<llvm::ArrayType>(type);
            elem_type = arr_type->getElementType();
        }
        int a_kind;
        for (size_t i = 0; i < arr_const_size; i++) {
            ASR::expr_t* elem = ASRUtils::fetch_ArrayConstant_value(al, arr_const, i);
            a_kind = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(elem));
            if (ASR::is_a<ASR::IntegerConstant_t>(*elem)) {
                ASR::IntegerConstant_t* int_const = ASR::down_cast<ASR::IntegerConstant_t>(elem);
                arr_elements.push_back(llvm::ConstantInt::get(
                    context, llvm::APInt(8 * a_kind, int_const->m_n, true)));
            } else if (ASR::is_a<ASR::RealConstant_t>(*elem)) {
                ASR::RealConstant_t* real_const = ASR::down_cast<ASR::RealConstant_t>(elem);
                if (a_kind == 4) {
                    arr_elements.push_back(llvm::ConstantFP::get(
                        context, llvm::APFloat((float) real_const->m_r)));
                } else if (a_kind == 8) {
                    arr_elements.push_back(llvm::ConstantFP::get(
                        context, llvm::APFloat((double) real_const->m_r)));
                }
            } else if (ASR::is_a<ASR::LogicalConstant_t>(*elem)) {
                ASR::LogicalConstant_t* logical_const = ASR::down_cast<ASR::LogicalConstant_t>(elem);
                int kind = ASRUtils::extract_kind_from_ttype_t(logical_const->m_type);
                arr_elements.push_back(llvm::ConstantInt::get(
                    context, llvm::APInt(kind * 8, logical_const->m_value)));
            } else if (ASR::is_a<ASR::ComplexConstant_t>(*elem)) {
                ASR::ComplexConstant_t* comp_const = ASR::down_cast<ASR::ComplexConstant_t>(elem);
                llvm::Constant *re, *im;
                llvm::StructType* struct_type;
                if (a_kind == 4) {
                    re = llvm::ConstantFP::get(context, llvm::APFloat((float) comp_const->m_re));
                    im = llvm::ConstantFP::get(context, llvm::APFloat((float) comp_const->m_im));
                    struct_type = llvm::cast<llvm::StructType>(complex_type_4);
                } else if (a_kind == 8) {
                    re = llvm::ConstantFP::get(context, llvm::APFloat((double) comp_const->m_re));
                    im = llvm::ConstantFP::get(context, llvm::APFloat((double) comp_const->m_im));
                    struct_type = llvm::cast<llvm::StructType>(complex_type_8);
                } else {
                    throw CodeGenError("Complex kind " + std::to_string(a_kind) + " not supported in array initializer");
                }
                arr_elements.push_back(llvm::ConstantStruct::get(struct_type, {re, im}));
            }
        }
        if (!arr_type) {
            arr_type = llvm::ArrayType::get(elem_type, arr_const_size);
        }
        llvm::Constant* initializer = nullptr;
        if (isNullValueArray(arr_elements)) {
            initializer = llvm::ConstantArray::getNullValue(arr_type);
        } else {
            initializer = llvm::ConstantArray::get(arr_type, arr_elements);
        }
        return initializer;
    }

    bool isNullValueArray(const std::vector<llvm::Constant*>& elements) {
        return std::all_of(elements.begin(), elements.end(),
            [](llvm::Constant* elem) { return elem->isNullValue(); });
    }

    bool needs_common_linkage_for_global(const ASR::Variable_t &x) {
        // bind(C) variables without initializers should use CommonLinkage
        // to allow merging with C definitions of the same symbol
        if (x.m_abi == ASR::abiType::BindC && x.m_symbolic_value == nullptr) {
            return true;
        }
        if (!compiler_options.separate_compilation
                || x.m_abi == ASR::abiType::ExternalUndefined) {
            return false;
        }
        ASR::symbol_t *owner = ASRUtils::get_asr_owner(
            const_cast<ASR::symbol_t*>(&x.base));
        if (owner && ASR::is_a<ASR::Module_t>(*owner)) {
            return startswith(ASRUtils::symbol_name(owner), "file_common_block_");
        }
        return false;
    }

    void set_global_variable_linkage_as_common(llvm::Value* ptr,
            const ASR::Variable_t &x) {
        if (needs_common_linkage_for_global(x)) {
            /*
                In case of global variables without initialization, clang
                generates a common symbol. For the following C code:

                ```
                int global_var;
                int global_variable_initalised = 42;
                ```

                on using `clang -S -emit-llvm` we get:

                ```
                ; ModuleID = 'a.c'
                source_filename = "a.c"
                target datalayout = "e-m:o-i64:64-i128:128-n32:64-S128"
                target triple = "arm64-apple-macosx14.0.0"

                @global_var_initialized = global i32 42, align 4
                @global_var = common global i32 0, align 4

                !llvm.module.flags = !{!0, !1, !2, !3, !4}
                !llvm.ident = !{!5}

                !0 = !{i32 2, !"SDK Version", [2 x i32] [i32 14, i32 4]}
                !1 = !{i32 1, !"wchar_size", i32 4}
                !2 = !{i32 8, !"PIC Level", i32 2}
                !3 = !{i32 7, !"uwtable", i32 1}
                !4 = !{i32 7, !"frame-pointer", i32 1}
                !5 = !{!"Apple clang version 15.0.0 (clang-1500.3.9.4)"}
                ```

                Hence, we set the linkage to CommonLinkage for
                global variables without initialization in case
                `compiler_options.separate_compilation` is set to true.
            */
            llvm::GlobalVariable *gv = llvm::cast<llvm::GlobalVariable>(
                ptr
            );
            gv->setLinkage(llvm::GlobalValue::CommonLinkage);
        }
    }

    void visit_Variable(const ASR::Variable_t &x) {
        if (x.m_value && x.m_storage == ASR::storage_typeType::Parameter) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        uint32_t h = get_hash((ASR::asr_t*)&x);
        // This happens at global scope, so the intent can only be either local
        // (global variable declared/initialized in this translation unit), or
        // external (global variable not declared/initialized in this
        // translation unit, just referenced).
        LCOMPILERS_ASSERT(x.m_intent == intent_local || x.m_intent == ASRUtils::intent_unspecified
            || x.m_abi == ASR::abiType::ExternalUndefined);
        bool external = (x.m_abi != ASR::abiType::Source);
        // BindC variables defined in this module (intent local) are
        // definitions, not external declarations
        if (x.m_abi == ASR::abiType::BindC &&
                (x.m_symbolic_value != nullptr || x.m_intent == intent_local)) {
            external = false;
        }
        llvm::Constant* init_value = nullptr;
        llvm::Constant* alias_target = nullptr;
        if (x.m_symbolic_value != nullptr &&
            !ASRUtils::is_string_only(x.m_type)){
            if (ASR::is_a<ASR::GetPointer_t>(*x.m_symbolic_value)) {
                ASR::GetPointer_t* get_ptr = ASR::down_cast<ASR::GetPointer_t>(x.m_symbolic_value);
                if (ASR::is_a<ASR::Var_t>(*get_ptr->m_arg) || ASR::is_a<ASR::ArrayItem_t>(*get_ptr->m_arg)) {
                    ASR::Variable_t* target_var = nullptr;
                    if (ASR::is_a<ASR::Var_t>(*get_ptr->m_arg)) {
                        target_var = ASR::down_cast<ASR::Variable_t>(ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(get_ptr->m_arg)->m_v));
                    } else {
                        ASR::ArrayItem_t* arr_item = ASR::down_cast<ASR::ArrayItem_t>(get_ptr->m_arg);
                        if (ASR::is_a<ASR::Var_t>(*arr_item->m_v)) {
                            target_var = ASR::down_cast<ASR::Variable_t>(ASRUtils::symbol_get_past_external(ASR::down_cast<ASR::Var_t>(arr_item->m_v)->m_v));
                        }
                    }
                    if (target_var) {
                        uint32_t target_h = get_hash((ASR::asr_t*)target_var);
                        if (llvm_symtab.find(target_h) == llvm_symtab.end()) {
                            visit_Variable(*target_var);
                        }
                        if (llvm_symtab.find(target_h) != llvm_symtab.end()) {
                            alias_target = llvm::dyn_cast<llvm::Constant>(llvm_symtab[target_h]);
                            
                            // Flatten chained aliases so we don't alias an alias
                            while (alias_target && llvm::isa<llvm::GlobalAlias>(alias_target)) {
                                alias_target = llvm::cast<llvm::GlobalAlias>(alias_target)->getAliasee();
                            }
                            
                            if (ASR::is_a<ASR::ArrayItem_t>(*get_ptr->m_arg)) {
                                ASR::ArrayItem_t* arr_item = ASR::down_cast<ASR::ArrayItem_t>(get_ptr->m_arg);
                                if (arr_item->n_args > 0 && arr_item->m_args[0].m_right && ASR::is_a<ASR::IntegerConstant_t>(*arr_item->m_args[0].m_right)) {
                                    int64_t offset = ASR::down_cast<ASR::IntegerConstant_t>(arr_item->m_args[0].m_right)->m_n;
                                    offset -= 1; // 1-based to 0-based
                                    if (offset != 0 && alias_target) {
                                        llvm::Constant* indices[] = {
                                            llvm::ConstantInt::get(context, llvm::APInt(32, 0)),
                                            llvm::ConstantInt::get(context, llvm::APInt(32, offset))
                                        };
                                        llvm::Type* target_ty = llvm_utils->get_type_from_ttype_t_util(x.m_symbolic_value, target_var->m_type, module.get());
                                        alias_target = llvm::ConstantExpr::getGetElementPtr(target_ty, alias_target, indices);
                                    }
                                }
                            }
                        }
                    }
                }
            }
            
            if (!alias_target) {
                this->visit_expr_wrapper(x.m_symbolic_value, true);
                init_value = llvm::dyn_cast<llvm::Constant>(tmp);
            }
        }

        // variable name to use in declaration
        std::string llvm_var_name = "";
        if (x.m_abi == ASR::abiType::BindC) {
            if (x.m_bindc_name && x.m_bindc_name[0] != '\0') {
                // bind(c, name='foo') — use the explicit name
                llvm_var_name = x.m_bindc_name;
            } else if (!x.m_bindc_name) {
                // bind(c) without name= — use the Fortran name
                llvm_var_name = x.m_name;
            } else {
                // bind(c, name='') — empty name, use mangled name
                llvm_var_name = mangle_prefix + x.m_name;
            }
        } else {
            llvm_var_name = mangle_prefix + x.m_name;
        }

        if (alias_target) {
            llvm::Type *type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                    ASR::make_Var_t(al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))), x.m_type, module.get());
            if (alias_target->getType() != type->getPointerTo()) {
                alias_target = llvm::ConstantExpr::getBitCast(alias_target, type->getPointerTo());
            }
            llvm::GlobalAlias* alias = llvm::GlobalAlias::create(type, 0, llvm::GlobalValue::ExternalLinkage, llvm_var_name, alias_target, module.get());
            llvm_symtab[h] = alias;
            return;
        }

        if (x.m_type->type == ASR::ttypeType::Integer
            || x.m_type->type == ASR::ttypeType::UnsignedInteger) {
            int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
            llvm::Type *type;
            int init_value_bits = 8*a_kind;
            type = llvm_utils->getIntType(a_kind);
            llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name,
                type);
            if (!external) {
                if (ASRUtils::is_array(x.m_type)) {
                    throw CodeGenError("Arrays are not supported by visit_Variable");
                }
                if (init_value) {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            init_value);
                } else {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            llvm::ConstantInt::get(context,
                                llvm::APInt(init_value_bits, 0)));
                    set_global_variable_linkage_as_common(ptr, x);
                }
            }
            llvm_symtab[h] = ptr;
        } else if (x.m_type->type == ASR::ttypeType::Real) {
            int a_kind = down_cast<ASR::Real_t>(x.m_type)->m_kind;
            llvm::Type *type;
            int init_value_bits = 8*a_kind;
            type = llvm_utils->getFPType(a_kind);
            llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name, type);
            if (!external) {
                if (init_value) {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            init_value);
                } else {
                    if( init_value_bits == 32 ) {
                        module->getNamedGlobal(llvm_var_name)->setInitializer(
                                llvm::ConstantFP::get(context,
                                    llvm::APFloat((float)0)));
                    } else if( init_value_bits == 64 ) {
                        module->getNamedGlobal(llvm_var_name)->setInitializer(
                                llvm::ConstantFP::get(context,
                                    llvm::APFloat((double)0)));
                    }
                    set_global_variable_linkage_as_common(ptr, x);
                }
            }
            llvm_symtab[h] = ptr;
        } else if (x.m_type->type == ASR::ttypeType::Array) {
            llvm::Constant *ptr{};
            if(ASRUtils::is_character(*x.m_type)){
                ASR::expr_t* value = nullptr;
                if( x.m_value ) {
                    value = x.m_value;
                } else if( x.m_symbolic_value &&
                        ASRUtils::is_value_constant(x.m_symbolic_value) ) {
                    value = x.m_symbolic_value;
                }
                if(value){LCOMPILERS_ASSERT(ASR::is_a<ASR::ArrayConstant_t>(*value));}
                ptr = llvm::cast<llvm::Constant>(llvm_utils->handle_global_nonallocatable_stringArray(al,
                    ASR::down_cast<ASR::Array_t>(x.m_type), value?ASR::down_cast<ASR::ArrayConstant_t>(value):nullptr, llvm_var_name));

            } else {
                llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                    ASR::make_Var_t(al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))),
                    x.m_type, module.get());
                ptr = module->getOrInsertGlobal(llvm_var_name, type);
                if (!external) {
                    ASR::expr_t* value = nullptr;
                    if( x.m_value ) {
                        value = x.m_value;
                    } else if( x.m_symbolic_value &&
                            ASRUtils::is_value_constant(x.m_symbolic_value) ) {
                        value = x.m_symbolic_value;
                    }
                    if (value) {

                         if (ASR::is_a<ASR::PointerNullConstant_t>(*value)) {
                             module->getNamedGlobal(llvm_var_name)->setInitializer(
                                llvm::ConstantArray::getNullValue(type));
                          } else {
                             llvm::Constant* initializer = get_const_array(value, type->getArrayElementType());
                             module->getNamedGlobal(llvm_var_name)->setInitializer(initializer);
                          }
                      } else {
                        module->getNamedGlobal(llvm_var_name)->setInitializer(llvm::ConstantArray::getNullValue(type));
                        set_global_variable_linkage_as_common(ptr, x);
                    }
                }
            }
            llvm_symtab[h] = ptr;
        } else if (x.m_type->type == ASR::ttypeType::Logical) {
            int a_kind = down_cast<ASR::Logical_t>(x.m_type)->m_kind;
            llvm::Type *logical_type = llvm_utils->getIntType(a_kind);
            llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name,
                logical_type);
            if (!external) {
                if (init_value) {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            init_value);
                } else {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            llvm::ConstantInt::get(context,
                                llvm::APInt(a_kind * 8, 0)));
                    set_global_variable_linkage_as_common(ptr, x);
                }
            }
            llvm_symtab[h] = ptr;
        } else if (x.m_type->type == ASR::ttypeType::String) {
            ASR::String_t* str = ASRUtils::get_string_type(x.m_type);
            llvm::Value *ptr{};
                bool is_const = (x.m_storage == ASR::storage_typeType::Parameter);
                if (!external) {
                    std::string initial_string_value;
                    if (x.m_symbolic_value) {
                        ASR::StringConstant_t* str_const = ASR::down_cast<ASR::StringConstant_t>(ASRUtils::expr_value(x.m_symbolic_value));
                        int64_t len; ASRUtils::extract_value(ASRUtils::get_string_type(str_const->m_type)->m_len, len);
                        initial_string_value = std::string(str_const->m_s, len);
                    } else {
                        initial_string_value = "";
                    }
                    llvm::GlobalValue::LinkageTypes string_linkage;
                    if (x.m_abi == ASR::abiType::BindC || compiler_options.separate_compilation) {
                        string_linkage = llvm::GlobalValue::ExternalLinkage;
                    } else {
                        string_linkage = llvm::GlobalValue::PrivateLinkage;
                    }
                    // For bind(C) char variables, the C-visible name must be
                    // the data storage (not the descriptor). Use an internal
                    // name for the descriptor and the C name for the data.
                    std::string desc_name = llvm_var_name;
                    std::string data_name = llvm_var_name + "_data";
                    if (x.m_abi == ASR::abiType::BindC) {
                        desc_name = llvm_var_name + "__desc";
                        data_name = llvm_var_name;
                    }
                    ptr = llvm_utils->declare_global_string(str, initial_string_value,
                        is_const, desc_name, string_linkage, data_name);
                } else {
                    ptr = module->getOrInsertGlobal(llvm_var_name, llvm_utils->get_StringType(x.m_type));
                }
            llvm_symtab[h] = ptr;
        } else if( x.m_type->type == ASR::ttypeType::CPtr ) {
            llvm::Type* void_ptr = llvm::Type::getVoidTy(context)->getPointerTo();
            llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name,
                void_ptr);
            if (!external) {
                if (init_value) {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            init_value);
                } else {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            llvm::ConstantPointerNull::get(
                                static_cast<llvm::PointerType*>(void_ptr))
                            );
                    set_global_variable_linkage_as_common(ptr, x);
                }
            }
            llvm_symtab[h] = ptr;
        } else if( x.m_type->type == ASR::ttypeType::StructType ) {
            ASR::StructType_t* struct_t = ASR::down_cast<ASR::StructType_t>(x.m_type);
            bool is_class = !struct_t->m_is_cstruct;
            if (init_value == nullptr) {
                std::vector<llvm::Constant*> field_values;
                if (is_class) {
                    // for class types, first field is vtable ptr
                    llvm::Constant* vtable_ptr = struct_api->get_pointer_to_method(
                        ASRUtils::symbol_get_past_external(x.m_type_declaration), module.get());
                    field_values.push_back(vtable_ptr);
                }
                get_type_default_field_values(x.m_type_declaration, field_values,
                    ASRUtils::symbol_get_past_external(x.m_type_declaration));
                llvm::StructType* llvm_struct_type = llvm::cast<llvm::StructType>(llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                    ASR::make_Var_t(al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))), x.m_type, module.get()));
                init_value = llvm::ConstantStruct::get(llvm_struct_type, field_values);
            }
            if (!is_class) {
                if( x.m_type_declaration && ASRUtils::is_c_ptr(x.m_type_declaration) ) {
                    llvm::Type* void_ptr = llvm::Type::getVoidTy(context)->getPointerTo();
                    llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name,
                        void_ptr);
                    if (!external) {
                        if (init_value) {
                            module->getNamedGlobal(llvm_var_name)->setInitializer(
                                    init_value);
                        } else {
                            module->getNamedGlobal(llvm_var_name)->setInitializer(
                                    llvm::ConstantPointerNull::get(
                                        static_cast<llvm::PointerType*>(void_ptr))
                                    );
                            set_global_variable_linkage_as_common(ptr, x);
                        }
                    }
                    llvm_symtab[h] = ptr;
                } else {
                    llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                    ASR::make_Var_t(al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))), x.m_type, module.get());
                    llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name,
                        type);
                    if (!external) {
                        if (init_value) {
                            module->getNamedGlobal(llvm_var_name)->setInitializer(
                                    init_value);
                            // For common blocks (structs with zeroinitializer), use CommonLinkage
                            // to allow multiple definitions across compilation units to be merged.
                            if (init_value->isNullValue()) {
                                set_global_variable_linkage_as_common(ptr, x);
                            }
                        } else {
                            module->getNamedGlobal(llvm_var_name)->setInitializer(
                                    llvm::Constant::getNullValue(type)
                                );
                            set_global_variable_linkage_as_common(ptr, x);
                        }
                    }
                    llvm_symtab[h] = ptr;
                }
            } else {
                llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(
                    ASRUtils::EXPR(ASR::make_Var_t(al, x.base.base.loc, x.m_type_declaration)),
                    x.m_type, module.get());
                llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name,
                    type);
                if (!external) {
                    if (init_value) {
                        module->getNamedGlobal(llvm_var_name)->setInitializer(
                                init_value);
                        // For common blocks (structs with zeroinitializer), use CommonLinkage
                        // to allow multiple definitions across compilation units to be merged.
                        if (init_value->isNullValue()) {
                            set_global_variable_linkage_as_common(ptr, x);
                        }
                    } else {
                        module->getNamedGlobal(llvm_var_name)
                            ->setInitializer(llvm::Constant::getNullValue(type));
                        set_global_variable_linkage_as_common(ptr, x);
                    }
                }
                llvm_symtab[h] = ptr;
            }
            // Skip queueing runtime initialization for module-scope struct
            // globals that already have a static initializer from a
            // StructConstant (e.g. COMMON block struct instances populated by
            // BLOCK DATA).  Their string members already point at constant
            // string globals; running the runtime init path would issue a
            // malloc+strcpy that overwrites the static pointer and leaks under
            // --detect-leaks (the corresponding finalize is also skipped for
            // such variables; see not_finalizable_variable).
            bool skip_runtime_struct_init = false;
            if (x.m_symbolic_value != nullptr
                    && x.m_parent_symtab != nullptr
                    && x.m_parent_symtab->asr_owner != nullptr
                    && ASR::is_a<ASR::symbol_t>(*x.m_parent_symtab->asr_owner)) {
                ASR::symbol_t* owner = ASR::down_cast<ASR::symbol_t>(
                    x.m_parent_symtab->asr_owner);
                if (ASR::is_a<ASR::Module_t>(*owner)) {
                    skip_runtime_struct_init = true;
                }
            }
            if (!skip_runtime_struct_init) {
                allocatable_struct_array_members_details.push_back(std::make_pair(
                    ASRUtils::symbol_get_past_external(x.m_type_declaration), llvm_symtab[h]));
            }
        } else if(x.m_type->type == ASR::ttypeType::Pointer ||
                    x.m_type->type == ASR::ttypeType::Allocatable) {
            ASR::dimension_t* m_dims = nullptr;
            int n_dims = 0, a_kind = -1;
            bool is_array_type = false, is_malloc_array_type = false, is_list = false;
            llvm::Type* x_ptr = llvm_utils->get_type_from_ttype_t(
                ASRUtils::EXPR(ASR::make_Var_t(
                    al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))), x.m_type,
                    x.m_type_declaration, x.m_storage, is_array_type,
                    is_malloc_array_type, is_list, m_dims, n_dims, a_kind, module.get());
            llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name,
                x_ptr);
            llvm::Type* type_ = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                ASR::make_Var_t(al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))),
                ASRUtils::type_get_past_pointer(
                ASRUtils::type_get_past_allocatable(x.m_type)),
                module.get(), x.m_abi);
            if (ASRUtils::is_array(x.m_type)) {  // memorize arrays only.
                // For array pointers/allocatables, an init from null() is a
                // bare element-pointer constant whose LLVM type doesn't match
                // the pointer-to-descriptor global type; drop it and let the
                // companion-descriptor path below run instead.
                if (init_value && init_value->getType() != x_ptr) {
                    init_value = nullptr;
                }
                allocatable_array_details.push_back(
                    { ASRUtils::EXPR(ASR::make_Var_t(
                          al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))),
                      ptr,
                      type_,
                      x.m_type,
                      ASRUtils::extract_dimensions_from_ttype(x.m_type, m_dims) });
            } else if (ASRUtils::is_character(*x.m_type) && !init_value) {
                // set all members of string_descriptor to null and zeroes.
                init_value = llvm::ConstantAggregateZero::get(string_descriptor);
            }
            if (!external) {
                if (init_value) {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            init_value);
                } else if (ASRUtils::is_array(x.m_type)) {
                    // For pointer/allocatable array module variables, create a
                    // companion global descriptor so the pointer is never NULL.
                    // In separate compilation, main() cannot initialize
                    // submodule-private variables, so we need a valid
                    // descriptor from the start.
                    std::string desc_name = llvm_var_name + "_descriptor__";
                    llvm::GlobalVariable *desc_global = new llvm::GlobalVariable(
                        *module, type_, false,
                        llvm::GlobalVariable::InternalLinkage,
                        llvm::ConstantAggregateZero::get(type_),
                        desc_name);
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            desc_global);
                } else {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            llvm::ConstantPointerNull::get(
                                static_cast<llvm::PointerType*>(x_ptr))
                            );
                    set_global_variable_linkage_as_common(ptr, x);
                }
            }
            llvm_symtab[h] = ptr;
        } else if (x.m_type->type == ASR::ttypeType::List) {
            llvm::StructType* list_type = static_cast<llvm::StructType*>(
                llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                ASR::make_Var_t(al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))), x.m_type, module.get()));
            llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name, list_type);
            module->getNamedGlobal(llvm_var_name)->setInitializer(
                llvm::ConstantStruct::get(list_type,
                llvm::Constant::getNullValue(list_type)));
            llvm_symtab[h] = ptr;
        } else if (x.m_type->type == ASR::ttypeType::Tuple) {
            llvm::StructType* tuple_type = static_cast<llvm::StructType*>(
                llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                ASR::make_Var_t(al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))), x.m_type, module.get()));
            llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name, tuple_type);
            module->getNamedGlobal(llvm_var_name)->setInitializer(
                llvm::ConstantStruct::get(tuple_type,
                llvm::Constant::getNullValue(tuple_type)));
            llvm_symtab[h] = ptr;
        } else if(x.m_type->type == ASR::ttypeType::Dict) {
            llvm::StructType* dict_type = static_cast<llvm::StructType*>(
                llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                ASR::make_Var_t(al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))), x.m_type, module.get()));
            llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name, dict_type);
            module->getNamedGlobal(llvm_var_name)->setInitializer(
                llvm::ConstantStruct::get(dict_type,
                llvm::Constant::getNullValue(dict_type)));
            llvm_symtab[h] = ptr;
        } else if(x.m_type->type == ASR::ttypeType::Set) {
            llvm::StructType* set_type = static_cast<llvm::StructType*>(
                llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                ASR::make_Var_t(al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))), x.m_type, module.get()));
            llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name, set_type);
            module->getNamedGlobal(llvm_var_name)->setInitializer(
                llvm::ConstantStruct::get(set_type,
                llvm::Constant::getNullValue(set_type)));
            llvm_symtab[h] = ptr;
        } else if (x.m_type->type == ASR::ttypeType::Complex) {
            int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);

            llvm::Constant* re;
            llvm::Constant* im;
            llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                ASR::make_Var_t(al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))), x.m_type, module.get());;
            llvm::Constant* ptr = module->getOrInsertGlobal(llvm_var_name, type);

            if (!external) {
                double x_re = 0.0, x_im = 0.0;
                if (x.m_value) {
                    LCOMPILERS_ASSERT(ASR::is_a<ASR::ComplexConstant_t>(*x.m_value));
                    ASR::ComplexConstant_t* x_cc = ASR::down_cast<ASR::ComplexConstant_t>(x.m_value);
                    x_re = x_cc->m_re; x_im = x_cc->m_im;
                }
                if (init_value) {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(init_value);
                } else {
                    switch (a_kind) {
                        case 4: {
                            re = llvm::ConstantFP::get(context, llvm::APFloat((float) x_re));
                            im = llvm::ConstantFP::get(context, llvm::APFloat((float) x_im));
                            type = complex_type_4;
                            break;
                        }
                        case 8: {
                            re = llvm::ConstantFP::get(context, llvm::APFloat((double) x_re));
                            im = llvm::ConstantFP::get(context, llvm::APFloat((double) x_im));
                            type = complex_type_8;
                            break;
                        }
                        default: {
                            throw CodeGenError("kind type is not supported");
                        }
                    }
                    // Create a constant structure to represent the complex number
                    std::vector<llvm::Constant*> elements = { re, im };
                    llvm::Constant* complex_init = llvm::ConstantStruct::get(static_cast<llvm::StructType*>(type), elements);
                    module->getNamedGlobal(llvm_var_name)->setInitializer(complex_init);
                }
            }
            llvm_symtab[h] = ptr;
        } else if (x.m_type->type == ASR::ttypeType::TypeParameter) {
            // Ignore type variables
        } else if (x.m_type->type == ASR::ttypeType::FunctionType) {
            llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                ASR::make_Var_t(al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))), x.m_type, module.get());
            llvm::Constant *ptr = module->getOrInsertGlobal(llvm_var_name, type);
            if (!external) {
                if (init_value) {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            init_value);
                } else {
                    module->getNamedGlobal(llvm_var_name)->setInitializer(
                            llvm::Constant::getNullValue(type)
                        );
                    set_global_variable_linkage_as_common(ptr, x);
                }
            }
            llvm_symtab[h] = ptr;
        } else {
            throw CodeGenError("Variable type not supported " + ASRUtils::type_to_str_python_symbol(x.m_type, x.m_type_declaration), x.base.base.loc);
        }
    }

    void visit_PointerNullConstant(const ASR::PointerNullConstant_t& x){
        llvm::Type* value_type;

        value_type = ASRUtils::is_array(x.m_type)?
            llvm_utils->get_el_type(x.m_var_expr,
                ASRUtils::extract_type(x.m_type), module.get())->getPointerTo():
            llvm_utils->get_type_from_ttype_t_util(x.m_var_expr, x.m_type, module.get());
        if (ASRUtils::is_string_only(x.m_type)) {
            value_type = llvm_utils->i8_ptr;
        }
        tmp = llvm::ConstantPointerNull::get(static_cast<llvm::PointerType*>(value_type));
    }

    void visit_Enum(const ASR::Enum_t& x) {
        if ( x.m_abi == ASR::abiType::ExternalUndefined ) {
            return;
        }
        if( x.m_enum_value_type == ASR::enumtypeType::IntegerUnique &&
            x.m_abi == ASR::abiType::BindC ) {
            throw CodeGenError("C-interoperation support for non-consecutive but uniquely "
                               "valued integer enums isn't available yet.");
        }
        bool is_integer = ASR::is_a<ASR::Integer_t>(*x.m_type);
        ASR::storage_typeType m_storage = ASR::storage_typeType::Default;
        bool is_array_type = false, is_malloc_array_type = false, is_list = false;
        ASR::dimension_t* m_dims = nullptr;
        int n_dims = -1, a_kind = -1;
        llvm::Type* value_type = llvm_utils->get_type_from_ttype_t(ASRUtils::EXPR(ASR::make_Var_t(
                    al, x.base.base.loc, const_cast<ASR::symbol_t*>(&x.base))),
            x.m_type, nullptr, m_storage, is_array_type,
            is_malloc_array_type, is_list, m_dims, n_dims, a_kind, module.get());
        if( is_integer ) {
            int64_t min_value = INT64_MAX;
            int64_t max_value = INT64_MIN;
            size_t max_name_len = 0;
            llvm::Value* itr_value = nullptr;
            for( auto itr: x.m_symtab->get_scope() ) {
                ASR::Variable_t* itr_var = ASR::down_cast<ASR::Variable_t>(itr.second);
                ASR::expr_t* value = ASRUtils::expr_value(itr_var->m_symbolic_value);
                int64_t value_int64 = -1;
                this->visit_expr(*value);
                itr_value = tmp;
                ASRUtils::extract_value(value, value_int64);
                min_value = std::min(value_int64, min_value);
                max_value = std::max(value_int64, max_value);
                max_name_len = std::max(max_name_len, itr.first.size());
            }

            llvm::ArrayType* name_array_type = llvm::ArrayType::get(llvm::Type::getInt8Ty(context),
                                                                    max_name_len + 1);
            llvm::StructType* enum_value_type = llvm::StructType::create({name_array_type, value_type});
            llvm::Constant* empty_vt = llvm::ConstantStruct::get(enum_value_type, {llvm::ConstantArray::get(name_array_type,
                {llvm::ConstantInt::get(llvm::Type::getInt8Ty(context), llvm::APInt(8, '\0'))}),
                (llvm::Constant*) itr_value});
            std::vector<llvm::Constant*> enum_value_pairs(max_value - min_value + 1, empty_vt);

            for( auto itr: x.m_symtab->get_scope() ) {
                ASR::Variable_t* itr_var = ASR::down_cast<ASR::Variable_t>(itr.second);
                ASR::expr_t* value = ASRUtils::expr_value(itr_var->m_symbolic_value);
                int64_t value_int64 = -1;
                ASRUtils::extract_value(value, value_int64);
                this->visit_expr(*value);
                std::vector<llvm::Constant*> itr_var_name_v;
                itr_var_name_v.reserve(itr.first.size());
                for( size_t i = 0; i < itr.first.size(); i++ ) {
                    itr_var_name_v.push_back(llvm::ConstantInt::get(
                        llvm::Type::getInt8Ty(context), llvm::APInt(8, itr_var->m_name[i])));
                }
                itr_var_name_v.push_back(llvm::ConstantInt::get(
                    llvm::Type::getInt8Ty(context), llvm::APInt(8, '\0')));
                llvm::Constant* name = llvm::ConstantArray::get(name_array_type, itr_var_name_v);
                enum_value_pairs[value_int64 - min_value] = llvm::ConstantStruct::get(
                    enum_value_type, {name, (llvm::Constant*) tmp});
            }

            llvm::ArrayType* global_enum_array = llvm::ArrayType::get(enum_value_type,
                                                    max_value - min_value + 1);
            llvm::Constant *array = module->getOrInsertGlobal(x.m_name,
                                        global_enum_array);
            module->getNamedGlobal(x.m_name)->setInitializer(
                llvm::ConstantArray::get(global_enum_array, enum_value_pairs));
            uint32_t h = get_hash((ASR::asr_t*)&x);
            llvm_symtab[h] = array;
        } else {
            size_t max_name_len = 0;

            for( auto itr: x.m_symtab->get_scope() ) {
                max_name_len = std::max(max_name_len, itr.first.size());
            }

            llvm::ArrayType* name_array_type = llvm::ArrayType::get(llvm::Type::getInt8Ty(context),
                                                                    max_name_len + 1);
            llvm::StructType* enum_value_type = llvm::StructType::create({name_array_type, value_type});
            std::vector<llvm::Constant*> enum_value_pairs(x.n_members, nullptr);

            for( auto itr: x.m_symtab->get_scope() ) {
                ASR::Variable_t* itr_var = ASR::down_cast<ASR::Variable_t>(itr.second);
                ASR::expr_t* value = itr_var->m_symbolic_value;
                int64_t value_int64 = -1;
                ASRUtils::extract_value(value, value_int64);
                this->visit_expr(*value);
                std::vector<llvm::Constant*> itr_var_name_v;
                itr_var_name_v.reserve(itr.first.size());
                for( size_t i = 0; i < itr.first.size(); i++ ) {
                    itr_var_name_v.push_back(llvm::ConstantInt::get(
                        llvm::Type::getInt8Ty(context), llvm::APInt(8, itr_var->m_name[i])));
                }
                itr_var_name_v.push_back(llvm::ConstantInt::get(
                    llvm::Type::getInt8Ty(context), llvm::APInt(8, '\0')));
                llvm::Constant* name = llvm::ConstantArray::get(name_array_type, itr_var_name_v);
                size_t dest_idx = 0;
                for( size_t j = 0; j < x.n_members; j++ ) {
                    if( std::string(x.m_members[j]) == itr.first ) {
                        dest_idx = j;
                        break ;
                    }
                }
                enum_value_pairs[dest_idx] = llvm::ConstantStruct::get(
                    enum_value_type, {name, (llvm::Constant*) tmp});
            }

            llvm::ArrayType* global_enum_array = llvm::ArrayType::get(enum_value_type,
                                                    x.n_members);
            llvm::Constant *array = module->getOrInsertGlobal(x.m_name,
                                        global_enum_array);
            module->getNamedGlobal(x.m_name)->setInitializer(
                llvm::ConstantArray::get(global_enum_array, enum_value_pairs));
            uint32_t h = get_hash((ASR::asr_t*)&x);
            llvm_symtab[h] = array;
        }
    }

    void start_module_init_function_prototype(const ASR::Module_t &x) {
        uint32_t h = get_hash((ASR::asr_t*)&x);
        llvm::FunctionType *function_type = llvm::FunctionType::get(
                llvm::Type::getVoidTy(context), {}, false);
        LCOMPILERS_ASSERT(llvm_symtab_fn.find(h) == llvm_symtab_fn.end());
        std::string module_fn_name = "__lfortran_module_init_" + std::string(x.m_name);
        llvm::Function *F = llvm::Function::Create(function_type,
                llvm::Function::ExternalLinkage, module_fn_name, module.get());
        llvm::BasicBlock *BB = llvm::BasicBlock::Create(context, ".entry", F);
        builder->SetInsertPoint(BB);

        llvm_symtab_fn[h] = F;
    }

    void finish_module_init_function_prototype(const ASR::Module_t &x) {
        uint32_t h = get_hash((ASR::asr_t*)&x);
        builder->CreateRetVoid();
        llvm_symtab_fn[h]->removeFromParent();
    }

    void visit_Module(const ASR::Module_t &x) {
        SymbolTable* current_scope_copy = current_scope;
        current_scope = x.m_symtab;
        mangle_prefix = "__module_" + std::string(x.m_name) + "_";

        start_module_init_function_prototype(x);
        std::vector<ASR::symbol_t*> variables;
        std::vector<ASR::symbol_t*> functions;
        std::vector<ASR::symbol_t*> structs;

        for (auto &item : x.m_symtab->get_scope()) {
            if (is_a<ASR::Variable_t>(*item.second)) {
                variables.push_back(item.second);
            } else if (is_a<ASR::Function_t>(*item.second)) {
                functions.push_back(item.second);
            } else if (ASR::is_a<ASR::Struct_t>(*item.second)) {
                structs.push_back(item.second);
            }
        }
        // Visit in order: Function --> Struct --> Variables
        for (auto &sym: functions) {
            ASR::Function_t *v = down_cast<ASR::Function_t>(
                        sym);
            ASR::FunctionType_t* func_type = ASR::down_cast<ASR::FunctionType_t>(v->m_function_signature);
            if (x.m_parent_module && func_type->m_module) {
                std::string root_module = std::string(x.m_parent_module);
                SymbolTable* tu_symtab = x.m_symtab->parent;
                if (tu_symtab) {
                    ASR::symbol_t* parent_sym = tu_symtab->get_symbol(root_module);
                    while (parent_sym && ASR::is_a<ASR::Module_t>(*parent_sym)) {
                        ASR::Module_t* parent_mod = ASR::down_cast<ASR::Module_t>(parent_sym);
                        if (parent_mod->m_parent_module) {
                            root_module = std::string(parent_mod->m_parent_module);
                            parent_sym = tu_symtab->get_symbol(root_module);
                        } else {
                            break;
                        }
                    }
                }
                mangle_prefix = "__module_" + root_module + "_";
            }
            instantiate_function(*v);
            mangle_prefix = "__module_" + std::string(x.m_name) + "_";
        }
        for (auto &sym: variables) {
            ASR::Variable_t *v = down_cast<ASR::Variable_t>(
                    sym);
            if( v->m_storage != ASR::storage_typeType::Parameter ) {
                visit_Variable(*v);
            }
        }
        finish_module_init_function_prototype(x);

        visit_procedures(x);
        mangle_prefix = "";
        current_scope = current_scope_copy;
    }

#ifdef HAVE_TARGET_WASM
    void add_wasm_start_function() {
        llvm::FunctionType *function_type = llvm::FunctionType::get(
            llvm::Type::getVoidTy(context), {}, false);
        llvm::Function *F = llvm::Function::Create(function_type,
                llvm::Function::ExternalLinkage, "_start", module.get());
        llvm::BasicBlock *BB = llvm::BasicBlock::Create(context, ".entry", F);
        builder->SetInsertPoint(BB);
        std::vector<llvm::Value *> args;
        args.push_back(llvm::ConstantInt::get(context, llvm::APInt(32, 0)));
        args.push_back(llvm_utils->CreateAlloca(*builder, character_type));
        builder->CreateCall(module->getFunction("main"), args);
        builder->CreateRet(nullptr);
    }
#endif

    void visit_Program(const ASR::Program_t &x) {
        llvm::DIScope* debug_current_scope_copy = debug_current_scope;
        llvm::DISubprogram *SP = nullptr;
        loop_head.clear();
        loop_head_names.clear();
        loop_or_block_end.clear();
        loop_or_block_end_names.clear();
        call_arg_alloca_pool.clear();
        call_arg_alloca_idx.clear();
        convert_call_args_depth = 0;
        list_call_arg_to_finalize.clear();
        strings_to_be_deallocated.reserve(al, 1);
        heap_fixed_size_arrays.reserve(al, 1);
        SymbolTable* current_scope_copy = current_scope;
        current_scope = x.m_symtab;
        bool is_dict_present_copy_lp = dict_api_lp->is_dict_present();
        bool is_dict_present_copy_sc = dict_api_sc->is_dict_present();
        dict_api_lp->set_is_dict_present(false);
        dict_api_sc->set_is_dict_present(false);
        bool is_set_present_copy_lp = set_api_lp->is_set_present();
        bool is_set_present_copy_sc = set_api_sc->is_set_present();
        set_api_lp->set_is_set_present(false);
        set_api_sc->set_is_set_present(false);
        llvm_goto_targets.clear();
        // Generate code for the main program
        std::vector<llvm::Type*> command_line_args = {
            llvm::Type::getInt32Ty(context),
            character_type->getPointerTo()
        };
        llvm::FunctionType *function_type = llvm::FunctionType::get(
                llvm::Type::getInt32Ty(context), command_line_args, false);
        llvm::Function *F = llvm::Function::Create(function_type,
                llvm::Function::ExternalLinkage, "main", module.get());
        if (compiler_options.po.fast && !compiler_options.po.no_fast_math) {
            F->addFnAttr("no-nans-fp-math", "true");
            F->addFnAttr("no-infs-fp-math", "true");
            F->addFnAttr("no-signed-zeros-fp-math", "true");
            F->addFnAttr("unsafe-fp-math", "true");
        }
        llvm::BasicBlock *BB = llvm::BasicBlock::Create(context,
                ".entry", F);
        if (compiler_options.emit_debug_info) {
            debug_emit_function(x, SP);
            F->setSubprogram(SP);
            debug_current_scope = SP;
        }
        builder->SetInsertPoint(BB);
        if (compiler_options.emit_debug_info) {
            builder->SetCurrentDebugLocation(nullptr);
            debug_emit_loc(x);
        }

        // Generate code for nested subroutines and functions first:
        for (auto &item : x.m_symtab->get_scope()) {
            if (is_a<ASR::Function_t>(*item.second)) {
                ASR::Function_t *v = down_cast<ASR::Function_t>(
                        item.second);
                instantiate_function(*v);
            }
        }
        visit_procedures(x);
        llvm_goto_targets.clear();

        builder->SetInsertPoint(BB);
        // Clear alloca pool after nested functions are processed, before main body
        call_arg_alloca_pool.clear();
        call_arg_alloca_idx.clear();
        convert_call_args_depth = 0;
        list_call_arg_to_finalize.clear();
        if (compiler_options.emit_debug_info) {
            debug_current_scope = SP;
            builder->SetCurrentDebugLocation(nullptr);
            debug_emit_loc(x);
        }
        // Call the `_lpython_call_initial_functions` function to assign command line argument
        // values to `argc` and `argv`, and set the random seed to the system clock.
        {
            if (compiler_options.emit_debug_info) debug_emit_loc(x);
            llvm::Function *fn = module->getFunction("_lpython_call_initial_functions");
            if(!fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        llvm::Type::getInt32Ty(context),
                        character_type->getPointerTo()
                    }, false);
                fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, "_lpython_call_initial_functions", module.get());
            }
            std::vector<llvm::Value *> args;
            for (llvm::Argument &llvm_arg : F->args()) {
                args.push_back(&llvm_arg);
            }
            builder->CreateCall(fn, args);
        }
        // Enable floating point exception trapping if requested
        if (compiler_options.fpe_traps != 0) {
            if (compiler_options.emit_debug_info) debug_emit_loc(x);
            llvm::Function *fpe_fn = module->getFunction("_lfortran_enable_fpe_traps");
            if(!fpe_fn) {
                llvm::FunctionType *fpe_function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        llvm::Type::getInt32Ty(context)
                    }, false);
                fpe_fn = llvm::Function::Create(fpe_function_type,
                    llvm::Function::ExternalLinkage, "_lfortran_enable_fpe_traps", module.get());
            }
            llvm::Value *mask_val = llvm::ConstantInt::get(context,
                llvm::APInt(32, compiler_options.fpe_traps));
            builder->CreateCall(fpe_fn, {mask_val});
        }
        // Enable CFI debug mode so CFI_allocate/CFI_deallocate use the
        // tracked allocator, keeping them in sync with Fortran allocate/deallocate.
        if (compiler_options.detect_leaks) {
            llvm::Function *fn = module->getFunction("_lfortran_set_cfi_debug_mode");
            if (!fn) {
                llvm::FunctionType *ft = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        llvm::Type::getInt32Ty(context)
                    }, false);
                fn = llvm::Function::Create(ft,
                    llvm::Function::ExternalLinkage, "_lfortran_set_cfi_debug_mode", module.get());
            }
            builder->CreateCall(fn, {llvm::ConstantInt::get(context, llvm::APInt(32, 1))});
        }
        // Set runtime color preference based on compiler option (only when enabled)
        if (compiler_options.use_runtime_colors) {
            if (compiler_options.emit_debug_info) debug_emit_loc(x);
            llvm::Function *fn = module->getFunction("_lfortran_set_use_runtime_colors");
            if(!fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        llvm::Type::getInt32Ty(context)
                    }, false);
                fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, "_lfortran_set_use_runtime_colors", module.get());
            }
            llvm::Value *use_colors = llvm::ConstantInt::get(context,
                llvm::APInt(32, 1));
            builder->CreateCall(fn, {use_colors});
        }
        for(to_be_allocated_array array : allocatable_array_details){
            fill_array_details_(array.expr, array.pointer_to_array_type, array.array_type, nullptr, array.n_dims,
                true, true, false, array.var_type);
        }
        for(auto& st : allocatable_struct_array_members_details) {
            allocate_array_members_of_struct(ASR::down_cast<ASR::Struct_t>(st.first),
                st.second, ASRUtils::symbol_type(st.first), false, false);
        }
        declare_vars(x);
        for(variable_inital_value var_to_initalize : variable_inital_value_vec){
            set_VariableInital_value(var_to_initalize.v, var_to_initalize.target_var);
        }
        proc_return = llvm::BasicBlock::Create(context, "return");
        predeclare_goto_targets(F, x.m_body, x.n_body);
        for (size_t i=0; i<x.n_body; i++) {
            this->visit_stmt(*x.m_body[i]);
        }
        if (compiler_options.emit_debug_info) debug_emit_loc(x);
        start_new_block(proc_return);
        llvm_symtab_finalizer.finalize_symtab(x.m_symtab);
        finalize_list_call_arg_allocas();
        // Free globals if detecting leaks is ON, to print clean report
        if(compiler_options.detect_leaks){
            SymbolTable* tranlsationUnit_symtab = ASRUtils::get_tu_symtab(x.m_symtab);
            for(auto &name_sym_pair : tranlsationUnit_symtab->get_scope()){
                auto &sym = name_sym_pair.second;
                if(ASR::is_a<ASR::Module_t>(*sym)){
                    llvm_symtab_finalizer.finalize_symtab(
                        ASR::down_cast<ASR::Module_t>(sym)->m_symtab);
                }
            }
        }
        free_heap_fixed_size_arrays();
        {
            llvm::Function *fn_finalize = module->getFunction(
                "_lfortran_internal_alloc_finalize");
            if (!fn_finalize) {
                llvm::FunctionType *ft = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {}, false);
                fn_finalize = llvm::Function::Create(ft,
                    llvm::Function::ExternalLinkage,
                    "_lfortran_internal_alloc_finalize", module.get());
            }
            builder->CreateCall(fn_finalize, {});
        }
        if (compiler_options.detect_leaks) {
            llvm::Function *fn = module->getFunction("dbg_report");
            if(!fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {}, false);
                fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, "dbg_report", module.get());
            }
            builder->CreateCall(fn, {});
        }
        llvm::Value *ret_val2 = llvm::ConstantInt::get(context,
            llvm::APInt(32, 0));
        builder->CreateRet(ret_val2);
        dict_api_lp->set_is_dict_present(is_dict_present_copy_lp);
        dict_api_sc->set_is_dict_present(is_dict_present_copy_sc);
        set_api_lp->set_is_set_present(is_set_present_copy_lp);
        set_api_sc->set_is_set_present(is_set_present_copy_sc);

        // Finalize the debug info.
        if (compiler_options.emit_debug_info) DBuilder->finalize();
        if (compiler_options.emit_debug_info) debug_current_scope = debug_current_scope_copy;
        current_scope = current_scope_copy;
        loop_head.clear();
        loop_head_names.clear();
        loop_or_block_end.clear();
        loop_or_block_end_names.clear();
        strings_to_be_deallocated.reserve(al, 1);

#ifdef HAVE_TARGET_WASM
        if (startswith(compiler_options.target, "wasm")) {
            add_wasm_start_function();
        }
#endif
    }

    /*
    * This function detects if the current variable is an argument.
    * of a function or argument. Some manipulations are to be done
    * only on arguments and not on local variables.
    */
    bool is_argument(ASR::Variable_t* v, ASR::expr_t** m_args,
                        int n_args) {
        for( int i = 0; i < n_args; i++ ) {
            ASR::expr_t* m_arg = m_args[i];
            uint32_t h_m_arg = get_hash((ASR::asr_t*)m_arg);
            uint32_t h_v = get_hash((ASR::asr_t*)v);
            if( h_m_arg == h_v ) {
                return true;
            }
        }
        return false;
    }

    void fill_array_details_(ASR::expr_t* expr, llvm::Value* ptr, llvm::Type* type_, ASR::dimension_t* m_dims,
        size_t n_dims, bool is_malloc_array_type, bool is_array_type,
        bool is_list, [[maybe_unused]]ASR::ttype_t* m_type, bool is_data_only=false,
        bool reserve_data_memory=true) {
        // Skip function call for AssumedLength strings
        // Their descriptors come from the original parameter inside subroutines
        if (ASRUtils::is_character(*m_type)) {
            ASR::String_t* str_type = ASR::down_cast<ASR::String_t>(
                ASRUtils::extract_type(m_type));
            if (str_type->m_len_kind == ASR::string_length_kindType::AssumedLength) {
                return;
            }
        }
        ASR::ttype_t* asr_data_type = ASRUtils::type_get_past_array(
            ASRUtils::type_get_past_pointer(
                ASRUtils::type_get_past_allocatable(ASRUtils::expr_type(expr))));
        // Use the array element *storage* type (e.g. logical arrays are i8-backed).
        llvm::Type* llvm_data_type = llvm_utils->get_el_type(expr, asr_data_type, module.get());
        llvm::Value* ptr_ = nullptr;
        if( is_malloc_array_type && !is_list && !is_data_only ) {
            ptr_ = arr_descr->create_descriptor_alloca(type_, "arr_desc");
            if(ASRUtils::is_character(*m_type)){
                llvm::Value* str_desc = create_and_setup_string_for_array(m_type, nullptr, false, "arr_desc_str_desc");
                builder->CreateStore(str_desc, arr_descr->get_pointer_to_data(type_, ptr_));
            } else if (ASRUtils::non_unlimited_polymorphic_class(m_type)){ 
                // For polymorphic allocatable arrays, set data pointer to NULL initially.
                // The wrapper will be allocated when `allocate` is called.
                auto const struct_sym = ASR::down_cast<ASR::Struct_t>(ASRUtils::get_struct_sym_from_struct_expr(expr));
                llvm::Type* const llvm_class_type = llvm_utils->getClassType(struct_sym);
                llvm::Value* const array_data = arr_descr->get_pointer_to_data(type_, ptr_);  
                builder->CreateStore(llvm::ConstantPointerNull::get(llvm_class_type->getPointerTo()), array_data);
            }
            arr_descr->fill_dimension_descriptor(type_, ptr_, n_dims);
        }
        if( is_array_type && !is_malloc_array_type &&
            !is_list ) {
            llvm::Type* ptr_typ = llvm_utils->get_type_from_ttype_t_util(expr, ASRUtils::expr_type(expr), module.get());
            fill_array_details(ptr_typ, ptr, llvm_data_type, m_dims, n_dims, is_data_only, reserve_data_memory);
            // For non-allocatable DescriptorArray character arrays,
            // fill_array_details allocated an array of string_descriptors
            // but their data pointers are uninitialized. Set the string
            // length on descriptors[0], then allocate a contiguous
            // character data buffer via set_array_of_strings_memory_on_heap.
            if (!is_data_only && ASRUtils::is_character(*m_type)) {
                ASR::String_t* str_type = ASR::down_cast<ASR::String_t>(
                    ASRUtils::extract_type(m_type));
                if (str_type->m_len != nullptr &&
                    str_type->m_len_kind == ASR::string_length_kindType::ExpressionLength) {
                    // Load descriptors[0] from the array's data pointer
                    llvm::Value* data_ptr_ptr = arr_descr->get_pointer_to_data(
                        ptr_typ, ptr);
                    llvm::Value* first_desc = builder->CreateLoad(
                        llvm_data_type->getPointerTo(), data_ptr_ptr);
                    // Set descriptors[0].length from the ASR string length
                    setup_string_length(first_desc, str_type, str_type->m_len);
                    // Compute array size (product of dimension lengths)
                    int64_t ptr_loads_copy = ptr_loads;
                    ptr_loads = 2;
                    llvm::Value* prod = llvm::ConstantInt::get(context, llvm::APInt(32, 1));
                    for (size_t r = 0; r < n_dims; r++) {
                        load_array_size_deep_copy(m_dims[r].m_length);
                        prod = builder->CreateMul(prod, tmp);
                    }
                    ptr_loads = ptr_loads_copy;
                    // Allocate the flat character buffer
                    llvm_utils->set_array_of_strings_memory_on_heap(
                        str_type, first_desc,
                        llvm_utils->get_string_length(str_type, first_desc),
                        prod, false);
                }
            }
        }
        const bool special_array_type = ASRUtils::is_character(*m_type) || ASRUtils::non_unlimited_polymorphic_class(m_type); // already Nullified
        if( is_array_type && is_malloc_array_type &&
            !is_list && !is_data_only && !special_array_type) {
            // Set allocatable arrays as unallocated
            LCOMPILERS_ASSERT(ptr_ != nullptr);
            arr_descr->reset_is_allocated_flag(type_, ptr_, llvm_data_type);
        }
        if( ptr_ ) {
            LLVM::CreateStore(*builder, ptr_, ptr);
        }
    }
    
    void set_pointer_variable_to_null(ASR::Variable_t* v, llvm::Value* null_value, llvm::Value* ptr,
        bool ignore_intent = false) {
        // Initialize pointer/allocatable variables to NULL when:
        // - Type is Allocatable or Pointer
        // - Intent is local or return_var
        // - It's NOT a simple character type (but character arrays ARE included)
        if( (ASR::is_a<ASR::Allocatable_t>(*v->m_type) ||
                ASR::is_a<ASR::Pointer_t>(*v->m_type)) &&
            v->m_storage != ASR::storage_typeType::Save &&
            (ignore_intent ||
             v->m_intent == ASRUtils::intent_local ||
             v->m_intent == ASRUtils::intent_return_var ) &&
             (!ASRUtils::is_character(*v->m_type) ||
              ASRUtils::is_array(v->m_type))) {
            if (ASRUtils::is_class_type(ASRUtils::type_get_past_allocatable_pointer(v->m_type))) {
                ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(v->m_type_declaration);
                llvm::Type* wrapper_struct_llvm_type = llvm_utils->get_type_from_ttype_t_util(v->m_type, struct_sym, module.get());
                builder->CreateStore(llvm::ConstantPointerNull::getNullValue(wrapper_struct_llvm_type), ptr);
            } else {
                builder->CreateStore(null_value, ptr);
            }
        }
    }

    void allocate_array_members_of_struct(ASR::Struct_t* struct_sym, llvm::Value* ptr,
            ASR::ttype_t* asr_type, bool is_intent_out = false, bool initialize_val = true,
            bool skip_allocatable_array_descriptor_init = false) {
        LCOMPILERS_ASSERT(ASR::is_a<ASR::StructType_t>(*asr_type));
        ASR::Struct_t* struct_type_t = nullptr;
        if (ASR::is_a<ASR::StructType_t>(*asr_type)) {
            struct_type_t = struct_sym;
        } else {
            return;
        }
        bool struct_has_finalizer = false;
        {
            ASR::Struct_t* s = struct_sym;
            while (s) {
                if (s->n_member_functions > 0) {
                    struct_has_finalizer = true;
                    break;
                }
                if (s->m_parent) {
                    s = ASR::down_cast<ASR::Struct_t>(
                        ASRUtils::symbol_get_past_external(s->m_parent));
                } else {
                    s = nullptr;
                }
            }
        }

        if (ASRUtils::is_class_type(ASRUtils::extract_type(asr_type))) {
            llvm::Type* const class_type = llvm_utils->getClassType(struct_sym, false);
            llvm::Type* const struct_type = llvm_utils->getStructType(struct_sym, module.get(), true);
            ptr = llvm_utils->create_gep2(class_type, ptr, 1);
            ptr = llvm_utils->CreateLoad2(struct_type, ptr);
        }
        while (struct_type_t) {
            std::string struct_type_name = get_type_key(struct_type_t);
            for( auto item: struct_type_t->m_symtab->get_scope() ) {
                ASR::symbol_t *sym = ASRUtils::symbol_get_past_external(item.second);
                if (name2memidx[struct_type_name].find(item.first) == name2memidx[struct_type_name].end()) {
                    continue;
                }
                if( ASR::is_a<ASR::StructMethodDeclaration_t>(*sym) ||
                    ASR::is_a<ASR::GenericProcedure_t>(*sym) ||
                    ASR::is_a<ASR::Union_t>(*sym) ||
                    ASR::is_a<ASR::Struct_t>(*sym) ||
                    ASR::is_a<ASR::CustomOperator_t>(*sym) ) {
                    continue ;
                }
                ASR::ttype_t* symbol_type = ASRUtils::symbol_type(sym);
                int idx = 0;
                idx = name2memidx[struct_type_name][item.first];
                llvm::Type* type = name2dertype[struct_type_name];
                llvm::Value* ptr_member = llvm_utils->create_gep2(type, ptr, idx);
                ASR::Variable_t* v = nullptr;
                // Don't reinitialize arrays where the type's intent is out because array descriptor is allocated on the stack
                // and might be returned.
                if( ASR::is_a<ASR::Variable_t>(*sym) && !(is_intent_out ) ) {
                    v = ASR::down_cast<ASR::Variable_t>(sym);
                    if (!LLVM::is_llvm_pointer(*v->m_type) &&
                            ASRUtils::is_class_type(ASRUtils::extract_type(v->m_type))) {
                        struct_api->store_class_vptr(ASRUtils::symbol_get_past_external(v->m_type_declaration), 
                            ptr_member, module.get());
                        ASR::symbol_t* sym_past = ASRUtils::symbol_get_past_external(v->m_type_declaration);
                        ASR::Struct_t* struct_t = ASR::down_cast<ASR::Struct_t>(sym_past);
                        if (!ASRUtils::is_unlimited_polymorphic_type(sym_past)) {
                            llvm::Type* class_type = llvm_utils->getClassType(struct_t);
                            llvm::Value* struct_ptr_field = llvm_utils->CreateGEP2(class_type, ptr_member, 1);
                            llvm::Type* inner_ptr_type = llvm_utils->getStructType(struct_t, module.get(), true);
                            builder->CreateStore(
                                llvm::ConstantPointerNull::get(llvm::cast<llvm::PointerType>(inner_ptr_type)),
                                struct_ptr_field);
                        }
                    } else {
                        set_pointer_variable_to_null(v, llvm::Constant::getNullValue(
                            llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                        al, v->base.base.loc, &v->base)), v->m_type, module.get())),
                            ptr_member, true);
                    }
                }
                if( ASRUtils::is_array(symbol_type) && v && !is_intent_out) {
                    if (skip_allocatable_array_descriptor_init && ASRUtils::is_allocatable(v->m_type)) {
                        continue;
                    }
                    ASR::dimension_t* m_dims = nullptr;
                    size_t n_dims = ASRUtils::extract_dimensions_from_ttype(symbol_type, m_dims);
                    ASR::array_physical_typeType phy_type = ASRUtils::extract_physical_type(symbol_type);
                    bool is_data_only = (phy_type == ASR::array_physical_typeType::PointerArray ||
                                        phy_type == ASR::array_physical_typeType::UnboundedPointerArray ||
                                        phy_type == ASR::array_physical_typeType::FixedSizeArray ||
                                        (phy_type == ASR::array_physical_typeType::StringArraySinglePointer &&
                                        ASRUtils::is_fixed_size_array(symbol_type)));
                    if (phy_type == ASR::array_physical_typeType::DescriptorArray ||
                        (phy_type == ASR::array_physical_typeType::StringArraySinglePointer &&
                        ASRUtils::is_dimension_empty(m_dims, n_dims))) {
                        int n_dims = 0, a_kind=4;
                        ASR::dimension_t* m_dims = nullptr;
                        bool is_array_type = false;
                        bool is_malloc_array_type = false;
                        bool is_list = false;
                        llvm_utils->get_type_from_ttype_t(ASRUtils::EXPR(ASR::make_Var_t(
                            al, v->base.base.loc, &v->base)), v->m_type,
                                    v->m_type_declaration, v->m_storage, is_array_type,
                                    is_malloc_array_type, is_list, m_dims, n_dims, a_kind,
                                    module.get());
                        llvm::Type* type_ = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(
                            ASR::make_Var_t(al, v->base.base.loc, &v->base)),
                            ASRUtils::type_get_past_pointer(
                                ASRUtils::type_get_past_allocatable(v->m_type)), module.get(), v->m_abi);
                        
                        llvm::Value* arr = arr_descr->allocate_descriptor_on_heap(type_);
                        builder->CreateStore(arr, ptr_member);

                        llvm::Value* ptr_to_dim_desc = arr_descr->get_pointer_to_dimension_descriptor_array(type_, arr);

                        // Initialize dimension_descriptor (lower_bound=1, extent=1)
                        builder->CreateStore(llvm::ConstantInt::get(context, llvm::APInt(64, 1)),
                            llvm_utils->create_gep2(arr_descr->get_dimension_descriptor_type(), ptr_to_dim_desc,
                                LLVMArrUtils::SimpleCMODescriptor::DIM_LOWER_BOUND));
                        builder->CreateStore(llvm::ConstantInt::get(context, llvm::APInt(64, 1)),
                            llvm_utils->create_gep2(arr_descr->get_dimension_descriptor_type(), ptr_to_dim_desc,
                                LLVMArrUtils::SimpleCMODescriptor::DIM_EXTENT));

                        ASR::ttype_t* element_type = ASRUtils::type_get_past_array(
                            ASRUtils::type_get_past_allocatable_pointer(v->m_type));
                        // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                        llvm::Type* data_type = llvm_utils->get_el_type(
                            ASRUtils::EXPR(ASR::make_Var_t(al, v->base.base.loc, &v->base)),
                            element_type, module.get());
                        if(ASRUtils::is_character(*v->m_type)){
                            llvm::Value* str_desc = llvm_utils->allocate_string_descriptor_on_heap(data_type);
                            builder->CreateStore(str_desc, arr_descr->get_pointer_to_data(type_, arr));
                            ASR::String_t* str_type = ASRUtils::get_string_type(v->m_type);
                            if (str_type->m_len) {
                                setup_string_length(str_desc, str_type, str_type->m_len);
                            }
                        } else {
                            arr_descr->reset_is_allocated_flag(type_, arr, data_type);
                        }
                    } else {
                        fill_array_details_(ASRUtils::EXPR(ASR::make_Var_t(
                            al, v->base.base.loc, &v->base)), ptr_member, nullptr, m_dims, n_dims, false, true, false, symbol_type, is_data_only);
                    }
                    if (ASRUtils::is_character(*symbol_type) &&
                            phy_type == ASR::array_physical_typeType::PointerArray &&
                            ASRUtils::is_fixed_size_array(symbol_type)) {
                        bool has_init = v && (v->m_symbolic_value || v->m_value);
                        if (!has_init) {
                            ASR::String_t* str_type = ASRUtils::get_string_type(symbol_type);
                            builder->CreateStore(llvm::Constant::getNullValue(string_descriptor), ptr_member);
                            setup_string_length(ptr_member, str_type, str_type->m_len);
                            int64_t array_size = ASRUtils::get_fixed_size_of_array(symbol_type);
                            llvm::Value* array_size_val = llvm::ConstantInt::get(
                                context, llvm::APInt(64, array_size));
                            llvm_utils->set_array_of_strings_memory_on_heap(
                                str_type, ptr_member,
                                llvm_utils->get_string_length(str_type, ptr_member),
                                array_size_val, false);
                        }
                    }
                    if (ASR::is_a<ASR::StructType_t>(*ASRUtils::type_get_past_array(symbol_type))
                        && !ASRUtils::is_class_type(ASRUtils::type_get_past_array(symbol_type))) {
                        allocate_array_members_of_struct_arrays(ASRUtils::get_expr_from_sym(al, sym), ptr_member, symbol_type);
                    }
                } else if (ASR::is_a<ASR::StructType_t>(*symbol_type) && !ASRUtils::is_class_type(symbol_type)) {
                    ASR::Struct_t* struct_sym = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(
                        ASR::down_cast<ASR::Variable_t>(sym)->m_type_declaration));
                    allocate_array_members_of_struct(struct_sym, ptr_member, symbol_type,
                        is_intent_out, initialize_val, skip_allocatable_array_descriptor_init);
                }  else if(ASRUtils::is_string_only(symbol_type) && !is_intent_out) {
                    // Skip string descriptor setup for bind(C)/SEQUENCE struct
                    // non-pointer character members (inline [len x i8]).
                    bool is_bindc = (struct_type_t->m_abi == ASR::abiType::BindC) ||
                                    struct_type_t->m_is_sequence;
                    ASR::Variable_t* v_sym = ASR::down_cast<ASR::Variable_t>(sym);
                    bool is_direct_char = is_bindc &&
                        !ASR::is_a<ASR::Pointer_t>(*v_sym->m_type) &&
                        !ASR::is_a<ASR::Allocatable_t>(*v_sym->m_type);
                    if (!is_direct_char) {
                        setup_string(ptr_member, symbol_type);
                        if (!initialize_val && v && v->m_symbolic_value &&
                            ASRUtils::is_string_only(ASRUtils::expr_type(v->m_symbolic_value))) {
                            visit_expr(*v->m_symbolic_value);
                            llvm_utils->lfortran_str_copy(
                                ptr_member, tmp,
                                ASRUtils::get_string_type(symbol_type),
                                ASRUtils::get_string_type(ASRUtils::expr_type(v->m_symbolic_value)),
                                ASRUtils::is_allocatable(symbol_type));
                        }
                    }
                }
                if( ASR::is_a<ASR::Variable_t>(*sym) && initialize_val &&
                    !(is_intent_out && struct_has_finalizer)) {
                    v = ASR::down_cast<ASR::Variable_t>(sym);
                    if( v->m_symbolic_value ) {
                        ASR::expr_t* init_value = ASRUtils::expr_value(v->m_symbolic_value);
                        ASR::ttype_t* init_type = ASRUtils::extract_type(symbol_type);
                        bool use_constant_init = init_value != nullptr &&
                            !ASRUtils::is_array(symbol_type) &&
                            !ASRUtils::is_pointer(symbol_type) &&
                            !ASRUtils::is_allocatable(symbol_type) &&
                            (ASR::is_a<ASR::Integer_t>(*init_type) ||
                             ASR::is_a<ASR::UnsignedInteger_t>(*init_type) ||
                             ASR::is_a<ASR::Logical_t>(*init_type) ||
                             ASR::is_a<ASR::Real_t>(*init_type) ||
                             ASR::is_a<ASR::Complex_t>(*init_type));
                        if( use_constant_init ) {
                            llvm::Constant* init_constant =
                                create_llvm_constant_from_asr_expr(v->m_symbolic_value, symbol_type);
                            LLVM::CreateStore(*builder, init_constant, ptr_member);
                            continue;
                        }
                        visit_expr(*v->m_symbolic_value);
                        if( ASR::is_a<ASR::PointerNullConstant_t>(*v->m_symbolic_value) &&
                            ASRUtils::is_array(v->m_type)){ // Store into array's data pointer.
                            if(ASR::is_a<ASR::Pointer_t>(*v->m_type)){
                                // Pointer array: store null into descriptor's data pointer.
                                llvm::Value* pointer_array = builder->CreateLoad(
                                    llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                                    al, v->base.base.loc, &v->base)), v->m_type, module.get()),
                                    ptr_member);
                                llvm::Type* const array_desc_type = llvm_utils->arr_api->get_array_type(
                                    ASRUtils::EXPR(ASR::make_Var_t(al, v->base.base.loc, (ASR::symbol_t*)v)),
                                    ASRUtils::type_get_past_allocatable_pointer(v->m_type),
                                    llvm_utils->get_el_type(
                                        ASRUtils::EXPR(ASR::make_Var_t(al, v->base.base.loc, &v->base)),
                                        ASRUtils::extract_type(v->m_type),
                                        module.get()),
                                    false);
                                builder->CreateStore(
                                    tmp, llvm_utils->create_gep2(array_desc_type, pointer_array, 0));
                            } else {
                                // Non-pointer array (e.g. c_funptr array): the inline
                                // fixed-size storage is null when zero-initialized.
                                llvm::Type* array_type = llvm_utils->get_type_from_ttype_t_util(
                                    ASRUtils::EXPR(ASR::make_Var_t(al, v->base.base.loc, &v->base)),
                                    v->m_type, module.get());
                                uint64_t total_bytes = module->getDataLayout().getTypeAllocSize(array_type);
                                builder->CreateMemSet(ptr_member,
                                    llvm::ConstantInt::get(context, llvm::APInt(8, 0)),
                                    llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), total_bytes),
                                    llvm::MaybeAlign());
                            }
                        } else if(ASRUtils::is_string_only(expr_type(v->m_symbolic_value))) {
                            llvm_utils->lfortran_str_copy(
                            ptr_member, tmp,
                            ASRUtils::get_string_type(symbol_type),
                            ASRUtils::get_string_type(expr_type(v->m_symbolic_value)),
                            ASRUtils::is_allocatable(symbol_type));
                        } else if (ASRUtils::is_array(v->m_type)) {
                            ASR::ArrayConstant_t* arr_const = ASR::down_cast<ASR::ArrayConstant_t>(ASRUtils::expr_value(v->m_symbolic_value));
                            llvm::Type* array_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::expr_value(v->m_symbolic_value), arr_const->m_type, module.get());
                            llvm::Value* arg_size = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context),
                            llvm::APInt(32, ASRUtils::get_fixed_size_of_array(arr_const->m_type)));
                            llvm::Type* llvm_data_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::expr_value(v->m_symbolic_value),
                                ASRUtils::type_get_past_array(ASRUtils::expr_type(v->m_symbolic_value)), module.get());
                            llvm::DataLayout data_layout(module->getDataLayout());
                            size_t dt_size = data_layout.getTypeAllocSize(llvm_data_type);
                            arg_size = builder->CreateMul(llvm::ConstantInt::get(
                                llvm::Type::getInt32Ty(context), llvm::APInt(32, dt_size)), arg_size);
                            builder->CreateMemCpy(llvm_utils->create_gep2(array_type,ptr_member, 0),
                                llvm::MaybeAlign(), tmp, llvm::MaybeAlign(), arg_size, v->m_is_volatile);
                        } else if ((ASRUtils::is_pointer(v->m_type) &&
                                !ASR::is_a<ASR::PointerNullConstant_t>(
                                    *v->m_symbolic_value) &&
                                !ASR::is_a<ASR::FunctionType_t>(
                                    *ASRUtils::type_get_past_pointer(v->m_type)))  ||
                            ASRUtils::is_allocatable(v->m_type)) { // Any non primitve
                            throw LCompilersException("Not implemented");
                        } else {
                            LLVM::CreateStore(*builder, tmp, ptr_member);
                        }
                    }
                }
            }
            if (struct_type_t->m_parent) {
                ptr = llvm_utils->create_gep2(name2dertype[get_type_key(struct_type_t)], ptr, 0);
                struct_type_t = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(struct_type_t->m_parent));
            } else {
                struct_type_t = nullptr;
            }
        }
    }

    void allocate_array_members_of_struct_arrays(ASR::expr_t* expr, llvm::Value* ptr, ASR::ttype_t* v_m_type) {
        ASR::array_physical_typeType phy_type = ASRUtils::extract_physical_type(v_m_type);
        llvm::Type* el_type = llvm_utils->get_type_from_ttype_t_util(expr,
            ASRUtils::extract_type(v_m_type), module.get());
        llvm::Value* array_size = llvm_utils->CreateAlloca(
                llvm::Type::getInt32Ty(context), nullptr, "array_size");
        switch( phy_type ) {
            case ASR::array_physical_typeType::FixedSizeArray: {
                ASR::dimension_t* m_dims = nullptr;
                size_t n_dims = ASRUtils::extract_dimensions_from_ttype(v_m_type, m_dims);
                LLVM::CreateStore(*builder, llvm::ConstantInt::get(llvm::Type::getInt32Ty(context),
                    llvm::APInt(32, ASRUtils::get_fixed_size_of_array(m_dims, n_dims))), array_size);
                break;
            }
            case ASR::array_physical_typeType::DescriptorArray: {
                llvm::Type* ptr_type = llvm_utils->get_type_from_ttype_t_util(expr, ASRUtils::type_get_past_allocatable_pointer(v_m_type), module.get());
                llvm::Value* array_size_value = arr_descr->get_array_size(ptr_type, ptr, nullptr, 4);
                LLVM::CreateStore(*builder, array_size_value, array_size);
                break;
            }
            case ASR::array_physical_typeType::PointerArray: {
                ASR::dimension_t* m_dims = nullptr;
                size_t n_dims = ASRUtils::extract_dimensions_from_ttype(v_m_type, m_dims);
                llvm::Value* llvm_size = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, 1));
                int ptr_loads_copy = ptr_loads;
                ptr_loads = 2;
                for( size_t i = 0; i < n_dims; i++ ) {
                    visit_expr_wrapper(m_dims[i].m_length, true);
                    llvm_size = builder->CreateMul(tmp, llvm_size);
                }
                ptr_loads = ptr_loads_copy;
                LLVM::CreateStore(*builder, llvm_size, array_size);
                break;
            }
            default: {
                LCOMPILERS_ASSERT_MSG(false, std::to_string(phy_type));
            }
        }
        llvm::Value* llvmi = llvm_utils->CreateAlloca(llvm::Type::getInt32Ty(context), nullptr, "i");
        llvm::Type* t = llvm::Type::getInt32Ty(context);
        LLVM::CreateStore(*builder,
            llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, 0)), llvmi);
        create_loop(nullptr, [=]() {
                llvm::Value* llvmi_loaded = llvm_utils->CreateLoad2(t, llvmi);
                llvm::Value* array_size_loaded = llvm_utils->CreateLoad2(t, array_size);
                return builder->CreateICmpSLT(
                    llvmi_loaded, array_size_loaded);
            },
            [=]() {
                llvm::Value* ptr_i = nullptr;
                switch (phy_type) {
                    case ASR::array_physical_typeType::FixedSizeArray: {
                        llvm::Type* ptr_i_type = llvm_utils->get_type_from_ttype_t_util(expr, v_m_type, module.get());
                        ptr_i = llvm_utils->create_gep2(ptr_i_type, ptr, llvm_utils->CreateLoad2(t, llvmi));
                        break;
                    }
                    case ASR::array_physical_typeType::DescriptorArray: {
                        llvm::Type* ptr_i_type = llvm_utils->get_type_from_ttype_t_util(expr, ASRUtils::type_get_past_allocatable_pointer(v_m_type), module.get());
                        if (ASRUtils::is_class_type(ASRUtils::extract_type(v_m_type))) {
                            // For class arrays: data pointer points to a single class wrapper
                            // which contains [vptr, struct_array_ptr]
                            ASR::Struct_t* struct_sym = ASR::down_cast<ASR::Struct_t>(
                                ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(expr)));
                            ASR::StructType_t* struct_type = ASR::down_cast<ASR::StructType_t>(
                                ASRUtils::extract_type(v_m_type));
                            llvm::Value* class_wrapper = llvm_utils->CreateLoad2(el_type->getPointerTo(),
                                arr_descr->get_pointer_to_data(ptr_i_type, ptr));
                            ptr_i = llvm_utils->get_class_element_from_array(struct_sym, struct_type,
                                class_wrapper, llvm_utils->CreateLoad2(t, llvmi));
                        } else {
                            ptr_i = llvm_utils->create_ptr_gep2(el_type,
                                llvm_utils->CreateLoad2(el_type->getPointerTo(), arr_descr->get_pointer_to_data(ptr_i_type, ptr)),
                                llvm_utils->CreateLoad2(t, llvmi));
                        }
                        break;
                    }
                    case ASR::array_physical_typeType::PointerArray: {
                        ptr_i = llvm_utils->create_ptr_gep2(el_type, ptr, llvm_utils->CreateLoad2(t, llvmi));
                        break;
                    }
                    default: {
                        LCOMPILERS_ASSERT(false);
                    }
                }
                allocate_array_members_of_struct(
                    ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(expr))),
                        ptr_i, ASRUtils::extract_type(v_m_type), false, true, true);
                LLVM::CreateStore(*builder,
                    builder->CreateAdd(llvm_utils->CreateLoad2(t, llvmi),
                        llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, 1))),
                    llvmi);
            });
    }

    void create_vtab_for_struct_type(ASR::symbol_t* struct_type_sym, SymbolTable* symtab) {
        LCOMPILERS_ASSERT(ASR::is_a<ASR::Struct_t>(*struct_type_sym));
        ASR::Struct_t* struct_type_t = ASR::down_cast<ASR::Struct_t>(struct_type_sym);
        if( type2vtab.find(struct_type_sym) != type2vtab.end() &&
            type2vtab[struct_type_sym].find(symtab) != type2vtab[struct_type_sym].end() ) {
            return ;
        }
        if( type2vtabtype.find(struct_type_sym) == type2vtabtype.end() ) {
            std::vector<llvm::Type*> type_vec = {llvm_utils->getIntType(8)};
            type2vtabtype[struct_type_sym] = llvm::StructType::create(
                                                context, type_vec,
                                                std::string("__vtab_") +
                                                std::string(struct_type_t->m_name));
        }
        llvm::Type* vtab_type = type2vtabtype[struct_type_sym];
        llvm::Value* vtab_obj = llvm_utils->CreateAlloca(*builder, vtab_type);
        llvm::Value* struct_type_hash_ptr = llvm_utils->create_gep2(vtab_type, vtab_obj, 0);
        llvm::Value* struct_type_hash = llvm::ConstantInt::get(llvm_utils->getIntType(8),
            llvm::APInt(64, get_class_hash(struct_type_sym)));
        builder->CreateStore(struct_type_hash, struct_type_hash_ptr);
        type2vtab[struct_type_sym][symtab] = vtab_obj;
        ASR::symbol_t* struct_type_ = struct_type_sym;
        bool base_found = false;
        while( !base_found ) {
            if( ASR::is_a<ASR::Struct_t>(*struct_type_) ) {
                ASR::Struct_t* struct_type = ASR::down_cast<ASR::Struct_t>(struct_type_);
                if( struct_type->m_parent == nullptr ) {
                    base_found = true;
                } else {
                    struct_type_ = ASRUtils::symbol_get_past_external(struct_type->m_parent);
                }
            } else {
                LCOMPILERS_ASSERT(false);
            }
        }
        class2vtab[struct_type_][symtab].push_back(vtab_obj);
    }

    void get_type_default_field_values(ASR::symbol_t* struct_sym,
            std::vector<llvm::Constant*>& field_values, ASR::symbol_t* orig_struct_sym) {
        struct_sym = ASRUtils::symbol_get_past_external(struct_sym);
        ASR::Struct_t* struct_t = ASR::down_cast<ASR::Struct_t>(struct_sym);

        if (struct_t->m_parent != nullptr) {
            std::vector<llvm::Constant*> tmp_field_values;
            get_type_default_field_values(struct_t->m_parent, tmp_field_values, orig_struct_sym);
            llvm::StructType* llvm_struct_type = llvm::cast<llvm::StructType>(
                llvm_utils->get_type_from_ttype_t_util(ASRUtils::symbol_type(
                    struct_t->m_parent), struct_t->m_parent, module.get()));
            field_values.push_back(llvm::ConstantStruct::get(llvm_struct_type, tmp_field_values));
        }
        for (size_t i = 0; i < struct_t->n_members; i++) {
            std::string member_name = struct_t->m_members[i];
            ASR::symbol_t* sym = struct_t->m_symtab->get_symbol(member_name);
            if (!sym || !ASR::is_a<ASR::Variable_t>(*sym))
                continue;
            ASR::Variable_t* var = ASR::down_cast<ASR::Variable_t>(sym);
            if (var->m_value != nullptr) {
                llvm::Constant* c = create_llvm_constant_from_asr_expr(var->m_value, var->m_type,
                    orig_struct_sym);
                field_values.push_back(c);
            } else {
                llvm::Type* member_type = llvm_utils->get_type_from_ttype_t_util(
                    ASRUtils::EXPR(ASR::make_Var_t(al, var->base.base.loc, &var->base)),var->m_type, module.get());
                field_values.push_back(llvm::Constant::getNullValue(member_type));
            }
        }
    }

    llvm::Constant* create_llvm_constant_from_asr_expr(ASR::expr_t* expr,
            ASR::ttype_t* target_type = nullptr, ASR::symbol_t* orig_struct_sym = nullptr) {
        ASR::expr_t* expr_value = ASRUtils::expr_value(expr);
        if (expr_value != nullptr && expr_value != expr) {
            return create_llvm_constant_from_asr_expr(expr_value, target_type, orig_struct_sym);
        }
        switch (expr->type) {
            case ASR::exprType::IntegerConstant: {
                ASR::IntegerConstant_t* ic = ASR::down_cast<ASR::IntegerConstant_t>(expr);
                ASR::ttype_t* const_type = target_type != nullptr ? target_type : ic->m_type;
                llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(nullptr, const_type, module.get());
                return llvm::ConstantInt::get(llvm_type, ic->m_n, true);
            }
            case ASR::exprType::UnsignedIntegerConstant: {
                ASR::UnsignedIntegerConstant_t* uic = ASR::down_cast<ASR::UnsignedIntegerConstant_t>(expr);
                ASR::ttype_t* const_type = target_type != nullptr ? target_type : uic->m_type;
                llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(nullptr, const_type, module.get());
                return llvm::ConstantInt::get(llvm_type, uic->m_n, false);
            }
            case ASR::exprType::LogicalConstant: {
                ASR::LogicalConstant_t* lc = ASR::down_cast<ASR::LogicalConstant_t>(expr);
                ASR::ttype_t* const_type = target_type != nullptr ? target_type : lc->m_type;
                llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(nullptr, const_type, module.get());
                return llvm::ConstantInt::get(llvm_type, lc->m_value ? 1 : 0, false);
            }
            case ASR::exprType::RealConstant: {
                ASR::RealConstant_t* rc = ASR::down_cast<ASR::RealConstant_t>(expr);
                ASR::ttype_t* const_type = target_type != nullptr ? target_type : rc->m_type;
                llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(nullptr, const_type, module.get());
                if (llvm_type->isFloatTy()) {
                    return llvm::ConstantFP::get(llvm_type, static_cast<float>(rc->m_r));
                } else if (llvm_type->isDoubleTy()) {
                    return llvm::ConstantFP::get(llvm_type, rc->m_r);
                }
                break;
            }
            case ASR::exprType::ComplexConstant: {
                ASR::ComplexConstant_t* cc = ASR::down_cast<ASR::ComplexConstant_t>(expr);
                ASR::ttype_t* const_type = target_type != nullptr ? target_type : cc->m_type;
                int a_kind = ASRUtils::extract_kind_from_ttype_t(const_type);
                llvm::StructType* complex_type = nullptr;
                llvm::Constant* re = nullptr;
                llvm::Constant* im = nullptr;
                switch (a_kind) {
                    case 4: {
                        complex_type = complex_type_4;
                        re = llvm::ConstantFP::get(context, llvm::APFloat(static_cast<float>(cc->m_re)));
                        im = llvm::ConstantFP::get(context, llvm::APFloat(static_cast<float>(cc->m_im)));
                        break;
                    }
                    case 8: {
                        complex_type = complex_type_8;
                        re = llvm::ConstantFP::get(context, llvm::APFloat(cc->m_re));
                        im = llvm::ConstantFP::get(context, llvm::APFloat(cc->m_im));
                        break;
                    }
                    default: {
                        throw CodeGenError("Only 32 and 64 bits complex kinds are supported.");
                    }
                }
                return llvm::ConstantStruct::get(complex_type, {re, im});
            }
            case ASR::exprType::ArrayConstant: {
                // Infer LLVM element/array type from the ASR expression
                llvm::Type* elem_type = nullptr;
                elem_type = llvm_utils->get_el_type(expr, ASRUtils::extract_type(ASRUtils::expr_type(expr)), module.get());
                llvm::Constant* c = get_const_array(expr, elem_type);
                return c;
            }
            case ASR::exprType::StringConstant: {
                ASR::StringConstant_t* sc = ASR::down_cast<ASR::StringConstant_t>(expr);
                llvm::Value* v = llvm_utils->declare_string_constant(sc);
                if (!v) break;
                llvm::GlobalVariable* gv = llvm::cast<llvm::GlobalVariable>(v);
                if (gv->hasInitializer()) {
                    return gv->getInitializer();
                }
                break;
            }
            case ASR::exprType::StructConstant: {
                std::vector<llvm::Constant*> field_values;
                ASR::symbol_t* struct_sym = ASRUtils::get_struct_sym_from_struct_expr(expr);
                if (orig_struct_sym == nullptr) {
                    orig_struct_sym = struct_sym;
                }
                get_type_default_field_values(struct_sym, field_values, orig_struct_sym);
                llvm::StructType* llvm_struct_type = llvm::cast<llvm::StructType>(
                    llvm_utils->get_type_from_ttype_t_util(expr, ASRUtils::expr_type(expr), module.get()));
                return llvm::ConstantStruct::get(llvm_struct_type, field_values);
            }
            case ASR::exprType::PointerNullConstant: {
                ASR::PointerNullConstant_t* pnc = ASR::down_cast<ASR::PointerNullConstant_t>(expr);
                llvm::Type* value_type = ASRUtils::is_array(pnc->m_type)
                    ? llvm_utils->get_el_type(pnc->m_var_expr,
                        ASRUtils::extract_type(pnc->m_type), module.get())->getPointerTo()
                    : llvm_utils->get_type_from_ttype_t_util(pnc->m_var_expr, pnc->m_type, module.get());
                return llvm::ConstantPointerNull::get(llvm::cast<llvm::PointerType>(value_type));
            }
            default:
                throw LCompilersException( "Unsupported constant expression in struct default initializer.");
        }
        return nullptr;
    }

    void collect_variable_types_and_struct_types(
        std::set<std::string>& variable_type_names,
        std::vector<ASR::symbol_t*>& struct_types,
        SymbolTable* x_symtab) {
        if (x_symtab == nullptr) {
            return ;
        }
        for (auto &item : x_symtab->get_scope()) {
            ASR::symbol_t* var_sym = item.second;
            if (ASR::is_a<ASR::Variable_t>(*var_sym)) {
                ASR::Variable_t *v = ASR::down_cast<ASR::Variable_t>(var_sym);
                ASR::ttype_t* v_type = ASRUtils::extract_type(v->m_type);
                if (ASR::is_a<ASR::StructType_t>(*v_type)) {
                    variable_type_names.insert(ASRUtils::symbol_name(var_sym));
                }
            } else if (ASR::is_a<ASR::Struct_t>(
                        *ASRUtils::symbol_get_past_external(var_sym))) {
                struct_types.push_back(ASRUtils::symbol_get_past_external(var_sym));
            }
        }
        collect_variable_types_and_struct_types(variable_type_names, struct_types, x_symtab->parent);
    }
    void set_VariableInital_value(ASR::Variable_t* v, llvm::Value* target_var){
        if (v->m_value != nullptr) {
            this->visit_expr_wrapper(v->m_value, true, v->m_is_volatile);
        } else {
            this->visit_expr_wrapper(v->m_symbolic_value, true, v->m_is_volatile);
        }
        llvm::Value *init_value = tmp;
        auto is_array_const_or_ctor = [](ASR::expr_t* e) {
            return e && (ASR::is_a<ASR::ArrayConstant_t>(*e) ||
                         ASR::is_a<ASR::ArrayConstructor_t>(*e));
        };
        if( ASRUtils::is_array(v->m_type) &&
            ASRUtils::is_array(ASRUtils::expr_type(v->m_symbolic_value)) &&
            (is_array_const_or_ctor(v->m_symbolic_value) ||
             is_array_const_or_ctor(v->m_value))) {
            ASR::array_physical_typeType target_ptype = ASRUtils::extract_physical_type(v->m_type);
            if( target_ptype == ASR::array_physical_typeType::DescriptorArray ) {
                llvm::Type* target_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                    al, v->base.base.loc, &v->base)), v->m_type, module.get());
                target_var = arr_descr->get_pointer_to_data(target_type, target_var);
                builder->CreateStore(init_value, target_var, v->m_is_volatile);
            } else if( target_ptype == ASR::array_physical_typeType::FixedSizeArray ) {
                ASR::expr_t* arr_value = v->m_value ? v->m_value : v->m_symbolic_value;
                llvm::Value* arg_size = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context),
                llvm::APInt(32, ASRUtils::get_fixed_size_of_array(ASRUtils::expr_type(arr_value))));
                llvm::Type* llvm_data_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                    al, v->base.base.loc, &v->base)),
                    ASRUtils::type_get_past_array(ASRUtils::expr_type(arr_value)), module.get());
                llvm::DataLayout data_layout(module->getDataLayout());
                size_t dt_size = data_layout.getTypeAllocSize(llvm_data_type);
                arg_size = builder->CreateMul(llvm::ConstantInt::get(
                    llvm::Type::getInt32Ty(context), llvm::APInt(32, dt_size)), arg_size);
                llvm::Type* llvm_type
                    = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                    al, v->base.base.loc, &v->base)), v->m_type, module.get());
                builder->CreateMemCpy(llvm_utils->create_gep2(llvm_type,target_var, 0),
                    llvm::MaybeAlign(), init_value, llvm::MaybeAlign(), arg_size, v->m_is_volatile);
            } else if(target_ptype == ASR::PointerArray){
                if(ASRUtils::is_array_of_strings(v->m_type)){
                    builder->CreateMemCpy(
                        llvm_utils->get_stringArray_data(v->m_type, target_var), llvm::MaybeAlign(),
                        llvm_utils->get_stringArray_data(v->m_type, init_value), llvm::MaybeAlign(),
                        llvm_utils->get_stringArray_whole_size(v->m_type));
                } else {
                    throw LCompilersException("Unhandled Array Type");
                }
            } else {
                throw LCompilersException("Unhandled ArrayPhysicalType");
            }
        } else if (ASR::is_a<ASR::ArrayReshape_t>(*v->m_symbolic_value)) {
            llvm::Type* arr_type = llvm_utils->get_type_from_ttype_t_util(
                v->m_symbolic_value, ASRUtils::expr_type(v->m_symbolic_value), module.get());
            builder->CreateStore(builder->CreateLoad(arr_type, init_value), target_var, v->m_is_volatile);
        } else if (is_a<ASR::String_t>(*v->m_type)) {
            ASR::String_t *t = down_cast<ASR::String_t>(v->m_type);
            visit_expr(*t->m_len);
            if( t->m_len_kind == ASR::string_length_kindType::DeferredLength ){LCOMPILERS_ASSERT(false);}
            // target decides if the str_copy is performed on string descriptor or pointer.
            tmp = llvm_utils->lfortran_str_copy(
                target_var, init_value,
                ASR::down_cast<ASR::String_t>(ASRUtils::extract_type(v->m_type)),
                ASR::down_cast<ASR::String_t>((ASRUtils::extract_type(expr_type(v->m_symbolic_value? v->m_symbolic_value : v->m_value)))),
                ASRUtils::is_allocatable(v->m_type));
            if (v->m_intent == intent_local) {
                llvm::Type* v_llvm_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                    al, v->base.base.loc, &v->base)), v->m_type, module.get());
                strings_to_be_deallocated.push_back(al, llvm_utils->CreateLoad2(v_llvm_type, target_var, v->m_is_volatile));
            }
        } else if(ASRUtils::is_array(v->m_type) &&
                (ASR::is_a<ASR::PointerNullConstant_t>(*v->m_symbolic_value) ||
                (v->m_value && ASR::is_a<ASR::PointerNullConstant_t>(*v->m_value)))){
                LCOMPILERS_ASSERT(ASR::is_a<ASR::Pointer_t>(*v->m_type));
                LCOMPILERS_ASSERT(ASRUtils::extract_physical_type(v->m_type) ==
                                     ASR::array_physical_typeType::DescriptorArray);
                if (v->m_storage == ASR::storage_typeType::Save) {
                    // Save pointer arrays are globals initialized to null,
                    // which already represents "pointer not associated".
                    // Attempting to dereference the null pointer to set
                    // the descriptor's data field would segfault.
                } else {
                    llvm::Type* const array_desc_type = llvm_utils->arr_api->get_array_type(
                        ASRUtils::EXPR(ASR::make_Var_t(al, v->base.base.loc, (ASR::symbol_t*)v)),
                        ASRUtils::type_get_past_allocatable_pointer(v->m_type),
                        llvm_utils->get_el_type(
                            ASRUtils::EXPR(ASR::make_Var_t(al, v->base.base.loc, &v->base)),
                            ASRUtils::extract_type(v->m_type),
                            module.get()),
                        false);
                    llvm::Value* data_ptr = llvm_utils->create_gep2(
                        array_desc_type, llvm_utils->CreateLoad2(array_desc_type->getPointerTo(), target_var), 0);
                    builder->CreateStore(init_value, data_ptr, v->m_is_volatile);
                }
        } else {
            if (v->m_storage == ASR::storage_typeType::Save
                && v->m_value
                && ((ASR::is_a<ASR::Integer_t>(*v->m_type)
                || ASR::is_a<ASR::Real_t>(*v->m_type)
                || ASR::is_a<ASR::Logical_t>(*v->m_type))
                || (ASR::is_a<ASR::ArrayConstant_t>(*v->m_value)
                && ASRUtils::is_array(v->m_type)
                && ASRUtils::extract_physical_type(v->m_type)
                    == ASR::array_physical_typeType::FixedSizeArray
                && (ASR::is_a<ASR::Integer_t>(*ASRUtils::type_get_past_array(v->m_type))
                    || ASR::is_a<ASR::Real_t>(*ASRUtils::type_get_past_array(v->m_type))
                    || ASR::is_a<ASR::Logical_t>(*ASRUtils::type_get_past_array(v->m_type)))))
                && !v->m_is_volatile) {
                // Do nothing, the value is already initialized
                // in the global variable
            } else {
                builder->CreateStore(init_value, target_var, v->m_is_volatile);
            }
        }
    }

    template<typename T>
    void process_Variable(ASR::symbol_t* var_sym, T& x, uint32_t &debug_arg_count) {
        llvm::Value *target_var = nullptr;
        ASR::Variable_t *v = down_cast<ASR::Variable_t>(var_sym);
        uint32_t h = get_hash((ASR::asr_t*)v);
        llvm::Type *type;
        int n_dims = 0, a_kind = 4;
        ASR::dimension_t* m_dims = nullptr;
        bool is_array_type = false;
        bool is_malloc_array_type = false;
        bool is_list = false;
        bool is_dict = ASR::is_a<ASR::Dict_t>(*v->m_type);
        bool is_tuple = ASR::is_a<ASR::Tuple_t>(*v->m_type);
        bool is_set = ASR::is_a<ASR::Set_t>(*v->m_type);
        if (v->m_intent == intent_local ||
            v->m_intent == intent_return_var ||
            !v->m_intent) {
            type = llvm_utils->get_type_from_ttype_t(ASRUtils::EXPR(ASR::make_Var_t(
                    al, v->base.base.loc, &v->base)),
                v->m_type, v->m_type_declaration, v->m_storage, is_array_type,
                is_malloc_array_type, is_list, m_dims, n_dims, a_kind, module.get());
            llvm::Type* type_ = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                    al, v->base.base.loc, &v->base)),
                ASRUtils::type_get_past_pointer(
                    ASRUtils::type_get_past_allocatable(v->m_type)), module.get(), v->m_abi);

            /*
            * The following if block is used for converting any
            * general array descriptor to a pointer type which
            * can be passed as an argument in a function call in LLVM IR.
            */
            if( x.class_type == ASR::symbolType::Function) {
                std::string m_name = std::string(x.m_name);
                ASR::Function_t* _func = (ASR::Function_t*)(&(x.base));
                std::uint32_t m_h = get_hash((ASR::asr_t*)_func);
                ASR::abiType abi_type = ASRUtils::get_FunctionType(_func)->m_abi;
                bool is_v_arg = is_argument(v, _func->m_args, _func->n_args);
                if( is_array_type && !is_list && !is_dict) {
                    /* The first element in an array descriptor can be either of
                    * llvm::ArrayType or llvm::PointerType. However, a
                    * function only accepts llvm::PointerType for arrays. Hence,
                    * the following if block extracts the pointer to first element
                    * of an array from its descriptor. Note that this happens only
                    * for arguments and not for local function variables.
                    */
                    if( abi_type == ASR::abiType::Source && is_v_arg ) {
                        type = arr_descr->get_argument_type(type, m_h, v->m_name, arr_arg_type_cache);
                        is_array_type = false;
                    } else if( abi_type == ASR::abiType::Intrinsic &&
                        fname2arg_type.find(m_name) != fname2arg_type.end() ) {
                        type = fname2arg_type[m_name].second;
                        is_array_type = false;
                    }
                }
            }

            llvm::Value* array_size = nullptr;
            bool is_array_of_strings = ASRUtils::is_array(v->m_type) && ASRUtils::is_character(*v->m_type);
            if( ASRUtils::is_array(v->m_type) &&
                ASRUtils::extract_physical_type(v->m_type) ==
                ASR::array_physical_typeType::PointerArray &&
                !LLVM::is_llvm_pointer(*v->m_type) ) {
                ASR::ttype_t* element_type = ASRUtils::type_get_past_array(v->m_type);
                // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                type = llvm_utils->get_el_type(
                    ASRUtils::EXPR(ASR::make_Var_t(al, v->base.base.loc, &v->base)),
                    element_type, module.get());
                ASR::dimension_t* m_dims = nullptr;
                size_t n_dims = ASRUtils::extract_dimensions_from_ttype(v->m_type, m_dims);
                array_size = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, 1));
                int ptr_loads_copy = ptr_loads;
                ptr_loads = 2;
                for( size_t i = 0; i < n_dims; i++ ) {
                    this->visit_expr_wrapper(m_dims[i].m_length, true);

                    if (m_dims[i].m_length != nullptr &&  ASR::is_a<ASR::Var_t>(*m_dims[i].m_length)) {
                        ASR::Var_t* m_length_var = ASR::down_cast<ASR::Var_t>(m_dims[i].m_length);
                        ASR::symbol_t* m_length_sym = ASRUtils::symbol_get_past_external(m_length_var->m_v);
                        if (m_length_sym != nullptr && ASR::is_a<ASR::Variable_t>(*m_length_sym)) {
                            ASR::Variable_t* m_length_variable = ASR::down_cast<ASR::Variable_t>(m_length_sym);
                            uint32_t m_length_variable_h = get_hash((ASR::asr_t*)m_length_variable);
                            llvm::Type* deep_type = llvm_utils->get_type_from_ttype_t_util(m_dims[i].m_length,
                                ASRUtils::type_get_past_allocatable_pointer(ASRUtils::expr_type(m_dims[i].m_length)), module.get());
                            llvm::Value* deep = new llvm::GlobalVariable(
                                    *module,
                                    deep_type,
                                    false,
                                    llvm::GlobalValue::InternalLinkage,
                                    llvm::Constant::getNullValue(deep_type),
                                    "deep_" + std::to_string(global_deep_count++));
                            builder->CreateStore(tmp, deep, v->m_is_volatile);
                            llvm::Type* m_dims_length_llvm_type = llvm_utils->get_type_from_ttype_t_util(m_dims[i].m_length,
                                ASRUtils::type_get_past_allocatable_pointer(ASRUtils::expr_type(m_dims[i].m_length)), module.get());
                            tmp = llvm_utils->CreateLoad2(m_dims_length_llvm_type,deep, v->m_is_volatile);
                            llvm_symtab_deep_copy[{m_length_variable_h, current_scope}] = deep;
                        }
                    }

                    // Make dimension length and return size compatible.(TODO : array_size should be of type int64)
                    if(ASRUtils::extract_kind_from_ttype_t(
                        ASRUtils::expr_type(m_dims[i].m_length)) > 4){
                        tmp = builder->CreateTrunc(tmp, llvm::IntegerType::get(context, 32));
                    } else if (ASRUtils::extract_kind_from_ttype_t(
                        ASRUtils::expr_type(m_dims[i].m_length)) < 4){
                        tmp = builder->CreateSExt(tmp, llvm::IntegerType::get(context, 32));
                    }

                    array_size = builder->CreateMul(array_size, tmp);
                }
                ptr_loads = ptr_loads_copy;
            }
            llvm::Value *ptr = nullptr;
            // Allocate the variable
            if( is_array_of_strings &&
                    (v->m_storage == ASR::Save || v->m_storage == ASR::Parameter) &&
                    !ASRUtils::is_allocatable(v->m_type) &&
                    ASRUtils::extract_physical_type(v->m_type) == ASR::array_physical_typeType::PointerArray ) {
                ASR::ArrayConstant_t* arr_const = nullptr;
                ASR::expr_t* value = v->m_value != nullptr ? v->m_value : v->m_symbolic_value;
                if (value) {
                    arr_const = ASR::down_cast<ASR::ArrayConstant_t>(ASRUtils::expr_value(value));
                }
                ptr = llvm_utils->handle_global_nonallocatable_stringArray(
                    al, ASR::down_cast<ASR::Array_t>(v->m_type), arr_const, v->m_name);
            } else if( !compiler_options.stack_arrays && array_size ) { // malloc for PointerArray
                if(is_array_of_strings){
                    ptr = create_and_setup_string_for_array(v->m_type, array_size, compiler_options.stack_arrays, v->m_name);
                } else {
                    llvm::DataLayout data_layout(module->getDataLayout());
                    uint64_t size = data_layout.getTypeAllocSize(type);
                    array_size = builder->CreateMul(array_size,
                        llvm::ConstantInt::get(context, llvm::APInt(32, size)));
                    llvm::Value* ptr_i8 = LLVMArrUtils::lfortran_malloc(
                        context, *module, *builder, array_size);
                    ptr = builder->CreateBitCast(ptr_i8, type->getPointerTo());
                }
            } else if(ASRUtils::is_string_only(v->m_type)){
                if(v->m_storage == ASR::Save || v->m_storage == ASR::Parameter){
                    if(v->m_storage == ASR::Parameter) {LCOMPILERS_ASSERT(v->m_symbolic_value)}
                    std::string str_initial_value_string {};
                    if(v->m_symbolic_value){ // Get initial value if exist.
                        char* str_inital_value{};
                        ASRUtils::extract_value(ASRUtils::expr_value(v->m_symbolic_value), str_inital_value);
                        int kind = ASRUtils::get_string_type(v->m_type)->m_kind;
                        if (kind > 1 && str_inital_value) {
                            // For kind>1, logical length != physical byte length
                            // (UTF-8 multi-byte encoding). Use strlen to read the
                            // full UTF-8 string without truncation.
                            str_initial_value_string = std::string(str_inital_value);
                        } else {
                            int str_initial_value_len{};
                            ASRUtils::extract_value(ASRUtils::expr_value(
                                ASRUtils::get_string_type(ASRUtils::expr_type(v->m_symbolic_value))->m_len),
                                str_initial_value_len);
                            str_initial_value_string = std::string(str_inital_value, str_initial_value_len);
                        }
                    }
                    ptr = llvm_utils->declare_global_string(ASRUtils::get_string_type(v->m_type),
                            str_initial_value_string, false,
                            v->m_name);
                } else {
                    // Create String + Set it up
                    ptr = llvm_utils->create_string(ASRUtils::get_string_type(v->m_type), v->m_name);
                    setup_string(ptr, v->m_type);
                }
            } else { // Alloca for rest of types (not its internals if exist).
                if (v->m_storage == ASR::storage_typeType::Save) {
                    std::string parent_function_name = std::string(x.m_name);
                    if (x.class_type == ASR::symbolType::Block) {
                        parent_function_name += "_" + x.m_symtab->get_counter();
                    }
                    std::string global_name = parent_function_name+ "." + v->m_name;
                    ptr = module->getOrInsertGlobal(global_name, type);
                    llvm::GlobalVariable *gptr = module->getNamedGlobal(global_name);
                    gptr->setLinkage(llvm::GlobalValue::InternalLinkage);
                    llvm::Constant *init_value;
                    if (v->m_value
                            && (ASR::is_a<ASR::Integer_t>(*v->m_type)
                            || ASR::is_a<ASR::Real_t>(*v->m_type)
                            || ASR::is_a<ASR::Logical_t>(*v->m_type))) {
                        this->visit_expr(*v->m_value);
                        init_value = llvm::dyn_cast<llvm::Constant>(tmp);
                    } else if (v->m_value
                            && ASR::is_a<ASR::ArrayConstant_t>(*v->m_value)
                            && ASRUtils::is_array(v->m_type)
                            && ASRUtils::extract_physical_type(v->m_type)
                                == ASR::array_physical_typeType::FixedSizeArray
                            && (ASR::is_a<ASR::Integer_t>(*ASRUtils::type_get_past_array(v->m_type))
                                || ASR::is_a<ASR::Real_t>(*ASRUtils::type_get_past_array(v->m_type))
                                || ASR::is_a<ASR::Logical_t>(*ASRUtils::type_get_past_array(v->m_type)))) {
                        llvm::Type* el_type = type->getArrayElementType();
                        init_value = get_const_array(v->m_value, el_type);
                    } else if (v->m_symbolic_value
                            && ASR::is_a<ASR::FunctionType_t>(*ASRUtils::type_get_past_pointer(v->m_type))
                            && ASR::is_a<ASR::Var_t>(*v->m_symbolic_value)) {
                        // Procedure pointer initialized to a function target
                        ASR::symbol_t* target_sym = ASRUtils::symbol_get_past_external(
                            ASR::down_cast<ASR::Var_t>(v->m_symbolic_value)->m_v);
                        uint32_t th = get_hash((ASR::asr_t*)target_sym);
                        if (llvm_symtab_fn.find(th) != llvm_symtab_fn.end()) {
                            llvm::Function* fn = llvm_symtab_fn[th];
                            if (fn->getType() == type) {
                                init_value = fn;
                            } else {
                                init_value = llvm::ConstantExpr::getBitCast(fn, type);
                            }
                        } else {
                            init_value = llvm::Constant::getNullValue(type);
                        }
                    } else {
                        init_value = llvm::Constant::getNullValue(type);
                    }
                    gptr->setInitializer(init_value);
                } else {
                    // Large fixed-size arrays (> 4KB-1) use heap allocation to prevent stack overflow.
                    // This is recursion-safe unlike static storage, as each call gets its own copy.
                    // Memory is freed at function exit.
                    // BLOCK constructs always use heap regardless of size (can be in loops).
                    constexpr uint64_t MAX_STACK_ARRAY_SIZE = 4 * 1024 - 1;  // 4KB arrays and above use heap
                    bool use_heap_allocation = false;
                    uint64_t type_size = 0;
                    if (!compiler_options.stack_arrays &&
                        ASRUtils::is_array(v->m_type) &&
                        ASRUtils::extract_physical_type(v->m_type) ==
                            ASR::array_physical_typeType::FixedSizeArray) {
                        const llvm::DataLayout& data_layout = module->getDataLayout();
                        type_size = data_layout.getTypeAllocSize(type);
                        // BLOCKs always use heap (can be in loops), others use threshold
                        if (in_block_context || type_size > MAX_STACK_ARRAY_SIZE) {
                            use_heap_allocation = true;
                        }
                    }

                    if (use_heap_allocation) {
                        // Allocate on heap to avoid stack overflow (recursion-safe)
                        llvm::Value* malloc_size = llvm::ConstantInt::get(
                            llvm_utils->getIntType(8), llvm::APInt(64, type_size));
                        llvm::Value* ptr_i8 = LLVMArrUtils::lfortran_malloc(
                            context, *module, *builder, malloc_size);
                        ptr = builder->CreateBitCast(ptr_i8, type->getPointerTo());
                        // Track for cleanup at function exit
                        heap_fixed_size_arrays.push_back(al, ptr_i8);
                    } else {
#if LLVM_VERSION_MAJOR >= 15
                        bool is_llvm_ptr = false;
                        if ( LLVM::is_llvm_pointer(*v->m_type) &&
                                !ASRUtils::is_class_type(ASRUtils::type_get_past_pointer(
                                ASRUtils::type_get_past_allocatable(v->m_type))) &&
                                !ASRUtils::is_descriptorString(v->m_type) ) {
                            is_llvm_ptr = true;
                        }
                        if (!is_llvm_ptr && is_array_type && !is_list &&
                                ASRUtils::extract_physical_type(v->m_type) ==
                                    ASR::array_physical_typeType::DescriptorArray) {
                            ptr = arr_descr->create_descriptor_alloca(type_, v->m_name);
                        } else {
                            ptr = llvm_utils->CreateAlloca(*builder, type_, array_size,
                                v->m_name, is_llvm_ptr);
                        }
#else
                        if (is_array_type && !is_list &&
                                !LLVM::is_llvm_pointer(*v->m_type) &&
                                ASRUtils::extract_physical_type(v->m_type) ==
                                    ASR::array_physical_typeType::DescriptorArray) {
                            ptr = arr_descr->create_descriptor_alloca(type, v->m_name);
                        } else {
                            ptr = llvm_utils->CreateAlloca(*builder, type, array_size, v->m_name);
                        }
#endif
                    }
                }
            }
            if (!LLVM::is_llvm_pointer(*v->m_type) &&
                    ASRUtils::is_class_type(ASRUtils::extract_type(v->m_type))) {
                struct_api->store_class_vptr(ASRUtils::symbol_get_past_external(v->m_type_declaration),
                    ptr, module.get());
                // Null-initialize the inner struct pointer (field 1) so that
                // finalizers do not dereference uninitialized memory when the
                // class wrapper is only default-initialized (e.g. for absent
                // optional class() arguments).
                ASR::symbol_t* sym_past = ASRUtils::symbol_get_past_external(v->m_type_declaration);
                ASR::Struct_t* struct_t = ASR::down_cast<ASR::Struct_t>(sym_past);
                if (!ASRUtils::is_unlimited_polymorphic_type(sym_past)) {
                    llvm::Type* class_type = llvm_utils->getClassType(struct_t);
                    llvm::Value* struct_ptr_field = llvm_utils->CreateGEP2(class_type, ptr, 1);
                    llvm::Type* inner_ptr_type = llvm_utils->getStructType(struct_t, module.get(), true);
                    builder->CreateStore(
                        llvm::ConstantPointerNull::get(llvm::cast<llvm::PointerType>(inner_ptr_type)),
                        struct_ptr_field);
                } else { // Unlimited polymorphic type
                    // Intialize type with NULL value (Zeroes)
                    llvm::Type* const class_type = llvm_utils->getClassType(struct_t);
                    builder->CreateStore(llvm::Constant::getNullValue(class_type), ptr);

                }
            } else {
                set_pointer_variable_to_null(v, llvm::ConstantPointerNull::get(
                    static_cast<llvm::PointerType*>(type)), ptr);
            }
            // std::cout<< "Allocated variable " << v->m_name << " of type ";
            // type->print(llvm::outs()); llvm::outs() << "\n";
            // type_->print(llvm::outs()); llvm::outs() << "\n";
            ASR::expr_t* var_expr = ASRUtils::EXPR(ASR::make_Var_t(al, v->base.base.loc, &v->base));
            // Initialize non-primitve types
            if( ASR::is_a<ASR::StructType_t>(
                *ASRUtils::type_get_past_array(v->m_type))
                && !ASRUtils::is_class_type(
                    ASRUtils::type_get_past_array(v->m_type))) {
                // For save variables of struct type, member initialization
                // (default values, string descriptors, array descriptors) must
                // run exactly once rather than on every call, otherwise saved
                // values are overwritten. We emit a one-time guard.
                llvm::BasicBlock* struct_init_bb = nullptr;
                llvm::BasicBlock* struct_skip_bb = nullptr;
                if (v->m_storage == ASR::storage_typeType::Save) {
                    std::string parent_function_name = std::string(x.m_name);
                    if (x.class_type == ASR::symbolType::Block) {
                        parent_function_name += "_" + x.m_symtab->get_counter();
                    }
                    std::string guard_name = parent_function_name + "." + v->m_name + ".__struct_init";
                    llvm::Type* i1_type = llvm::Type::getInt1Ty(context);
                    module->getOrInsertGlobal(guard_name, i1_type);
                    llvm::GlobalVariable* guard = module->getNamedGlobal(guard_name);
                    guard->setLinkage(llvm::GlobalValue::InternalLinkage);
                    guard->setInitializer(llvm::ConstantInt::getFalse(context));

                    llvm::Value* already_done = builder->CreateLoad(i1_type, guard);
                    struct_init_bb = llvm::BasicBlock::Create(context, "struct_init");
                    struct_skip_bb = llvm::BasicBlock::Create(context, "struct_init_done");
                    builder->CreateCondBr(already_done, struct_skip_bb, struct_init_bb);

                    llvm::Function* fn = builder->GetInsertBlock()->getParent();
                    llvm_fn_insert_bb(fn, struct_init_bb);
                    builder->SetInsertPoint(struct_init_bb);
                    builder->CreateStore(llvm::ConstantInt::getTrue(context), guard);
                }
                if( ASRUtils::is_array(v->m_type) ) {
                    // For DescriptorArray, the array descriptor (ndim, dim_desc, data
                    // ptr) is not initialized yet at this point. Defer the member
                    // initialization until after fill_array_details_ sets it up.
                    if (ASRUtils::extract_physical_type(v->m_type) !=
                            ASR::array_physical_typeType::DescriptorArray) {
                        allocate_array_members_of_struct_arrays(var_expr, ptr, v->m_type);
                    }
                } else {
                    bool is_intent_out_var = (v->m_intent == ASR::intentType::Out);
                    allocate_array_members_of_struct(ASR::down_cast<ASR::Struct_t>(
                        ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(var_expr))), ptr, v->m_type,
                        is_intent_out_var);
                }
                if (struct_skip_bb != nullptr) {
                    builder->CreateBr(struct_skip_bb);
                    llvm::Function* fn = builder->GetInsertBlock()->getParent();
                    llvm_fn_insert_bb(fn, struct_skip_bb);
                    builder->SetInsertPoint(struct_skip_bb);
                }
            }
            if (compiler_options.emit_debug_info) {
                // Reset the debug location
                builder->SetCurrentDebugLocation(nullptr);
                uint32_t line, column;
                debug_get_line_column(v->base.base.loc.first, line, column);
                std::string type_name;
                uint32_t type_size, type_encoding;
                get_type_debug_info(v->m_type, type_name, type_size,
                    type_encoding);
                llvm::DIType* di_type = DBuilder->createBasicType(
                    type_name, type_size, type_encoding);
                llvm::DILocalVariable* debug_var = nullptr;
                if (v->m_intent == ASR::intentType::In ||
                    v->m_intent == ASR::intentType::Out ||
                    v->m_intent == ASR::intentType::InOut ||
                    v->m_intent == ASR::intentType::Unspecified) {
                    debug_var = DBuilder->createParameterVariable(
                        debug_current_scope, v->m_name, ++debug_arg_count, debug_Unit, line,
                        di_type, true);
                } else {
                    debug_var = DBuilder->createAutoVariable(
                        debug_current_scope, v->m_name, debug_Unit, line,
                        di_type, true);
                }
                DBuilder->insertDeclare(ptr, debug_var, DBuilder->createExpression(),
                    llvm::DILocation::get(debug_current_scope->getContext(),
                    line, 0, debug_current_scope), builder->GetInsertBlock());
            }

            if( ASR::is_a<ASR::StructType_t>(*v->m_type) && !ASRUtils::is_class_type(v->m_type) ) {
                ASR::Struct_t* struct_type = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(v->m_type_declaration));
                int64_t alignment_value = -1;
                if( ASRUtils::extract_value(struct_type->m_alignment, alignment_value) ) {
                    llvm::Align align(alignment_value);
                    reinterpret_cast<llvm::AllocaInst*>(ptr)->setAlignment(align);
                }
            }
            llvm_symtab[h] = ptr;
            if( (ASRUtils::is_array(v->m_type) &&
                ((ASRUtils::extract_physical_type(v->m_type) == ASR::array_physical_typeType::DescriptorArray) ||
                (ASRUtils::extract_physical_type(v->m_type) == ASR::array_physical_typeType::StringArraySinglePointer &&
                ASRUtils::is_dimension_empty(m_dims,n_dims))))
            ) {
                auto is_array_const_or_ctor = [](ASR::expr_t* e) {
                    return e && (ASR::is_a<ASR::ArrayConstant_t>(*e) ||
                                 ASR::is_a<ASR::ArrayConstructor_t>(*e));
                };
                ASR::expr_t* init_value = v->m_value ? v->m_value : v->m_symbolic_value;
                bool skip_data_alloc = (v->m_storage == ASR::storage_typeType::Parameter)
                    && !is_malloc_array_type
                    && ASRUtils::extract_physical_type(v->m_type) == ASR::array_physical_typeType::DescriptorArray
                    && !ASRUtils::is_character(*ASRUtils::type_get_past_array(v->m_type))
                    && !ASR::is_a<ASR::StructType_t>(*ASRUtils::type_get_past_array(v->m_type))
                    && is_array_const_or_ctor(init_value);
                fill_array_details_(ASRUtils::EXPR(ASR::make_Var_t(
                    al, v->base.base.loc, &v->base)), ptr, type_, m_dims, n_dims,
                    is_malloc_array_type, is_array_type, is_list, v->m_type,
                    /*is_data_only=*/false, /*reserve_data_memory=*/!skip_data_alloc);
                // Now that the descriptor is initialized, initialize allocatable
                // members of each element for DescriptorArray struct arrays.
                if( ASR::is_a<ASR::StructType_t>(
                        *ASRUtils::type_get_past_array(v->m_type))
                    && !ASRUtils::is_class_type(
                        ASRUtils::type_get_past_array(v->m_type))
                    && ASRUtils::extract_physical_type(v->m_type) ==
                        ASR::array_physical_typeType::DescriptorArray
                    && !is_malloc_array_type ) {
                    allocate_array_members_of_struct_arrays(var_expr, ptr, v->m_type);
                }
            }
            ASR::expr_t* init_expr = v->m_symbolic_value;
            if( v->m_storage != ASR::storage_typeType::Parameter ) {
                for( size_t i = 0; i < v->n_dependencies; i++ ) {
                    std::string variable_name = v->m_dependencies[i];
                    ASR::symbol_t* dep_sym = x.m_symtab->resolve_symbol(variable_name);
                    if (dep_sym) {
                        if (ASR::is_a<ASR::Variable_t>(*dep_sym)) {
                            ASR::Variable_t* dep_v = ASR::down_cast<ASR::Variable_t>(dep_sym);
                            if ( dep_v->m_symbolic_value == nullptr &&
                                !(ASRUtils::is_array(dep_v->m_type) && ASRUtils::extract_physical_type(dep_v->m_type) ==
                                    ASR::array_physical_typeType::FixedSizeArray)) {
                                init_expr = nullptr;
                                break;
                            }
                        }
                    }
                }
            }
            if( init_expr != nullptr && !is_list && !is_dict && !is_tuple && !is_set) {
                target_var = ptr;
                if ((v->m_storage == ASR::Save   ||
                    v->m_storage == ASR::Parameter)
                    &&
                    (ASRUtils::is_string_only(v->m_type) ||
                     (ASRUtils::is_array_of_strings(v->m_type) &&
                      !ASRUtils::is_allocatable(v->m_type) &&
                      ASRUtils::extract_physical_type(v->m_type) == ASR::array_physical_typeType::PointerArray))) {
                    // DO Nothing
                    // (String + Save) variable is declared as global llvm variable with the intended inital value
                } else if(v->m_storage == ASR::storage_typeType::Save &&
                    ASR::is_a<ASR::Function_t>(
                        *ASR::down_cast<ASR::symbol_t>(v->m_parent_symtab->asr_owner))){
                    variable_inital_value_vec.push_back({v, target_var});
                } else {
                    set_VariableInital_value(v, target_var);
                }
            } else {
                if (is_list) {
                    ASR::List_t* asr_list = ASR::down_cast<ASR::List_t>(v->m_type);
                    std::string type_code = ASRUtils::get_type_code(asr_list->m_type);
                    list_api->list_init(type_code, ptr, module.get());
                } else if (is_dict) {
                    ASR::Dict_t* asr_dict = ASR::down_cast<ASR::Dict_t>(v->m_type);
                    std::string key_type_code = ASRUtils::get_type_code(asr_dict->m_key_type);
                    std::string value_type_code = ASRUtils::get_type_code(asr_dict->m_value_type);

                    llvm_utils->get_type_from_ttype_t_util(nullptr, ASRUtils::TYPE(ASR::make_List_t(al, asr_dict->base.base.loc, asr_dict->m_key_type)), module.get());
                    llvm_utils->get_type_from_ttype_t_util(nullptr, ASRUtils::TYPE(ASR::make_List_t(al, asr_dict->base.base.loc, asr_dict->m_value_type)), module.get());
                    llvm_utils->get_type_from_ttype_t_util(nullptr, v->m_type, module.get());

                    if (ASRUtils::is_character(*asr_dict->m_key_type))
                        dict_api_sc->dict_init(asr_dict, ptr, module.get(), 0);
                    else
                        dict_api_lp->dict_init(asr_dict, ptr, module.get(), 0);
                } else if (is_tuple) {
                    auto tuple_type_ = ASR::down_cast<ASR::Tuple_t>(v->m_type);
                    for (size_t i=0; i<tuple_type_->n_type; i++) {
                        if (ASRUtils::is_descriptorString(tuple_type_->m_type[i]) && ASRUtils::is_deferredLength_string(tuple_type_->m_type[i])) {
                            auto item_ptr = llvm_utils->create_gep2(type, ptr, i);

                            builder->CreateStore(llvm::Constant::getNullValue(llvm_utils->string_descriptor), item_ptr);
                            ASR::String_t *t = ASR::down_cast<ASR::String_t>(ASRUtils::extract_type(tuple_type_->m_type[i]));
                            llvm_utils->set_string_memory_on_heap(t->m_physical_type, item_ptr, llvm_utils->get_string_length(t, item_ptr), t->m_kind);
                        }
                    }
                } else if (is_set) {
                    ASR::Set_t* asr_set = ASR::down_cast<ASR::Set_t>(v->m_type);
                    llvm_utils->set_set_api(asr_set);
                    std::string el_type_code = ASRUtils::get_type_code(asr_set->m_type);
                    llvm_utils->set_api->set_init(el_type_code, ptr, module.get(), 0);
                }
            }
        }
    }

    template<typename T>
    void declare_vars(const T &x) {
        uint32_t debug_arg_count = 0;
        std::vector<std::string> var_order = ASRUtils::determine_variable_declaration_order(x.m_symtab);
        for (auto &item : var_order) {
            ASR::symbol_t* var_sym = x.m_symtab->get_symbol(item);
            if (is_a<ASR::Variable_t>(*var_sym)) {
                process_Variable(var_sym, x, debug_arg_count);
            }
        }
    }

    bool is_function_variable(const ASR::Variable_t &v) {
        if (v.m_type_declaration) {
            return ASR::is_a<ASR::Function_t>(*ASRUtils::symbol_get_past_external(v.m_type_declaration));
        } else {
            return false;
        }
    }

    bool is_function_variable(const ASR::symbol_t *v) {
        if( !ASR::is_a<ASR::Variable_t>(*v) ) {
            return false;
        }
        return is_function_variable(*ASR::down_cast<ASR::Variable_t>(v));
    }

    // F is the function that we are generating and we go over all arguments
    // (F.args()) and handle three cases:
    //     * Variable (`integer :: x`)
    //     * Function (callback) Variable (`procedure(fn) :: x`)
    //     * Function (`fn`)
    void declare_args(const ASR::Function_t &x, llvm::Function &F) {
        size_t asr_arg_idx = 0;
        auto arg_it = F.arg_begin();

        // Windows complex(kind=8) uses "pass-as-subroutine" (sret-style) ABI:
        // the LLVM function has an extra hidden first argument to store the
        // return value, while ASR still models it as a function with a return var.
        if (compiler_options.platform == Platform::Windows &&
            x.m_return_var != nullptr &&
            ASR::is_a<ASR::Complex_t>(*ASRUtils::expr_type(x.m_return_var)) &&
            ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(x.m_return_var)) == 8 &&
            F.getReturnType()->isVoidTy() &&
            F.arg_size() == x.n_args + 1) {
            llvm::Argument* sret = &*arg_it;
            sret->setName("_sret");
            current_sret_arg = sret;
            ++arg_it;
        }

        for (; arg_it != F.arg_end(); ++arg_it) {
            LCOMPILERS_ASSERT(asr_arg_idx < x.n_args);
            llvm::Argument &llvm_arg = *arg_it;
            ASR::symbol_t *s = symbol_get_past_external(
                    ASR::down_cast<ASR::Var_t>(x.m_args[asr_arg_idx])->m_v);
            ASR::symbol_t* arg_sym = s;
            if (is_a<ASR::Variable_t>(*s)) {
                ASR::Variable_t *v = ASR::down_cast<ASR::Variable_t>(s);
                if (is_function_variable(*v)) {
                    // * Function (callback) Variable (`procedure(fn) :: x`)
                    s = ASRUtils::symbol_get_past_external(v->m_type_declaration);
                } else {
                    // * Variable (`integer :: x`)
                    ASR::Variable_t *arg = EXPR2VAR(x.m_args[asr_arg_idx]);
                    LCOMPILERS_ASSERT(is_arg_dummy(arg->m_intent));

                    llvm::Value* llvm_sym = &llvm_arg;
                    // For bind(C) allocatable/pointer array parameters, the
                    // function signature receives %array* (single pointer),
                    // but the rest of the codegen expects an extra level of
                    // indirection (%array**) for allocatable/pointer types.
                    // Bridge the gap with a local alloca.
                    if (ASRUtils::get_FunctionType(x)->m_abi == ASR::abiType::BindC &&
                        ASRUtils::is_array(arg->m_type) &&
                        (ASRUtils::is_allocatable(arg->m_type) ||
                         ASRUtils::is_pointer(arg->m_type))) {
                        llvm::Value* alloca_ptr = builder->CreateAlloca(
                            llvm_arg.getType(), nullptr, std::string(arg->m_name) + "_ptr");
                        builder->CreateStore(&llvm_arg, alloca_ptr);
                        llvm_sym = alloca_ptr;
                    }
                    // For bind(C) complex value parameters, the ABI type
                    // (e.g. [2 x float] on macOS ARM, <2 x float> on Linux,
                    // i64 on Windows) may differ from the internal complex
                    // struct type (%complex_4). Convert via memory bitcast
                    // so the rest of the codegen sees the expected type.
                    if (ASRUtils::get_FunctionType(x)->m_abi == ASR::abiType::BindC &&
                        arg->m_value_attr &&
                        ASR::is_a<ASR::Complex_t>(*arg->m_type)) {
                        llvm::Type* internal_type = llvm_utils->getComplexType(
                            ASRUtils::extract_kind_from_ttype_t(arg->m_type));
                        if (llvm_arg.getType() != internal_type) {
                            llvm::Value* arg_alloca = builder->CreateAlloca(
                                llvm_arg.getType(), nullptr);
                            builder->CreateStore(&llvm_arg, arg_alloca);
                            llvm::Value* cast_ptr = builder->CreateBitCast(
                                arg_alloca, internal_type->getPointerTo());
                            llvm_sym = llvm_utils->CreateLoad2(internal_type, cast_ptr);
                        }
                    }
                    // For non-BindC functions with value attribute,
                    // create a local copy so modifications to the
                    // parameter don't affect the caller's variable.
                    // Skip CPtr: it is already passed by value at
                    // the call site and handled by the existing
                    // is_cptr_dummy_passed_by_value path.
                    if (arg->m_value_attr &&
                        ASRUtils::get_FunctionType(x)->m_abi != ASR::abiType::BindC &&
                        !ASR::is_a<ASR::CPtr_t>(*arg->m_type) &&
                        !ASRUtils::is_array(arg->m_type) &&
                        llvm_arg.getType()->isPointerTy()) {
                        llvm::Type* val_type = llvm_utils->get_type_from_ttype_t_util(
                            nullptr, arg->m_type, module.get());
                        llvm::Value* loaded = llvm_utils->CreateLoad2(val_type, llvm_sym);
                        llvm::Value* local_copy = builder->CreateAlloca(
                            val_type, nullptr, std::string(arg->m_name) + "_value");
                        builder->CreateStore(loaded, local_copy);
                        llvm_sym = local_copy;
                    }
                    uint32_t h = get_hash((ASR::asr_t*)arg);
                    std::string arg_s = arg->m_name;
                    llvm_arg.setName(arg_s);
                    llvm_symtab[h] = llvm_sym;
                }
            }
            if (is_a<ASR::Function_t>(*s)) {
                // * Function (`fn`)
                // Deal with case where procedure passed in as argument
                ASR::Function_t *arg = ASR::down_cast<ASR::Function_t>(s);
                uint32_t h = get_hash((ASR::asr_t*)arg_sym);
                std::string arg_s = ASRUtils::symbol_name(arg_sym);
                llvm_arg.setName(arg_s);
                llvm_symtab_fn_arg[h] = &llvm_arg;
                if( is_function_variable(arg_sym) ) {
                    llvm_symtab[h] = &llvm_arg;
                }
                if (llvm_symtab_fn.find(h) == llvm_symtab_fn.end()) {
                    llvm::FunctionType* fntype = llvm_utils->get_function_type(*arg, module.get());
                    llvm::Function* fn = llvm::Function::Create(fntype, llvm::Function::ExternalLinkage, arg->m_name, module.get());
                    llvm_symtab_fn[h] = fn;
                }
            }
            asr_arg_idx++;
        }

        // Second pass: handle array dummy arguments with VALUE attribute.
        // We deferred this to after all arguments have been registered in
        // llvm_symtab, so that variable-size dimension lengths (e.g. another
        // dummy argument) can be evaluated. This emits a local buffer and
        // memcpy of the caller's data so callee modifications do not affect
        // the caller. Supports both fixed-size and runtime-size PointerArray
        // (explicit-shape) arguments.
        for (size_t i = 0; i < x.n_args; i++) {
            ASR::symbol_t* sym = ASRUtils::symbol_get_past_external(
                ASR::down_cast<ASR::Var_t>(x.m_args[i])->m_v);
            if (!ASR::is_a<ASR::Variable_t>(*sym)) continue;
            ASR::Variable_t* arg = ASR::down_cast<ASR::Variable_t>(sym);
            if (!arg->m_value_attr) continue;
            if (ASRUtils::get_FunctionType(x)->m_abi == ASR::abiType::BindC) continue;
            if (ASR::is_a<ASR::CPtr_t>(*arg->m_type)) continue;
            if (!ASRUtils::is_array(arg->m_type)) continue;
            ASR::Array_t* array_t = ASR::down_cast<ASR::Array_t>(
                ASRUtils::type_get_past_allocatable_pointer(arg->m_type));
            if (array_t->m_physical_type != ASR::array_physical_typeType::PointerArray) continue;
            uint32_t h = get_hash((ASR::asr_t*)arg);
            if (llvm_symtab.find(h) == llvm_symtab.end()) continue;
            llvm::Value* current_ptr = llvm_symtab[h];
            if (!current_ptr->getType()->isPointerTy()) continue;
            llvm::Type* el_type = llvm_utils->get_el_type(
                nullptr, array_t->m_type, module.get());
            uint64_t el_size_bytes = module->getDataLayout().getTypeAllocSize(el_type);
            std::string name = std::string(arg->m_name) + "_value";
            if (ASRUtils::is_fixed_size_array(array_t->m_dims, array_t->n_dims)) {
                int64_t size = ASRUtils::get_fixed_size_of_array(
                    array_t->m_dims, array_t->n_dims);
                llvm::Type* arr_type = llvm::ArrayType::get(el_type, size);
                llvm::Value* local_copy_arr = builder->CreateAlloca(
                    arr_type, nullptr, name);
                uint64_t total_bytes = el_size_bytes * (uint64_t)size;
                builder->CreateMemCpy(
                    local_copy_arr, llvm::MaybeAlign(),
                    current_ptr, llvm::MaybeAlign(),
                    total_bytes);
                llvm::Value* zero = llvm::ConstantInt::get(
                    context, llvm::APInt(32, 0));
                llvm::Value* data_ptr = llvm_utils->CreateInBoundsGEP2(
                    arr_type, local_copy_arr, {zero, zero});
                llvm_symtab[h] = data_ptr;
            } else {
                // Runtime-size array: compute total element count from m_dims.
                llvm::Type* i64_ty = llvm::Type::getInt64Ty(context);
                llvm::Value* num_elements = llvm::ConstantInt::get(i64_ty, 1);
                int64_t ptr_loads_copy = ptr_loads;
                for (size_t d = 0; d < array_t->n_dims; d++) {
                    ASR::expr_t* m_length = array_t->m_dims[d].m_length;
                    if (m_length == nullptr) {
                        num_elements = nullptr;
                        break;
                    }
                    ptr_loads = 2 - !LLVM::is_llvm_pointer(
                        *ASRUtils::expr_type(m_length));
                    this->visit_expr_wrapper(m_length, true);
                    llvm::Value* dim_len = tmp;
                    dim_len = builder->CreateSExtOrTrunc(dim_len, i64_ty);
                    num_elements = builder->CreateMul(num_elements, dim_len);
                }
                ptr_loads = ptr_loads_copy;
                if (num_elements == nullptr) continue;
                llvm::Value* local_copy = builder->CreateAlloca(
                    el_type, num_elements, name);
                llvm::Value* total_bytes = builder->CreateMul(
                    num_elements,
                    llvm::ConstantInt::get(i64_ty, el_size_bytes));
                builder->CreateMemCpy(
                    local_copy, llvm::MaybeAlign(),
                    current_ptr, llvm::MaybeAlign(),
                    total_bytes);
                llvm_symtab[h] = local_copy;
            }
        }
    }

    template <typename T>
    void declare_local_vars(const T &x) {
        declare_vars(x);
    }

    void visit_Function(const ASR::Function_t &x) {
        llvm::DIScope* debug_current_scope_copy = debug_current_scope;
        loop_head.clear();
        loop_head_names.clear();
        loop_or_block_end.clear();
        loop_or_block_end_names.clear();
        call_arg_alloca_pool.clear();
        call_arg_alloca_idx.clear();
        convert_call_args_depth = 0;
        list_call_arg_to_finalize.clear();
        strings_to_be_deallocated.reserve(al, 1);
        heap_fixed_size_arrays.reserve(al, 1);
        SymbolTable* current_scope_copy = current_scope;
        current_scope = x.m_symtab;
        bool is_dict_present_copy_lp = dict_api_lp->is_dict_present();
        bool is_dict_present_copy_sc = dict_api_sc->is_dict_present();
        dict_api_lp->set_is_dict_present(false);
        dict_api_sc->set_is_dict_present(false);
        bool is_set_present_copy_lp = set_api_lp->is_set_present();
        bool is_set_present_copy_sc = set_api_sc->is_set_present();
        set_api_lp->set_is_set_present(false);
        set_api_sc->set_is_set_present(false);
        llvm_goto_targets.clear();
        instantiate_function(x);
        if (ASRUtils::get_FunctionType(x)->m_deftype == ASR::deftypeType::Interface) {
            // Interface does not have an implementation and it is already
            // declared, so there is nothing to do here
            if (compiler_options.emit_debug_info) debug_current_scope = debug_current_scope_copy;
            dict_api_lp->set_is_dict_present(is_dict_present_copy_lp);
            dict_api_sc->set_is_dict_present(is_dict_present_copy_sc);
            set_api_lp->set_is_set_present(is_set_present_copy_lp);
            set_api_sc->set_is_set_present(is_set_present_copy_sc);
            current_scope = current_scope_copy;
            return;
        }
        visit_procedures(x);
        generate_function(x);
        parent_function = nullptr;
        dict_api_lp->set_is_dict_present(is_dict_present_copy_lp);
        dict_api_sc->set_is_dict_present(is_dict_present_copy_sc);
        set_api_lp->set_is_set_present(is_set_present_copy_lp);
        set_api_sc->set_is_set_present(is_set_present_copy_sc);

        // Finalize the debug info.
        if (compiler_options.emit_debug_info) DBuilder->finalize();
        if (compiler_options.emit_debug_info) debug_current_scope = debug_current_scope_copy;
        current_scope = current_scope_copy;
        loop_head.clear();
        loop_head_names.clear();
        loop_or_block_end.clear();
        loop_or_block_end_names.clear();
        strings_to_be_deallocated.reserve(al, 1);
        heap_fixed_size_arrays.reserve(al, 1);
    }

    void instantiate_function(const ASR::Function_t &x){
        llvm::DIScope* debug_current_scope_copy = debug_current_scope;
        uint32_t h = get_hash((ASR::asr_t*)&x);
        llvm::Function *F = nullptr;
        std::string sym_name = x.m_name;
        if (sym_name == "main") {
            sym_name = "_xx_lcompilers_changed_main_xx";
        }
        if (llvm_symtab_fn.find(h) != llvm_symtab_fn.end()) {
            /*
            throw CodeGenError("Function code already generated for '"
                + std::string(x.m_name) + "'");
            */
            F = llvm_symtab_fn[h];
        } else {
            llvm::FunctionType* function_type = llvm_utils->get_function_type(x, module.get());
            if( ASRUtils::get_FunctionType(x)->m_deftype == ASR::deftypeType::Interface && !ASRUtils::get_FunctionType(x)->m_module ) {
                // Skip pre-declaration only for submodule interfaces with class(*) args,
                // not for program-scoped interfaces (where m_module is also false).
                bool parent_is_program = false;
                if (x.m_symtab->parent && x.m_symtab->parent->asr_owner &&
                        x.m_symtab->parent->asr_owner->type == ASR::asrType::symbol) {
                    parent_is_program = ASR::is_a<ASR::Program_t>(
                        *ASR::down_cast<ASR::symbol_t>(x.m_symtab->parent->asr_owner));
                }
                if (!parent_is_program) {
                    ASR::FunctionType_t* asr_function_type = ASRUtils::get_FunctionType(x);
                    for( size_t i = 0; i < asr_function_type->n_arg_types; i++ ) {
                        if( ASRUtils::is_class_type(asr_function_type->m_arg_types[i]) ) {
                            if (compiler_options.emit_debug_info) {
                                debug_current_scope = debug_current_scope_copy;
                            }
                            return ;
                        }
                    }
                }
            }
            // Compute the mangled function name using centralized logic
            ASR::FunctionType_t *ftype = ASRUtils::get_FunctionType(x);
            std::string fn_name = compute_llvm_function_name(
                sym_name, ftype, compiler_options, mangle_prefix, parent_function
            );
            if (llvm_symtab_fn_names.find(fn_name) == llvm_symtab_fn_names.end()) {
                llvm_symtab_fn_names[fn_name] = h;
                F = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, fn_name, module.get());
                // Compiler-generated helper functions (e.g.
                // _lcompiler_optimization_exponent_*) are emitted identically
                // in every compilation unit that uses them.  Use
                // LinkOnceODRLinkage so the linker keeps only one copy
                // instead of erroring with "multiple definition".
                if (fn_name.rfind("_lcompiler_", 0) == 0) {
                    F->setLinkage(llvm::Function::LinkOnceODRLinkage);
                }
            } else {
                uint32_t old_h = llvm_symtab_fn_names[fn_name];
                F = llvm_symtab_fn[old_h];
            }
            llvm_symtab_fn[h] = F;

            // Instantiate (pre-declare) all nested interfaces
            for (auto &item : x.m_symtab->get_scope()) {
                if (is_a<ASR::Function_t>(*item.second)) {
                    const ASR::Function_t* parent_function_copy = parent_function;
                    parent_function = &x;
                    ASR::Function_t *v = down_cast<ASR::Function_t>(
                            item.second);
                    // check if item.second is present in x.m_args
                    bool interface_as_arg = false;
                    for (size_t i=0; i<x.n_args; i++) {
                        if (is_a<ASR::Var_t>(*x.m_args[i])) {
                            ASR::Var_t *arg = down_cast<ASR::Var_t>(x.m_args[i]);
                            if ( arg->m_v == item.second ) {
                                interface_as_arg = true;
                                llvm::FunctionType* fntype = llvm_utils->get_function_type(*v, module.get());
                                llvm::Function* fn = llvm::Function::Create(fntype, llvm::Function::ExternalLinkage, v->m_name, module.get());
                                uint32_t hash = get_hash((ASR::asr_t*)v);
                                llvm_symtab_fn[hash] = fn;
                            }
                        }
                    }
                    if (!interface_as_arg) {
                        instantiate_function(*v);
                    }
                    parent_function = parent_function_copy;
                } else if ( ASR::is_a<ASR::Variable_t>(*ASRUtils::symbol_get_past_external(item.second)) && is_function_variable(ASRUtils::symbol_get_past_external(item.second)) ) {
                    ASR::Variable_t *v = down_cast<ASR::Variable_t>(ASRUtils::symbol_get_past_external(item.second));
                    bool interface_as_arg = false;
                    for (size_t i=0; i<x.n_args; i++) {
                        if (is_a<ASR::Var_t>(*x.m_args[i])) {
                            ASR::Var_t *arg = down_cast<ASR::Var_t>(x.m_args[i]);
                            if ( arg->m_v == item.second ) {
                                interface_as_arg = true;
                            }
                        }
                    }
                    if ( interface_as_arg ) {
                        continue;
                    }
                    ASR::Function_t *var = ASR::down_cast<ASR::Function_t>(
                            ASRUtils::symbol_get_past_external(v->m_type_declaration));
                    uint32_t h = get_hash((ASR::asr_t*)v);
                    if (llvm_symtab_fn.find(h) != llvm_symtab_fn.end()) {
                        continue;
                    }
                    llvm::FunctionType* function_type = llvm_utils->get_function_type(*var, module.get());
                    std::string fn_name;
                    std::string sym_name = v->m_name;
                    if (ASRUtils::get_FunctionType(*var)->m_abi == ASR::abiType::BindC) {
                        if (ASRUtils::get_FunctionType(*var)->m_bindc_name &&
                            ASRUtils::get_FunctionType(*var)->m_bindc_name[0] != '\0') {
                            fn_name = ASRUtils::get_FunctionType(*var)->m_bindc_name;
                        } else if (!ASRUtils::get_FunctionType(*var)->m_bindc_name) {
                            fn_name = sym_name;
                        } else {
                            fn_name = mangle_prefix + sym_name;
                        }
                    } else if (ASRUtils::get_FunctionType(*var)->m_deftype == ASR::deftypeType::Interface &&
                        ASRUtils::get_FunctionType(*var)->m_abi != ASR::abiType::Intrinsic) {
                        fn_name = sym_name;
                    } else {
                        fn_name = mangle_prefix + sym_name;
                    }
                    if (llvm_symtab_fn_names.find(fn_name) == llvm_symtab_fn_names.end()) {
                        llvm_symtab_fn_names[fn_name] = h;
                        llvm::Function* F = llvm::Function::Create(function_type,
                            llvm::Function::ExternalLinkage, fn_name, module.get());
                        llvm_symtab_fn[h] = F;
                    } else {
                        uint32_t old_h = llvm_symtab_fn_names[fn_name];
                        llvm_symtab_fn[h] = llvm_symtab_fn[old_h];
                    }
                }
            }
        }
        if (compiler_options.emit_debug_info) {
            debug_current_scope = debug_current_scope_copy;
        }
    }

    inline void define_function_entry(const ASR::Function_t& x) {
        uint32_t h = get_hash((ASR::asr_t*)&x);
        parent_function = &x;
        llvm::Function* F = llvm_symtab_fn[h];
        llvm_goto_targets.clear();
        current_sret_arg = nullptr;
        // Clear alloca pool for each new function to avoid cross-function references
        call_arg_alloca_pool.clear();
        call_arg_alloca_idx.clear();
        convert_call_args_depth = 0;
        list_call_arg_to_finalize.clear();
        bindc_stride_exits.clear();
        if (compiler_options.po.fast && !compiler_options.po.no_fast_math) {
            F->addFnAttr("no-nans-fp-math", "true");
            F->addFnAttr("no-infs-fp-math", "true");
            F->addFnAttr("no-signed-zeros-fp-math", "true");
            F->addFnAttr("unsafe-fp-math", "true");
        }
        if (compiler_options.emit_debug_info) {
            llvm::DISubprogram *SP = nullptr;
            debug_emit_function(x, SP);
            F->setSubprogram(SP);
            debug_current_scope = SP;
        }
        proc_return = llvm::BasicBlock::Create(context, "return");
        llvm::BasicBlock *BB = llvm::BasicBlock::Create(context,
                ".entry", F);
        builder->SetInsertPoint(BB);
        if (compiler_options.emit_debug_info) debug_emit_loc(x);
        declare_args(x, *F);

        // For bind(C) functions, convert incoming CFI descriptor parameters
        // to internal descriptor layout (which has an extra offset field
        // and uses element strides instead of byte strides).
        if (ASRUtils::get_FunctionType(x)->m_abi == ASR::abiType::BindC) {
            for (size_t i = 0; i < x.n_args; i++) {
                ASR::symbol_t *s = symbol_get_past_external(
                    ASR::down_cast<ASR::Var_t>(x.m_args[i])->m_v);
                if (!ASR::is_a<ASR::Variable_t>(*s)) continue;
                ASR::Variable_t *arg = ASR::down_cast<ASR::Variable_t>(s);
                ASR::ttype_t* arg_type = arg->m_type;
                if (!ASRUtils::is_array(arg_type)) continue;
                ASR::array_physical_typeType phys = ASRUtils::extract_physical_type(arg_type);
                if (phys != ASR::array_physical_typeType::DescriptorArray &&
                    phys != ASR::array_physical_typeType::AssumedRankArray) continue;

                uint32_t h = get_hash((ASR::asr_t*)arg);
                llvm::Value* sym_val = llvm_symtab[h];

                ASR::ttype_t* elem_asr_type = ASRUtils::type_get_past_allocatable(
                    ASRUtils::type_get_past_pointer(arg_type));
                // Skip assumed-type (type(*)) / polymorphic (class(*))
                // — element type is unknown at compile time
                if (ASR::is_a<ASR::TypeParameter_t>(
                        *ASRUtils::type_get_past_array(elem_asr_type))) continue;
                llvm::Type* elem_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    ASRUtils::get_expr_from_sym(al, s),
                    ASRUtils::extract_type(elem_asr_type), module.get());
                int n_dims = ASRUtils::extract_n_dims_from_ttype(arg_type);
                llvm::DataLayout data_layout(module->getDataLayout());
                uint64_t elem_size = data_layout.getTypeAllocSize(elem_llvm_type);
                // Get the cached internal descriptor type
                llvm::Type* desc_type = llvm_utils->get_type_from_ttype_t_util(
                    ASRUtils::get_expr_from_sym(al, s),
                    ASRUtils::type_get_past_allocatable_pointer(arg_type),
                    module.get());

                if (ASRUtils::is_allocatable(arg_type) ||
                    ASRUtils::is_pointer(arg_type)) {
                    // sym_val is a double pointer (alloca holding descriptor ptr).
                    // Load the descriptor pointer (typed as internal struct*),
                    // cfi_to_internal() will bitcast it to CFI type.
                    llvm::Value* cfi_ptr = builder->CreateLoad(
                        desc_type->getPointerTo(), sym_val);
                    llvm::Value* internal_desc = arr_descr->cfi_to_internal(
                        desc_type, elem_llvm_type, cfi_ptr, n_dims, elem_size);
                    builder->CreateStore(internal_desc, sym_val);
                    // For intent(out/inout), schedule writeback at function exit
                    ASR::intentType intent = arg->m_intent;
                    if (intent == ASRUtils::intent_out ||
                        intent == ASRUtils::intent_inout) {
                        int8_t type_code = get_cfi_type_code(elem_asr_type);
                        int cfi_attr = ASRUtils::is_allocatable(arg_type) ? 2 : 1;
                        bindc_stride_exits.push_back({sym_val, desc_type,
                            elem_size, n_dims, cfi_ptr, elem_llvm_type,
                            type_code, cfi_attr});
                    }
                } else {
                    // sym_val is the CFI descriptor pointer directly.
                    // For optional parameters, the pointer may be NULL
                    // (not present). Skip conversion if NULL — the body
                    // must check present() before accessing the array.
                    bool is_optional = (arg->m_presence == ASR::presenceType::Optional);
                    if (is_optional) {
                        llvm::Value* is_null = builder->CreateICmpEQ(sym_val,
                            llvm::ConstantPointerNull::get(
                                llvm::cast<llvm::PointerType>(sym_val->getType())));
                        llvm::BasicBlock* entry_bb = builder->GetInsertBlock();
                        llvm::Function* fn = entry_bb->getParent();
                        llvm::BasicBlock* convert_bb = llvm::BasicBlock::Create(
                            context, "cfi_convert", fn);
                        llvm::BasicBlock* merge_bb = llvm::BasicBlock::Create(
                            context, "cfi_merge", fn);
                        builder->CreateCondBr(is_null, merge_bb, convert_bb);
                        builder->SetInsertPoint(convert_bb);
                        llvm::Value* internal_desc = arr_descr->cfi_to_internal(
                            desc_type, elem_llvm_type, sym_val, n_dims, elem_size);
                        builder->CreateBr(merge_bb);
                        builder->SetInsertPoint(merge_bb);
                        llvm::PHINode* phi = builder->CreatePHI(
                            sym_val->getType(), 2);
                        phi->addIncoming(sym_val, entry_bb);
                        phi->addIncoming(internal_desc, convert_bb);
                        llvm_symtab[h] = phi;
                    } else {
                        llvm::Value* internal_desc = arr_descr->cfi_to_internal(
                            desc_type, elem_llvm_type, sym_val, n_dims, elem_size);
                        llvm_symtab[h] = internal_desc;
                    }
                }
            }
        }

        for( auto& sym: x.m_symtab->get_scope() ) {
            if( !ASR::is_a<ASR::Variable_t>(*sym.second) ) {
                continue ;
            }
            ASR::intentType symbol_intent = ASRUtils::symbol_intent(
                ASRUtils::symbol_get_past_external(sym.second));
            if( !(symbol_intent == ASRUtils::intent_in ||
                 symbol_intent == ASRUtils::intent_inout ||
                 symbol_intent == ASRUtils::intent_out ||
                 symbol_intent == ASRUtils::intent_unspecified) ) {
                continue;
            }
            ASR::ttype_t* symbol_type = ASRUtils::symbol_type(sym.second);
            // Reinitialize StructType members if intent is intent_out
            if (ASR::is_a<ASR::StructType_t>(*symbol_type) && symbol_intent == ASRUtils::intent_out) {
                uint32_t h = get_hash((ASR::asr_t*)sym.second);
                LCOMPILERS_ASSERT(llvm_symtab.find(h) != llvm_symtab.end());
                llvm::Value* st_desc = llvm_symtab[h];

                ASR::Struct_t* struct_sym = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(
                        ASR::down_cast<ASR::Variable_t>(sym.second)->m_type_declaration));

                if (ASRUtils::is_class_type(symbol_type)) {
                    allocate_array_members_of_struct(struct_sym, st_desc, ASR::down_cast<ASR::Variable_t>(sym.second)->m_type, true);
                } else {
                    allocate_array_members_of_struct(struct_sym, st_desc, ASR::down_cast<ASR::Variable_t>(sym.second)->m_type, true);
                }
            }
            if (ASRUtils::is_string_only(symbol_type) && ASRUtils::is_allocatable(symbol_type)) {                                                                  
                ASR::String_t* str_t = ASRUtils::get_string_type(symbol_type);                                             
                if (str_t->m_len_kind == ASR::ExpressionLength && str_t->m_len) {                                      
                    uint32_t h = get_hash((ASR::asr_t*)sym.second);                                                   
                    LCOMPILERS_ASSERT(llvm_symtab.find(h) != llvm_symtab.end());                                           
                    llvm::Value* str_desc = llvm_symtab[h];                                                         
                    setup_string_length(str_desc, str_t, str_t->m_len);                                     
                }                                                                                            
            }                                                                                                              
            if( !(ASRUtils::is_pointer(symbol_type) || ASRUtils::is_allocatable(symbol_type)) &&
                ASRUtils::is_array(symbol_type) &&
                ASRUtils::extract_physical_type(symbol_type)
                    == ASR::array_physical_typeType::DescriptorArray ) {
                ASR::Variable_t* v = ASR::down_cast<ASR::Variable_t>(sym.second);
                bool is_optional = (v->m_presence == ASR::presenceType::Optional);
                llvm::Type* desc_array_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::get_expr_from_sym(al, sym.second),
                        ASRUtils::type_get_past_allocatable_pointer(symbol_type),
                        module.get());
                llvm::Value* array_desc = arr_descr->create_descriptor_alloca(
                    desc_array_type,
                    "array_descriptor_local");
                uint32_t h = get_hash((ASR::asr_t*)sym.second);
                LCOMPILERS_ASSERT(llvm_symtab.find(h) != llvm_symtab.end());
                llvm::Value* arg_array_desc = llvm_symtab[h];

                // For optional parameters, guard descriptor copy with
                // a null check to avoid crashing on absent (NULL) args.
                llvm::BasicBlock *copy_bb = nullptr, *merge_bb = nullptr;
                if (is_optional) {
                    copy_bb = llvm::BasicBlock::Create(context, "opt_present");
                    merge_bb = llvm::BasicBlock::Create(context, "opt_merge");
                    llvm::Value* is_null = builder->CreateICmpEQ(
                        arg_array_desc,
                        llvm::ConstantPointerNull::get(
                            llvm::cast<llvm::PointerType>(arg_array_desc->getType())));
                    llvm::Function* fn = builder->GetInsertBlock()->getParent();
                    llvm_fn_insert_bb(fn, copy_bb);
                    llvm_fn_insert_bb(fn, merge_bb);
                    // If null, store null data pointer and jump to merge
                    llvm::BasicBlock* absent_bb = llvm::BasicBlock::Create(
                        context, "opt_absent", fn);
                    builder->CreateCondBr(is_null, absent_bb, copy_bb);
                    builder->SetInsertPoint(absent_bb);
                    // Store null base_addr so present() returns false
                    llvm::Type* data_ptr_type = llvm_utils->get_el_type(
                        ASRUtils::get_expr_from_sym(al, sym.second),
                        ASRUtils::extract_type(symbol_type), module.get())->getPointerTo();
                    builder->CreateStore(
                        llvm::ConstantPointerNull::get(
                            llvm::cast<llvm::PointerType>(data_ptr_type)),
                        arr_descr->get_pointer_to_data(desc_array_type, array_desc));
                    builder->CreateBr(merge_bb);
                    builder->SetInsertPoint(copy_bb);
                }

                // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                llvm::Type *data_type = llvm_utils->get_el_type(ASRUtils::get_expr_from_sym(al, sym.second),
                    ASRUtils::extract_type(symbol_type), module.get());
                builder->CreateStore(llvm_utils->create_ptr_gep2(data_type,
                llvm_utils->CreateLoad2(data_type->getPointerTo(), arr_descr->get_pointer_to_data(desc_array_type, arg_array_desc)),
                    arr_descr->get_offset(desc_array_type, arg_array_desc)), arr_descr->get_pointer_to_data(desc_array_type, array_desc));
                ASR::dimension_t* m_dims = nullptr;
                int n_dims = ASRUtils::extract_dimensions_from_ttype(symbol_type, m_dims);
                Vec<llvm::Value*> lbs, lengths;
                lbs.reserve(al, n_dims);
                lengths.reserve(al, n_dims);
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = 2;
                for( int i = 0; i < n_dims; i++ ) {
                    if( m_dims[i].m_start ) {
                        visit_expr_wrapper(m_dims[i].m_start);
                        lbs.push_back(al, tmp);
                    } else {
                        lbs.push_back(al, nullptr);
                    }
                    if( m_dims[i].m_length ) {
                        visit_expr_wrapper(m_dims[i].m_length);
                        lengths.push_back(al, tmp);
                    } else {
                        lengths.push_back(al, nullptr);
                    }
                }
                arr_descr->reset_array_details(
                    desc_array_type, array_desc, arg_array_desc, lbs.p, lengths.p, n_dims);
                ptr_loads = ptr_loads_copy;
                // Note: For bind(C) functions, the CFI→internal conversion
                // (including byte→element stride conversion) is already
                // handled in the prologue. No additional conversion needed.
                if (is_optional) {
                    builder->CreateBr(merge_bb);
                    builder->SetInsertPoint(merge_bb);
                }
                llvm_symtab[h] = array_desc;
            }
            // Note: For bind(C) functions, the CFI→internal conversion
            // (including byte→element stride conversion) is already handled
            // in the prologue above. No additional stride conversion needed.
        }
        declare_local_vars(x);
    }

    inline void free_strings_to_be_deallocated(size_t start_n) {
        for (size_t i = start_n; i < strings_to_be_deallocated.n; i++) {
            llvm::Value* str_desc = strings_to_be_deallocated[i];
            llvm::Value* str_data_ptr = llvm_utils->create_gep2(llvm_utils->string_descriptor, str_desc, 0);
            llvm::Value* str_data = llvm_utils->CreateLoad2(llvm_utils->character_type->getPointerTo(), str_data_ptr);
            llvm_utils->lfortran_free_nocheck(str_data);
        }
        strings_to_be_deallocated.n = start_n;
    }

    struct DeallocateStringsScope {
        ASRToLLVMVisitor* visitor;
        size_t start_n;
        DeallocateStringsScope(ASRToLLVMVisitor* visitor)
            : visitor(visitor), start_n(visitor->strings_to_be_deallocated.n) {}
        ~DeallocateStringsScope() {
            visitor->free_strings_to_be_deallocated(start_n);
        }
    };

    inline void free_heap_fixed_size_arrays() {
        // Free all heap-allocated large fixed-size arrays
        for (size_t i = 0; i < heap_fixed_size_arrays.n; i++) {
            llvm_utils->lfortran_free(heap_fixed_size_arrays[i]);
        }
        heap_fixed_size_arrays.n = 0;
    }

    inline void convert_bindc_strides_to_byte() {
        for (auto& info : bindc_stride_exits) {
            // Convert the internal descriptor back to CFI layout
            // and write to the original CFI descriptor pointer.
            llvm::Value* internal_ptr = llvm_utils->CreateLoad2(
                info.desc_array_type->getPointerTo(), info.desc_ptr_ptr);
            llvm::Value* cfi_result = arr_descr->internal_to_cfi(
                info.desc_array_type, internal_ptr, info.el_type,
                info.n_dims, info.el_size, info.type_code, info.cfi_attr);
            // Copy CFI data back to the original CFI descriptor
            llvm::StructType* cfi_type = arr_descr->get_cfi_type(info.el_type, info.n_dims);
            llvm::DataLayout dl(module->getDataLayout());
            uint64_t cfi_size = dl.getTypeAllocSize(cfi_type);
            llvm::Value* orig_cfi_i8 = builder->CreateBitCast(
                info.original_cfi_ptr,
                llvm::Type::getInt8Ty(context)->getPointerTo());
            llvm::Value* new_cfi_i8 = builder->CreateBitCast(
                cfi_result,
                llvm::Type::getInt8Ty(context)->getPointerTo());
            builder->CreateMemCpy(orig_cfi_i8, llvm::MaybeAlign(),
                new_cfi_i8, llvm::MaybeAlign(), cfi_size);
        }
    }

    inline void define_function_exit(const ASR::Function_t& x) {
        if (compiler_options.emit_debug_info) debug_emit_loc(x);
        if (x.m_return_var) {
            start_new_block(proc_return);
            llvm_symtab_finalizer.finalize_symtab(x.m_symtab);
            finalize_list_call_arg_allocas();
            free_heap_fixed_size_arrays();
            convert_bindc_strides_to_byte();
            llvm::Function* fn = builder->GetInsertBlock()->getParent();
            if (fn->getReturnType()->isVoidTy()) {
                // On Windows, complex(kind=8) returns use the "pass-as-subroutine" ABI:
                // write the return value into the hidden first argument.
                ASR::ttype_t* ret_type = ASRUtils::expr_type(x.m_return_var);
                if (compiler_options.platform == Platform::Windows &&
                    ASR::is_a<ASR::Complex_t>(*ret_type) &&
                    ASRUtils::extract_kind_from_ttype_t(ret_type) == 8) {
                    LCOMPILERS_ASSERT(current_sret_arg != nullptr);
                    ASR::Variable_t *asr_retval = EXPR2VAR(x.m_return_var);
                    uint32_t h = get_hash((ASR::asr_t*)asr_retval);
                    llvm::Value *ret_val_ptr = llvm_symtab[h];
                    llvm::Value *ret_val = llvm_utils->CreateLoad2(complex_type_8, ret_val_ptr);
                    builder->CreateStore(ret_val, current_sret_arg);
                }
                builder->CreateRetVoid();
                return;
            }
            ASR::Variable_t *asr_retval = EXPR2VAR(x.m_return_var);
            uint32_t h = get_hash((ASR::asr_t*)asr_retval);
            llvm::Value *ret_val = llvm_symtab[h];
            llvm::Value *ret_val2 = ret_val;
            if (!ASRUtils::is_class_type(ASRUtils::extract_type(asr_retval->m_type)) &&
                !(ASR::is_a<ASR::Pointer_t>(*asr_retval->m_type) && ASR::is_a<ASR::String_t>(*ASRUtils::extract_type(asr_retval->m_type))) ) {
                llvm::Type* asr_retval_llvm_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                    al, asr_retval->base.base.loc, &asr_retval->base)), asr_retval->m_type, module.get());
                ret_val2 = llvm_utils->CreateLoad2(asr_retval_llvm_type, ret_val);
            }
            // Handle Complex type return value - convert to platform ABI:
            {
                ASR::ttype_t* arg_type = asr_retval->m_type;
                llvm::Value *tmp = ret_val;
                if (is_a<ASR::Complex_t>(*arg_type)) {
                    int c_kind = ASRUtils::extract_kind_from_ttype_t(arg_type);
                    llvm::Type* complex_val_type = llvm_utils->get_type_from_ttype_t_util(x.m_return_var,
                        arg_type,
                        module.get());
                    if (c_kind == 4) {
                        if (compiler_options.platform == Platform::Windows) {
                            // tmp is {float, float}*
                            // type_fx2p is i64*
                            llvm::Type* type_fx2p = llvm::Type::getInt64Ty(context)->getPointerTo();
                            // Convert {float,float}* to i64* using bitcast
                            tmp = builder->CreateBitCast(tmp, type_fx2p);
                            // Then convert i64* -> i64
                            tmp = llvm_utils->CreateLoad2(llvm::Type::getInt64Ty(context), tmp);
                        } else if (compiler_options.platform == Platform::macOS_ARM) {
                            // Pass by value

                            tmp = llvm_utils->CreateLoad2(complex_val_type, tmp);
                        } else {
                            // tmp is {float, float}*
                            // type_fx2 is <2 x float>
                            llvm::Type* type_fx2 = FIXED_VECTOR_TYPE::get(llvm::Type::getFloatTy(context), 2);
                            // Convert {float,float}* to <2 x float>* using bitcast
                            tmp = builder->CreateBitCast(tmp, type_fx2->getPointerTo());
                            // Then convert <2 x float>* -> <2 x float>
                            tmp = llvm_utils->CreateLoad2(type_fx2, tmp);
                        }
                    } else {
                        LCOMPILERS_ASSERT(c_kind == 8)
                        if (compiler_options.platform == Platform::Windows) {
                            // 128 bit aggregate type is passed by reference
                        } else {
                            // Pass by value
                            tmp = llvm_utils->CreateLoad2(complex_val_type, tmp);
                        }
                    }
                ret_val2 = tmp;
                }
            }
            builder->CreateRet(ret_val2);
        } else {
            start_new_block(proc_return);
            llvm_symtab_finalizer.finalize_symtab(x.m_symtab);
            finalize_list_call_arg_allocas();
            free_heap_fixed_size_arrays();
            convert_bindc_strides_to_byte();
            builder->CreateRetVoid();
        }
    }

    /* Case: We don't know type of obj at compile time. 
    We use this runtime function from runtime vptr to deep copy properly
    subroutine describe(obj)
        class(base_t), intent(in) :: obj
        class(base_t), allocatable :: obj_tmp
        obj_tmp = obj
    end subroutine
    Example: `class_65.f90`
    */

    void generate_function(const ASR::Function_t &x) {
        bool interactive = (ASRUtils::get_FunctionType(x)->m_abi == ASR::abiType::ExternalUndefined);
        if (ASRUtils::get_FunctionType(x)->m_deftype == ASR::deftypeType::Implementation ) {

            if (interactive) return;

            if (!prototype_only) {
                define_function_entry(x);
                predeclare_goto_targets(builder->GetInsertBlock()->getParent(), x.m_body, x.n_body);

                for (size_t i=0; i<x.n_body; i++) {
                    this->visit_stmt(*x.m_body[i]);
                }
                
                define_function_exit(x);
            }
        } else if( ASRUtils::get_FunctionType(x)->m_abi == ASR::abiType::Intrinsic &&
                   ASRUtils::get_FunctionType(x)->m_deftype == ASR::deftypeType::Interface ) {
            std::string m_name = x.m_name;
            if( m_name == "lbound" || m_name == "ubound" ) {
                define_function_entry(x);

                // Defines the size intrinsic's body at LLVM level.
                ASR::Variable_t *arg = EXPR2VAR(x.m_args[0]);
                uint32_t h = get_hash((ASR::asr_t*)arg);
                llvm::Value* llvm_arg1 = llvm_symtab[h];
                llvm::Type* llvm_arg1_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[0], arg->m_type, module.get());

                arg = EXPR2VAR(x.m_args[1]);
                h = get_hash((ASR::asr_t*)arg);
                llvm::Value* llvm_arg2 = llvm_symtab[h];
                llvm::Type* llvm_arg2_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[1], arg->m_type, module.get());

                ASR::Variable_t *ret = EXPR2VAR(x.m_return_var);
                h = get_hash((ASR::asr_t*)ret);
                llvm::Value* llvm_ret_ptr = llvm_symtab[h];

                llvm::Value* dim_des_val = llvm_utils->CreateLoad2(llvm_arg1_type, llvm_arg1);
                llvm::Value* dim_val = llvm_utils->CreateLoad2(llvm_arg2_type, llvm_arg2);
                llvm::Value* const_1 = llvm::ConstantInt::get(context, llvm::APInt(32, 1));
                dim_val = builder->CreateSub(dim_val, const_1);
                llvm::Value* dim_struct = arr_descr->get_pointer_to_dimension_descriptor(dim_des_val, dim_val);
                llvm::Value* res = nullptr;
                if( m_name == "lbound" ) {
                    res = arr_descr->get_lower_bound(dim_struct);
                } else if( m_name == "ubound" ) {
                    res = arr_descr->get_upper_bound(dim_struct);
                }
                builder->CreateStore(res, llvm_ret_ptr);

                define_function_exit(x);
            }
        }
    }


    template<typename T>
    void visit_procedures(const T &x) {
        for (auto &item : x.m_symtab->get_scope()) {
            if (is_a<ASR::Function_t>(*item.second)) {
                ASR::Function_t *s = ASR::down_cast<ASR::Function_t>(item.second);
                visit_Function(*s);
            }
        }
    }

    bool is_nested_pointer(llvm::Value* val) {
        // TODO: Remove this in future
        // Related issue, https://github.com/lcompilers/lpython/pull/707#issuecomment-1169773106.
        return val->getType()->isPointerTy() &&
               val->getType()->getContainedType(0)->isPointerTy();
    }

    void visit_CLoc(const ASR::CLoc_t& x) {
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_arg);
        ptr_loads = ptr_loads_copy;
        if( is_nested_pointer(tmp) ) {
            llvm::Type* nested_type = llvm_utils->get_type_from_ttype_t_util(x.m_arg, ASRUtils::expr_type(x.m_arg), module.get());
            tmp = llvm_utils->CreateLoad2(nested_type, tmp);
        }
        ASR::ttype_t* arg_type = ASRUtils::get_contained_type(ASRUtils::expr_type(x.m_arg));
        if( ASRUtils::is_array(arg_type) ) {
            llvm::Type* elem_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::get_contained_type(arg_type), nullptr, module.get());
            tmp = llvm_utils->CreateLoad2(elem_type->getPointerTo(), arr_descr->get_pointer_to_data(elem_type, tmp));
        }
        tmp = builder->CreateBitCast(tmp,
                    llvm::Type::getVoidTy(context)->getPointerTo());
    }


    llvm::Value* GetPointerCPtrUtil(llvm::Value* llvm_tmp, ASR::expr_t* asr_expr) {
        // If the input is a simple variable and not a pointer
        // then this check will fail and load will not happen
        // (which is what we want for simple variables).
        // For pointers, the actual LLVM variable will be a
        // double pointer, so we need to load one time and then
        // use it later on.
        ASR::ttype_t* asr_type = ASRUtils::expr_type(asr_expr);
        if((ASR::is_a<ASR::Pointer_t>(*asr_type) || ASR::is_a<ASR::Allocatable_t>(*asr_type)) &&
            (LLVM::is_llvm_pointer(*ASRUtils::type_get_past_allocatable_pointer(asr_type))
             || ASR::is_a<ASR::Array_t>(*ASRUtils::type_get_past_allocatable_pointer(asr_type))
             || ASR::is_a<ASR::Pointer_t>(*asr_type))) {
            ASR::ttype_t* contained_type = ASRUtils::type_get_past_allocatable_pointer(asr_type);
            llvm::Type* llvm_ty = llvm_utils->get_type_from_ttype_t_util(asr_expr, contained_type, module.get());
            // FunctionType already maps to a pointer type (fntype*) in LLVM,
            // so we don't need getPointerTo() for it.
            llvm::Type* load_type = ASR::is_a<ASR::FunctionType_t>(*contained_type)
                ? llvm_ty : llvm_ty->getPointerTo();
            llvm_tmp = llvm_utils->CreateLoad2(load_type, llvm_tmp);
        }
        asr_type = ASRUtils::get_contained_type(asr_type);

        if( ASRUtils::is_array(asr_type) &&
            !ASR::is_a<ASR::CPtr_t>(*asr_type) ) {
            ASR::array_physical_typeType physical_type = ASRUtils::extract_physical_type(asr_type);
            llvm::Type *el_type = llvm_utils->get_el_type(
                asr_expr, ASRUtils::extract_type(asr_type), module.get());
            switch( physical_type ) {
                case ASR::array_physical_typeType::DescriptorArray: {
                    llvm_tmp = llvm_utils->CreateLoad2(el_type->getPointerTo(), arr_descr->get_pointer_to_data(llvm_utils->get_type_from_ttype_t_util(asr_expr, asr_type, module.get()), llvm_tmp));
                    break;
                }
                case ASR::array_physical_typeType::FixedSizeArray: {
                    llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(asr_expr, asr_type, module.get());
                    llvm_tmp = llvm_utils->create_gep2(type, llvm_tmp, 0);
                    break;
                }
                case ASR::array_physical_typeType::UnboundedPointerArray:
                case ASR::array_physical_typeType::PointerArray: {
                    break;
                }
                default: {
                    LCOMPILERS_ASSERT(false);
                }
            }
        }

        // // TODO: refactor this into a function, it is being used a few times
        // llvm::Type *target_type = llvm_tmp->getType();
        // // Create alloca to get a pointer, but do it
        // // at the beginning of the function to avoid
        // // using alloca inside a loop, which would
        // // run out of stack
        // llvm::BasicBlock &entry_block = builder->GetInsertBlock()->getParent()->getEntryBlock();
        // llvm::IRBuilder<> builder0(context);
        // builder0.SetInsertPoint(&entry_block, entry_block.getFirstInsertionPt());
        // llvm::AllocaInst *target = builder0.CreateAlloca(
        //     target_type, nullptr, "call_arg_value_ptr");
        // builder->CreateStore(llvm_tmp, target);
        // llvm_tmp = target;
        return llvm_tmp;
    }

    void visit_GetPointer(const ASR::GetPointer_t& x) {
        if (ASR::is_a<ASR::ComplexRe_t>(*x.m_arg) ||
                ASR::is_a<ASR::ComplexIm_t>(*x.m_arg)) {
            ASR::expr_t* complex_arg = ASR::is_a<ASR::ComplexRe_t>(*x.m_arg)
                ? ASR::down_cast<ASR::ComplexRe_t>(x.m_arg)->m_arg
                : ASR::down_cast<ASR::ComplexIm_t>(x.m_arg)->m_arg;
            unsigned int member_idx = ASR::is_a<ASR::ComplexRe_t>(*x.m_arg) ? 0 : 1;
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr(*complex_arg);
            ptr_loads = ptr_loads_copy;
            ASR::ttype_t* complex_type_asr = ASRUtils::expr_type(complex_arg);
            int kind = ASRUtils::extract_kind_from_ttype_t(complex_type_asr);
            llvm::Type* ctype = (kind == 4) ? complex_type_4 : complex_type_8;
            tmp = llvm_utils->create_gep2(ctype, tmp, (int)member_idx);
            return;
        }
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_arg);
        ptr_loads = ptr_loads_copy;
        tmp = GetPointerCPtrUtil(tmp, x.m_arg);
    }

    void visit_PointerToCPtr(const ASR::PointerToCPtr_t& x) {
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_arg);
        ptr_loads = ptr_loads_copy;
        if( !ASR::is_a<ASR::GetPointer_t>(*x.m_arg) ) {
            tmp = GetPointerCPtrUtil(tmp, x.m_arg);
        } else if(ASRUtils::is_character(*expr_type(x.m_arg))){ // Targetted physicalType is `char*`
            tmp = llvm_utils->get_string_data(ASRUtils::get_string_type(x.m_arg), tmp);
        }
        tmp = builder->CreateBitCast(tmp,
                    llvm::Type::getVoidTy(context)->getPointerTo());
    }


    void visit_CPtrToPointer(const ASR::CPtrToPointer_t& x) {
        ASR::expr_t *cptr = x.m_cptr, *fptr = x.m_ptr, *shape = x.m_shape;
        int reduce_loads = 0;
        if( ASR::is_a<ASR::Var_t>(*cptr) ) {
            ASR::Variable_t* cptr_var = ASRUtils::EXPR2VAR(cptr);
            reduce_loads = cptr_var->m_intent == ASRUtils::intent_in;
        }
        if( ASRUtils::is_array(ASRUtils::expr_type(fptr)) ) {
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 1 - reduce_loads;
            this->visit_expr(*cptr);
            llvm::Value* llvm_cptr = tmp;
            if (ASR::is_a<ASR::StructInstanceMember_t>(*cptr)) {
                // `type(c_ptr)` requires an extra load here
                // TODO: be more explicit about ptr_loads: https://github.com/lfortran/lfortran/issues/4245
                llvm::Type* llvm_cptr_type = llvm_utils->get_type_from_ttype_t_util(cptr, ASRUtils::expr_type(cptr), module.get());
                llvm_cptr = llvm_utils->CreateLoad2(llvm_cptr_type, llvm_cptr);
            }
            ptr_loads = 0;
            this->visit_expr(*fptr);
            llvm::Value* llvm_fptr = tmp;
            ptr_loads = ptr_loads_copy;
            llvm::Value* llvm_shape = nullptr;
            ASR::ttype_t* asr_shape_type = nullptr;
            if( shape ) {
                asr_shape_type = ASRUtils::get_contained_type(ASRUtils::expr_type(shape));
                this->visit_expr(*shape);
                llvm_shape = tmp;
            }
            ASR::ttype_t* fptr_type = ASRUtils::expr_type(fptr);
            llvm::Type* llvm_fptr_type = llvm_utils->get_type_from_ttype_t_util(fptr,
                ASRUtils::get_contained_type(fptr_type), module.get());
            llvm_fptr = llvm_utils->CreateLoad2(llvm_fptr_type->getPointerTo(), llvm_fptr);
            llvm::Type* idx_type = arr_descr->get_index_type();
            unsigned idx_bit_width = idx_type->getIntegerBitWidth();
            builder->CreateStore(llvm::ConstantInt::get(context, llvm::APInt(idx_bit_width, 0)),
                arr_descr->get_offset(llvm_fptr_type, llvm_fptr, false));
            ASR::dimension_t* fptr_dims;
            int fptr_rank = ASRUtils::extract_dimensions_from_ttype(
                                ASRUtils::expr_type(fptr),
                                fptr_dims);
            ASR::ttype_t* fptr_data_type = ASRUtils::duplicate_type_without_dims(al, ASRUtils::get_contained_type(fptr_type), fptr_type->base.loc);
            llvm::Type* llvm_fptr_data_type = nullptr;
            if (ASRUtils::is_descriptorString(fptr_data_type)) {
                llvm_fptr_data_type = llvm_utils->get_type_from_ttype_t_util(fptr, fptr_data_type, module.get());
            } else {
                // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                llvm_fptr_data_type = llvm_utils->get_el_type(
                    fptr, ASRUtils::extract_type(fptr_type), module.get());
            }
            llvm::Value* fptr_data = arr_descr->get_pointer_to_data(llvm_fptr_type, llvm_fptr);
            llvm::Value* fptr_des = arr_descr->get_pointer_to_dimension_descriptor_array(llvm_fptr_type, llvm_fptr);
            llvm::Value* shape_data = llvm_shape;
            if( llvm_shape && (ASRUtils::extract_physical_type(asr_shape_type) ==
                ASR::array_physical_typeType::DescriptorArray) ) {
                ASR::ttype_t* shape_elem_asr_type = ASRUtils::type_get_past_pointer(ASRUtils::expr_type(shape));
                llvm::Type* shape_elem_llvm_type = llvm_utils->get_type_from_ttype_t_util( nullptr, shape_elem_asr_type, module.get());
                llvm::Type* shape_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    nullptr, ASRUtils::expr_type(shape), module.get());
                shape_data = llvm_utils->CreateLoad2(shape_elem_llvm_type, arr_descr->get_pointer_to_data(shape_llvm_type, llvm_shape));
            }
            if( llvm_shape && (ASRUtils::extract_physical_type(asr_shape_type) ==
                ASR::array_physical_typeType::FixedSizeArray) ) {
                llvm::Type* shape_scalar_type = llvm_utils->get_type_from_ttype_t_util(
                    nullptr, ASRUtils::extract_type(ASRUtils::expr_type(shape)), module.get());
                shape_data = builder->CreateBitCast(shape_data, shape_scalar_type->getPointerTo());
            }

            if (ASRUtils::is_descriptorString(fptr_data_type)) {
                fptr_data = llvm_utils->CreateLoad2(llvm_fptr_data_type->getPointerTo(), fptr_data);
                fptr_data = llvm_utils->create_gep2(llvm_fptr_data_type, fptr_data, 0);
                llvm_cptr = builder->CreateBitCast(llvm_cptr, llvm_utils->i8_ptr);
            } else {
                llvm_cptr = builder->CreateBitCast(llvm_cptr, llvm_fptr_data_type->getPointerTo());
            }
            builder->CreateStore(llvm_cptr, fptr_data);

            llvm::Value* prod = llvm::ConstantInt::get(context, llvm::APInt(idx_bit_width, 1));
            for( int i = 0; i < fptr_rank; i++ ) {
                llvm::Value* curr_dim = llvm::ConstantInt::get(context, llvm::APInt(32, i));
                llvm::Value* desi = arr_descr->get_pointer_to_dimension_descriptor(fptr_des, curr_dim);
                llvm::Value* desi_stride = arr_descr->get_stride(desi, false);
                llvm::Value* desi_lb = arr_descr->get_lower_bound(desi, false);
                llvm::Value* desi_size = arr_descr->get_dimension_size(fptr_des, curr_dim, false);
                builder->CreateStore(prod, desi_stride);
                llvm::Value* idx_one = llvm::ConstantInt::get(context, llvm::APInt(idx_bit_width, 1));
                llvm::Value* new_lb = idx_one;
                if( x.m_lower_bounds ) {
                    int ptr_loads_copy = ptr_loads;
                    ptr_loads = 2;
                    this->visit_expr_wrapper(x.m_lower_bounds, true);
                    ptr_loads = ptr_loads_copy;
                    llvm::Type* lower_bound_type = llvm_utils->get_type_from_ttype_t_util(
                        nullptr, ASRUtils::extract_type(ASRUtils::expr_type(x.m_lower_bounds)),
                        module.get());
                    tmp = llvm_utils->create_ptr_gep2(lower_bound_type, tmp, i);
                    new_lb = builder->CreateSExtOrTrunc(
                        llvm_utils->CreateLoad2(lower_bound_type, tmp), idx_type);
                }
                llvm::Value* new_size = nullptr;
                if( ASRUtils::extract_physical_type(asr_shape_type) == ASR::array_physical_typeType::DescriptorArray ||
                    ASRUtils::extract_physical_type(asr_shape_type) == ASR::array_physical_typeType::PointerArray ) {
                    llvm::Type* shape_int_type = llvm_utils->get_type_from_ttype_t_util(
                        nullptr, ASRUtils::extract_type(ASRUtils::expr_type(shape)), module.get());
                    new_size = shape_data ? builder->CreateSExtOrTrunc(llvm_utils->CreateLoad2(
                        shape_int_type, llvm_utils->create_ptr_gep2(shape_int_type, shape_data, i)), idx_type) : idx_one;
                } else if( ASRUtils::extract_physical_type(asr_shape_type) == ASR::array_physical_typeType::FixedSizeArray ) {
                    llvm::Type* shape_scalar_type = llvm_utils->get_type_from_ttype_t_util(
                        nullptr, ASRUtils::extract_type(ASRUtils::expr_type(shape)), module.get());
                    new_size = shape_data ? builder->CreateSExtOrTrunc(llvm_utils->CreateLoad2(
                        shape_scalar_type, llvm_utils->create_ptr_gep2(shape_scalar_type, shape_data, i)), idx_type) : idx_one;
                }
                builder->CreateStore(new_lb, desi_lb);
                builder->CreateStore(new_size, desi_size);
                prod = builder->CreateMul(prod, new_size);
            }
        } else {
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 1 - reduce_loads;
            this->visit_expr(*cptr);
            llvm::Value* llvm_cptr = tmp;
            if (ASR::is_a<ASR::StructInstanceMember_t>(*cptr)) {
                // Load the actual pointer value for type(c_ptr) members
                llvm::Type* cptr_llvm_type = llvm::Type::getInt8Ty(context)->getPointerTo(); // void*
                llvm_cptr = llvm_utils->CreateLoad2(cptr_llvm_type, llvm_cptr);
            } else if (ASR::is_a<ASR::ArrayItem_t>(*cptr)) {
                llvm::Type* cptr_llvm_type = llvm_utils->get_type_from_ttype_t_util(cptr, ASRUtils::expr_type(cptr), module.get());
                llvm_cptr = llvm_utils->CreateLoad2(cptr_llvm_type, llvm_cptr);
            }
            this->visit_expr_load_wrapper(fptr, 0);
            llvm::Value*  const llvm_fptr = tmp;
            ASR::ttype_t* const fptr_type = ASRUtils::expr_type(fptr);
            if (ASRUtils::is_string_only(fptr_type)) {
                llvm::Value* const cptr_to_charPTR = builder->CreateBitCast(llvm_cptr, llvm_utils->character_type);
                ASR::String_t* str_type = ASRUtils::get_string_type(fptr_type);
                llvm::Value* const charPTR_ref = llvm_utils->get_string_data(str_type, llvm_fptr, true);
                builder->CreateStore(cptr_to_charPTR, charPTR_ref);
                setup_string_length(llvm_fptr, str_type, str_type->m_len);
            } else {
                ASR::ttype_t* fptr_asr_type = ASRUtils::expr_type(fptr);
                ASR::ttype_t* fptr_contained = ASRUtils::type_get_past_pointer(fptr_asr_type);
                bool is_proc_ptr = ASR::is_a<ASR::FunctionType_t>(*fptr_contained);
                llvm::Type* llvm_fptr_elem_type =llvm_utils->get_type_from_ttype_t_util(fptr, fptr_contained, module.get());
                if (is_proc_ptr) {
                    // procedure pointer cast to function pointer
                    llvm_cptr = builder->CreateBitCast(llvm_cptr,llvm_fptr_elem_type);
                    builder->CreateStore(llvm_cptr, llvm_fptr);
                } else {
                    llvm_cptr = builder->CreateBitCast(llvm_cptr,llvm_fptr_elem_type->getPointerTo());
                    builder->CreateStore(llvm_cptr, llvm_fptr);
                }
            }
            ptr_loads = ptr_loads_copy;
            tmp = nullptr;
        }
    }

    void visit_PointerAssociated(const ASR::PointerAssociated_t& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        llvm::AllocaInst *res = llvm_utils->CreateAlloca(
            llvm::Type::getInt1Ty(context), nullptr, "is_associated");
        ASR::ttype_t* p_type = ASRUtils::expr_type(x.m_ptr);
        llvm::Value *ptr, *nptr;
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        visit_expr_wrapper(x.m_ptr, false);
        ptr = tmp;
        if(ASRUtils::is_character(*p_type)){ // String OR array of strings
            if (ASRUtils::is_array_of_strings(p_type) &&
                LLVM::is_llvm_pointer(*p_type)) {
                // For pointer/allocatable arrays of strings the storage is a
                // pointer to the array descriptor pointer; load once so that
                // get_stringArray_data sees `array_descriptor*`.
                llvm::Type* p_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_ptr, p_type, module.get());
                ptr = llvm_utils->CreateLoad2(p_llvm_type, ptr);
            }
            ptr = ASRUtils::is_array_of_strings(p_type) ?
                llvm_utils->get_stringArray_data(p_type, ptr) :
                llvm_utils->get_string_data(ASRUtils::get_string_type(p_type), ptr);
        } else if (!ASR::is_a<ASR::PointerNullConstant_t>(*x.m_ptr)) {
            llvm::Type* p_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_ptr, p_type, module.get());
            bool load_cptr = true;
            if (ASR::is_a<ASR::CPtr_t>(*p_type) && ASR::is_a<ASR::Var_t>(*x.m_ptr)) {
                ASR::Variable_t* p_var = ASRUtils::EXPR2VAR(x.m_ptr);
                load_cptr = !is_cptr_dummy_passed_by_value(p_var);
            }
            if (load_cptr) {
                ptr = llvm_utils->CreateLoad2(p_llvm_type, ptr);
            }
        }
        if( ASRUtils::is_array(p_type) &&
            !ASRUtils::is_array_of_strings(p_type) &&
            ASRUtils::extract_physical_type(p_type) ==
            ASR::array_physical_typeType::DescriptorArray) {
            LCOMPILERS_ASSERT(ASR::is_a<ASR::Pointer_t>(*p_type));
            ptr = arr_descr->get_pointer_to_data(x.m_ptr,
                ASRUtils::type_get_past_allocatable_pointer(p_type), ptr, module.get());
            llvm::Type* array_inner_type = llvm_utils->get_type_from_ttype_t_util(x.m_ptr,
                ASRUtils::extract_type(p_type), module.get());
            ptr = llvm_utils->CreateLoad2(array_inner_type->getPointerTo(), ptr);
            if (ASRUtils::is_unlimited_polymorphic_type(x.m_ptr)) { // {VTable*, i8*} -- Check equality on data field
                ptr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr,
                    llvm_utils->create_gep2(array_inner_type, ptr, 1));
            }
        }
        ptr_loads = ptr_loads_copy;
        auto to_int64 = [&](llvm::Value* v) {
            if (v->getType()->isStructTy()) {
                v = builder->CreateExtractValue(v, {0});
            }
            if (v->getType()->isPointerTy()) {
                return builder->CreatePtrToInt(v, llvm_utils->getIntType(8, false));
            } else {
                return builder->CreateIntCast(v, llvm_utils->getIntType(8, false), false);
            }
        };

        if( ASR::is_a<ASR::CPtr_t>(*ASRUtils::expr_type(x.m_ptr)) &&
            x.m_tgt && ASR::is_a<ASR::CPtr_t>(*ASRUtils::expr_type(x.m_tgt)) ) {
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_tgt, true);
            ptr_loads = ptr_loads_copy;
            llvm::Value* tgt = tmp;
            bool load_tgt = false;
            if (ASR::is_a<ASR::Var_t>(*x.m_tgt)) {
                ASR::Variable_t* t_var = ASRUtils::EXPR2VAR(x.m_tgt);
                load_tgt = !is_cptr_dummy_passed_by_value(t_var);
            }
            if (load_tgt) {
                llvm::Type* t_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_tgt, ASRUtils::expr_type(x.m_tgt), module.get());
                tgt = llvm_utils->CreateLoad2(t_llvm_type, tgt);
            }
            tmp = builder->CreateICmpEQ(to_int64(ptr), to_int64(tgt));
            return ;
        }
        llvm_utils->create_if_else(builder->CreateICmpEQ(
            to_int64(ptr),
            llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), llvm::APInt(64, 0))),
        [&]() {
            builder->CreateStore(
                llvm::ConstantInt::get(llvm::Type::getInt1Ty(context), llvm::APInt(1, 0)),
                res);
        },
        [&]() {
            if (x.m_tgt) {
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = 0;
                ASR::ttype_t* tgt_type = ASRUtils::expr_type(x.m_tgt);
                bool tgt_is_array_or_member =
                    (ASR::is_a<ASR::ArrayItem_t>(*x.m_tgt) ||
                     ASR::is_a<ASR::StructInstanceMember_t>(*x.m_tgt)) &&
                    !ASRUtils::is_array(tgt_type) &&
                    !ASR::is_a<ASR::Pointer_t>(*tgt_type);
                if (tgt_is_array_or_member) {
                    this->visit_expr(*x.m_tgt);
                } else {
                    this->visit_expr_wrapper(x.m_tgt, true);
                }
                ptr_loads = ptr_loads_copy;
                // ASR::Variable_t *t = EXPR2VAR(x.m_tgt);
                // uint32_t t_h = get_hash((ASR::asr_t*)t);
                // nptr = llvm_symtab[t_h];
                nptr = tmp;
                // Load the pointer value for simple Var targets.
                // StructInstanceMember/ArrayItem targets are already
                // loaded by visit_expr_wrapper with load_ref=true.
                // For class types with new_classes, visit_expr_wrapper
                // already loaded the struct pointer, so skip the
                // extra Pointer_t load to avoid reading the vtable.
                if( !ASRUtils::is_array(tgt_type) &&
                    ASR::is_a<ASR::Pointer_t>(*tgt_type) &&
                    ASR::is_a<ASR::Var_t>(*x.m_tgt) &&
                    !ASRUtils::is_class_type(ASRUtils::extract_type(tgt_type)) ) {
                    llvm::Type* tgt_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_tgt, tgt_type, module.get());
                    nptr = llvm_utils->CreateLoad2(tgt_llvm_type, nptr);
                }
                if( ASRUtils::is_array(ASRUtils::expr_type(x.m_tgt)) ) {
                    ASR::array_physical_typeType tgt_ptype = ASRUtils::extract_physical_type(
                        ASRUtils::expr_type(x.m_tgt));
                    llvm::Type* array_type = llvm_utils->get_type_from_ttype_t_util(x.m_tgt,
                        ASRUtils::expr_type(x.m_tgt), module.get());
                    if( tgt_ptype == ASR::array_physical_typeType::FixedSizeArray ) {
                        nptr = llvm_utils->create_gep2(array_type, nptr, 0);
                    } else if( tgt_ptype == ASR::array_physical_typeType::DescriptorArray ) {
                        llvm::Type *array_inner_type = llvm_utils->get_type_from_ttype_t_util(x.m_tgt,
                            ASRUtils::extract_type(tgt_type), module.get());
                        if (ASRUtils::is_unlimited_polymorphic_type(x.m_tgt)) {
                            // For class(*) targets, visit_expr_wrapper already
                            // loaded the descriptor pointer, so nptr is at
                            // descriptor level — use same pattern as ptr path.
                            nptr = arr_descr->get_pointer_to_data(x.m_tgt,
                                ASRUtils::type_get_past_allocatable_pointer(tgt_type), nptr, module.get());
                            nptr = llvm_utils->CreateLoad2(array_inner_type->getPointerTo(), nptr);
                            nptr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr,
                                llvm_utils->create_gep2(array_inner_type, nptr, 1)); // {VTable*, i8*} -- Check equality on data field
                        } else {
                            llvm::Type* array_type_desc = llvm_utils->get_type_from_ttype_t_util(x.m_tgt,
                                ASRUtils::expr_type(x.m_tgt), module.get());
                            nptr = builder->CreateLoad(array_type_desc, nptr);
                            nptr = llvm_utils->CreateLoad2(array_inner_type->getPointerTo(),
                                arr_descr->get_pointer_to_data(x.m_tgt,
                                    ASRUtils::expr_type(x.m_tgt), nptr, module.get()));
                        }
                    }
                }
                if (!ASRUtils::is_array(p_type) &&
                    ASRUtils::is_class_type(ASRUtils::extract_type(p_type))) {
                    llvm::Type* p_class_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_ptr, ASRUtils::extract_type(p_type),
                        module.get());
                    ptr = llvm_utils->create_gep2(p_class_type, ptr, 1);
                    ptr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, ptr);
                }
                if (!ASRUtils::is_array(tgt_type) &&
                    ASRUtils::is_class_type(ASRUtils::extract_type(tgt_type))) {
                    llvm::Type* tgt_class_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_tgt, ASRUtils::extract_type(tgt_type),
                        module.get());
                    nptr = llvm_utils->create_gep2(tgt_class_type, nptr, 1);
                    nptr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, nptr);
                }
                builder->CreateStore(builder->CreateICmpEQ(to_int64(ptr), to_int64(nptr)), res);
            } else {
                llvm::Value* zero = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), llvm::APInt(64, 0));
                builder->CreateStore(builder->CreateICmpNE(to_int64(ptr), zero), res);
            }
        });
        tmp = llvm_utils->CreateLoad2(llvm::Type::getInt1Ty(context), res);
    }

    // Check if the expression is a component array: an array formed by
    // accessing a scalar struct member across an array of structs
    // e.g., token%first where token is array of structs and first is scalar member
    bool is_component_array_expr(ASR::expr_t* expr) {
        if (!expr) {
            return false;
        }
        // Look past ArrayPhysicalCast
        if (ASR::is_a<ASR::ArrayPhysicalCast_t>(*expr)) {
            expr = ASR::down_cast<ASR::ArrayPhysicalCast_t>(expr)->m_arg;
        }
        if (!ASR::is_a<ASR::StructInstanceMember_t>(*expr)) {
            return false;
        }
        ASR::StructInstanceMember_t* sim = ASR::down_cast<ASR::StructInstanceMember_t>(expr);
        if (!ASRUtils::is_array(sim->m_type)) {
            return false;
        }
        ASR::ttype_t* base_type = ASRUtils::type_get_past_pointer(
            ASRUtils::type_get_past_allocatable(ASRUtils::expr_type(sim->m_v)));
        ASR::ttype_t* member_type = ASRUtils::symbol_type(sim->m_m);
        member_type = ASRUtils::type_get_past_pointer(
            ASRUtils::type_get_past_allocatable(member_type));
        return ASRUtils::is_array(base_type) && !ASRUtils::is_array(member_type);
    }

    // Handle associate statement where the value is a component array
    // e.g., associate(first => token%first)
    // This creates a descriptor with proper stride to access struct members
    void handle_component_array_association(const ASR::Associate_t& x) {
        ASR::expr_t* value_expr = x.m_value;
        // Look past ArrayPhysicalCast
        if (ASR::is_a<ASR::ArrayPhysicalCast_t>(*value_expr)) {
            value_expr = ASR::down_cast<ASR::ArrayPhysicalCast_t>(value_expr)->m_arg;
        }
        ASR::StructInstanceMember_t* sim = ASR::down_cast<ASR::StructInstanceMember_t>(value_expr);
        ASR::Variable_t* member = ASR::down_cast<ASR::Variable_t>(
            ASRUtils::symbol_get_past_external(sim->m_m));

        // Get the base struct array
        ASR::ttype_t* base_type = ASRUtils::expr_type(sim->m_v);
        ASR::ttype_t* base_struct_array_type = ASRUtils::type_get_past_pointer(
            ASRUtils::type_get_past_allocatable(base_type));
        ASR::ttype_t* struct_elem_type = ASRUtils::type_get_past_array(base_struct_array_type);

        // Get the target and value types
        ASR::ttype_t* target_type = ASRUtils::expr_type(x.m_target);
        ASR::ttype_t* component_elem_type = ASRUtils::type_get_past_array(
            ASRUtils::type_get_past_pointer(target_type));

        // Visit target (the pointer variable)
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        visit_expr(*x.m_target);
        llvm::Value* target_ptr = tmp;
        ptr_loads = ptr_loads_copy;

        // Visit the base struct array
        ptr_loads_copy = ptr_loads;
        ptr_loads = 1 - !LLVM::is_llvm_pointer(*base_type);
        visit_expr(*sim->m_v);
        llvm::Value* base_array_desc = tmp;
        ptr_loads = ptr_loads_copy;

        // Get LLVM types
        llvm::Type* target_desc_type = llvm_utils->get_type_from_ttype_t_util(
            x.m_target, ASRUtils::type_get_past_pointer(target_type), module.get());
        llvm::Type* base_array_llvm_type = llvm_utils->get_type_from_ttype_t_util(
            sim->m_v, base_struct_array_type, module.get());
        llvm::Type* struct_llvm_type = llvm_utils->get_type_from_ttype_t_util(
            sim->m_v, struct_elem_type, module.get());
        llvm::Type* component_llvm_type = llvm_utils->get_type_from_ttype_t_util(
            x.m_target, component_elem_type, module.get());

        // Create a new descriptor for the component array
        llvm::Value* new_desc = arr_descr->create_descriptor_alloca(
            target_desc_type, "component_array_desc");

        // Get the struct symbol name for member index lookup
        ASR::symbol_t* struct_sym = ASRUtils::get_struct_sym_from_struct_expr(sim->m_v);
        std::string struct_name = get_type_key(struct_sym);
        std::string member_name = std::string(member->m_name);

        // Find the member index in the struct
        int member_idx = 0;
        std::string current_type_name = struct_name;
        while (name2memidx[current_type_name].find(member_name) == name2memidx[current_type_name].end()) {
            if (dertype2parent.find(current_type_name) == dertype2parent.end()) {
                throw CodeGenError(current_type_name + " doesn't have member " + member_name,
                                  x.base.base.loc);
            }
            current_type_name = dertype2parent[current_type_name];
        }
        member_idx = name2memidx[current_type_name][member_name];

        // Get the base array data pointer (pointer to first struct)
        ASR::array_physical_typeType base_ptype = ASRUtils::extract_physical_type(base_struct_array_type);
        llvm::Value* base_data_ptr = nullptr;

        if (base_ptype == ASR::array_physical_typeType::DescriptorArray) {
            // If sim->m_v is itself a struct member access of an
            // allocatable/pointer array (e.g. v%n where n is allocatable),
            // the visited value is a pointer-to-pointer-to-descriptor
            // because the array descriptor for an allocatable struct
            // component is stored as a pointer inside the parent struct.
            // Dereference once so base_array_desc is %descriptor*.
            if (LLVM::is_llvm_pointer(*base_type) &&
                ASR::is_a<ASR::StructInstanceMember_t>(*sim->m_v)) {
                base_array_desc = llvm_utils->CreateLoad2(
                    base_array_llvm_type->getPointerTo(), base_array_desc);
            }
            base_data_ptr = arr_descr->get_pointer_to_data(
                sim->m_v, base_struct_array_type, base_array_desc, module.get());
            base_data_ptr = llvm_utils->CreateLoad2(struct_llvm_type->getPointerTo(), base_data_ptr);
        } else if (base_ptype == ASR::array_physical_typeType::FixedSizeArray) {
            base_data_ptr = llvm_utils->create_gep2(base_array_llvm_type, base_array_desc, 0);
        } else {
            base_data_ptr = base_array_desc;
        }

        // Get pointer to the first struct's member
        llvm::Value* first_member_ptr = llvm_utils->create_gep2(
            struct_llvm_type, base_data_ptr, member_idx);

        // Store data pointer (pointing to first element's member)
        builder->CreateStore(first_member_ptr,
            arr_descr->get_pointer_to_data(target_desc_type, new_desc));

        // Get rank from base array
        int n_dims = ASRUtils::extract_n_dims_from_ttype(base_struct_array_type);

        LCOMPILERS_ASSERT(n_dims == 1 );

        // Compute the stride multiplier: sizeof(struct) / sizeof(component)
        // This is the number of component-sized elements between consecutive struct elements
        llvm::DataLayout data_layout(module->getDataLayout());
        uint64_t struct_size = data_layout.getTypeAllocSize(struct_llvm_type);
        uint64_t component_size = data_layout.getTypeAllocSize(component_llvm_type);
    
        LCOMPILERS_ASSERT(component_size > 0);

        uint64_t stride_multiplier = struct_size / component_size;

        if (base_ptype == ASR::array_physical_typeType::DescriptorArray) {
            // Initialize descriptor using the base descriptor, then adjust strides
            arr_descr->reset_array_details(target_desc_type, new_desc, base_array_llvm_type,
                base_array_desc, n_dims);
            llvm::Value* dim_des_array = arr_descr->get_pointer_to_dimension_descriptor_array(
                target_desc_type, new_desc);
            for (int i = 0; i < n_dims; i++) {
                llvm::Value* dim_idx = llvm::ConstantInt::get(context, llvm::APInt(32, i));
                llvm::Value* target_dim_des = arr_descr->get_pointer_to_dimension_descriptor(
                    dim_des_array, dim_idx);
                llvm::Value* base_stride = arr_descr->get_stride(target_dim_des, true);
                llvm::Value* new_stride = builder->CreateMul(
                    base_stride,
                    llvm::ConstantInt::get(arr_descr->get_index_type(), stride_multiplier));
                builder->CreateStore(new_stride, arr_descr->get_stride(target_dim_des, false));
            }
        } else {
            // Set offset to 0
            unsigned index_bit_width = arr_descr->get_index_type()->getIntegerBitWidth();
            builder->CreateStore(
                llvm::ConstantInt::get(context, llvm::APInt(index_bit_width, 0)),
                arr_descr->get_offset(target_desc_type, new_desc, false));

            // Set rank
            arr_descr->set_rank(target_desc_type, new_desc,
                llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), n_dims));

            // Initialize inlined dimension descriptors
            arr_descr->fill_dimension_descriptor(target_desc_type, new_desc, n_dims);
            llvm::Value* dim_des_array = arr_descr->get_pointer_to_dimension_descriptor_array(
                target_desc_type, new_desc);

            // Copy dimension info from base array with adjusted stride
            for (int i = 0; i < n_dims; i++) {
                llvm::Value* dim_idx = llvm::ConstantInt::get(context, llvm::APInt(32, i));
                llvm::Value* target_dim_des = arr_descr->get_pointer_to_dimension_descriptor(
                    dim_des_array, dim_idx);

                // For fixed-size arrays, extract dims from the type
                ASR::dimension_t* dims = nullptr;
                [[maybe_unused]] int rank = ASRUtils::extract_dimensions_from_ttype(base_struct_array_type, dims);
                LCOMPILERS_ASSERT(rank > 0 && i < rank);

                // Lower bound (default 1)
                llvm::Value* lb = llvm::ConstantInt::get(arr_descr->get_index_type(), 1);
                if (dims[i].m_start) {
                    visit_expr_wrapper(dims[i].m_start, true);
                    lb = builder->CreateSExtOrTrunc(tmp, arr_descr->get_index_type());
                }
                builder->CreateStore(lb, arr_descr->get_lower_bound(target_dim_des, false));

                // Dimension size
                llvm::Value* dim_size = llvm::ConstantInt::get(arr_descr->get_index_type(), 1);
                if (dims[i].m_length) {
                    visit_expr_wrapper(dims[i].m_length, true);
                    dim_size = builder->CreateSExtOrTrunc(tmp, arr_descr->get_index_type());
                }
                builder->CreateStore(dim_size, arr_descr->get_dimension_size(target_dim_des, false));

                // Stride for first dimension is stride_multiplier, for subsequent it's cumulative
                llvm::Value* stride = llvm::ConstantInt::get(
                    arr_descr->get_index_type(), stride_multiplier);
                if (i > 0) {
                    // Get previous dimension's stride and size
                    llvm::Value* prev_dim_idx = llvm::ConstantInt::get(context, llvm::APInt(32, i - 1));
                    llvm::Value* prev_dim_des = arr_descr->get_pointer_to_dimension_descriptor(
                        dim_des_array, prev_dim_idx);
                    llvm::Value* prev_stride = arr_descr->get_stride(prev_dim_des, true);
                    llvm::Value* prev_size = arr_descr->get_dimension_size(prev_dim_des, true);
                    stride = builder->CreateMul(prev_stride, prev_size);
                }
                builder->CreateStore(stride, arr_descr->get_stride(target_dim_des, false));
            }
        }

        // Store the new descriptor to the target pointer
        builder->CreateStore(new_desc, target_ptr);
    }

    void handle_pointer_section_target(const ASR::Associate_t& x) {
        ASR::ArraySection_t* target_section = ASR::down_cast<ASR::ArraySection_t>(x.m_target);
        
        // Get the base pointer variable from the target section
        ASR::Variable_t* ptr_var = ASRUtils::EXPR2VAR(target_section->m_v);
        ASR::ttype_t* value_type = ASRUtils::expr_type(x.m_value);
        
        // Get the llvm values
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        visit_expr(*target_section->m_v);
        llvm::Value* target_desc = tmp;
        
        ptr_loads = 1 - !LLVM::is_llvm_pointer(*value_type);
        visit_expr(*x.m_value);
        llvm::Value* value_desc = tmp;
        ptr_loads = ptr_loads_copy;
        
        
        llvm::Type* value_el_type = llvm_utils->get_el_type(x.m_value,
            ASRUtils::extract_type(value_type), module.get());
        
        // Create a new descriptor for the pointer with the specified bounds
        ASR::ttype_t* target_ptr_type = ASRUtils::expr_type(target_section->m_v);
        ASR::ttype_t* desc_type = ASRUtils::duplicate_type_with_empty_dims(al,
            ASRUtils::type_get_past_allocatable(
                ASRUtils::type_get_past_pointer(target_ptr_type)),
             ASR::array_physical_typeType::DescriptorArray, true);
        llvm::Type* target_type_llvm = llvm_utils->get_type_from_ttype_t_util(
            target_section->m_v, desc_type, module.get());
        
        // Extract bounds from the target section
        int target_rank = 0;
        Vec<llvm::Value*> lbs; lbs.reserve(al, target_section->n_args);
        Vec<llvm::Value*> ubs; ubs.reserve(al, target_section->n_args);
        Vec<llvm::Value*> target_strides; target_strides.reserve(al, target_section->n_args);
        
        for (size_t i = 0; i < target_section->n_args; i++) {
            ASR::array_index_t& idx = target_section->m_args[i];
            // For pointer section, we have left:right bounds
            if (idx.m_left != nullptr) {
                visit_expr_wrapper(idx.m_left, true);
                lbs.push_back(al, tmp);

                // Detect a buggy/unsupplied upper bound that resolves to a
                // bound of the target pointer itself (which is unassociated
                // at this point). In that case (e.g. `p(lb:) => a`), derive
                // the upper bound from the value's size in that dimension:
                // ub = lb + size(value, dim) - 1.
                bool right_refers_to_target = false;
                if (idx.m_right != nullptr && ASR::is_a<ASR::ArrayBound_t>(*idx.m_right)) {
                    ASR::ArrayBound_t* ab = ASR::down_cast<ASR::ArrayBound_t>(idx.m_right);
                    if (ASR::is_a<ASR::Var_t>(*ab->m_v) &&
                        ASR::is_a<ASR::Var_t>(*target_section->m_v) &&
                        ASR::down_cast<ASR::Var_t>(ab->m_v)->m_v ==
                            ASR::down_cast<ASR::Var_t>(target_section->m_v)->m_v) {
                        right_refers_to_target = true;
                    }
                }

                if (idx.m_right != nullptr && !right_refers_to_target) {
                    visit_expr_wrapper(idx.m_right, true);
                    ubs.push_back(al, tmp);
                } else if (right_refers_to_target) {
                    // Compute ub = lb + size(value, dim) - 1 using ASR ArraySize.
                    ASR::ttype_t* int_type = ASRUtils::TYPE(
                        ASR::make_Integer_t(al, x.base.base.loc, 4));
                    ASR::expr_t* dim_expr = ASRUtils::EXPR(
                        ASR::make_IntegerConstant_t(al, x.base.base.loc,
                            (int64_t)(i + 1), int_type));
                    ASR::expr_t* size_expr = ASRUtils::EXPR(
                        ASR::make_ArraySize_t(al, x.base.base.loc, x.m_value,
                            dim_expr, int_type, nullptr));
                    visit_expr_wrapper(size_expr, true);
                    llvm::Value* size_val = tmp;

                    if (ASR::is_a<ASR::ArraySection_t>(*x.m_value)) {
                        ASR::ArraySection_t* value_section = ASR::down_cast<ASR::ArraySection_t>(x.m_value);
                        ASR::array_index_t& vidx = value_section->m_args[i];
                        
                        llvm::Type* value_desc_type = llvm_utils->get_type_from_ttype_t_util(x.m_value,
                            ASRUtils::type_get_past_allocatable(
                            ASRUtils::type_get_past_pointer(ASRUtils::expr_type(value_section->m_v))), module.get());
                        llvm::Value* loaded_desc = value_desc;
                        if (ASRUtils::is_allocatable(ASRUtils::expr_type(value_section->m_v)) || ASRUtils::is_pointer(ASRUtils::expr_type(value_section->m_v))) {
                            loaded_desc = llvm_utils->CreateLoad2(value_desc_type->getPointerTo(), value_desc);
                        }
                        llvm::Value* src_dim_des_arr = arr_descr->get_pointer_to_dimension_descriptor_array(
                            value_desc_type, loaded_desc);
                        llvm::Type* idx_type = arr_descr->get_index_type();
                        unsigned idx_bits = idx_type->getIntegerBitWidth();
                        llvm::Value* dim_des = arr_descr->get_pointer_to_dimension_descriptor(
                            src_dim_des_arr, llvm::ConstantInt::get(context, llvm::APInt(idx_bits, i)));
                        
                        llvm::Value* step_val = nullptr;
                        if (vidx.m_step != nullptr) {
                            visit_expr_wrapper(vidx.m_step, true);
                            step_val = tmp;
                        } else {
                            step_val = llvm::ConstantInt::get(idx_type, 1);
                        }
                        step_val = builder->CreateSExtOrTrunc(step_val, idx_type);
                        
                        llvm::Value* base_stride = arr_descr->get_stride(dim_des, true);
                        target_strides.push_back(al, builder->CreateMul(base_stride, step_val));
                    }
                    size_val = builder->CreateSExtOrTrunc(size_val, lbs.p[i]->getType());
                    llvm::Value* one = llvm::ConstantInt::get(lbs.p[i]->getType(), 1);
                    llvm::Value* ub_val = builder->CreateSub(
                        builder->CreateAdd(lbs.p[i], size_val), one);
                    ubs.push_back(al, ub_val);
                } else {
                    // Use left as both bounds for single element
                    ubs.push_back(al, lbs.p[i]);
                }
                target_rank++;
            } else if (idx.m_right != nullptr) {
                // Single index - treat as 1:right
                llvm::Type* idx_type = arr_descr->get_index_type();
                unsigned idx_bits = idx_type->getIntegerBitWidth();
                lbs.push_back(al, llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 1)));
                visit_expr_wrapper(idx.m_right, true);
                ubs.push_back(al, tmp);
                target_rank++;
            }
        }
        
        // For local pointer variables, reuse the existing descriptor
        // or stack-allocate a new one. This avoids heap leaks since
        // pointer variables have no automatic deallocation at scope exit.
        // For pointer dummy arguments, always reuse the caller-provided
        // descriptor so we don't allocate and leak a new descriptor per call.
        // For other non-local pointers, heap-allocate so the descriptor
        // survives after the function returns.
        llvm::Value* new_desc;
        bool target_is_local = (ptr_var->m_intent == ASR::intentType::Local);
        bool target_is_dummy = ASRUtils::is_arg_dummy(ptr_var->m_intent);
        if (target_is_local || target_is_dummy) {
            llvm::Value* current_desc_ptr = llvm_utils->CreateLoad2(
                target_type_llvm->getPointerTo(), target_desc);
            if (target_is_dummy) {
                new_desc = current_desc_ptr;
            } else {
                llvm::Value* is_null = builder->CreateICmpEQ(current_desc_ptr,
                    llvm::ConstantPointerNull::get(target_type_llvm->getPointerTo()));
                llvm::Function* cur_fn = builder->GetInsertBlock()->getParent();
                llvm::BasicBlock& entry_bb = cur_fn->getEntryBlock();
                llvm::IRBuilder<> entry_builder(&entry_bb, entry_bb.getFirstInsertionPt());
                llvm::Value* stack_desc = entry_builder.CreateAlloca(
                    target_type_llvm, nullptr, "ptr_section_desc");
                new_desc = builder->CreateSelect(is_null, stack_desc, current_desc_ptr);
            }
        } else {
            // Allocate descriptor on the heap so it survives after this
            // function returns (the caller holds a pointer).
            new_desc = arr_descr->allocate_descriptor_on_heap(
                target_type_llvm);
        }
        // Fill the descriptor with the value's data and the target's bounds
        // Dims are inline at FIELD_DIMS - get pointer to first dim element
        llvm::Value* dim_des_val = arr_descr->get_pointer_to_dimension_descriptor_array(
            target_type_llvm, new_desc, false);
        
        // Get value data pointer
        llvm::Value* value_data = nullptr;
        llvm::Type* idx_type = arr_descr->get_index_type();
        unsigned idx_bits = idx_type->getIntegerBitWidth();
        ASR::array_physical_typeType value_physical_type = ASRUtils::extract_physical_type(value_type);
        if (value_physical_type == ASR::array_physical_typeType::DescriptorArray) {
            ASR::ttype_t* value_desc_asr_type = value_type;
            if (ASR::is_a<ASR::ArraySection_t>(*x.m_value)) {
                ASR::ArraySection_t* value_section = ASR::down_cast<ASR::ArraySection_t>(x.m_value);
                value_desc_asr_type = ASRUtils::expr_type(value_section->m_v);
            }
            ASR::ttype_t* value_type_past_alloc = ASRUtils::type_get_past_allocatable(
                ASRUtils::type_get_past_pointer(value_desc_asr_type));
            llvm::Type* value_desc_type = llvm_utils->get_type_from_ttype_t_util(x.m_value,
                value_type_past_alloc, module.get());
            if (ASR::is_a<ASR::StructInstanceMember_t>(*x.m_value)) {
                value_desc = llvm_utils->CreateLoad2(value_desc_type->getPointerTo(), value_desc);
            } else if (ASR::is_a<ASR::ArraySection_t>(*x.m_value)) {
                ASR::ArraySection_t* as = ASR::down_cast<ASR::ArraySection_t>(x.m_value);
                ASR::ttype_t* v_type = ASRUtils::expr_type(as->m_v);
                if (ASRUtils::is_allocatable(v_type) || ASRUtils::is_pointer(v_type)) {
                    value_desc = llvm_utils->CreateLoad2(value_desc_type->getPointerTo(), value_desc);
                }
            }
            value_data = arr_descr->get_pointer_to_data(value_desc_type, value_desc);
            value_data = llvm_utils->CreateLoad2(value_el_type->getPointerTo(), value_data);
            // If the value is an ArraySection, compute offset to the first
            // element of the section and adjust the data pointer accordingly.
            if (ASR::is_a<ASR::ArraySection_t>(*x.m_value)) {
                ASR::ArraySection_t* value_section = ASR::down_cast<ASR::ArraySection_t>(x.m_value);
                llvm::Value* src_dim_des_arr = arr_descr->get_pointer_to_dimension_descriptor_array(
                    value_desc_type, value_desc);
                llvm::Value* section_offset = arr_descr->get_offset(value_desc_type, value_desc, true);
                for (size_t vi = 0; vi < value_section->n_args; vi++) {
                    ASR::array_index_t& vidx = value_section->m_args[vi];
                    llvm::Value* first_idx;
                    if (vidx.m_step != nullptr) {
                        // Sliced dimension: use the lower bound of the slice
                        if (vidx.m_left != nullptr) {
                            visit_expr_wrapper(vidx.m_left, true);
                            first_idx = tmp;
                        } else {
                            first_idx = llvm::ConstantInt::get(idx_type, 1);
                        }
                    } else {
                        // Non-sliced (fixed index) dimension
                        visit_expr_wrapper(vidx.m_right, true);
                        first_idx = tmp;
                    }
                    first_idx = builder->CreateSExtOrTrunc(first_idx, idx_type);
                    llvm::Value* dim_des = arr_descr->get_pointer_to_dimension_descriptor(
                        src_dim_des_arr,
                        llvm::ConstantInt::get(context, llvm::APInt(idx_bits, vi)));
                    llvm::Value* stride = arr_descr->get_stride(dim_des, true);
                    llvm::Value* lb = arr_descr->get_lower_bound(dim_des, true);
                    section_offset = builder->CreateAdd(section_offset,
                        builder->CreateMul(stride, builder->CreateSub(first_idx, lb)));
                }
                value_data = llvm_utils->create_ptr_gep2(value_el_type, value_data, section_offset);
            }
        } else if (value_physical_type == ASR::array_physical_typeType::FixedSizeArray ||
                   value_physical_type == ASR::array_physical_typeType::PointerArray) {
            llvm::Type* val_type = llvm_utils->get_type_from_ttype_t_util(x.m_value,
                ASRUtils::type_get_past_allocatable(
                ASRUtils::type_get_past_pointer(value_type)),
                module.get());
            value_data = llvm_utils->create_gep2(val_type, value_desc, 0);
        } else {
            value_data = value_desc;
        }
        
        // Store data pointer in descriptor. If the target is an unlimited
        // polymorphic array (class(*)), the descriptor's data field is a
        // pointer to a class wrapper struct (vtable + data pointer).
        // Allocate (or reuse) a wrapper, set its vtable based on the value's
        // type, store the value's data pointer in the wrapper's data field,
        // and store the wrapper pointer in the descriptor's data field.
        llvm::Value* target_data_field = arr_descr->get_pointer_to_data(
            target_type_llvm, new_desc);
        ASR::ttype_t* target_elem_type = ASRUtils::type_get_past_array(
            ASRUtils::type_get_past_allocatable_pointer(target_ptr_type));
        bool target_is_unlimited_poly =
            ASRUtils::is_unlimited_polymorphic_type(target_elem_type);
        if (target_is_unlimited_poly) {
            llvm::Type* wrapper_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                target_section->m_v, target_elem_type, module.get());
            llvm::Value* wrapper_size = SizeOfTypeUtil(target_section->m_v,
                target_elem_type, llvm_utils->getIntType(4),
                ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4)));
            llvm::Value* wrapper_ptr = allocate_class_wrapper_storage(
                target_section->m_v, wrapper_llvm_type, wrapper_size);
            // Store vtable for value's intrinsic element type into wrapper
            // (vtable keys are scalar; strip array wrappers from value_type).
            ASR::ttype_t* value_elem_type = ASRUtils::extract_type(value_type);
            struct_api->store_intrinsic_type_vptr(value_elem_type,
                ASRUtils::extract_kind_from_ttype_t(value_elem_type),
                wrapper_ptr, module.get());
            // Store value's data pointer (as i8*) into wrapper's data field.
            llvm::Value* wrapper_data_field = llvm_utils->create_gep2(
                wrapper_llvm_type, wrapper_ptr, 1);
            llvm::Value* value_data_i8 = builder->CreateBitCast(
                value_data, llvm_utils->i8_ptr);
            builder->CreateStore(value_data_i8, wrapper_data_field);
            // Store wrapper pointer in descriptor's data field.
            builder->CreateStore(wrapper_ptr, target_data_field);
        } else {
            llvm::Type* target_data_field_elem_type =
                llvm::cast<llvm::StructType>(target_type_llvm)->getElementType(0);
            if (value_data->getType() != target_data_field_elem_type) {
                value_data = builder->CreateBitCast(value_data, target_data_field_elem_type);
            }
            builder->CreateStore(value_data, target_data_field);
        }
        
        // Set offset to 0
        llvm::Value* offset_ptr = arr_descr->get_offset(target_type_llvm, new_desc, false);
        builder->CreateStore(llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 0)), offset_ptr);
        
        
        bool has_target_strides = (target_strides.n == (uint32_t)target_rank);
        llvm::Value* current_stride = llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 1));
        // Set dimension descriptors with the target bounds
        for (int i = 0; i < target_rank; i++) {
            llvm::Value* dim_idx = llvm::ConstantInt::get(context, llvm::APInt(idx_bits, i));
            llvm::Value* dim_des = arr_descr->get_pointer_to_dimension_descriptor(dim_des_val, dim_idx);

            // Get pointers to dimension descriptor fields
            // Structure: index 0 = stride, index 1 = lower_bound, index 2 = size
            llvm::Value* stride_ptr = arr_descr->get_stride(dim_des, false);
            llvm::Value* lb_ptr = arr_descr->get_lower_bound(dim_des, false);
            llvm::Value* size_ptr = arr_descr->get_dimension_size(dim_des, false);

            // Set stride: use target_strides if available, otherwise running product
            if (has_target_strides) {
                builder->CreateStore(target_strides.p[i], stride_ptr);
            } else {
                builder->CreateStore(current_stride, stride_ptr);
            }

            // Set lower bound from target section
            llvm::Value* lb_idx = builder->CreateSExtOrTrunc(lbs.p[i], idx_type);
            builder->CreateStore(lb_idx, lb_ptr);

            // Calculate and set size: ub - lb + 1
            llvm::Value* ub_idx = builder->CreateSExtOrTrunc(ubs.p[i], idx_type);
            llvm::Value* size = builder->CreateAdd(
                builder->CreateSub(ub_idx, lb_idx),
                llvm::ConstantInt::get(idx_type, 1));
            builder->CreateStore(size, size_ptr);

            // Update running product for next dimension
            if (!has_target_strides) {
                current_stride = builder->CreateMul(current_stride, size);
            }
        }
        
        // Store the new descriptor to the target pointer
        builder->CreateStore(new_desc, target_desc);
    }

    void handle_array_section_association_to_pointer(const ASR::Associate_t& x) {
        ASR::ArraySection_t* array_section = ASR::down_cast<ASR::ArraySection_t>(x.m_value);
        ASR::ttype_t* value_array_type = ASRUtils::expr_type(array_section->m_v);

        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 1 - !LLVM::is_llvm_pointer(*value_array_type);
        visit_expr_wrapper(array_section->m_v);
        llvm::Value* value_desc = tmp;
        llvm::Type* value_desc_type = llvm_utils->get_type_from_ttype_t_util(array_section->m_v,
            ASRUtils::type_get_past_allocatable(
                ASRUtils::type_get_past_pointer(ASRUtils::expr_type(array_section->m_v))), module.get());
        if( ASR::is_a<ASR::StructInstanceMember_t>(*array_section->m_v) &&
            ASRUtils::extract_physical_type(value_array_type) !=
                ASR::array_physical_typeType::FixedSizeArray ) {
            value_desc = llvm_utils->CreateLoad2(value_desc_type->getPointerTo(), value_desc);
        }
        llvm::Type *value_el_type = llvm_utils->get_el_type(array_section->m_v,
              ASRUtils::extract_type(value_array_type), module.get());
        ptr_loads = 0;
        visit_expr(*x.m_target);
        llvm::Value* target_desc = tmp;
        ptr_loads = ptr_loads_copy;

        ASR::ttype_t* target_desc_type = ASRUtils::duplicate_type_with_empty_dims(al,
            ASRUtils::type_get_past_allocatable(
                ASRUtils::type_get_past_pointer(ASRUtils::expr_type(x.m_target))),
             ASR::array_physical_typeType::DescriptorArray, true);
        llvm::Type* target_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, target_desc_type, module.get());
        int value_rank = array_section->n_args, target_rank = 0;
        llvm::Value *target = arr_descr->create_descriptor_alloca(
            target_type, "array_section_descriptor");
        if( ASRUtils::is_character(*expr_type(x.m_target))){
            llvm::Value* str_desc = llvm_utils->create_string_descriptor("array_section_string_desc");
            builder->CreateStore(str_desc, arr_descr->get_pointer_to_data(target_type, target));
        }
        Vec<llvm::Value*> lbs; lbs.reserve(al, value_rank);
        Vec<llvm::Value*> ubs; ubs.reserve(al, value_rank);
        Vec<llvm::Value*> ds; ds.reserve(al, value_rank);
        Vec<llvm::Value*> non_sliced_indices; non_sliced_indices.reserve(al, value_rank);
        ASR::ttype_t* array_type = ASRUtils::expr_type(array_section->m_v);
        ASR::array_physical_typeType arr_physical_type = ASRUtils::extract_physical_type(array_type);
        ASR::dimension_t* m_dims = nullptr;
        [[maybe_unused]] int array_value_rank = ASRUtils::extract_dimensions_from_ttype(array_type, m_dims);
        LCOMPILERS_ASSERT(array_value_rank == value_rank);
        for( int i = 0; i < value_rank; i++ ) {
            lbs.p[i] = nullptr; ubs.p[i] = nullptr; ds.p[i] = nullptr;
            non_sliced_indices.p[i] = nullptr;
            if( array_section->m_args[i].m_step != nullptr ) {
                if (array_section->m_args[i].m_left) {
                    visit_expr_wrapper(array_section->m_args[i].m_left, true);
                    lbs.p[i] = tmp;
                } else if (m_dims[i].m_start) {
                    visit_expr_wrapper(m_dims[i].m_start, true);
                    lbs.p[i] = tmp;
                } else {
                    llvm::Type* idx_type = arr_descr->get_index_type();
                    unsigned idx_bits = idx_type->getIntegerBitWidth();
                    lbs.p[i] = llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 1));
                }

                if (array_section->m_args[i].m_right) {
                    if (arr_physical_type == ASR::array_physical_typeType::UnboundedPointerArray &&
                        i == value_rank - 1 &&
                        ASR::is_a<ASR::ArrayBound_t>(*array_section->m_args[i].m_right) &&
                        ASR::down_cast<ASR::ArrayBound_t>(array_section->m_args[i].m_right)->m_value == nullptr) {
                        llvm::Value *lbound = builder->CreateSExtOrTrunc(
                            lbs.p[i], arr_descr->get_index_type());
                        ubs.p[i] = lbound;
                    } else {
                        visit_expr_wrapper(array_section->m_args[i].m_right, true);
                        ubs.p[i] = tmp;
                    }
                } else {
                    llvm::Type* idx_type = arr_descr->get_index_type();
                    unsigned idx_bits = idx_type->getIntegerBitWidth();
                    llvm::Value *lbound = builder->CreateSExtOrTrunc(lbs.p[i], idx_type);
                    if (m_dims[i].m_length) {
                        visit_expr_wrapper(m_dims[i].m_length, true);
                        llvm::Value *length = builder->CreateSExtOrTrunc(tmp, idx_type);
                        ubs.p[i] = builder->CreateSub(builder->CreateAdd(lbound, length),
                            llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 1)));
                    } else {
                        // Assumed-size array: last dimension has no length
                        ubs.p[i] = lbound;
                    }
                }

                if (array_section->m_args[i].m_step) {
                    visit_expr_wrapper(array_section->m_args[i].m_step, true);
                    ds.p[i] = tmp;
                } else {
                    llvm::Type* idx_type = arr_descr->get_index_type();
                    unsigned idx_bits = idx_type->getIntegerBitWidth();
                    ds.p[i] = llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 1));
                }
                target_rank++;
            } else {
                visit_expr_wrapper(array_section->m_args[i].m_right, true);
                non_sliced_indices.p[i] = tmp;
            }
        }
        LCOMPILERS_ASSERT(target_rank > 0);
        // Sequence association (--legacy-array-sections): a multi-dim section
        // may have more sliced dims than the callee's 1D assumed-size descriptor
        // can hold. Cap target_rank at the descriptor's rank so we only fill
        // dims that fit. The first sliced dim gets stride=1 (before any
        // accumulation), which is correct for contiguous sequence association.
        int desc_rank = ASRUtils::extract_n_dims_from_ttype(
            ASRUtils::type_get_past_allocatable(
                ASRUtils::type_get_past_pointer(ASRUtils::expr_type(x.m_target))));
        if (target_rank > desc_rank) {
            LCOMPILERS_ASSERT(desc_rank == 1 && "Rank mismatch only expected "
                "for 1D assumed-size sequence association");
            target_rank = desc_rank;
        }
        arr_descr->fill_dimension_descriptor(target_type, target, target_rank);
        if( arr_physical_type == ASR::array_physical_typeType::PointerArray ||
            arr_physical_type == ASR::array_physical_typeType::UnboundedPointerArray ||
            arr_physical_type == ASR::array_physical_typeType::FixedSizeArray ||
            arr_physical_type == ASR::array_physical_typeType::StringArraySinglePointer) {
            if( arr_physical_type == ASR::array_physical_typeType::FixedSizeArray ||
                arr_physical_type == ASR::array_physical_typeType::StringArraySinglePointer) {
                llvm::Type *val_type = llvm_utils->get_type_from_ttype_t_util(array_section->m_v,
                    ASRUtils::type_get_past_allocatable(
                    ASRUtils::type_get_past_pointer(value_array_type)),
                    module.get());
#if LLVM_VERSION_MAJOR < 15
                // On typed pointers, only GEP if value_desc points to the
                // array type. For parameters whose m_value was visited via
                // visit_ArrayConstant, the pointer is already at element level.
                if (value_desc->getType()->getPointerElementType() == val_type) {
                    value_desc = llvm_utils->create_gep2(val_type, value_desc, 0);
                }
#else
                value_desc = llvm_utils->create_gep2(val_type, value_desc, 0);
#endif
            }
            Vec<llvm::Value*> llvm_diminfo;
            llvm_diminfo.reserve(al, value_rank * 2);
            for( int i = 0; i < value_rank; i++ ) {
                llvm::Type* idx_type = arr_descr->get_index_type();
                unsigned idx_bits = idx_type->getIntegerBitWidth();
                llvm::Value *dim_start = nullptr;
                if (m_dims[i].m_start) {
                    visit_expr_wrapper(m_dims[i].m_start, true);
                    dim_start = tmp;
                } else {
                    dim_start = llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 1));
                }
                llvm_diminfo.push_back(al, dim_start);

                llvm::Value *dim_length = nullptr;
                if (m_dims[i].m_length) {
                    visit_expr_wrapper(m_dims[i].m_length, true);
                    dim_length = tmp;
                } else {
                    dim_length = llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 0));
                }
                llvm_diminfo.push_back(al, dim_length);
            }
            arr_descr->fill_descriptor_for_array_section_data_only(value_desc, value_el_type, expr_type(x.m_value),
                target, expr_type(x.m_target), x.m_target,
                target_type,
                lbs.p, ubs.p, ds.p, non_sliced_indices.p,
                llvm_diminfo.p, value_rank, target_rank, location_manager);
        } else {
            arr_descr->fill_descriptor_for_array_section(value_desc, value_el_type, expr_type(x.m_value),
                value_desc_type,
                target, expr_type(x.m_target), x.m_target,
                target_type,
                lbs.p, ubs.p, ds.p, non_sliced_indices.p,
                array_section->n_args, target_rank, location_manager);
        }
        builder->CreateStore(target, target_desc);
    }

    void visit_Associate(const ASR::Associate_t& x) {
        bool is_target_pointer_section = false;
        if (ASR::is_a<ASR::ArraySection_t>(*x.m_target)) {
            ASR::ArraySection_t* target_section = ASR::down_cast<ASR::ArraySection_t>(x.m_target);
            ASR::ttype_t* target_section_type = ASRUtils::expr_type(target_section->m_v);
            if (ASRUtils::is_pointer(target_section_type)) {
                is_target_pointer_section = true;
            }
        }
        if (is_target_pointer_section) {
            handle_pointer_section_target(x);
        } else if( ASR::is_a<ASR::ArraySection_t>(*x.m_value) ) {
            handle_array_section_association_to_pointer(x);
        } else if (is_component_array_expr(x.m_value)) {
            handle_component_array_association(x);
        } else {
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            visit_expr(*x.m_target);
            llvm::Value* llvm_target = tmp;
            visit_expr(*x.m_value);
            llvm::Value* llvm_value = tmp;
            ptr_loads = ptr_loads_copy;
            ASR::ttype_t* target_type = ASRUtils::expr_type(x.m_target);
            ASR::ttype_t* value_type = ASRUtils::expr_type(x.m_value);
            ASR::dimension_t* m_dims = nullptr;
            int n_dims = ASRUtils::extract_dimensions_from_ttype(target_type, m_dims);
            ASR::ttype_t *type = ASRUtils::get_contained_type(target_type);
            type = ASRUtils::type_get_past_allocatable(type);
            [[maybe_unused]] bool is_target_class = ASRUtils::is_class_type(
                ASRUtils::type_get_past_allocatable_pointer(target_type));
            [[maybe_unused]] bool is_value_class = ASRUtils::is_class_type(
                ASRUtils::type_get_past_pointer(
                    ASRUtils::type_get_past_allocatable(value_type)));
            llvm::Type *i64 = llvm::Type::getInt64Ty(context);
            if (ASR::is_a<ASR::PointerNullConstant_t>(*x.m_value)) {
                if(ASRUtils::is_array(target_type) ){ // Fetch data ptr
                    LCOMPILERS_ASSERT(ASRUtils::extract_physical_type(target_type) ==
                        ASR::array_physical_typeType::DescriptorArray);
                    llvm::Type* target_type_llvm = llvm_utils->get_type_from_ttype_t_util(
                        x.m_target, target_type, module.get());
                    llvm_target = llvm_utils->CreateLoad2(target_type_llvm, llvm_target);
                    llvm_target = arr_descr->get_pointer_to_data(x.m_target, target_type, llvm_target, module.get());
                }
                if (ASRUtils::is_string_only(target_type)) {
                    llvm_target = llvm_utils->get_string_data(
                        ASRUtils::get_string_type(target_type), llvm_target, true);
                }
                llvm_value = llvm_utils->CreateBitCastForStore(llvm_value, llvm_target);
                builder->CreateStore(llvm_value, llvm_target);
            } else if ((ASRUtils::is_string_only(value_type)) && (ASRUtils::is_unlimited_polymorphic_type(target_type))){
                // String to unlimited polymorphic association
                ASR::ttype_t* target_base_type = ASRUtils::type_get_past_pointer(target_type);
                llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_target, target_base_type, module.get());
                llvm::Value* target_ptr = llvm_target;
                if (LLVM::is_llvm_pointer(*target_type)) {
                    // Allocate wrapper on heap (This is the current convention)
                    llvm::Value* null_cond = builder->CreateICmpEQ(
                        llvm_utils->CreateLoad2(target_llvm_type->getPointerTo(), llvm_target),
                            llvm::ConstantPointerNull::get(target_llvm_type->getPointerTo()));
                    llvm_utils->create_if_else(
                        null_cond,
                        [&]() {
                            llvm::Value* wrapper_size = SizeOfTypeUtil(x.m_target, target_base_type,
                                llvm_utils->getIntType(4), ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4)));
                            llvm::Value* wrapper_ptr = allocate_class_wrapper_storage(
                                x.m_target, target_llvm_type, wrapper_size);
                            builder->CreateStore(wrapper_ptr, llvm_target);
                        }, []() {});
                    target_ptr = llvm_utils->CreateLoad2(target_llvm_type->getPointerTo(), llvm_target);
                }
                struct_api->store_intrinsic_type_vptr(value_type,
                    ASRUtils::extract_kind_from_ttype_t(value_type), target_ptr, module.get());
                llvm::Value* value_ptr = llvm_value;
                if (!llvm_value->getType()->isPointerTy()) {
                    // String descriptor is a value, need to allocate and get its address
                    llvm::Value* temp_alloca = llvm_utils->CreateAlloca(*builder, llvm_value->getType());
                    builder->CreateStore(llvm_value, temp_alloca);
                    value_ptr = temp_alloca;
                }
                llvm::Value* void_data_ptr = builder->CreateBitCast(value_ptr, llvm_utils->i8_ptr); //i8*
                builder->CreateStore(void_data_ptr, llvm_utils->create_gep2(target_llvm_type, target_ptr, 1));
            
            } else if(ASRUtils::is_string_only(value_type) || // Maybe we can handle this case in a string api function
                      (ASRUtils::is_unlimited_polymorphic_type(value_type) && ASRUtils::is_string_only(target_type))) {
                // Handle for unlimited_polymorphic to string pointers as well
                LCOMPILERS_ASSERT(ASRUtils::is_string_only(target_type));
                // If extracting from unlimited polymorphic, get the string descriptor pointer first
                if (ASRUtils::is_unlimited_polymorphic_type(value_type)) {    
                    ASR::ttype_t* value_base_type = ASRUtils::type_get_past_pointer(value_type);
                    llvm::Type* value_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_value, value_base_type, module.get());
                    llvm::Value* value_ptr = llvm_value;
                    if (LLVM::is_llvm_pointer(*value_type)) { 
                        value_ptr = llvm_utils->CreateLoad2(value_llvm_type->getPointerTo(), llvm_value);
                    } 
                    // Extract data pointer and cast to string_descriptor*  
                    llvm::Value* data_field_ptr = llvm_utils->create_gep2(value_llvm_type, value_ptr, 1);
                    llvm::Value* data_ptr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, data_field_ptr);
                    llvm::Type* string_desc_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_target, target_type, module.get());
                    llvm_value = builder->CreateBitCast(data_ptr, string_desc_type->getPointerTo());
                }

                llvm::Value* target_ptr = llvm_utils->get_string_data(
                    ASRUtils::get_string_type(target_type), llvm_target, true); //i8**
                llvm::Value* value_ptr = llvm_utils->get_string_data(
                    ASRUtils::get_string_type(target_type), llvm_value); // i8*
                builder->CreateStore(value_ptr, target_ptr);
                switch (ASRUtils::get_string_type(target_type)->m_physical_type)
                {
                    case ASR::DescriptorString:{
                        llvm::Value* target_length = llvm_utils->get_string_length(
                            ASRUtils::get_string_type(target_type), llvm_target, true); // i64*
                        llvm::Value* value_length = llvm_utils->get_string_length(
                            ASRUtils::get_string_type(target_type), llvm_value); // i64
                        builder->CreateStore(value_length, target_length);
                        break;
                    }
                    default:
                        throw LCompilersException("Unhandled String Physical Type");
                }
            } else if (ASR::is_a<ASR::StructInstanceMember_t>(*x.m_value) &&
                        ASR::is_a<ASR::FunctionType_t>(*value_type)) {
                llvm::Type* llvm_value_type = llvm_utils->get_type_from_ttype_t_util(x.m_value, value_type, module.get());
                llvm_value = llvm_utils->CreateLoad2(llvm_value_type, llvm_value);
                builder->CreateStore(llvm_value, llvm_target);
            } else if (is_target_class &&
                       !ASRUtils::is_array(target_type) &&
                       ASRUtils::is_array(value_type) &&
                       ASRUtils::extract_physical_type(value_type) == ASR::array_physical_typeType::AssumedRankArray) {
                // select rank (assoc => arg) rank(0) case:
                // `arg` is represented as an assumed-rank array descriptor even when rank==0,
                // but `assoc` is a scalar class pointer. Associate the scalar wrapper pointer
                // (descriptor.data) rather than bitcasting the whole descriptor.
                LCOMPILERS_ASSERT(ASRUtils::is_class_type(ASRUtils::extract_type(value_type)))
                llvm::Type* const array_desc_type = llvm_utils->arr_api->
                    get_array_type(x.m_value, ASRUtils::type_get_past_allocatable_pointer(value_type),
                        llvm_utils->get_el_type(x.m_value, ASRUtils::extract_type(value_type), module.get()), false);
                llvm::Value* value_data_ptr = llvm_utils->create_gep2(array_desc_type, llvm_value, 0);
                llvm::Type* value_el_type = llvm_utils->get_el_type(
                    x.m_value, ASRUtils::extract_type(value_type), module.get());
                llvm::Value* wrapper_ptr = llvm_utils->CreateLoad2(
                    value_el_type->getPointerTo(), value_data_ptr);
                llvm::Type* const target_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_target, ASRUtils::extract_type(target_type), module.get());
                llvm::Value* const wrapper_size = SizeOfTypeUtil(
                    x.m_target, ASRUtils::type_get_past_pointer(target_type),
                    llvm_utils->getIntType(4),
                    ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4)));
                // Allocate wrapper if doesn't exist.
                llvm::Value* null_cond = builder->CreateICmpEQ(
                    llvm_utils->CreateLoad2(target_llvm_type->getPointerTo(), llvm_target),
                    llvm::ConstantPointerNull::get(target_llvm_type->getPointerTo()));
                llvm_utils->create_if_else(
                    null_cond, [&]() {
                        llvm::Value* target_wrapper = llvm_utils->alloc_zeroed_type(target_llvm_type);
                        builder->CreateStore(target_wrapper, llvm_target);
                    }, []() {});
                llvm::Value* const target_wrapper = llvm_utils->CreateLoad2(target_llvm_type->getPointerTo(), llvm_target);
                builder->CreateMemCpy(target_wrapper, llvm::MaybeAlign(),
                                      wrapper_ptr, llvm::MaybeAlign(), wrapper_size);
            } else if (!is_target_class &&
                       ASR::is_a<ASR::Pointer_t>(*target_type) &&
                       ASR::is_a<ASR::StructType_t>(*ASRUtils::type_get_past_pointer(target_type)) &&
                       !ASRUtils::is_array(target_type) &&
                       ASRUtils::is_array(value_type) &&
                       ASRUtils::is_class_type(ASRUtils::extract_type(value_type)) &&
                       ASRUtils::extract_physical_type(value_type) == ASR::array_physical_typeType::AssumedRankArray) {
                llvm::Type* const array_desc_type = llvm_utils->arr_api->
                    get_array_type(x.m_value, ASRUtils::type_get_past_allocatable_pointer(value_type),
                        llvm_utils->get_el_type(x.m_value, ASRUtils::extract_type(value_type), module.get()), false);
                llvm::Value* value_data_ptr = llvm_utils->create_gep2(array_desc_type, llvm_value, 0);
                llvm::Type* value_el_type = llvm_utils->get_el_type(
                    x.m_value, ASRUtils::extract_type(value_type), module.get());
                llvm::Value* wrapper_ptr = llvm_utils->CreateLoad2(
                    value_el_type->getPointerTo(), value_data_ptr);
                // Class wrapper layout is { vptr, data* }; load the data field.
                llvm::Value* data_field_ptr = llvm_utils->create_gep2(value_el_type, wrapper_ptr, 1);
                llvm::Value* data_i8_ptr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, data_field_ptr);
                llvm::Type* target_struct_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_target, ASRUtils::type_get_past_pointer(target_type), module.get());
                llvm::Value* casted_data = builder->CreateBitCast(
                    data_i8_ptr, target_struct_llvm_type->getPointerTo());
                builder->CreateStore(casted_data, llvm_target);
            } else if ((is_target_class || is_value_class) &&
                    !ASRUtils::is_array(ASRUtils::type_get_past_pointer(value_type)) &&
                    !ASRUtils::is_array(ASRUtils::type_get_past_pointer(target_type))) {
                if (LLVM::is_llvm_pointer(*value_type)) {
                    llvm::Type* value_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_value, value_type, module.get());
                    llvm_value = llvm_utils->CreateLoad2(value_llvm_type, llvm_value);
                }
                if (is_target_class && is_value_class) {
                    llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_target, ASRUtils::extract_type(target_type), module.get());
                    llvm::Value* value_is_null = builder->CreateICmpEQ(
                        llvm_value,
                        llvm::ConstantPointerNull::get(
                            llvm::cast<llvm::PointerType>(llvm_value->getType())));
                    llvm_utils->create_if_else(
                        value_is_null,
                        [&]() {
                            // Source class wrapper is null (unallocated);
                            // store null into target to preserve unallocated status
                            builder->CreateStore(
                                llvm::ConstantPointerNull::get(target_llvm_type->getPointerTo()),
                                llvm_target);
                        },
                        [&]() {
                            // Allocate wrapper on heap if needed and memcpy from source
                            llvm::Value* null_cond = builder->CreateICmpEQ(
                                llvm_utils->CreateLoad2(target_llvm_type->getPointerTo(), llvm_target),
                                llvm::ConstantPointerNull::get(target_llvm_type->getPointerTo()));
                            llvm::Value* wrapper_size = SizeOfTypeUtil(x.m_target, ASRUtils::type_get_past_pointer(target_type),
                                llvm_utils->getIntType(4), ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4)));
                            llvm_utils->create_if_else(
                                null_cond,
                                [&]() {
                                    llvm::Value* wrapper_ptr = allocate_class_wrapper_storage(
                                        x.m_target, target_llvm_type, wrapper_size);
                                    builder->CreateStore(wrapper_ptr, llvm_target);
                                }, []() {});
                            builder->CreateMemCpy(
                                llvm_utils->CreateLoad2(target_llvm_type->getPointerTo(), llvm_target), llvm::MaybeAlign(),
                                llvm_value, llvm::MaybeAlign(),
                                wrapper_size);
                        });
                } else if (is_target_class) {
                    // check_and_allocate_scalar(x.m_target, x.m_value, value_type, true);
                    llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_target, ASRUtils::extract_type(target_type), module.get());
                    // Allocate wrapper on heap (This is the current convention we use for class types)
                    llvm::Value* null_cond = builder->CreateICmpEQ(
                        llvm_utils->CreateLoad2(target_llvm_type->getPointerTo(), llvm_target),
                        llvm::ConstantPointerNull::get(target_llvm_type->getPointerTo()));
                    llvm_utils->create_if_else(
                        null_cond,
                        [&]() {
                            llvm::Value* wrapper_size = SizeOfTypeUtil(x.m_target, ASRUtils::type_get_past_pointer(target_type),
                                llvm_utils->getIntType(4), ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4)));
                            llvm::Value* wrapper_ptr = allocate_class_wrapper_storage(
                                x.m_target, target_llvm_type, wrapper_size);
                            builder->CreateStore(wrapper_ptr, llvm_target);
                        }, []() {});
                    llvm_target = llvm_utils->CreateLoad2(target_llvm_type->getPointerTo(), llvm_target);

                    // Store vptr
                    if (ASRUtils::is_array(value_type)) {
                        llvm::Type* value_el_type = llvm_utils->get_el_type(x.m_value, ASRUtils::type_get_past_array(ASRUtils::type_get_past_allocatable_pointer(value_type)), module.get());
                        llvm::Value* src_data_ptr = llvm_utils->CreateLoad2(value_el_type->getPointerTo(), 
                            arr_descr->get_pointer_to_data(x.m_value, ASRUtils::type_get_past_allocatable_pointer(value_type), llvm_value, module.get()));
                        
                        if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(value_type))) {
                           llvm::Value* src_vptr = llvm_utils->CreateLoad2(llvm_utils->vptr_type, 
                               llvm_utils->create_gep2(value_el_type, src_data_ptr, 0));
                           
                           llvm::Value* dest_vptr_ptr = llvm_utils->create_gep2(target_llvm_type, llvm_target, 0);
                           builder->CreateStore(src_vptr, dest_vptr_ptr);
                           
                           llvm_value = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, 
                               llvm_utils->create_gep2(value_el_type, src_data_ptr, 1));
                        }
                    } else if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(value_type))) {
                        struct_api->store_class_vptr(
                            ASRUtils::get_struct_sym_from_struct_expr(x.m_value), llvm_target, module.get());
                    } else {
                        struct_api->store_intrinsic_type_vptr(value_type,
                            ASRUtils::extract_kind_from_ttype_t(value_type), llvm_target, module.get());
                    }

                    llvm_target = llvm_utils->create_gep2(target_llvm_type, llvm_target, 1);
                    if (ASRUtils::is_unlimited_polymorphic_type(x.m_target)) {
                        llvm_value = builder->CreateBitCast(llvm_value, llvm_utils->i8_ptr);
                    } else {
                        llvm::Type* actual_struct_type = llvm_utils->getStructType(
                            ASR::down_cast<ASR::Struct_t>(
                                ASRUtils::symbol_get_past_external(
                                    ASRUtils::get_struct_sym_from_struct_expr(x.m_target))),
                            module.get(), true);
                        llvm_value = builder->CreateBitCast(llvm_value, actual_struct_type);
                    }
                    builder->CreateStore(llvm_value, llvm_target);
                } else {
                    // Guard against null class wrapper pointer (e.g., a
                    // function returning class(T), pointer => null()).
                    llvm::Value* is_null = builder->CreateICmpEQ(
                        llvm_value,
                        llvm::ConstantPointerNull::get(
                            llvm::cast<llvm::PointerType>(llvm_value->getType())));
                    llvm_utils->create_if_else(
                        is_null,
                        [&]() {
                            ASR::ttype_t* target_base = ASRUtils::type_get_past_pointer(target_type);
                            if (ASRUtils::is_unlimited_polymorphic_type(target_base) ||
                                ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(target_base))) {
                                ASR::Struct_t* struct_type_t = ASR::down_cast<ASR::Struct_t>(
                                    ASRUtils::symbol_get_past_external(
                                        ASRUtils::get_struct_sym_from_struct_expr(x.m_target)));
                                llvm::Type* struct_type = llvm_utils->getStructType(
                                    struct_type_t, module.get(), true);
                                builder->CreateStore(
                                    llvm::ConstantPointerNull::get(
                                        llvm::cast<llvm::PointerType>(struct_type)),
                                    llvm_target);
                            } else {
                                llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                                    x.m_target, target_type, module.get());
                                builder->CreateStore(
                                    llvm::Constant::getNullValue(target_llvm_type),
                                    llvm_target);
                            }
                        },
                        [&]() {
                            llvm::Type* value_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                                x.m_value, ASRUtils::extract_type(value_type), module.get());
                            llvm::Value* val_data_ptr = llvm_utils->create_gep2(value_llvm_type, llvm_value, 1);
                            if (ASRUtils::is_unlimited_polymorphic_type(x.m_value)) {
                                val_data_ptr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, val_data_ptr);
                            } else {
                                ASR::Struct_t* value_struct_type_t = ASR::down_cast<ASR::Struct_t>(
                                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_value)));
                                llvm::Type* value_struct_type = llvm_utils->getStructType(value_struct_type_t, module.get(), true);
                                val_data_ptr = llvm_utils->CreateLoad2(value_struct_type, val_data_ptr);
                            }

                            ASR::ttype_t* target_base_type = ASRUtils::type_get_past_pointer(target_type);
                            if (ASRUtils::is_unlimited_polymorphic_type(target_base_type) ||
                                ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(target_base_type))) {
                                ASR::Struct_t* struct_type_t = ASR::down_cast<ASR::Struct_t>(
                                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_target)));
                                llvm::Type* struct_type = llvm_utils->getStructType(struct_type_t, module.get(), true);
                                val_data_ptr = builder->CreateBitCast(val_data_ptr, struct_type);
                                builder->CreateStore(val_data_ptr, llvm_target);
                            } else {
                                llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                                    x.m_target, target_type, module.get());
                                val_data_ptr = builder->CreateBitCast(val_data_ptr, target_llvm_type);
                                builder->CreateStore(val_data_ptr, llvm_target);
                            }
                        });
                }
            } else if (is_target_class && !is_value_class) {
                ASR::Struct_t* struct_type_t = ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_target)));
                llvm_value = builder->CreateBitCast(
                    llvm_value, llvm_utils->getStructType(struct_type_t, module.get(), true));
                builder->CreateStore(llvm_value, llvm_target);
            } else if (!is_target_class && is_value_class &&
                       ASRUtils::is_array(ASRUtils::type_get_past_pointer(target_type)) &&
                       ASR::is_a<ASR::ArrayPhysicalCast_t>(*x.m_value)) {
                ASR::ArrayPhysicalCast_t* apc = ASR::down_cast<ASR::ArrayPhysicalCast_t>(x.m_value);
                if (apc->m_old == ASR::array_physical_typeType::AssumedRankArray &&
                    apc->m_new == ASR::array_physical_typeType::DescriptorArray) {
                    ASR::ttype_t* src_asr_type = ASRUtils::expr_type(apc->m_arg);
                    ASR::ttype_t* dst_asr_type = ASRUtils::type_get_past_pointer(target_type);
                    ASR::ttype_t* src_arr_asr_type = ASRUtils::type_get_past_allocatable_pointer(src_asr_type);
                    ASR::ttype_t* dst_arr_asr_type = ASRUtils::type_get_past_allocatable_pointer(dst_asr_type);

                    int64_t ptr_loads_copy = ptr_loads;
                    ptr_loads = 2 - LLVM::is_llvm_pointer(*src_asr_type);
                    this->visit_expr_wrapper(apc->m_arg, false);
                    ptr_loads = ptr_loads_copy;
                    llvm::Value* source_desc = tmp;

                    llvm::Type* src_el_type = llvm_utils->get_el_type(
                        apc->m_arg, ASRUtils::extract_type(src_arr_asr_type), module.get());
                    llvm::Type* dst_el_type = llvm_utils->get_el_type(
                        x.m_target, ASRUtils::extract_type(dst_arr_asr_type), module.get());

                    llvm::Type* target_desc_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_target, dst_arr_asr_type, module.get());
                    llvm::Value* target_desc = arr_descr->create_descriptor_alloca(
                        target_desc_type, "select_type_desc");

                    llvm::Value* source_desc_as_target = builder->CreateBitCast(
                        source_desc, target_desc_type->getPointerTo());

                    llvm::Value* src_data_ptr = llvm_utils->CreateLoad2(
                        src_el_type->getPointerTo(),
                        arr_descr->get_pointer_to_data(apc->m_arg, src_asr_type, source_desc, module.get()));
                    llvm::Value* src_offset = arr_descr->get_offset(target_desc_type, source_desc_as_target);
                    llvm::Value* src_first_el_ptr = llvm_utils->create_ptr_gep2(
                        src_el_type, src_data_ptr, src_offset);

                    bool dst_is_class_array = ASRUtils::is_class_type(
                        ASRUtils::extract_type(dst_arr_asr_type));
                    llvm::Value* dst_first_el_ptr = nullptr;
                    if (!dst_is_class_array) {
                        dst_first_el_ptr = get_typed_array_data_ptr(
                            src_asr_type, dst_asr_type, src_el_type, dst_el_type, src_first_el_ptr);
                    } else {
                        dst_first_el_ptr = builder->CreateBitCast(
                            src_first_el_ptr, dst_el_type->getPointerTo());
                    }

                    builder->CreateStore(dst_first_el_ptr,
                        arr_descr->get_pointer_to_data(target_desc_type, target_desc));

                    int desc_n_dims = ASRUtils::extract_n_dims_from_ttype(dst_asr_type);
                    if (desc_n_dims == 0) {
                        desc_n_dims = ASRUtils::extract_n_dims_from_ttype(src_asr_type);
                    }
                    arr_descr->reset_array_details(target_desc_type, target_desc,
                        target_desc_type, source_desc_as_target, desc_n_dims);

                    builder->CreateStore(target_desc, llvm_target);
                } else {
                    throw CodeGenError("Unsupported ArrayPhysicalCast in Associate with class value and non-class array target");
                }
            } else if (!is_target_class && is_value_class) {
                llvm::Type* llvm_value_type = llvm_utils->get_type_from_ttype_t_util(x.m_value, value_type, module.get());
                if (!llvm_value->getType()->isPointerTy()) {
                    llvm::Value* temp = llvm_utils->CreateAlloca(*builder, llvm_value->getType());
                    builder->CreateStore(llvm_value, temp);
                    llvm_value = temp;
                }
                llvm::Value* val_data_ptr = llvm_utils->create_gep2(llvm_value_type, llvm_value, 1);
                ASR::Struct_t* struct_type_t = ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_target))));
                [[maybe_unused]] ASR::Struct_t* class_type_t = ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_value))));
                LCOMPILERS_ASSERT(ASRUtils::is_derived_type_similar(struct_type_t, class_type_t));
                llvm::Type* struct_type = llvm_utils->getStructType(struct_type_t, module.get(), true);
                llvm::Value* casted_val_ptr = builder->CreateBitCast(val_data_ptr, struct_type->getPointerTo());
                llvm::Value* struct_value = builder->CreateLoad(struct_type, casted_val_ptr);
                builder->CreateStore(struct_value, llvm_target);
            } else if( is_target_class && is_value_class ) {
                ASR::Struct_t* value_struct_t = ASR::down_cast<ASR::Struct_t>(
                        ASRUtils::symbol_get_past_external(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_value))));
                LCOMPILERS_ASSERT(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_target))
                                  == ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_value)));
                llvm::Type* value_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_value,
                    ASRUtils::type_get_past_allocatable_pointer(value_type), module.get());
                llvm::Type* ptr_type = llvm_utils->getStructType(
                    ASR::down_cast<ASR::Struct_t>(
                        ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_value))), module.get(), true);
                llvm::Value* gep = llvm_utils->create_gep2(value_llvm_type, llvm_value, 0);
                llvm::Value* value_vtabid = llvm_utils->CreateLoad2(i64, gep);
                llvm::Value* value_class = llvm_utils->CreateLoad2(ptr_type, llvm_utils->create_gep2(value_llvm_type, llvm_value, 1));
                builder->CreateStore(value_vtabid, llvm_utils->create_gep2(value_llvm_type, llvm_target, 0));

                if ( ASRUtils::is_unlimited_polymorphic_type(value_struct_t) ) {
                    // we need to cast `value_class` to `void*`
                    llvm::Type* void_ptr_type = llvm::Type::getVoidTy(context)->getPointerTo();
                    value_class = builder->CreateBitCast(value_class, void_ptr_type);
                }
                builder->CreateStore(value_class, llvm_utils->create_gep2(value_llvm_type, llvm_target, 1));
            } else if (ASR::is_a<ASR::Pointer_t>(*value_type) &&
                       ASR::is_a<ASR::Pointer_t>(*target_type) &&
                       ASR::is_a<ASR::FunctionCall_t>(*x.m_value)) {
                builder->CreateStore(llvm_value, llvm_target);
            } else {
                bool is_value_data_only_array = (ASRUtils::is_array(value_type) && (
                      ASRUtils::extract_physical_type(value_type) == ASR::array_physical_typeType::PointerArray ||
                      ASRUtils::extract_physical_type(value_type) == ASR::array_physical_typeType::UnboundedPointerArray ||
                      ASRUtils::extract_physical_type(value_type) == ASR::array_physical_typeType::FixedSizeArray ||
                      ASRUtils::extract_physical_type(value_type) == ASR::array_physical_typeType::DescriptorArray ||
                      ASRUtils::extract_physical_type(value_type) == ASR::array_physical_typeType::AssumedRankArray));
                if( LLVM::is_llvm_pointer(*value_type) ) {
                    llvm::Type* llvm_value_type = llvm_utils->get_type_from_ttype_t_util(x.m_value, value_type, module.get());
                    llvm_value = llvm_utils->CreateLoad2(llvm_value_type, llvm_value);
                }
                if( ASR::is_a<ASR::FunctionType_t>(*value_type) &&
                    (llvm::isa<llvm::AllocaInst>(llvm_value) ||
                     llvm::isa<llvm::GlobalVariable>(llvm_value)) ) {
                    llvm::Type* llvm_value_type = llvm_utils->get_type_from_ttype_t_util(x.m_value, value_type, module.get());
                    llvm_value = llvm_utils->CreateLoad2(llvm_value_type, llvm_value);
                }
                if( is_value_data_only_array ) { // This needs a refactor to handle
                    ASR::ttype_t* target_type_ = ASRUtils::type_get_past_pointer(target_type);
                    switch( ASRUtils::extract_physical_type(target_type_) ) {
                        case ASR::array_physical_typeType::DescriptorArray :
                        case ASR::array_physical_typeType::AssumedRankArray: {
                            // class(*) array pointer association (new classes):
                            //   class(*), pointer :: generic(:)
                            //   <concrete>, target :: x(:)
                            //   generic => x
                            //
                            // Here, the array descriptor data field points to the polymorphic wrapper
                            // (e.g. %"~unlimited_polymorphic_type"*) and NOT directly to the concrete data.
                            // The wrapper then stores (vptr, i8* data). We must set both correctly.
                            if (ASRUtils::is_unlimited_polymorphic_type(target_type_) &&
                                !ASRUtils::is_unlimited_polymorphic_type(value_type)) {

                                llvm::Type* llvm_target_type = llvm_utils->get_type_from_ttype_t_util(
                                    x.m_target, target_type_, module.get());
                                llvm::Type* const dim_desc_type = llvm_utils->arr_api->get_dimension_descriptor_type(false);
                                size_t n_dims = (size_t)ASRUtils::extract_n_dims_from_ttype(value_type);
                                LCOMPILERS_ASSERT(n_dims > 0);
                                llvm::Value* llvm_target_ = llvm_utils->CreateLoad2(llvm_target_type->getPointerTo(), llvm_target);
                                // Allocate and fill the target array descriptor dimensions
                                if (ASRUtils::extract_physical_type(value_type) == ASR::array_physical_typeType::DescriptorArray) {
                                    arr_descr->fill_dimension_descriptor(llvm_target_type, llvm_target_, (int)n_dims);
                                } else {
                                    ASR::dimension_t* m_dims = nullptr;
                                    ASRUtils::extract_dimensions_from_ttype(value_type, m_dims);
                                    ASR::ttype_t* data_type = ASRUtils::duplicate_type_without_dims(
                                        al, target_type_, target_type_->base.loc);
                                    llvm::Type* wrapper_llvm_type = llvm_utils->get_el_type(x.m_target, data_type, module.get());
                                    fill_array_details(llvm_target_type, llvm_target_, wrapper_llvm_type, m_dims, (int)n_dims, false, false);
                                }

                                // Reuse the existing upoly wrapper of target if exists, otherwise allocate one and use it.
                                ASR::ttype_t* wrapper_asr_type = ASRUtils::extract_type(target_type_);
                                llvm::Type* wrapper_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                                    x.m_target, wrapper_asr_type, module.get());
                                llvm::Value* target_data_ptr = arr_descr->get_pointer_to_data(
                                    llvm_target_type, llvm_target_);
                                llvm::Value* wrapper_ptr = llvm_utils->CreateLoad2(
                                    wrapper_llvm_type->getPointerTo(), target_data_ptr);
                                llvm_utils->create_if_else(builder->CreateIsNull(wrapper_ptr),
                                    [&]() {
                                        llvm::Value* target_wrapper =
                                            llvm_utils->alloc_zeroed_type(wrapper_llvm_type);
                                        builder->CreateStore(target_wrapper, target_data_ptr);
                                    },
                                    []() {});
                                wrapper_ptr = llvm_utils->CreateLoad2(
                                    wrapper_llvm_type->getPointerTo(), target_data_ptr);

                                // Get pointer to the first element of the concrete RHS array
                                llvm::Value* value_data_ptr = nullptr;
                                ASR::ttype_t* value_elem_asr_type = ASRUtils::extract_type(value_type);
                                llvm::Type* value_el_type = llvm_utils->get_el_type(
                                    x.m_value, value_elem_asr_type, module.get());
                                switch (ASRUtils::extract_physical_type(value_type)) {
                                    case ASR::array_physical_typeType::DescriptorArray: {
                                        llvm::Type* const src_array_desc_type = llvm_utils->arr_api->
                                            get_array_type(x.m_value, ASRUtils::type_get_past_allocatable_pointer(value_type),
                                                value_el_type, false);
                                        llvm::Value* src_data_ptr_ptr = llvm_utils->create_gep2(src_array_desc_type, llvm_value, 0);
                                        value_data_ptr = llvm_utils->CreateLoad2(value_el_type->getPointerTo(), src_data_ptr_ptr);
                                        break;
                                    }
                                    case ASR::array_physical_typeType::FixedSizeArray: {
                                        llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(
                                            x.m_value, value_type, module.get());
                                        value_data_ptr = llvm_utils->create_gep2(llvm_type, llvm_value, 0);
                                        break;
                                    }
                                    default: {
                                        // Other data-only physical types should be handled via explicit cases
                                        LCOMPILERS_ASSERT(false);
                                    }
                                }

                                // Store vptr + data pointer into the wrapper
                                if (ASR::is_a<ASR::StructType_t>(*value_elem_asr_type)) {
                                    llvm::Value* src_vptr = llvm_utils->CreateLoad2(
                                        llvm_utils->vptr_type, llvm_utils->create_gep2(value_el_type, value_data_ptr, 0));
                                    builder->CreateStore(src_vptr, llvm_utils->create_gep2(wrapper_llvm_type, wrapper_ptr, 0));
                                    llvm::Value* src_data_ptr = llvm_utils->CreateLoad2(
                                        llvm_utils->i8_ptr, llvm_utils->create_gep2(value_el_type, value_data_ptr, 1));
                                    builder->CreateStore(src_data_ptr, llvm_utils->create_gep2(wrapper_llvm_type, wrapper_ptr, 1));
                                } else {
                                    struct_api->store_intrinsic_type_vptr(value_elem_asr_type,
                                        ASRUtils::extract_kind_from_ttype_t(value_elem_asr_type), wrapper_ptr, module.get());
                                    llvm::Value* void_data_ptr = builder->CreateBitCast(value_data_ptr, llvm_utils->i8_ptr);
                                    builder->CreateStore(void_data_ptr, llvm_utils->create_gep2(wrapper_llvm_type, wrapper_ptr, 1));
                                }

                                // If the RHS has a descriptor, copy its dims + offset over to our new descriptor
                                if (ASRUtils::extract_physical_type(value_type) == ASR::array_physical_typeType::DescriptorArray) {
                                    llvm::Type* const src_array_desc_type = llvm_utils->arr_api->
                                        get_array_type(x.m_value, ASRUtils::type_get_past_allocatable_pointer(value_type),
                                            value_el_type, false);
                                    llvm::Value* src_dim_ptr = arr_descr->get_pointer_to_dimension_descriptor_array(
                                        src_array_desc_type, llvm_value);
                                    llvm::Value* target_dim_ptr = arr_descr->get_pointer_to_dimension_descriptor_array(
                                        llvm_target_type, llvm_target_);
                                    llvm::DataLayout data_layout(module->getDataLayout());
                                    int dim_desc_size = (int)data_layout.getTypeAllocSize(dim_desc_type);
                                    builder->CreateMemCpy(target_dim_ptr, llvm::MaybeAlign(8), src_dim_ptr, llvm::MaybeAlign(8),
                                        dim_desc_size*(int)n_dims);

                                    llvm::Value* src_offset = arr_descr->get_offset(src_array_desc_type, llvm_value, false);
                                    llvm::Value* target_offset = arr_descr->get_offset(llvm_target_type, llvm_target_, false);
                                    builder->CreateStore(
                                        llvm_utils->CreateLoad2(arr_descr->get_index_type(), src_offset),
                                        target_offset);
                                }

                                llvm_value = llvm_target_;
                                break;
                            }
                            if(ASRUtils::extract_physical_type(value_type) == ASR::AssumedRankArray) {n_dims = 15;}
                            if(ASRUtils::is_array_physically_descriptor(value_type)){
                                    // Declare llvm type for (Array descriptor, Dimension Descriptor)
                                    llvm::Type* const value_array_desc_type = llvm_utils->arr_api->
                                        get_array_type(x.m_value, ASRUtils::type_get_past_allocatable_pointer(value_type),
                                            llvm_utils->get_el_type(x.m_value, ASRUtils::extract_type(value_type), module.get()), false);
                                    llvm::Type* const target_array_desc_type = llvm_utils->arr_api->
                                        get_array_type(x.m_target, ASRUtils::type_get_past_allocatable_pointer(target_type),
                                            llvm_utils->get_el_type(x.m_target, ASRUtils::extract_type(target_type), module.get()), false);
                                    LCOMPILERS_ASSERT(value_array_desc_type->isStructTy());
                                    LCOMPILERS_ASSERT(target_array_desc_type->isStructTy());
                                    llvm::Type* const dim_desc_type = llvm_utils->arr_api->get_dimension_descriptor_type(false);
                                    llvm::Value* const value_rank = arr_descr->get_rank(
                                        value_array_desc_type, llvm_value);
                                    // If the target descriptor pointer is NULL, allocate one, then re-load.
                                    llvm::Value* target_desc_check = llvm_utils->CreateLoad2(
                                        target_array_desc_type->getPointerTo(), llvm_target);
                                    llvm_utils->create_if_else(
                                        builder->CreateICmpEQ(
                                            target_desc_check,
                                            llvm::ConstantPointerNull::get(target_array_desc_type->getPointerTo())),
                                        [&]() {
                                            llvm::Value* new_desc = arr_descr->create_descriptor_alloca(target_array_desc_type);
                                            builder->CreateMemSet(new_desc, llvm::ConstantInt::get(context, llvm::APInt(8, 0)),
                                                 llvm::DataLayout(module->getDataLayout()).getTypeAllocSize(new_desc->getType()), llvm::MaybeAlign());
                                            arr_descr->set_rank(target_array_desc_type, new_desc, value_rank);
                                            builder->CreateStore(new_desc, llvm_target);
                                        },
                                        []() {});
                                    llvm::Value* llvm_target_ = builder->CreateLoad(target_array_desc_type->getPointerTo(), llvm_target);

                                    // Get element types and check if source is a class wrapper
                                    llvm::Type* value_el_type = llvm_utils->get_el_type(x.m_value,
                                            ASRUtils::extract_type(value_type), module.get());
                                    llvm::Type* target_el_type = llvm_utils->get_el_type(x.m_target,
                                            ASRUtils::extract_type(target_type), module.get());
                                    
                                    // Check if source is a class type (has wrapper with vptr + data pointer)
                                    // and target is a concrete/derived type (not a class wrapper)
                                    bool source_is_class = ASRUtils::is_class_type(ASRUtils::extract_type(value_type));
                                    bool target_is_class = ASRUtils::is_class_type(ASRUtils::extract_type(target_type));
                                    
                                    llvm::Value* value_data_ptr = arr_descr->get_pointer_to_data(value_array_desc_type, llvm_value);
                                    llvm::Value* target_data_ptr = arr_descr->get_pointer_to_data(target_array_desc_type, llvm_target_);
                                    llvm::Value* loaded_data_ptr = builder->CreateLoad(value_el_type->getPointerTo(), value_data_ptr);
                                    
                                    if (source_is_class && !target_is_class) {
                                        // ONE-wrapper layout: extract data pointer from wrapper field 1.
                                        // For unlimited polymorphic, data is already contiguous.
                                        if (ASRUtils::is_unlimited_polymorphic_type(value_type)) {
                                            loaded_data_ptr = llvm_utils->CreateLoad2(
                                                llvm_utils->i8_ptr,
                                                llvm_utils->create_gep2(value_el_type, loaded_data_ptr, 1));
                                            // Character arrays: the ONE-wrapper data is an array of N
                                            // individual string_descriptors (from the copy function),
                                            // but character(*) access expects a single descriptor with
                                            // a flat contiguous buffer. Consolidate here.
                                            ASR::ttype_t* target_base = ASRUtils::extract_type(
                                                ASRUtils::type_get_past_allocatable_pointer(target_type));
                                            if (ASR::is_a<ASR::String_t>(*target_base)) {
                                                llvm::Value* n_elems = arr_descr->get_array_size(
                                                    value_array_desc_type, llvm_value, nullptr, 4);
                                                llvm::Value* n_elems_i64 = builder->CreateSExt(
                                                    n_elems, llvm::Type::getInt64Ty(context));
                                                llvm::Value* orig_descs = loaded_data_ptr;
                                                loaded_data_ptr = llvm_utils->consolidate_char_descriptors(
                                                    loaded_data_ptr, n_elems_i64);
                                                llvm::Value* consolidated_flat_buf = llvm_utils->CreateLoad2(
                                                    character_type,
                                                    llvm_utils->create_gep2(string_descriptor, loaded_data_ptr, 0));
                                                heap_fixed_size_arrays.push_back(al, consolidated_flat_buf);
                                                pending_char_writebacks.push_back(
                                                    {orig_descs, loaded_data_ptr, n_elems_i64});
                                            } else {
                                                loaded_data_ptr = builder->CreateBitCast(
                                                    loaded_data_ptr, target_el_type->getPointerTo());
                                            }
                                        } else {
                                            llvm::Value* wrapper_data_field = llvm_utils->create_gep2(value_el_type, loaded_data_ptr, 1);
                                            llvm::Type* wrapper_data_type = llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(
                                                ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_value))), module.get(), true);
                                            llvm::Value* base_data_ptr = llvm_utils->CreateLoad2(wrapper_data_type, wrapper_data_field);
                                            loaded_data_ptr = builder->CreateBitCast(base_data_ptr, target_el_type->getPointerTo());
                                        }
                                        builder->CreateStore(loaded_data_ptr, target_data_ptr);
                                    } else if (source_is_class && target_is_class) { // Check wrapper (allocate if needed) + memCpy wrapper
                                        llvm_utils->create_if_else(
                                            builder->CreateIsNull(llvm_utils->CreateLoad2(target_el_type->getPointerTo(), target_data_ptr)), 
                                                [&]() {
                                                llvm::Value* new_wrapper =
                                                    llvm_utils->alloc_zeroed_type(target_el_type);
                                                builder->CreateStore(new_wrapper, target_data_ptr);
                                            }, []() {});
                                        llvm::Value* target_wrapper = llvm_utils->CreateLoad2(target_el_type->getPointerTo(), target_data_ptr);
                                        llvm::DataLayout data_layout(module->getDataLayout());
                                        uint64_t wrapper_size = data_layout.getTypeAllocSize(target_el_type);
                                        builder->CreateMemCpy(
                                            target_wrapper, llvm::MaybeAlign(),
                                            loaded_data_ptr, llvm::MaybeAlign(),
                                            llvm::ConstantInt::get(
                                                llvm::Type::getInt64Ty(context), wrapper_size));
                                    } else {
                                        // Simple bitcast if element types differ
                                        loaded_data_ptr = builder->CreateBitCast(loaded_data_ptr, target_el_type->getPointerTo());
                                        builder->CreateStore(loaded_data_ptr, target_data_ptr);
                                    }
                                    
                                    // Deep Copy dimension descriptors (inline)
                                    llvm::Value* value_dim_ptr = arr_descr->get_pointer_to_dimension_descriptor_array(value_array_desc_type, llvm_value);
                                    llvm::Value* target_dim_ptr = arr_descr->get_pointer_to_dimension_descriptor_array(target_array_desc_type, llvm_target_);
                                    llvm::DataLayout data_layout(module->getDataLayout());
                                    int dim_desc_size = (int)data_layout.getTypeAllocSize(dim_desc_type);
                                    builder->CreateMemCpy(target_dim_ptr, llvm::MaybeAlign(8), value_dim_ptr, llvm::MaybeAlign(8), dim_desc_size*n_dims);
                                    arr_descr->set_rank(target_array_desc_type, llvm_target_, value_rank);
                                    // Copy offset
                                    llvm::Value* src_offset = arr_descr->get_offset(value_array_desc_type, llvm_value);
                                    builder->CreateStore(src_offset,
                                        arr_descr->get_offset(target_array_desc_type, llvm_target_, false));
                                    // Other fields of the array descriptor should be already set.
                                    return;
                            } else if( ASRUtils::extract_physical_type(value_type) == ASR::array_physical_typeType::FixedSizeArray ) {
                                llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_value,
                                    value_type, module.get());
                                llvm_value = llvm_utils->create_gep2(llvm_type, llvm_value, 0);
                            }
                            llvm::Type* llvm_target_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, target_type_, module.get());
                            ASR::dimension_t* m_dims = nullptr;
                            size_t n_dims = ASRUtils::extract_dimensions_from_ttype(value_type, m_dims);
                            llvm::Value* llvm_target_ = arr_descr->create_descriptor_alloca(
                                llvm_target_type);
                            ASR::ttype_t* data_type = ASRUtils::duplicate_type_without_dims(
                                                        al, target_type_, target_type_->base.loc);
                            // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                            llvm::Type* llvm_data_type = llvm_utils->get_el_type(x.m_target, data_type, module.get());
                            fill_array_details(llvm_target_type, llvm_target_, llvm_data_type, m_dims, n_dims, false, false);
                            llvm::Value* target_data_ptr = arr_descr->get_pointer_to_data(llvm_target_type, llvm_target_);
                            llvm_value = llvm_utils->CreateBitCastForStore(llvm_value, target_data_ptr);
                            builder->CreateStore(llvm_value, target_data_ptr);
                            llvm_value = llvm_target_;
                            break;
                        }
                        case ASR::array_physical_typeType::FixedSizeArray: {
                            llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_value,value_type, module.get());
                            llvm_value = llvm_utils->CreateLoad2(llvm_type, llvm_value);
                            break;
                        }
                        case ASR::array_physical_typeType::PointerArray: {
                            break;
                        }
                        case ASR::array_physical_typeType::UnboundedPointerArray: {
                            break;
                        }
                        default: {
                            LCOMPILERS_ASSERT(false);
                        }
                    }
                }
                if( LLVM::is_llvm_pointer(*target_type) &&
                    !LLVM::is_llvm_pointer(*value_type) &&
                    !is_value_data_only_array &&
                    !llvm_value->getType()->isPointerTy() ) {
                    llvm::Type* llvm_target_contents_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_target, ASRUtils::type_get_past_pointer(target_type), module.get());
                    llvm::Value* loaded_target = llvm_utils->CreateLoad2(
                        llvm_target_contents_type->getPointerTo(), llvm_target);
                    builder->CreateStore(llvm_value, loaded_target);
                } else {
                    builder->CreateStore(llvm_value, llvm_target);
                }
            }
        }
    }

    void handle_StringSection_Assignment(ASR::expr_t *target, ASR::expr_t *value) {
        // Handles the case when LHS of assignment is string.
        std::string runtime_func_name = "_lfortran_str_slice_assign_alloc";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    character_type, {
                        character_type,
                        character_type, llvm::Type::getInt64Ty(context),
                        character_type, llvm::Type::getInt64Ty(context),
                        llvm::Type::getInt32Ty(context),
                        llvm::Type::getInt32Ty(context), llvm::Type::getInt32Ty(context),
                        llvm::Type::getInt1Ty(context), llvm::Type::getInt1Ty(context)
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        ASR::StringSection_t *ss = ASR::down_cast<ASR::StringSection_t>(target);
        llvm::Value *lp, *rp;
        llvm::Value *str, *idx1, *idx2, *step, *str_val;
        visit_expr_load_wrapper(ss->m_arg, 0,true);
        str = tmp;
        visit_expr_load_wrapper(value, 0,true);
        str_val = tmp;
        if (ss->m_start) {
            this->visit_expr_wrapper(ss->m_start, true);
            idx1 = tmp;
            lp = llvm::ConstantInt::get(context,
                llvm::APInt(1, 1));
        } else {
            lp = llvm::ConstantInt::get(context,
                llvm::APInt(1, 0));
            idx1 = llvm::Constant::getNullValue(llvm::Type::getInt32Ty(context));
        }
        if (ss->m_end) {
            this->visit_expr_wrapper(ss->m_end, true);
            idx2 = tmp;
            rp = llvm::ConstantInt::get(context,
                llvm::APInt(1, 1));
        } else {
            rp = llvm::ConstantInt::get(context,
                llvm::APInt(1, 0));
            idx2 = llvm::Constant::getNullValue(llvm::Type::getInt32Ty(context));
        }
        if (ss->m_step) {
            this->visit_expr_wrapper(ss->m_step, true);
            step = tmp;
        } else {
            step = llvm::ConstantInt::get(context,
                llvm::APInt(32, 0));
        }
        llvm::Value* str_data, *str_len;
        std::tie(str_data, str_len) = llvm_utils->get_string_length_data(ASRUtils::get_string_type(ss->m_arg), str);

        llvm::Value* str_val_data, *str_val_len;
        std::tie(str_val_data, str_val_len) = llvm_utils->get_string_length_data(ASRUtils::get_string_type(value), str_val);

        llvm::Value* allocator = llvm_utils->get_allocator(module.get());
        tmp = builder->CreateCall(fn, {allocator, str_data, str_len, str_val_data, str_val_len, idx1, idx2, step, lp, rp});

        llvm::Value* str_arg_data {};
        llvm::Value* str_arg_len  {};
        std::tie(str_arg_data, str_arg_len) = llvm_utils->get_string_length_data(ASRUtils::get_string_type(ss->m_arg), str);
        tmp = builder->CreateMemCpy(
            str_arg_data,llvm::MaybeAlign(8),
            tmp, llvm::MaybeAlign(8),
            str_arg_len);
    }

    void visit_OverloadedStringConcat(const ASR::OverloadedStringConcat_t &x) {
        LCOMPILERS_ASSERT(x.m_overloaded != nullptr)
        this->visit_expr(*x.m_overloaded);
    }

    // Widen an i1 comparison result to the full logical kind width before
    // storing into a logical variable.  When the value is already the right
    // width (or not i1) this is a no-op.
    llvm::Value* logical_store_val(llvm::Value* v, ASR::expr_t* target) {
        if (v && v->getType()->isIntegerTy(1) && target) {
            ASR::ttype_t* t = ASRUtils::expr_type(target);
            if (ASRUtils::is_logical(*t)) {
                int kind = ASRUtils::extract_kind_from_ttype_t(t);
                return builder->CreateZExt(v, llvm_utils->getIntType(kind));
            }
        }
        return v;
    }

    llvm::Value* logical_load_val(llvm::Value* ptr, ASR::expr_t* x,
                                  bool is_volatile = false) {
        llvm::Type* t = llvm_utils->get_type_from_ttype_t_util(
            x, ASRUtils::expr_type(x), module.get());
        return llvm_utils->CreateLoad2(t, ptr, is_volatile);
    }

    void visit_Assignment(const ASR::Assignment_t &x) {
        DeallocateStringsScope _scope(this);
        if (compiler_options.emit_debug_info) debug_emit_loc(x);

        // Special-case: transfer(character, int8_array, size) lowered as BitCast.
        // When scalarized into element-wise assignments, extract the corresponding
        // byte from the source string.
        if (ASR::is_a<ASR::BitCast_t>(*x.m_value)) {
            ASR::BitCast_t* bc = ASR::down_cast<ASR::BitCast_t>(x.m_value);
            if (ASR::is_a<ASR::ArrayItem_t>(*x.m_target) &&
                ASRUtils::is_integer(*ASRUtils::expr_type(x.m_target)) &&
                ASR::down_cast<ASR::Integer_t>(ASRUtils::expr_type(x.m_target))->m_kind == 1 &&
                ASRUtils::is_string_only(ASRUtils::expr_type(bc->m_source))) {
                ASR::ArrayItem_t* ai = ASR::down_cast<ASR::ArrayItem_t>(x.m_target);
                if (ai->n_args == 1 && ai->m_args[0].m_right) {
                    bool is_assignment_target_copy = is_assignment_target;
                    is_assignment_target = true;
                    visit_expr(*x.m_target);
                    is_assignment_target = is_assignment_target_copy;
                    llvm::Value* dest_ptr = builder->CreateBitCast(tmp, llvm_utils->i8_ptr);

                    int64_t ptr_loads_copy = ptr_loads;
                    ptr_loads = 0;
                    visit_expr_wrapper(bc->m_source, true);
                    ptr_loads = ptr_loads_copy;
                    llvm::Value* src_desc = tmp;
                    llvm::Value* src_data = llvm_utils->get_string_data(
                        ASRUtils::get_string_type(bc->m_source), src_desc);
                    src_data = builder->CreateBitCast(src_data, llvm_utils->i8_ptr);

                    visit_expr_wrapper(ai->m_args[0].m_right, true);
                    llvm::Value* idx = tmp;
                    idx = builder->CreateZExtOrTrunc(idx, llvm::Type::getInt64Ty(context));
                    llvm::Value* zero_based = builder->CreateSub(idx, llvm::ConstantInt::get(idx->getType(), 1));
                    llvm::Value* src_byte_ptr = builder->CreateGEP(
                        llvm::Type::getInt8Ty(context), src_data, zero_based);
                    llvm::Value* byte_val = llvm_utils->CreateLoad2(
                        llvm::Type::getInt8Ty(context), src_byte_ptr);
                    builder->CreateStore(byte_val, dest_ptr);
                    return;
                }
            }

            ASR::ttype_t* target_type = ASRUtils::expr_type(x.m_target);
            ASR::ttype_t* target_type_past_alloc =
                ASRUtils::type_get_past_allocatable_pointer(target_type);
            if (ASRUtils::is_array(target_type_past_alloc) &&
                ASRUtils::extract_physical_type(target_type_past_alloc) ==
                    ASR::array_physical_typeType::DescriptorArray) {
                ASR::ttype_t* elem_type = ASRUtils::extract_type(target_type_past_alloc);
                bool is_int8 = ASRUtils::is_integer(*elem_type) &&
                               ASR::down_cast<ASR::Integer_t>(elem_type)->m_kind == 1;
                bool src_is_string = ASRUtils::is_string_only(ASRUtils::expr_type(bc->m_source));
                bool is_whole_array_target = (x.m_target->type == ASR::exprType::Var ||
                                              x.m_target->type == ASR::exprType::StructInstanceMember);
                if (is_int8 && src_is_string && is_whole_array_target) {
                    bool is_assignment_target_copy = is_assignment_target;
                    is_assignment_target = true;
                    visit_expr(*x.m_target);
                    is_assignment_target = is_assignment_target_copy;
                    llvm::Value* target = tmp;

                    visit_ArrayPhysicalCastUtil(
                        target, x.m_target,
                        target_type_past_alloc, target_type_past_alloc,
                        ASR::array_physical_typeType::DescriptorArray,
                        ASR::array_physical_typeType::PointerArray);
                    llvm::Value* dest_data = builder->CreateBitCast(tmp, llvm_utils->i8_ptr);

                    int64_t ptr_loads_copy = ptr_loads;
                    ptr_loads = 0;
                    visit_expr_wrapper(bc->m_source, true);
                    ptr_loads = ptr_loads_copy;
                    llvm::Value* src_desc = tmp;
                    llvm::Value* src_data = llvm_utils->get_string_data(
                        ASRUtils::get_string_type(bc->m_source), src_desc);
                    src_data = builder->CreateBitCast(src_data, llvm_utils->i8_ptr);

                    llvm::Value* nbytes = nullptr;
                    if (bc->m_size) {
                        visit_expr_wrapper(bc->m_size, true);
                        nbytes = tmp;
                    } else {
                        nbytes = llvm_utils->get_string_length(
                            ASRUtils::get_string_type(bc->m_source), src_desc);
                    }
                    if (nbytes->getType()->isIntegerTy(32)) {
                        nbytes = builder->CreateZExt(nbytes, llvm::Type::getInt64Ty(context));
                    } else if (!nbytes->getType()->isIntegerTy(64)) {
                        nbytes = builder->CreateIntCast(nbytes, llvm::Type::getInt64Ty(context), false);
                    }

                    builder->CreateMemCpy(
                        dest_data, llvm::MaybeAlign(1),
                        src_data, llvm::MaybeAlign(1),
                        nbytes);
                    return;
                }
            }

            if (bc->m_size != nullptr &&
                ASRUtils::is_array(ASRUtils::expr_type(bc->m_source)) &&
                !ASRUtils::is_string_only(ASRUtils::expr_type(bc->m_source))) {
                int64_t ptr_loads_copy = ptr_loads;

                ASR::ttype_t* source_type = ASRUtils::expr_type(bc->m_source);
                ASR::ttype_t* source_type_ = ASRUtils::type_get_past_allocatable(
                    ASRUtils::type_get_past_pointer(source_type));
                ASR::array_physical_typeType source_ptype =
                    ASRUtils::extract_physical_type(source_type_);

                ptr_loads = (source_ptype ==
                    ASR::array_physical_typeType::DescriptorArray) ? 1 : 0;
                visit_expr(*bc->m_source);
                llvm::Value* source_ptr = tmp;

                llvm::Value* src_data = nullptr;
                if (source_ptype == ASR::array_physical_typeType::FixedSizeArray) {
                    llvm::Type* src_llvm = llvm_utils->get_type_from_ttype_t_util(
                        bc->m_source, source_type_, module.get());
                    src_data = llvm_utils->create_gep2(src_llvm, source_ptr, 0);
                } else if (source_ptype ==
                        ASR::array_physical_typeType::DescriptorArray) {
                    llvm::Type* src_el = llvm_utils->get_type_from_ttype_t_util(
                        bc->m_source, ASRUtils::extract_type(source_type_),
                        module.get());
                    llvm::Value* data_ptr_ptr = arr_descr->get_pointer_to_data(
                        bc->m_source, source_type, source_ptr, module.get());
                    src_data = llvm_utils->CreateLoad2(
                        src_el->getPointerTo(), data_ptr_ptr);
                    llvm::Type* src_desc = llvm_utils->get_type_from_ttype_t_util(
                        bc->m_source, source_type_, module.get());
                    llvm::Value* offset = arr_descr->get_offset(
                        src_desc, source_ptr);
                    src_data = llvm_utils->create_ptr_gep2(
                        src_el, src_data, offset);
                } else {
                    src_data = source_ptr;
                }
                src_data = builder->CreateBitCast(
                    src_data, llvm::Type::getInt8Ty(context)->getPointerTo());

                ASR::ttype_t* tgt_type_ = ASRUtils::type_get_past_allocatable(
                    ASRUtils::type_get_past_pointer(target_type));
                ASR::array_physical_typeType tgt_ptype =
                    ASRUtils::extract_physical_type(tgt_type_);

                bool is_assignment_target_copy = is_assignment_target;
                is_assignment_target = true;
                ptr_loads = (tgt_ptype ==
                    ASR::array_physical_typeType::DescriptorArray) ? 1 : 0;
                visit_expr(*x.m_target);
                is_assignment_target = is_assignment_target_copy;
                llvm::Value* target_ptr = tmp;
                ptr_loads = ptr_loads_copy;

                llvm::Value* dest_data = nullptr;
                if (tgt_ptype == ASR::array_physical_typeType::FixedSizeArray) {
                    llvm::Type* tgt_llvm = llvm_utils->get_type_from_ttype_t_util(
                        x.m_target, tgt_type_, module.get());
                    dest_data = llvm_utils->create_gep2(
                        tgt_llvm, target_ptr, 0);
                } else if (tgt_ptype ==
                        ASR::array_physical_typeType::DescriptorArray) {
                    llvm::Type* tgt_el = llvm_utils->get_type_from_ttype_t_util(
                        x.m_target, ASRUtils::extract_type(tgt_type_),
                        module.get());
                    llvm::Value* data_ptr_ptr = arr_descr->get_pointer_to_data(
                        x.m_target, target_type, target_ptr, module.get());
                    dest_data = llvm_utils->CreateLoad2(
                        tgt_el->getPointerTo(), data_ptr_ptr);
                    llvm::Type* tgt_desc = llvm_utils->get_type_from_ttype_t_util(
                        x.m_target, tgt_type_, module.get());
                    llvm::Value* offset = arr_descr->get_offset(
                        tgt_desc, target_ptr);
                    dest_data = llvm_utils->create_ptr_gep2(
                        tgt_el, dest_data, offset);
                } else {
                    dest_data = target_ptr;
                }
                dest_data = builder->CreateBitCast(
                    dest_data, llvm::Type::getInt8Ty(context)->getPointerTo());

                visit_expr_wrapper(bc->m_size, true);
                llvm::Value* size_val = tmp;
                ASR::ttype_t* tgt_elem_type = ASRUtils::extract_type(tgt_type_);
                llvm::Type* tgt_elem_llvm_type =
                    llvm_utils->get_type_from_ttype_t_util(
                        x.m_target, tgt_elem_type, module.get());
                llvm::DataLayout data_layout(module->getDataLayout());
                uint64_t target_elem_size =
                    data_layout.getTypeAllocSize(tgt_elem_llvm_type);
                llvm::Value* nbytes = builder->CreateMul(
                    size_val,
                    llvm::ConstantInt::get(
                        size_val->getType(), target_elem_size));
                nbytes = builder->CreateZExtOrTrunc(
                    nbytes, llvm::Type::getInt64Ty(context));

                builder->CreateMemCpy(
                    dest_data, llvm::MaybeAlign(1),
                    src_data, llvm::MaybeAlign(1),
                    nbytes);
                return;
            }

            if (ASR::is_a<ASR::ArrayItem_t>(*x.m_target) &&
                !ASRUtils::is_string_only(ASRUtils::expr_type(bc->m_source)) &&
                !ASRUtils::is_array(ASRUtils::expr_type(bc->m_source)) &&
                ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(bc->m_source)) !=
                    ASRUtils::extract_kind_from_ttype_t(
                        ASRUtils::type_get_past_array(bc->m_type))) {
                ASR::ArrayItem_t* ai = ASR::down_cast<ASR::ArrayItem_t>(x.m_target);
                if (ai->n_args == 1 && ai->m_args[0].m_right) {
                    bool is_assignment_target_copy = is_assignment_target;
                    is_assignment_target = true;
                    visit_expr(*x.m_target);
                    is_assignment_target = is_assignment_target_copy;
                    llvm::Value* dest_ptr = tmp;

                    visit_expr_wrapper(ai->m_args[0].m_right, true);
                    llvm::Value* idx = tmp;

                    visit_expr_wrapper(bc->m_source, true);
                    llvm::Value* source_val = tmp;
                    llvm::Value* source_alloca;
                    if (source_val->getType()->isPointerTy()) {
                        source_alloca = source_val;
                    } else {
                        source_alloca = builder->CreateAlloca(
                            source_val->getType(), nullptr, "transfer_source");
                        builder->CreateStore(source_val, source_alloca);
                    }

                    llvm::Type* elem_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_target,
                        ASRUtils::type_get_past_array(bc->m_type),
                        module.get());
                    llvm::Value* src_as_elem_ptr = builder->CreateBitCast(
                        source_alloca, elem_type->getPointerTo());

                    // Map target index to zero-based source chunk index using
                    // the target array lower bound (not always 1).
                    llvm::Value* lower_bound = llvm::ConstantInt::get(
                        idx->getType(), 1);
                    ASR::dimension_t* target_dims = nullptr;
                    size_t n_target_dims = ASRUtils::extract_dimensions_from_ttype(
                        ASRUtils::expr_type(ai->m_v), target_dims);
                    if (n_target_dims > 0 && target_dims[0].m_start) {
                        int ptr_loads_copy = ptr_loads;
                        ptr_loads = 2 - !LLVM::is_llvm_pointer(
                            *ASRUtils::expr_type(target_dims[0].m_start));
                        this->visit_expr_wrapper(target_dims[0].m_start, true);
                        ptr_loads = ptr_loads_copy;
                        lower_bound = builder->CreateSExtOrTrunc(tmp, idx->getType());
                    }
                    llvm::Value* zero_based = builder->CreateSub(
                        idx, lower_bound);
                    zero_based = builder->CreateZExtOrTrunc(
                        zero_based, llvm::Type::getInt32Ty(context));
                    llvm::Value* elem_ptr = builder->CreateGEP(
                        elem_type, src_as_elem_ptr, zero_based);
                    llvm::Value* elem_val = llvm_utils->CreateLoad2(
                        elem_type, elem_ptr);
                    builder->CreateStore(elem_val, dest_ptr);
                    return;
                }
            }

            // Whole-array BitCast with different element sizes (e.g.,
            // transfer(int32_array, int8_mold)).  The array_op pass left this
            // assignment un-scalarised because the source and result have
            // different element counts.  Lower it as a memcpy.
            ASR::ttype_t* bc_src_type = ASRUtils::expr_type(bc->m_source);
            ASR::ttype_t* bc_tgt_type = ASRUtils::expr_type(x.m_target);
            if (ASRUtils::is_array(bc_src_type) && ASRUtils::is_array(bc_tgt_type) &&
                !ASRUtils::is_string_only(bc_src_type)) {
                int64_t src_elem_size = ASRUtils::get_type_byte_size(
                    ASRUtils::extract_type(bc_src_type));

                // Visit the source to get both the descriptor and data pointer
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = (ASRUtils::extract_physical_type(bc_src_type) ==
                    ASR::array_physical_typeType::DescriptorArray) ? 1 : 0;
                visit_expr_wrapper(bc->m_source, true);
                ptr_loads = ptr_loads_copy;
                llvm::Value* src_ptr = tmp;
                if (!src_ptr->getType()->isPointerTy()) {
                    llvm::Value* alloca_ = llvm_utils->CreateAlloca(
                        src_ptr->getType(), nullptr, "transfer_src");
                    builder->CreateStore(src_ptr, alloca_);
                    src_ptr = alloca_;
                }
                llvm::Value* src_desc = src_ptr;

                // Extract data pointer from descriptor arrays
                ASR::ttype_t* bc_src_type_unwrapped = ASRUtils::type_get_past_allocatable_pointer(bc_src_type);
                bool is_descriptor_src = false;
                llvm::Type* llvm_src_arr_type = nullptr;
                if (ASR::is_a<ASR::Array_t>(*bc_src_type_unwrapped)) {
                    ASR::Array_t* arr = ASR::down_cast<ASR::Array_t>(bc_src_type_unwrapped);
                    if (arr->m_physical_type == ASR::array_physical_typeType::DescriptorArray) {
                        is_descriptor_src = true;
                        llvm_src_arr_type = llvm_utils->get_type_from_ttype_t_util(
                            bc->m_source, bc_src_type_unwrapped, module.get());
                        llvm::Type* llvm_src_elem_type = llvm_utils->get_type_from_ttype_t_util(
                            bc->m_source, ASRUtils::extract_type(bc_src_type), module.get());
                        src_ptr = llvm_utils->create_gep2(llvm_src_arr_type, src_ptr, 0);
                        src_ptr = builder->CreateLoad(
                            llvm_src_elem_type->getPointerTo(), src_ptr);
                    }
                }

                // Get pointer to target array
                bool is_assignment_target_copy = is_assignment_target;
                is_assignment_target = true;
                visit_expr(*x.m_target);
                is_assignment_target = is_assignment_target_copy;
                llvm::Value* dest_ptr = tmp;

                // Extract data pointer from descriptor target arrays
                ASR::ttype_t* bc_tgt_type_unwrapped = ASRUtils::type_get_past_allocatable_pointer(bc_tgt_type);
                if (ASR::is_a<ASR::Array_t>(*bc_tgt_type_unwrapped)) {
                    ASR::Array_t* tgt_arr = ASR::down_cast<ASR::Array_t>(bc_tgt_type_unwrapped);
                    if (tgt_arr->m_physical_type == ASR::array_physical_typeType::DescriptorArray) {
                        bool created_alloca = false;
                        if (!dest_ptr->getType()->isPointerTy()) {
                            llvm::Value* alloca_ = llvm_utils->CreateAlloca(
                                dest_ptr->getType(), nullptr, "transfer_dest");
                            builder->CreateStore(dest_ptr, alloca_);
                            dest_ptr = alloca_;
                            created_alloca = true;
                        }
                        llvm::Type* llvm_tgt_elem_type = llvm_utils->get_type_from_ttype_t_util(
                            x.m_target, ASRUtils::extract_type(bc_tgt_type), module.get());
                        llvm::Type* llvm_tgt_arr_type = arr_descr->get_array_type(
                            x.m_target, bc_tgt_type_unwrapped,
                            llvm_tgt_elem_type, false);
                        // Handle extra indirection for allocatable/pointer members.
                        // Skip when we created an alloca from a loaded struct value:
                        // the alloca already points to the descriptor, so dereferencing
                        // through the allocatable pointer would double-dereference.
                        if (LLVM::is_llvm_pointer(*bc_tgt_type) && !created_alloca) {
                            dest_ptr = builder->CreateLoad(llvm_tgt_arr_type->getPointerTo(), dest_ptr);
                        }
                        dest_ptr = arr_descr->get_pointer_to_data(llvm_tgt_arr_type, dest_ptr);
                        dest_ptr = builder->CreateLoad(
                            llvm_tgt_elem_type->getPointerTo(), dest_ptr);
                    }
                }

                llvm::Value* src_i8 = builder->CreateBitCast(
                    src_ptr, llvm::Type::getInt8Ty(context)->getPointerTo());
                llvm::Value* dest_i8 = builder->CreateBitCast(
                    dest_ptr, llvm::Type::getInt8Ty(context)->getPointerTo());

                // Compute byte count
                int64_t src_n = ASRUtils::get_fixed_size_of_array(bc_src_type);
                if (src_n > 0) {
                    // Fixed-size source: constant byte count
                    int64_t nbytes = src_elem_size * src_n;
                    builder->CreateMemCpy(
                        dest_i8, llvm::MaybeAlign(1),
                        src_i8, llvm::MaybeAlign(1),
                        llvm::ConstantInt::get(
                            llvm::Type::getInt64Ty(context), nbytes));
                } else if (is_descriptor_src && llvm_src_arr_type) {
                    // Runtime-sized descriptor source: get size from descriptor
                    llvm::Value* num_elements = llvm_utils->arr_api->get_array_size(
                        llvm_src_arr_type, src_desc, nullptr, 4);
                    llvm::Value* nbytes = builder->CreateMul(
                        builder->CreateZExtOrTrunc(num_elements,
                            llvm::Type::getInt64Ty(context)),
                        llvm::ConstantInt::get(
                            llvm::Type::getInt64Ty(context), src_elem_size));
                    builder->CreateMemCpy(
                        dest_i8, llvm::MaybeAlign(1),
                        src_i8, llvm::MaybeAlign(1),
                        nbytes);
                } else {
                    // Source is FixedSizeArray with runtime dimensions
                    // (e.g., after pass_array_by_data converted descriptor to
                    // data pointer). Evaluate the result type upper bound
                    // to get element count, then compute byte count.
                    ASR::ttype_t* bc_result_type = bc->m_type;
                    ASR::ttype_t* bc_result_unwrapped = ASRUtils::type_get_past_allocatable_pointer(bc_result_type);
                    int64_t result_elem_size = ASRUtils::get_type_byte_size(
                        ASRUtils::extract_type(bc_result_type));
                    bool emitted = false;
                    if (ASR::is_a<ASR::Array_t>(*bc_result_unwrapped)) {
                        ASR::Array_t* res_arr = ASR::down_cast<ASR::Array_t>(bc_result_unwrapped);
                        if (res_arr->n_dims > 0 && res_arr->m_dims[0].m_length) {
                            // Evaluate the dimension length expression
                            visit_expr_wrapper(res_arr->m_dims[0].m_length, true);
                            llvm::Value* n_elements = tmp;
                            llvm::Value* nbytes = builder->CreateMul(
                                builder->CreateZExtOrTrunc(n_elements,
                                    llvm::Type::getInt64Ty(context)),
                                llvm::ConstantInt::get(
                                    llvm::Type::getInt64Ty(context), result_elem_size));
                            builder->CreateMemCpy(
                                dest_i8, llvm::MaybeAlign(1),
                                src_i8, llvm::MaybeAlign(1),
                                nbytes);
                            emitted = true;
                        }
                    }
                    if (!emitted) {
                        // Last resort: use target size if fixed
                        int64_t tgt_elem_size = ASRUtils::get_type_byte_size(
                            ASRUtils::extract_type(bc_tgt_type));
                        int64_t tgt_n = ASRUtils::get_fixed_size_of_array(bc_tgt_type);
                        if (tgt_n > 0) {
                            int64_t nbytes = tgt_elem_size * tgt_n;
                            builder->CreateMemCpy(
                                dest_i8, llvm::MaybeAlign(1),
                                src_i8, llvm::MaybeAlign(1),
                                llvm::ConstantInt::get(
                                    llvm::Type::getInt64Ty(context), nbytes));
                        }
                    }
                }
                return;
            }
        }

        if( x.m_overloaded ) {
            this->visit_stmt(*x.m_overloaded);
            return ;
        }

        // ClassToIntrinsic store-through: when the LHS is a Cast(Var, ClassToIntrinsic, ...),
        // we extract the data pointer from the polymorphic wrapper and store the RHS into it.
        if (ASR::is_a<ASR::Cast_t>(*x.m_target) &&
            ASR::down_cast<ASR::Cast_t>(x.m_target)->m_kind ==
                ASR::cast_kindType::ClassToIntrinsic) {
            ASR::Cast_t* target_cast = ASR::down_cast<ASR::Cast_t>(x.m_target);
            ASR::ttype_t* target_type = target_cast->m_type;   // e.g., integer(4)
            ASR::ttype_t* poly_type = ASRUtils::expr_type(target_cast->m_arg); // class(*)

            // 1. Evaluate RHS
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 2;  // load to value
            this->visit_expr_wrapper(x.m_value, true);
            llvm::Value* rhs_value = tmp;

            // 2. Evaluate LHS (the class(*) variable — NOT the Cast)
            ptr_loads = 0;  // get pointer to polymorphic wrapper
            this->visit_expr(*target_cast->m_arg);
            llvm::Value* poly_ptr = tmp;
            ptr_loads = ptr_loads_copy;

            // 3. Handle allocatable/pointer extra indirection
            llvm::Type* poly_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                target_cast->m_arg, ASRUtils::extract_type(poly_type), module.get());
            if (LLVM::is_llvm_pointer(*poly_type)) {
                poly_ptr = llvm_utils->CreateLoad2(poly_llvm_type->getPointerTo(), poly_ptr);
            }

            // 4. GEP to data field (index 1), load i8*, bitcast, store
            llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                const_cast<ASR::expr_t*>(&target_cast->base),
                ASRUtils::extract_type(target_type), module.get());
            llvm::Value* data_ptr = llvm_utils->create_gep2(poly_llvm_type, poly_ptr, 1);
            data_ptr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, data_ptr);
            data_ptr = builder->CreateBitCast(data_ptr, target_llvm_type->getPointerTo());

            if (ASRUtils::is_character(*target_type)) {
                // String assignment needs lfortran_str_copy
                llvm_utils->deepcopy(x.m_value, rhs_value, data_ptr,
                    target_type, target_type, module.get());
            } else {
                builder->CreateStore(rhs_value, data_ptr);
            }
            return;
        }

        ASR::ttype_t* asr_target_type = ASRUtils::expr_type(x.m_target);
        ASR::ttype_t* asr_value_type = ASRUtils::expr_type(x.m_value);
        bool is_target_list = ASR::is_a<ASR::List_t>(*asr_target_type);
        bool is_value_list = ASR::is_a<ASR::List_t>(*asr_value_type);
        bool is_target_tuple = ASR::is_a<ASR::Tuple_t>(*asr_target_type);
        bool is_value_tuple = ASR::is_a<ASR::Tuple_t>(*asr_value_type);
        bool is_target_dict = ASR::is_a<ASR::Dict_t>(*asr_target_type);
        bool is_value_dict = ASR::is_a<ASR::Dict_t>(*asr_value_type);
        bool is_target_set = ASR::is_a<ASR::Set_t>(*asr_target_type);
        bool is_value_set = ASR::is_a<ASR::Set_t>(*asr_value_type);
        bool is_target_struct = ASR::is_a<ASR::StructType_t>(
            *ASRUtils::type_get_past_allocatable(asr_target_type)) &&
            !ASRUtils::is_class_type(ASRUtils::type_get_past_allocatable(asr_target_type));
        bool is_value_struct = ASR::is_a<ASR::StructType_t>(
            *ASRUtils::type_get_past_allocatable_pointer(asr_value_type)) &&
             !ASRUtils::is_class_type(ASRUtils::type_get_past_allocatable_pointer(asr_value_type));
        // a class variable is always either an allocatable, a pointer, or a dummy argument of a procedure
        bool is_target_class = ASRUtils::is_class_type(
                                   ASRUtils::type_get_past_allocatable_pointer(asr_target_type));
        bool is_value_class = ASRUtils::is_class_type(
                                  ASRUtils::type_get_past_allocatable_pointer(asr_value_type));
        bool is_target_unlimited_polymorphic = ASRUtils::is_unlimited_polymorphic_type(x.m_target);
        bool is_value_unlimited_polymorphic = ASRUtils::is_unlimited_polymorphic_type(x.m_value);
        bool is_value_list_to_array = (ASR::is_a<ASR::Cast_t>(*x.m_value) &&
            ASR::down_cast<ASR::Cast_t>(x.m_value)->m_kind == ASR::cast_kindType::ListToArray);

        llvm::Type* asr_target_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, asr_target_type, module.get());


        // When assigning to a StructInstanceMember, whose instance is allocatable
        // Check if the underlying struct instance is allocated, if not allocate
        if (ASR::is_a<ASR::StructInstanceMember_t>(*x.m_target) &&
            !ASRUtils::is_character(*asr_value_type)) {
            if (ASRUtils::is_allocatable(x.m_target) && !ASRUtils::is_array(asr_target_type)) {
                check_and_allocate_scalar(x.m_target, x.m_value, asr_value_type);
            }
        }

        if( is_target_list && is_value_list ) {
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr(*x.m_target);
            llvm::Value* target_list = tmp;
            this->visit_expr(*x.m_value);
            llvm::Value* value_list = tmp;
            ptr_loads = ptr_loads_copy;
            ASR::List_t* value_asr_list = ASR::down_cast<ASR::List_t>(
                                            ASRUtils::expr_type(x.m_value));
            std::string value_type_code = ASRUtils::get_type_code(value_asr_list->m_type);
            // Free old target data (and any heap resources owned by its
            // elements, e.g. allocatable strings) before deepcopy for
            // variables that were initialized via list_init
            // (Local/ReturnVar/unset intent) or that point to
            // caller-initialized data (Out/InOut intent).
            if (ASR::is_a<ASR::Var_t>(*x.m_target)) {
                ASR::Variable_t* v = ASR::down_cast<ASR::Variable_t>(
                    ASR::down_cast<ASR::Var_t>(x.m_target)->m_v);
                if (v->m_intent == ASR::intentType::Local ||
                    v->m_intent == ASR::intentType::ReturnVar ||
                    v->m_intent == ASR::intentType::Out ||
                    v->m_intent == ASR::intentType::InOut ||
                    !v->m_intent) {
                    llvm_symtab_finalizer.finalize_temporary(
                        target_list, ASRUtils::expr_type(x.m_target));
                }
            } else if (ASR::is_a<ASR::ListItem_t>(*x.m_target)) {
                // List element is being overwritten — free old element data
                // before deepcopy replaces it.
                llvm_symtab_finalizer.finalize_temporary(
                    target_list, ASRUtils::expr_type(x.m_target));
            }
            list_api->list_deepcopy(x.m_value, value_list, target_list,
                                    value_asr_list, module.get());
            // Free source temp data for ListConstant expressions only.
            // ListConstant always creates fresh allocated data that can be
            // safely freed after deepcopy. Other non-Var sources (e.g.,
            // ListItem, FunctionCall) may hold shared or scope-managed data.
            // finalize_temporary also frees heap resources owned by elements
            // (e.g. allocatable strings) that were deep-copied above.
            if (ASR::is_a<ASR::ListConstant_t>(*x.m_value)) {
                llvm_symtab_finalizer.finalize_temporary(
                    value_list, ASRUtils::expr_type(x.m_value));
            }
            return ;
        } else if( is_target_tuple && is_value_tuple ) {
            int64_t ptr_loads_copy = ptr_loads;
            if( ASR::is_a<ASR::TupleConstant_t>(*x.m_target) &&
                !ASR::is_a<ASR::TupleConstant_t>(*x.m_value) ) {
                ptr_loads = 0;
                this->visit_expr(*x.m_value);
                llvm::Value* value_tuple = tmp;
                ASR::TupleConstant_t* const_tuple = ASR::down_cast<ASR::TupleConstant_t>(x.m_target);
                ASR::Tuple_t* tuple_type = ASR::down_cast<ASR::Tuple_t>(const_tuple->m_type);
                for( size_t i = 0; i < const_tuple->n_elements; i++ ) {
                    ptr_loads = 0;
                    visit_expr(*const_tuple->m_elements[i]);
                    llvm::Value* target_ptr = tmp;
                    llvm::Value* item = tuple_api->read_item(value_tuple, tuple_type, i, false);
                    builder->CreateStore(item, target_ptr);
                }
                ptr_loads = ptr_loads_copy;
            } else if( ASR::is_a<ASR::TupleConstant_t>(*x.m_target) &&
                       ASR::is_a<ASR::TupleConstant_t>(*x.m_value) ) {
                ASR::TupleConstant_t* asr_value_tuple = ASR::down_cast<ASR::TupleConstant_t>(x.m_value);
                ASR::TupleConstant_t* asr_target_tuple = ASR::down_cast<ASR::TupleConstant_t>(x.m_target);
                Vec<llvm::Value*> src_deepcopies;
                src_deepcopies.reserve(al, asr_value_tuple->n_elements);
                for( size_t i = 0; i < asr_value_tuple->n_elements; i++ ) {
                    ASR::ttype_t* asr_tuple_i_type = ASRUtils::expr_type(asr_value_tuple->m_elements[i]);
                    llvm::Type* llvm_tuple_i_type = llvm_utils->get_type_from_ttype_t_util(asr_value_tuple->m_elements[i], asr_tuple_i_type, module.get());
                    llvm::Value* llvm_tuple_i = llvm_utils->CreateAlloca(*builder, llvm_tuple_i_type);
                    ptr_loads = !LLVM::is_llvm_struct(asr_tuple_i_type);
                    visit_expr(*asr_value_tuple->m_elements[i]);
                    llvm_utils->deepcopy(asr_value_tuple->m_elements[i], tmp, llvm_tuple_i, expr_type(asr_target_tuple->m_elements[i]), asr_tuple_i_type, module.get());
                    src_deepcopies.push_back(al, llvm_tuple_i);
                }
                for( size_t i = 0; i < asr_target_tuple->n_elements; i++ ) {
                    ptr_loads = 0;
                    visit_expr(*asr_target_tuple->m_elements[i]);
                    llvm::Type* tuple_elem_type = llvm_utils->get_type_from_ttype_t_util(asr_target_tuple->m_elements[i], ASRUtils::expr_type(asr_target_tuple->m_elements[i]), module.get());
                    LLVM::CreateStore(*builder,
                        llvm_utils->CreateLoad2(tuple_elem_type, src_deepcopies[i]),
                        tmp
                    );
                }
                ptr_loads = ptr_loads_copy;
            } else {
                ptr_loads = 0;
                this->visit_expr(*x.m_value);
                llvm::Value* value_tuple = tmp;
                this->visit_expr(*x.m_target);
                llvm::Value* target_tuple = tmp;
                ptr_loads = ptr_loads_copy;
                ASR::Tuple_t* value_tuple_type = ASR::down_cast<ASR::Tuple_t>(asr_value_type);
                std::string type_code = ASRUtils::get_type_code(value_tuple_type->m_type,
                                                                value_tuple_type->n_type);
                // Free old allocatable string data in the target tuple
                // before deepcopy overwrites the descriptors.
                // String members are always initialized at variable
                // declaration, so this is safe for Var targets.
                if (ASR::is_a<ASR::Var_t>(*x.m_target)) {
                    for (size_t i = 0; i < value_tuple_type->n_type; i++) {
                        if (ASRUtils::is_descriptorString(value_tuple_type->m_type[i]) &&
                                ASRUtils::is_deferredLength_string(value_tuple_type->m_type[i])) {
                            llvm::Type* el_type = llvm_utils->get_type_from_ttype_t_util(
                                nullptr, value_tuple_type->m_type[i], module.get());
                            llvm::Value* dest_item_ptr = tuple_api->read_item_using_pos(
                                el_type, target_tuple, value_tuple_type, i, true);
                            ASR::String_t *t = ASR::down_cast<ASR::String_t>(
                                ASRUtils::extract_type(value_tuple_type->m_type[i]));
                            llvm::Value* old_data = llvm_utils->get_string_data(t, dest_item_ptr);
                            llvm_utils->lfortran_free(old_data);
                        }
                    }
                }
                tuple_api->tuple_deepcopy(x.m_value, value_tuple, target_tuple,
                                          value_tuple_type, module.get());
                // Free source temporary's heap resources after deepcopy.
                if (is_owned_tuple_temporary(x.m_value)) {
                    llvm_symtab_finalizer.finalize_temporary(
                        value_tuple, asr_value_type);
                }
            }
            return ;
        } else if( is_target_dict && is_value_dict ) {
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr(*x.m_value);
            llvm::Value* value_dict = tmp;
            this->visit_expr(*x.m_target);
            llvm::Value* target_dict = tmp;
            ptr_loads = ptr_loads_copy;
            ASR::Dict_t* value_dict_type = ASR::down_cast<ASR::Dict_t>(asr_value_type);
            llvm_utils->set_dict_api(value_dict_type);

            std::string key_type_code = ASRUtils::get_type_code(value_dict_type->m_key_type);
            std::string value_type_code = ASRUtils::get_type_code(value_dict_type->m_value_type);
            // Free old target data before deepcopy
            if (ASR::is_a<ASR::Var_t>(*x.m_target)) {
                ASR::Variable_t* v = ASR::down_cast<ASR::Variable_t>(
                    ASR::down_cast<ASR::Var_t>(x.m_target)->m_v);
                if (v->m_intent == ASR::intentType::Local ||
                    v->m_intent == ASR::intentType::ReturnVar ||
                    v->m_intent == ASR::intentType::Out ||
                    v->m_intent == ASR::intentType::InOut ||
                    !v->m_intent) {
                    llvm_utils->dict_api->free_data(value_dict_type, target_dict, module.get());
                }
            }
            llvm_utils->dict_api->dict_deepcopy(nullptr, value_dict, target_dict,
                                    value_dict_type, module.get());
            // Free source temp data for DictConstant expressions
            if (ASR::is_a<ASR::DictConstant_t>(*x.m_value)) {
                llvm_utils->dict_api->free_data(value_dict_type, value_dict, module.get());
            }
            return ;
        } else if( is_target_set && is_value_set ) {
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr(*x.m_value);
            llvm::Value* value_set = tmp;
            this->visit_expr(*x.m_target);
            llvm::Value* target_set = tmp;
            ptr_loads = ptr_loads_copy;
            ASR::Set_t* value_set_type = ASR::down_cast<ASR::Set_t>(asr_value_type);
            llvm_utils->set_set_api(value_set_type);
            std::string el_type_code = ASRUtils::get_type_code(value_set_type->m_type);
            // Free old target data before deepcopy
            if (ASR::is_a<ASR::Var_t>(*x.m_target)) {
                ASR::Variable_t* v = ASR::down_cast<ASR::Variable_t>(
                    ASR::down_cast<ASR::Var_t>(x.m_target)->m_v);
                if (v->m_intent == ASR::intentType::Local ||
                    v->m_intent == ASR::intentType::ReturnVar ||
                    v->m_intent == ASR::intentType::Out ||
                    v->m_intent == ASR::intentType::InOut ||
                    !v->m_intent) {
                    llvm_utils->set_api->free_data(el_type_code, target_set, module.get());
                }
            }
            llvm_utils->set_api->set_deepcopy(x.m_value, value_set, target_set,
                                    value_set_type, module.get());
            // Free source temp data for SetConstant expressions
            if (ASR::is_a<ASR::SetConstant_t>(*x.m_value)) {
                llvm_utils->set_api->free_data(el_type_code, value_set, module.get());
            }
            return ;
        } else if ((is_target_class || is_target_struct) &&
                    (is_value_class || is_value_struct)) {
            if (ASRUtils::is_allocatable(asr_target_type) && 
                    !(is_target_class && is_value_class)) {
                check_and_allocate_scalar(x.m_target, x.m_value, asr_value_type, true);
            }
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr(*x.m_value);
            llvm::Value* value_struct_orig = tmp;
            bool is_assignment_target_copy = is_assignment_target;
            is_assignment_target = true;
            this->visit_expr(*x.m_target);
            is_assignment_target = is_assignment_target_copy;
            llvm::Value* target_struct_orig = tmp;
            llvm::Value* ptr_to_target_struct = target_struct_orig;
            ptr_loads = ptr_loads_copy;

            llvm::Type* value_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                x.m_value, ASRUtils::extract_type(asr_value_type), module.get());
            llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                x.m_target, ASRUtils::extract_type(asr_target_type), module.get());

            if (LLVM::is_llvm_pointer(*asr_target_type)) {
                target_struct_orig = llvm_utils->CreateLoad2(target_llvm_type->getPointerTo(), target_struct_orig);
            }
            if (LLVM::is_llvm_pointer(*asr_value_type)) {
                value_struct_orig = llvm_utils->CreateLoad2(value_llvm_type->getPointerTo(), value_struct_orig);
            }

            llvm::Value* target_struct = target_struct_orig;
            llvm::Value* value_struct = value_struct_orig;
            llvm::Value* llvm_dt = value_struct;
            if (ASR::is_a<ASR::ArrayItem_t>(*x.m_value)) {
                ASR::ArrayItem_t* array_item = ASR::down_cast<ASR::ArrayItem_t>(x.m_value);
                ptr_loads = LLVM::is_llvm_pointer(*ASRUtils::expr_type(array_item->m_v));
                if (ASRUtils::is_unlimited_polymorphic_type(
                        ASRUtils::expr_type(array_item->m_v)) && ptr_loads > 0) {
                    ptr_loads--;
                }
                this->visit_expr_wrapper(array_item->m_v, true);
                ptr_loads = ptr_loads_copy;
                llvm::Type* struct_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    array_item->m_v, ASRUtils::extract_type(array_item->m_type), module.get());
                if (ASRUtils::extract_physical_type(
                        ASRUtils::expr_type(array_item->m_v)) == ASR::array_physical_typeType::DescriptorArray) {
                    llvm_dt = llvm_utils->CreateLoad2(
                        struct_llvm_type->getPointerTo(), arr_descr->get_pointer_to_data(
                        array_item->m_v, ASRUtils::expr_type(array_item->m_v), tmp, module.get()));
                } else {
                    llvm_dt = tmp;
                }
            }

            llvm::Value* target_data_gep = nullptr;
            if (is_target_unlimited_polymorphic) {
                if (ASRUtils::is_allocatable(asr_target_type)) {
                    // When target is an unallocated class(*), the wrapper
                    // struct is null. Allocate it before accessing its fields.
                    llvm::Value* is_wrapper_null = builder->CreateICmpEQ(
                        target_struct,
                        llvm::ConstantPointerNull::get(
                            llvm::cast<llvm::PointerType>(target_struct->getType())));
                    llvm_utils->create_if_else(is_wrapper_null, [&]() {
                        llvm::DataLayout data_layout(module->getDataLayout());
                        uint64_t sz = data_layout.getTypeAllocSize(target_llvm_type);
                        llvm::Value* wrapper_size = llvm::ConstantInt::get(
                            llvm::Type::getInt64Ty(context), sz);
                        llvm::Value* wrapper_ptr = allocate_class_wrapper_storage(
                            x.m_target, target_llvm_type, wrapper_size);
                        builder->CreateStore(wrapper_ptr, ptr_to_target_struct);
                    }, []() {});
                    target_struct = llvm_utils->CreateLoad2(
                        target_llvm_type->getPointerTo(), ptr_to_target_struct);
                }
                target_data_gep = llvm_utils->create_gep2(target_llvm_type, target_struct, 1);
                target_struct = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, target_data_gep);
            } else if (is_target_class) {
                target_struct = llvm_utils->create_gep2(target_llvm_type, target_struct, 1);
                llvm::Type* target_cstruct_type = llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_target))), module.get(), true);
                target_struct = llvm_utils->CreateLoad2(target_cstruct_type, target_struct);
            } 

            if (is_value_unlimited_polymorphic) {
                value_struct = llvm_utils->create_gep2(value_llvm_type, value_struct, 1);
                value_struct = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, value_struct);
            } else if (is_value_class) {
                value_struct = llvm_utils->create_gep2(value_llvm_type, value_struct, 1);
                llvm::Type* value_cstruct_type = llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_value))), module.get(), true);
                value_struct = llvm_utils->CreateLoad2(value_cstruct_type, value_struct);
            }

            if (is_target_class && is_value_class) {
                // Use runtime copy function for class to class assignments
                // As we don't know the type of value

                // Check if this is a derived-to-base assignment
                bool use_target_copy = false;
                if (!is_target_unlimited_polymorphic && !is_value_unlimited_polymorphic) {
                    ASR::symbol_t* target_sym = ASRUtils::get_struct_sym_from_struct_expr(x.m_target);
                    ASR::symbol_t* value_sym = ASRUtils::get_struct_sym_from_struct_expr(x.m_value);
                    if (target_sym && value_sym &&
                        ASR::is_a<ASR::Struct_t>(*target_sym) &&
                        ASR::is_a<ASR::Struct_t>(*value_sym)) {
                        ASR::Struct_t* target_struct_t = ASR::down_cast<ASR::Struct_t>(target_sym);
                        ASR::Struct_t* value_struct_t = ASR::down_cast<ASR::Struct_t>(value_sym);
                        // If target is parent of value, this is derived-to-base
                        use_target_copy = ASRUtils::is_parent(value_struct_t, target_struct_t);
                    }
                }

                // For derived-to-base assignments, use the target's copy function
                // to avoid writing past the target allocation.
                // For same-type or base-to-derived, use source's copy function.
                llvm::Value* copy_source = use_target_copy ? target_struct : llvm_dt;
                
                // Use runtime allocate function for class to class assignments
                if (ASRUtils::is_allocatable(asr_target_type)) {
                    // Deallocating target first
                    Vec<ASR::expr_t*> dealloc_stmts;
                    dealloc_stmts.reserve(al, 1);
                    dealloc_stmts.push_back(al, x.m_target);
                    ASR::stmt_t* del = ASRUtils::STMT(ASR::make_ImplicitDeallocate_t(al, 
                        x.m_target->base.loc, dealloc_stmts.p, dealloc_stmts.n));
                    this->visit_stmt(*del);
                    llvm::FunctionType* fnTy = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context), {llvm_utils->i8_ptr->getPointerTo()}, false);
                    llvm::PointerType *fnPtrTy = llvm::PointerType::get(fnTy, 0);
                    llvm::PointerType *fnPtrPtrTy = llvm::PointerType::get(fnPtrTy, 0);
                    llvm::PointerType *fnPtrPtrPtrTy = llvm::PointerType::get(fnPtrPtrTy, 0);
                    llvm::Value* vtable_ptr = builder->CreateBitCast(copy_source, fnPtrPtrPtrTy);
                    vtable_ptr = llvm_utils->CreateLoad2(fnPtrPtrTy, vtable_ptr);
                    llvm::Value* fn = (llvm_utils->create_ptr_gep2(fnPtrTy, vtable_ptr, 1));
                    fn = llvm_utils->CreateLoad2(fnPtrTy, fn);
                    
                    builder->CreateCall(fnTy, fn, {builder->CreateBitCast(
                        ptr_to_target_struct, llvm_utils->i8_ptr->getPointerTo())});

                    // Reload target_struct after allocation
                    target_struct_orig = llvm_utils->CreateLoad2(target_llvm_type->getPointerTo(), ptr_to_target_struct);
                    ptr_to_target_struct = llvm_utils->create_gep2(target_llvm_type, target_struct_orig, 1);
                    if (!is_target_unlimited_polymorphic) {
                        target_struct = llvm_utils->CreateLoad2(llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(
                            ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_target))), module.get(), true), ptr_to_target_struct);
                    } else {
                        target_struct = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, ptr_to_target_struct);
                    }
                }

                // ONE-wrapper layout: if target data is NULL (freshly allocated
                // wrapper for array element assignment), initialize the actual
                // ONE-wrapper's vptr and data buffer from the source's type info,
                // then re-visit the target to get the correct element pointer.
                if (is_target_unlimited_polymorphic &&
                        !ASRUtils::is_allocatable(asr_target_type) &&
                        target_data_gep != nullptr &&
                        ASR::is_a<ASR::ArrayItem_t>(*x.m_target)) {
                    llvm::Value* is_data_null = builder->CreateICmpEQ(
                        target_struct,
                        llvm::ConstantPointerNull::get(
                            llvm::cast<llvm::PointerType>(target_struct->getType())));
                    llvm_utils->create_if_else(is_data_null, [&]() {
                        llvm::Value* src_vptr = builder->CreateBitCast(
                            copy_source, llvm_utils->vptr_type->getPointerTo());
                        src_vptr = llvm_utils->CreateLoad2(llvm_utils->vptr_type, src_vptr);
                        llvm::Value* elem_size = llvm_utils->get_class_type_size_from_vptr(src_vptr);

                        ASR::ArrayItem_t* target_ai = ASR::down_cast<ASR::ArrayItem_t>(x.m_target);
                        llvm::Value* actual_wrapper = init_upoly_array_element_buffer(
                            target_ai, elem_size, target_llvm_type);
                        builder->CreateStore(src_vptr, builder->CreateBitCast(
                            actual_wrapper, llvm_utils->vptr_type->getPointerTo()));
                    }, []() {});

                    // Re-visit target to get correct element pointer now that
                    // the ONE-wrapper is initialized
                    {
                        int64_t saved_ptr_loads = ptr_loads;
                        ptr_loads = 0;
                        bool saved_iat = is_assignment_target;
                        is_assignment_target = true;
                        this->visit_expr(*x.m_target);
                        is_assignment_target = saved_iat;
                        target_struct_orig = tmp;
                        ptr_to_target_struct = target_struct_orig;
                        ptr_loads = saved_ptr_loads;
                        if (LLVM::is_llvm_pointer(*asr_target_type)) {
                            target_struct_orig = llvm_utils->CreateLoad2(
                                target_llvm_type->getPointerTo(), target_struct_orig);
                        }
                        target_struct = target_struct_orig;
                        target_data_gep = llvm_utils->create_gep2(target_llvm_type, target_struct, 1);
                        target_struct = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, target_data_gep);
                    }
                }

                llvm::FunctionType* fnTy = llvm_utils->struct_copy_functype;
                llvm::PointerType *fnPtrTy = llvm::PointerType::get(fnTy, 0);
                llvm::PointerType *fnPtrPtrTy = llvm::PointerType::get(fnPtrTy, 0);
                llvm::PointerType *fnPtrPtrPtrTy = llvm::PointerType::get(fnPtrPtrTy, 0);
                llvm::Value* vtable_ptr = builder->CreateBitCast(copy_source, fnPtrPtrPtrTy);
                vtable_ptr = llvm_utils->CreateLoad2(fnPtrPtrTy, vtable_ptr);
                llvm::Value* fn = llvm_utils->create_ptr_gep2(fnPtrTy, vtable_ptr, 0);
                fn = llvm_utils->CreateLoad2(fnPtrTy, fn);
                value_struct = builder->CreateBitCast(value_struct, llvm_utils->i8_ptr);
                target_struct = builder->CreateBitCast(target_struct, llvm_utils->i8_ptr);
                builder->CreateCall(fnTy, fn, {value_struct, target_struct});
            } else {
                ASR::symbol_t* vptr_sym;
                ASR::expr_t* deepcopy_var;
                ASR::ttype_t* deepcopy_type;
                if (is_value_class) {
                    vptr_sym = ASRUtils::get_struct_sym_from_struct_expr(x.m_target);
                    value_struct = builder->CreateBitCast(value_struct, target_llvm_type->getPointerTo());
                    deepcopy_type = ASRUtils::make_StructType_t_util(al, x.m_target->base.loc, vptr_sym, true);
                    deepcopy_var = x.m_target;
                } else {
                    vptr_sym = ASRUtils::get_struct_sym_from_struct_expr(x.m_value);
                    if (is_value_class) {
                        llvm::Type* value_cstruct_type = llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(
                            ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_value))),
                            module.get(), false);
                        target_struct = builder->CreateBitCast(target_struct, value_cstruct_type->getPointerTo());
                    } else {
                        target_struct = builder->CreateBitCast(target_struct, value_llvm_type->getPointerTo());
                    }
                    deepcopy_type = ASRUtils::make_StructType_t_util(al, x.m_value->base.loc, vptr_sym, true);
                    deepcopy_var = x.m_value;
                }

                llvm_utils->deepcopy(deepcopy_var, value_struct, target_struct,
                    deepcopy_type, deepcopy_type, module.get(), true);
            }
            if (is_target_unlimited_polymorphic || is_target_class) {
                // Store Vptr from original struct
                if (is_value_class || is_value_unlimited_polymorphic) {
                    llvm::Value* vptr = builder->CreateBitCast(llvm_dt, llvm_utils->vptr_type->getPointerTo());
                    vptr = llvm_utils->CreateLoad2(llvm_utils->vptr_type, vptr);
                    builder->CreateStore(vptr, builder->CreateBitCast(
                        target_struct_orig, llvm_utils->vptr_type->getPointerTo()));
                } else {
                    builder->CreateStore(struct_api->get_pointer_to_method(ASRUtils::symbol_get_past_external(
                        ASRUtils::get_struct_sym_from_struct_expr(x.m_value)), module.get()),
                        builder->CreateBitCast(target_struct_orig, llvm_utils->vptr_type->getPointerTo()));
                }
            }
            return;
        } else if ((is_value_unlimited_polymorphic || is_target_unlimited_polymorphic)
                   && !ASRUtils::is_array(asr_target_type)) {
            if (ASRUtils::is_allocatable(asr_target_type)) {
                check_and_allocate_scalar(x.m_target, x.m_value, asr_value_type, true);
            }
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = LLVM::is_llvm_pointer(*asr_value_type);
            this->visit_expr(*x.m_value);
            llvm::Value* value = tmp;
            ptr_loads = 0;
            bool is_assignment_target_copy = is_assignment_target;
            is_assignment_target = true;
            this->visit_expr(*x.m_target);
            is_assignment_target = is_assignment_target_copy;
            llvm::Value* target = tmp;
            ptr_loads = ptr_loads_copy;
            if (LLVM::is_llvm_pointer(*asr_target_type)) {
                llvm::Type* target_llvm_type_load = llvm_utils->get_type_from_ttype_t_util(
                    x.m_target, ASRUtils::extract_type(asr_target_type), module.get());
                target = llvm_utils->CreateLoad2(target_llvm_type_load->getPointerTo(), target);
            }

            // ONE-wrapper lazy init: if the target is an element of a class(*)
            // array whose ONE-wrapper has NULL data, initialize the actual
            // wrapper in the descriptor (set vptr, allocate data buffer for
            // all elements), then re-visit to get the correct element pointer.
            if (!is_value_unlimited_polymorphic &&
                    is_target_unlimited_polymorphic &&
                    ASR::is_a<ASR::ArrayItem_t>(*x.m_target)) {
                llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_target, ASRUtils::extract_type(asr_target_type), module.get());
                llvm::Value* data_field_ptr = llvm_utils->create_gep2(
                    target_llvm_type, target, 1);
                llvm::Value* data_ptr = llvm_utils->CreateLoad2(
                    llvm_utils->i8_ptr, data_field_ptr);
                llvm::Value* is_data_null = builder->CreateICmpEQ(data_ptr,
                    llvm::ConstantPointerNull::get(
                        llvm::cast<llvm::PointerType>(data_ptr->getType())));
                llvm_utils->create_if_else(is_data_null, [&]() {
                    ASR::ArrayItem_t* target_ai = ASR::down_cast<ASR::ArrayItem_t>(x.m_target);

                    // Get element size from the value type
                    llvm::Type* value_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_value, ASRUtils::extract_type(asr_value_type), module.get());
                    llvm::DataLayout data_layout(module->getDataLayout());
                    uint64_t elem_sz = data_layout.getTypeAllocSize(value_llvm_type);
                    llvm::Value* elem_size = llvm::ConstantInt::get(
                        llvm::Type::getInt64Ty(context), elem_sz);

                    llvm::Value* actual_wrapper = init_upoly_array_element_buffer(
                        target_ai, elem_size, target_llvm_type);
                    struct_api->store_intrinsic_type_vptr(asr_value_type,
                        ASRUtils::extract_kind_from_ttype_t(asr_value_type),
                        actual_wrapper, module.get());
                }, []() {});

                // Re-visit target to get correct element pointer
                {
                    int64_t saved_ptr_loads = ptr_loads;
                    ptr_loads = 0;
                    bool saved_iat = is_assignment_target;
                    is_assignment_target = true;
                    this->visit_expr(*x.m_target);
                    is_assignment_target = saved_iat;
                    target = tmp;
                    ptr_loads = saved_ptr_loads;
                }
            }

            if (is_value_unlimited_polymorphic) {
                llvm_utils->deepcopy(x.m_value, value, target,
                    asr_target_type, asr_value_type, module.get());
            } else {
                llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_target, ASRUtils::extract_type(asr_target_type), module.get());
                llvm::Type* value_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_value, ASRUtils::extract_type(asr_value_type), module.get());

                // For scalar class(*) targets, store vptr and allocate data if NULL
                if (!ASR::is_a<ASR::ArrayItem_t>(*x.m_target)) {
                    struct_api->store_intrinsic_type_vptr(asr_value_type,
                        ASRUtils::extract_kind_from_ttype_t(asr_value_type), target, module.get());
                }
                llvm::Value* data_field_ptr = llvm_utils->create_gep2(target_llvm_type, target, 1);
                llvm::Value* data_ptr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, data_field_ptr);
                llvm::Value* is_data_null = builder->CreateICmpEQ(data_ptr,
                    llvm::ConstantPointerNull::get(
                        llvm::cast<llvm::PointerType>(data_ptr->getType())));
                llvm_utils->create_if_else(is_data_null, [&]() {
                    llvm::DataLayout data_layout(module->getDataLayout());
                    uint64_t sz = data_layout.getTypeAllocSize(value_llvm_type);
                    llvm::Value* alloc_size = llvm::ConstantInt::get(
                        llvm::Type::getInt64Ty(context), sz);
                    llvm::Value* new_buf = LLVMArrUtils::lfortran_malloc(
                        context, *module, *builder, alloc_size);
                    builder->CreateStore(new_buf, data_field_ptr);
                }, []() {});
                data_ptr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, data_field_ptr);
                target = builder->CreateBitCast(data_ptr, value_llvm_type->getPointerTo());
                // With ptr_loads=0, variables produce a pointer while
                // constants produce a value directly.  deepcopy for scalar
                // intrinsic types expects a loaded value, so load only when
                // the LLVM value is actually a pointer.
                if (!ASRUtils::is_array(asr_value_type) &&
                        !ASRUtils::is_character(*asr_value_type) &&
                        value->getType()->isPointerTy()) {
                    value = llvm_utils->CreateLoad2(value_llvm_type, value);
                }
                // The target pointer has been reinterpreted as value type,
                // so pass value type wrapped in Allocatable as dest type
                // (retains the allocatable flag needed for string data
                // allocation inside the unlimited polymorphic container).
                ASR::ttype_t* dest_type_for_copy = ASRUtils::TYPE(
                    ASR::make_Allocatable_t(al, asr_value_type->base.loc, asr_value_type));
                llvm_utils->deepcopy(x.m_value, value, target,
                    dest_type_for_copy, asr_value_type, module.get());
            }
            return;
        } else if (is_target_class && is_value_class) {
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr(*x.m_value);
            llvm::Value* value_struct = tmp;
            bool is_assignment_target_copy = is_assignment_target;
            is_assignment_target = true;
            this->visit_expr(*x.m_target);
            is_assignment_target = is_assignment_target_copy;
            llvm::Value* target_struct = tmp;
            ptr_loads = ptr_loads_copy;
            llvm::Type *i64 = llvm::Type::getInt64Ty(context);

            // deepcopy the class hash
            llvm::Type* value_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_value, asr_value_type, module.get());
            llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, asr_target_type, module.get());

            llvm::Value* value_class_hash = llvm_utils->create_gep2(value_llvm_type, value_struct, 0);
            value_class_hash = llvm_utils->CreateLoad2(i64, value_class_hash);

            llvm::Value* target_class_hash_ptr = llvm_utils->create_gep2(target_llvm_type, target_struct, 0);

            builder->CreateStore(value_class_hash, target_class_hash_ptr);

            // deepcopy the class ptr
            ASR::ttype_t* wrapped_value_struct_type = ASRUtils::make_StructType_t_util(al, asr_value_type->base.loc,
                            ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_value)), true);
            llvm::Type* wrapper_value_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_value, wrapped_value_struct_type, module.get());

            ASR::ttype_t* wrapped_target_struct_type = ASRUtils::make_StructType_t_util(al, asr_target_type->base.loc,
                            ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_target)), true);
            llvm::Type* wrapper_target_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, wrapped_target_struct_type, module.get());


            llvm::Value* value_class_ptr = llvm_utils->create_gep2(value_llvm_type, value_struct, 1);
            // Get value object of value_class_type pointer
            value_class_ptr = llvm_utils->CreateLoad2(wrapper_value_llvm_type->getPointerTo(), value_class_ptr);
            value_class_ptr = llvm_utils->CreateLoad2(wrapper_value_llvm_type, value_class_ptr);

            llvm::Value* target_class_ptr = llvm_utils->create_gep2(target_llvm_type, target_struct, 1);
            // Bitcast target_class_type to value_class_type and then store value object in it
            target_class_ptr = llvm_utils->CreateLoad2(wrapper_target_llvm_type->getPointerTo(), target_class_ptr);
            target_class_ptr = builder->CreateBitCast(target_class_ptr, wrapper_value_llvm_type->getPointerTo());

            builder->CreateStore(value_class_ptr, target_class_ptr);
            return;
        } else if (ASR::is_a<ASR::Allocatable_t>(*asr_target_type) &&
                   ASRUtils::is_class_type(ASRUtils::type_get_past_allocatable(asr_target_type)) &&
                   is_value_struct) {
            check_and_allocate_scalar(x.m_target, x.m_value, asr_value_type);
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = ASRUtils::is_allocatable(asr_value_type);
            this->visit_expr(*x.m_value);
            llvm::Value* value_struct = tmp;

            bool is_assignment_target_copy = is_assignment_target;
            is_assignment_target = true;
            ptr_loads = 0;
            this->visit_expr(*x.m_target);
            is_assignment_target = is_assignment_target_copy;

            // Get and bitcast the wrapped struct in classtype
            ASR::ttype_t* wrapped_struct_type = ASRUtils::make_StructType_t_util(al, asr_target_type->base.loc,
                            ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_target)), true);
            llvm::Type* wrapper_struct_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, wrapped_struct_type, module.get())->getPointerTo();
            llvm::Value* vptr_hash = llvm_utils->create_gep2(asr_target_llvm_type, tmp, 0);
            llvm::Value* struct_type_hash = llvm::ConstantInt::get(llvm_utils->getIntType(8),
                llvm::APInt(64, get_class_hash(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(x.m_value)))));
            builder->CreateStore(struct_type_hash, vptr_hash);
            tmp = llvm_utils->create_gep2(asr_target_llvm_type, tmp, 1);
            tmp = llvm_utils->CreateLoad2(wrapper_struct_llvm_type, tmp);
            tmp = builder->CreateBitCast(tmp, value_struct->getType());
            ptr_loads = ptr_loads_copy;
            llvm::Value* target_struct = tmp;

            llvm_utils->deepcopy(x.m_value, value_struct, target_struct,
                asr_target_type, ASRUtils::type_get_past_allocatable(asr_value_type), module.get(), true);
            return;
        }

        if( ASR::is_a<ASR::Pointer_t>(*ASRUtils::expr_type(x.m_target)) &&
            ASR::is_a<ASR::GetPointer_t>(*x.m_value) ) {
            ASR::Variable_t *asr_target = EXPR2VAR(x.m_target);
            ASR::GetPointer_t* get_ptr = ASR::down_cast<ASR::GetPointer_t>(x.m_value);
            uint32_t target_h = get_hash((ASR::asr_t*)asr_target);
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 2 - LLVM::is_llvm_pointer(*ASRUtils::expr_type(get_ptr->m_arg));
            visit_expr_wrapper(get_ptr->m_arg, true);
            ptr_loads = ptr_loads_copy;
            if( ASRUtils::is_array(ASRUtils::expr_type(get_ptr->m_arg)) &&
                ASRUtils::extract_physical_type(ASRUtils::expr_type(get_ptr->m_arg)) !=
                ASR::array_physical_typeType::DescriptorArray) {
                visit_ArrayPhysicalCastUtil(
                    tmp, get_ptr->m_arg, ASRUtils::type_get_past_pointer(
                        ASRUtils::type_get_past_allocatable(get_ptr->m_type)),
                        ASRUtils::expr_type(get_ptr->m_arg),
                    ASRUtils::extract_physical_type(ASRUtils::expr_type(get_ptr->m_arg)),
                    ASR::array_physical_typeType::DescriptorArray);
            }
            builder->CreateStore(tmp, llvm_symtab[target_h]);
            return ;
        }

        // Handle BitCast (transfer intrinsic) to character arrays
        if (ASR::is_a<ASR::BitCast_t>(*x.m_value)) {
            ASR::ttype_t* target_type = ASRUtils::expr_type(x.m_target);
            // Only handle array-related targets
            bool is_array_target = (x.m_target->type == ASR::exprType::ArraySection) ||
                                   (x.m_target->type == ASR::exprType::ArrayItem) ||
                                   (x.m_target->type == ASR::exprType::Var && ASRUtils::is_array(target_type));
            if (is_array_target) {
                ASR::ttype_t* elem_type = target_type;
                if(ASRUtils::is_array(target_type)){
                    elem_type = ASRUtils::extract_type(target_type);
                }
                if (ASRUtils::is_character(*elem_type)){
                    handle_bitcast_assignment_char(x);
                    return;
                }
            }
        }

        llvm::Value *target, *value;
        uint32_t h;
        ASR::expr_t* target_expr_for_type = x.m_target;
        if( x.m_target->type == ASR::exprType::ArrayPhysicalCast ) {
            ASR::ArrayPhysicalCast_t* apc = ASR::down_cast<ASR::ArrayPhysicalCast_t>(x.m_target);
            ASR::ttype_t* src_asr_type = ASRUtils::expr_type(apc->m_arg);
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 2 - LLVM::is_llvm_pointer(*src_asr_type);
            this->visit_expr_wrapper(apc->m_arg, false);
            ptr_loads = ptr_loads_copy;
            llvm::Value* arg = tmp;
            if (apc->m_old == ASR::array_physical_typeType::AssumedRankArray &&
                !ASRUtils::is_array(apc->m_type)) {
                llvm::Type* data_type = llvm_utils->get_el_type(
                    apc->m_arg, ASRUtils::extract_type(src_asr_type), module.get());
                llvm::Type* arr_type = llvm_utils->get_type_from_ttype_t_util(apc->m_arg,
                    ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(src_asr_type)),
                    module.get());
                target = llvm_utils->CreateLoad2(data_type->getPointerTo(),
                    arr_descr->get_pointer_to_data(arr_type, arg));
            } else {
                // For assumed-rank allocatable/pointer targets (e.g. inside a
                // `select rank` block) we need the descriptor pointer
                // (%array.15*), not the loaded descriptor value. Use
                // ptr_loads=1 to load just once through the alloca.
                bool is_assumed_rank_alloc_target =
                    apc->m_old == ASR::array_physical_typeType::AssumedRankArray &&
                    LLVM::is_llvm_pointer(*src_asr_type);
                int64_t saved_ptr_loads = ptr_loads;
                if (is_assumed_rank_alloc_target) {
                    ptr_loads = 1;
                }
                is_assignment_target = true;
                this->visit_expr(*apc->m_arg);
                is_assignment_target = false;
                ptr_loads = saved_ptr_loads;
                target = tmp;
                if (apc->m_old == ASR::array_physical_typeType::AssumedRankArray) {
                    target_expr_for_type = apc->m_arg;
                }
            }
        } else if( x.m_target->type == ASR::exprType::ArrayItem ||
            x.m_target->type == ASR::exprType::StringItem ||
            x.m_target->type == ASR::exprType::StringSection ||
            x.m_target->type == ASR::exprType::ArraySection ||
            x.m_target->type == ASR::exprType::StructInstanceMember ||
            x.m_target->type == ASR::exprType::ListItem ||
            x.m_target->type == ASR::exprType::DictItem ||
            x.m_target->type == ASR::exprType::UnionInstanceMember ||
            x.m_target->type == ASR::exprType::CoarrayRef ||
            (x.m_target->type == ASR::exprType::FunctionCall &&
             ASRUtils::is_pointer(ASRUtils::expr_type(x.m_target))) ) {
            is_assignment_target = true;
            this->visit_expr(*x.m_target);
            is_assignment_target = false;
            target = tmp;
            // For pointer struct members, load the pointer so we store
            // through it rather than overwriting the pointer field itself.
            if (ASR::is_a<ASR::StructInstanceMember_t>(*x.m_target) &&
                ASR::is_a<ASR::Pointer_t>(*asr_target_type) &&
                !ASRUtils::is_array(asr_target_type) &&
                !ASRUtils::is_character(*asr_target_type)) {
                llvm::Type* member_ptr_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_target, asr_target_type, module.get());
                target = llvm_utils->CreateLoad2(member_ptr_type, target);
            }
            if( ASR::is_a<ASR::ListItem_t>(*x.m_target) ) {
                ASR::ListItem_t* asr_target0 = ASR::down_cast<ASR::ListItem_t>(x.m_target);
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr(*asr_target0->m_a);
                ptr_loads = ptr_loads_copy;
                llvm::Value* list = tmp;
                this->visit_expr_wrapper(asr_target0->m_pos, true);
                llvm::Value* pos = tmp;

                target = list_api->read_item_using_ttype(asr_target0->m_type, list, pos, compiler_options.po.bounds_checking,
                                             module.get(), true);
            }
        } else {
            if (!ASR::is_a<ASR::Var_t>(*x.m_target)) {
                std::string target_type = "unknown";
                if (x.m_target->type == ASR::exprType::StringConstant) {
                    target_type = "StringConstant (this is likely a bug in an ASR pass that incorrectly "
                                  "constant-folded an assignment target like ch(5:5) = 'X')";
                } else {
                    target_type = "type " + std::to_string(x.m_target->type);
                }
                throw CodeGenError("Assignment target must be a variable or subscripted expression, got: " + target_type);
            }
            ASR::Variable_t *asr_target = EXPR2VAR(x.m_target);
            h = get_hash((ASR::asr_t*)asr_target);
            target = llvm_symtab[h];
            if (ASR::is_a<ASR::Pointer_t>(*asr_target->m_type) &&
                !ASRUtils::is_character(*asr_target->m_type) &&
                !ASRUtils::is_array(ASRUtils::get_contained_type(asr_target->m_type))) {
                llvm::Type* target_load_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, asr_target->m_type, module.get());
                target = llvm_utils->CreateLoad2(target_load_type, target);
            }
            ASR::ttype_t *cont_type = ASRUtils::get_contained_type(asr_target_type);
            if ( ASRUtils::is_array(cont_type) ) {
                if( is_value_list_to_array ) {
                    this->visit_expr_wrapper(x.m_value, true);
                    llvm::Value* list_data = tmp;
                    int64_t ptr_loads_copy = ptr_loads;
                    ptr_loads = 0;
                    this->visit_expr(*ASR::down_cast<ASR::Cast_t>(x.m_value)->m_arg);
                    llvm::Value* plist = tmp;
                    ptr_loads = ptr_loads_copy;
                    llvm::Value* array_data = nullptr;
                    if( ASRUtils::extract_physical_type(asr_target_type) ==
                        ASR::array_physical_typeType::DescriptorArray ) {
                        llvm::Type* elem_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, ASRUtils::get_contained_type(asr_target_type), module.get());
                        llvm::Type* desc_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, asr_target_type, module.get());
                        array_data = llvm_utils->CreateLoad2(elem_type->getPointerTo(),
                            arr_descr->get_pointer_to_data(desc_type->getPointerTo(), llvm_utils->CreateLoad2(desc_type->getPointerTo(), target)));
                    } else if( ASRUtils::extract_physical_type(asr_target_type) ==
                               ASR::array_physical_typeType::FixedSizeArray ) {
                        llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, asr_target_type, module.get());
                        array_data = llvm_utils->create_gep2(target_llvm_type, target, 0);
                    } else {
                        LCOMPILERS_ASSERT(false);
                    }
                    llvm::Type* list_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, asr_target_type, module.get());
                    llvm::Value* size = list_api->len_using_type(list_llvm_type, plist);
                    llvm::Type* el_type = llvm_utils->get_type_from_ttype_t_util(x.m_value,
                        ASRUtils::extract_type(ASRUtils::expr_type(x.m_value)), module.get());
                    llvm::DataLayout data_layout(module->getDataLayout());
                    uint64_t size_ = data_layout.getTypeAllocSize(el_type);
                    size = builder->CreateMul(size, llvm::ConstantInt::get(
                        llvm::Type::getInt32Ty(context), llvm::APInt(32, size_)));
                    builder->CreateMemCpy(array_data, llvm::MaybeAlign(),
                                          list_data, llvm::MaybeAlign(), size);
                    return ;
                }
                if( asr_target->m_type->type == ASR::ttypeType::String ) {
                    llvm::Type* char_ptr_type = character_type->getPointerTo();
                    llvm::Type* desc_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, asr_target_type, module.get())->getPointerTo();
                    target = llvm_utils->CreateLoad2(char_ptr_type, arr_descr->get_pointer_to_data(desc_type, target));
                }
            }
        }
        if( ASR::is_a<ASR::UnionConstructor_t>(*x.m_value) ) {
            return ;
        }
        ASR::ttype_t* target_type = ASRUtils::expr_type(x.m_target);
        ASR::ttype_t* value_type = ASRUtils::expr_type(x.m_value);
        ASR::expr_t *m_value = x.m_value;
        if (ASRUtils::is_simd_array(x.m_target) && ASR::is_a<ASR::ArraySection_t>(*m_value)) {
            m_value = ASR::down_cast<ASR::ArraySection_t>(m_value)->m_v;
        }
        int ptr_loads_copy = ptr_loads;
        if(ASRUtils::is_string_only(value_type)){
            ptr_loads = 0;
        } else if(ASRUtils::is_array(value_type)){
            ptr_loads = 1;
        } else {
            ptr_loads = 2;
        }
        this->visit_expr_wrapper(m_value, true);
        ptr_loads = ptr_loads_copy;
        if( ASR::is_a<ASR::Var_t>(*x.m_value) &&
            ASR::is_a<ASR::UnionType_t>(*value_type) ) {
            llvm::Type* union_type = llvm_utils->get_type_from_ttype_t_util(x.m_value, value_type, module.get());
            tmp = llvm_utils->CreateLoad2(union_type, tmp);
        }
        value = tmp;
        load_non_array_non_character_pointers(m_value, value_type, value);
        value = logical_store_val(value, x.m_target);

        {
            ASR::ttype_t* target_type_past_ptr = ASRUtils::type_get_past_pointer(target_type);
            if (ASR::is_a<ASR::StructType_t>(*target_type_past_ptr) && !ASRUtils::is_class_type(target_type_past_ptr)) {
                if (value->getType()->isPointerTy()) {
                    llvm::Type* st_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, target_type_past_ptr, module.get());
                    value = llvm_utils->CreateLoad2(st_type, value);
                }
            }
        }
        if ( ASRUtils::is_string_only(ASRUtils::expr_type(x.m_value)) &&
             ASR::is_a<ASR::String_t>(*ASRUtils::extract_type(asr_target_type))) {
            // Check if target is a character member of a bind(C)/SEQUENCE struct
            bool is_bindc_char_member = false;
            if (ASR::is_a<ASR::StructInstanceMember_t>(*x.m_target)) {
                ASR::StructInstanceMember_t* sim = ASR::down_cast<ASR::StructInstanceMember_t>(x.m_target);
                ASR::symbol_t* member_sym = ASRUtils::symbol_get_past_external(sim->m_m);
                ASR::Variable_t* member_var = ASR::down_cast<ASR::Variable_t>(member_sym);
                ASR::symbol_t* struct_sym = ASR::down_cast<ASR::symbol_t>(
                    member_var->m_parent_symtab->asr_owner);
                struct_sym = ASRUtils::symbol_get_past_external(struct_sym);
                if (ASR::is_a<ASR::Struct_t>(*struct_sym) &&
                    (ASR::down_cast<ASR::Struct_t>(struct_sym)->m_abi == ASR::abiType::BindC ||
                     ASR::down_cast<ASR::Struct_t>(struct_sym)->m_is_sequence)) {
                    ASR::ttype_t* mem_type = sim->m_type;
                    is_bindc_char_member = !ASR::is_a<ASR::Pointer_t>(*mem_type) &&
                        !ASR::is_a<ASR::Allocatable_t>(*mem_type);
                }
            }
            bool is_cchar_array_item = false;
            if (ASR::is_a<ASR::ArrayItem_t>(*x.m_target)) {
                ASR::ttype_t* target_item_type = ASRUtils::extract_type(ASRUtils::expr_type(x.m_target));
                is_cchar_array_item = ASR::is_a<ASR::String_t>(*target_item_type) &&
                    ASR::down_cast<ASR::String_t>(target_item_type)->m_physical_type == ASR::CChar;
            }
            if (is_bindc_char_member) {
                // bind(C)/SEQUENCE character member: layout is inline [dst_len x i8].
                // memcpy min(src_len, dst_len) bytes, then space-pad the rest.
                ASR::String_t* dst_str_type = ASR::down_cast<ASR::String_t>(
                    ASRUtils::extract_type(asr_target_type));
                int64_t dst_len = 1;
                if (dst_str_type->m_len) {
                    ASRUtils::extract_value(dst_str_type->m_len, dst_len);
                }
                if (dst_len < 1) dst_len = 1;
                ASR::String_t* src_str_type = ASR::down_cast<ASR::String_t>(
                    ASRUtils::extract_type(asr_value_type));
                llvm::Value* src_data;
                llvm::Value* src_len_val;
                if (src_str_type->m_physical_type == ASR::CChar) {
                    src_data = value;
                    src_len_val = llvm::ConstantInt::get(
                        context, llvm::APInt(64, 1));
                } else {
                    // DescriptorString
                    src_data = llvm_utils->CreateLoad2(character_type,
                        llvm_utils->create_gep2(llvm_utils->string_descriptor, value, 0));
                    if (src_str_type->m_len &&
                        ASR::is_a<ASR::IntegerConstant_t>(*src_str_type->m_len)) {
                        int64_t l = ASR::down_cast<ASR::IntegerConstant_t>(
                            src_str_type->m_len)->m_n;
                        src_len_val = llvm::ConstantInt::get(
                            context, llvm::APInt(64, l));
                    } else {
                        src_len_val = llvm_utils->CreateLoad2(
                            llvm::Type::getInt64Ty(context),
                            llvm_utils->create_gep2(llvm_utils->string_descriptor, value, 1));
                    }
                }
                llvm::Value* dst_len_val = llvm::ConstantInt::get(
                    context, llvm::APInt(64, dst_len));
                // copy_len = min(src_len, dst_len)
                llvm::Value* cmp = builder->CreateICmpULT(src_len_val, dst_len_val);
                llvm::Value* copy_len = builder->CreateSelect(cmp, src_len_val, dst_len_val);
                builder->CreateMemCpy(target, llvm::MaybeAlign(),
                    src_data, llvm::MaybeAlign(), copy_len);
                // pad remaining bytes (dst_len - copy_len) with ' '
                llvm::Value* pad_len = builder->CreateSub(dst_len_val, copy_len);
                llvm::Value* target_i8 = builder->CreateBitCast(
                    target, character_type);
                llvm::Value* pad_dst = builder->CreateGEP(
                    llvm::Type::getInt8Ty(context), target_i8, copy_len);
                builder->CreateMemSet(pad_dst,
                    llvm::ConstantInt::get(context, llvm::APInt(8, ' ')),
                    pad_len, llvm::MaybeAlign());
                tmp = nullptr;
                return;
            }
            if (is_cchar_array_item) {
                // CChar array item: store first byte directly as i8
                ASR::String_t* src_str_type = ASR::down_cast<ASR::String_t>(
                    ASRUtils::extract_type(asr_value_type));
                llvm::Value* byte_val;
                if (src_str_type->m_physical_type == ASR::CChar) {
                    byte_val = llvm_utils->CreateLoad2(
                        llvm::Type::getInt8Ty(context), value);
                } else {
                    // DescriptorString: extract data pointer and load first byte
                    llvm::Value* data_ptr = llvm_utils->create_gep2(
                        llvm_utils->string_descriptor, value, 0);
                    llvm::Value* data = llvm_utils->CreateLoad2(
                        character_type, data_ptr);
                    byte_val = llvm_utils->CreateLoad2(
                        llvm::Type::getInt8Ty(context), data);
                }
                builder->CreateStore(byte_val, target);
                tmp = nullptr;
                return;
            }
            // For struct members (especially common blocks), treat as allocatable
            // so _lfortran_strcpy allocates memory if the pointer is NULL.
            // Common block structs are initialized with zeroinitializer, so
            // their string descriptor pointers start as NULL.
            bool is_dest_allocatable = ASRUtils::is_allocatable(asr_target_type) ||
                                       ASR::is_a<ASR::StructInstanceMember_t>(*x.m_target);
            // bind(C) allocatable string dummies have an extra pointer
            // level (string_descriptor** rather than string_descriptor*)
            // due to the BindC ABI. Dereference to string_descriptor*.
            if (ASR::is_a<ASR::Var_t>(*x.m_target)) {
                ASR::Variable_t* target_var = ASRUtils::EXPR2VAR(x.m_target);
                if (target_var->m_abi == ASR::abiType::BindC &&
                    ASRUtils::is_allocatable(asr_target_type) &&
                    ASR::down_cast<ASR::String_t>(ASRUtils::extract_type(asr_target_type))
                        ->m_physical_type == ASR::string_physical_typeType::DescriptorString) {
                    target = llvm_utils->CreateLoad2(
                        llvm_utils->string_descriptor->getPointerTo(), target);
                }
            }
            llvm_utils->lfortran_str_copy(target, value,
                ASR::down_cast<ASR::String_t>(ASRUtils::extract_type(asr_target_type)),
                ASR::down_cast<ASR::String_t>(ASRUtils::extract_type(asr_value_type)),
                is_dest_allocatable);
            tmp = nullptr;
            return;
        }
        if( ASRUtils::is_array(target_type) &&
            ASRUtils::is_array(value_type) &&
            ASRUtils::check_equal_type(target_type, value_type, x.m_target, x.m_value) ) {
            bool data_only_copy = false;
            ASR::array_physical_typeType target_ptype = ASRUtils::extract_physical_type(target_type);
            ASR::array_physical_typeType value_ptype = ASRUtils::extract_physical_type(value_type);
            bool is_target_data_only_array = (target_ptype == ASR::array_physical_typeType::PointerArray ||
                                             target_ptype == ASR::array_physical_typeType::UnboundedPointerArray);
            bool is_value_data_only_array = (value_ptype == ASR::array_physical_typeType::PointerArray ||
                                            value_ptype == ASR::array_physical_typeType::UnboundedPointerArray);
            bool is_target_fixed_sized_array = (target_ptype == ASR::array_physical_typeType::FixedSizeArray);
            bool is_value_fixed_sized_array = (value_ptype == ASR::array_physical_typeType::FixedSizeArray);
            bool is_target_simd_array = (target_ptype == ASR::array_physical_typeType::SIMDArray);
            bool is_target_descriptor_based_array = (target_ptype == ASR::array_physical_typeType::DescriptorArray);
            bool is_value_descriptor_based_array = (value_ptype == ASR::array_physical_typeType::DescriptorArray);
            llvm::Type* target_el_type = llvm_utils->get_el_type(
                x.m_target, ASRUtils::extract_type(target_type), module.get());
            llvm::Type* value_el_type = llvm_utils->get_el_type(
                x.m_value, ASRUtils::extract_type(value_type), module.get());
            bool is_allocatable_descriptor_target = (ASRUtils::is_allocatable(target_type) && target_ptype == ASR::array_physical_typeType::DescriptorArray);
            if( is_allocatable_descriptor_target && !x.m_realloc_lhs && !x.m_move_allocation ) {
                llvm::Value* is_not_allocated = expr_is_unallocated(x.m_target);
                generate_unallocated_array_runtime_error(is_not_allocated, x.m_target);
            }
            if( is_value_fixed_sized_array && is_target_fixed_sized_array ) {
                ASR::dimension_t* asr_dims = nullptr;
                size_t asr_n_dims = ASRUtils::extract_dimensions_from_ttype(target_type, asr_dims);
                int64_t size = ASRUtils::get_fixed_size_of_array(asr_dims, asr_n_dims);
                llvm::DataLayout data_layout(module->getDataLayout());
                uint64_t data_size = data_layout.getTypeAllocSize(target_el_type);
                llvm::Value* llvm_size = llvm::ConstantInt::get(context, llvm::APInt(32, size));
                llvm_size = builder->CreateMul(llvm_size,
                    llvm::ConstantInt::get(context, llvm::APInt(32, data_size)));
                builder->CreateMemCpy(target, llvm::MaybeAlign(), value, llvm::MaybeAlign(), llvm_size);
            } else if( is_value_descriptor_based_array && is_target_fixed_sized_array ) {
                value = llvm_utils->CreateLoad2(value_el_type->getPointerTo(), arr_descr->get_pointer_to_data(x.m_value, value_type, value, module.get()));
                llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, target_type, module.get());
                target = llvm_utils->create_gep2(target_llvm_type, target, 0);
                ASR::dimension_t* asr_dims = nullptr;
                size_t asr_n_dims = ASRUtils::extract_dimensions_from_ttype(target_type, asr_dims);
                int64_t size = ASRUtils::get_fixed_size_of_array(asr_dims, asr_n_dims);
                llvm::DataLayout data_layout(module->getDataLayout());
                uint64_t data_size = data_layout.getTypeAllocSize(target_el_type);
                llvm::Value* llvm_size = llvm::ConstantInt::get(context, llvm::APInt(32, size));
                llvm_size = builder->CreateMul(llvm_size,
                    llvm::ConstantInt::get(context, llvm::APInt(32, data_size)));
                builder->CreateMemCpy(target, llvm::MaybeAlign(), value, llvm::MaybeAlign(), llvm_size);
            } else if( is_target_descriptor_based_array && is_value_fixed_sized_array ) {
                if( ASRUtils::is_allocatable(target_type) || ASRUtils::is_pointer(target_type) ) {
                    llvm::Type* alloc_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, ASRUtils::type_get_past_allocatable_pointer(target_type), module.get());
                    target = llvm_utils->CreateLoad2(alloc_type->getPointerTo(), target);
                }
                llvm::Type* llvm_array_type = llvm_utils->get_type_from_ttype_t_util(x.m_target,
                        ASRUtils::type_get_past_allocatable_pointer(target_type), module.get());
                llvm::Value* llvm_size = arr_descr->get_array_size(llvm_array_type, target, nullptr, 4);
                target = llvm_utils->CreateLoad2(target_el_type->getPointerTo(), arr_descr->get_pointer_to_data(llvm_utils->get_type_from_ttype_t_util(x.m_target,
                    ASRUtils::type_get_past_allocatable_pointer(target_type),
                    module.get()), target));
                llvm::DataLayout data_layout(module->getDataLayout());
                uint64_t data_size = data_layout.getTypeAllocSize(value_el_type);
                llvm_size = builder->CreateMul(llvm_size,
                    llvm::ConstantInt::get(context, llvm::APInt(32, data_size)));
                builder->CreateMemCpy(target, llvm::MaybeAlign(), value, llvm::MaybeAlign(), llvm_size);
            } else if( is_target_descriptor_based_array && is_value_data_only_array ) {
                // DescriptorArray target with PointerArray value (e.g. ArrayReshape result)
                // Get size from the target descriptor which was already allocated with the correct shape.
                if( ASRUtils::is_allocatable(target_type) || ASRUtils::is_pointer(target_type) ) {
                    llvm::Type* alloc_type = llvm_utils->get_type_from_ttype_t_util(x.m_target,
                        ASRUtils::type_get_past_allocatable_pointer(target_type), module.get());
                    target = llvm_utils->CreateLoad2(alloc_type->getPointerTo(), target);
                }
                llvm::Type* llvm_array_type = llvm_utils->get_type_from_ttype_t_util(x.m_target,
                        ASRUtils::type_get_past_allocatable_pointer(target_type), module.get());
                llvm::Value* llvm_size = arr_descr->get_array_size(llvm_array_type, target, nullptr, 4);
                if( ASRUtils::is_character(*target_type) ) {
                    // For character arrays, PointerArray stores a string_descriptor
                    // pointing to contiguous char data. Copy the raw character data
                    // into the target's data buffer using the character length.
                    llvm::Value* src_data = llvm_utils->get_stringArray_data(value_type, value);
                    llvm::Value* dst_data = llvm_utils->get_stringArray_data(target_type, target);
                    llvm::Value* char_len = llvm_utils->get_stringArray_length(target_type, target);
                    char_len = builder->CreateTrunc(char_len, llvm::Type::getInt32Ty(context));
                    llvm_size = builder->CreateMul(llvm_size, char_len);
                    builder->CreateMemCpy(dst_data, llvm::MaybeAlign(), src_data, llvm::MaybeAlign(), llvm_size);
                } else {
                    target = llvm_utils->CreateLoad2(target_el_type->getPointerTo(), arr_descr->get_pointer_to_data(llvm_utils->get_type_from_ttype_t_util(x.m_target,
                        ASRUtils::type_get_past_allocatable_pointer(target_type),
                        module.get()), target));
                    llvm::DataLayout data_layout(module->getDataLayout());
                    uint64_t data_size = data_layout.getTypeAllocSize(value_el_type);
                    llvm_size = builder->CreateMul(llvm_size,
                        llvm::ConstantInt::get(context, llvm::APInt(32, data_size)));
                    builder->CreateMemCpy(target, llvm::MaybeAlign(), value, llvm::MaybeAlign(), llvm_size);
                }
            } else if( is_target_data_only_array || is_value_data_only_array ) {
                if( is_value_fixed_sized_array ) {
                    is_value_data_only_array = true;
                }
                if( is_target_fixed_sized_array ) {
                    is_target_data_only_array = true;
                }
                llvm::Value *target_data = nullptr, *value_data = nullptr, *llvm_size = nullptr;
                llvm_size = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, 1));
                if( is_target_data_only_array ) {
                    target_data =
                        ASRUtils::is_array_of_strings(target_type) ?
                        llvm_utils->get_stringArray_data(target_type, target):
                        target;
                    ASR::dimension_t* target_dims = nullptr;
                    int target_ndims = ASRUtils::extract_dimensions_from_ttype(target_type, target_dims);
                    data_only_copy = true;
                    for( int i = 0; i < target_ndims; i++ ) {
                        if( target_dims[i].m_length == nullptr ) {
                            data_only_copy = false;
                            break;
                        }
                        this->visit_expr_wrapper(target_dims[i].m_length, true);
                        llvm_size = builder->CreateMul(llvm_size, builder->CreateSExtOrTrunc(tmp,
                                llvm::Type::getInt32Ty(context)));
                    }
                    if(ASRUtils::is_array_of_strings(target_type)){ // Neglect previous llvm_size
                        llvm_size = llvm_utils->get_stringArray_whole_size(target_type);
                    }
                } else {
                    llvm::Type* load_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, ASRUtils::type_get_past_allocatable_pointer(target_type), module.get());
                    target = llvm_utils->CreateLoad2(load_type->getPointerTo(), target);
                    target_data =
                            ASRUtils::is_array_of_strings(target_type) ?
                            llvm_utils->get_stringArray_data(target_type, target) :
                            llvm_utils->CreateLoad2(llvm_utils->get_type_from_ttype_t_util(x.m_target, ASRUtils::get_contained_type(target_type), module.get())->getPointerTo(), arr_descr->get_pointer_to_data(llvm_utils->get_type_from_ttype_t_util(x.m_target, ASRUtils::type_get_past_allocatable_pointer(target_type), module.get()), target));
                }
                if( is_value_data_only_array ) {
                    value_data =
                            ASRUtils::is_array_of_strings(value_type) ?
                            llvm_utils->get_stringArray_data(value_type, value):
                            value;
                    ASR::dimension_t* value_dims = nullptr;
                    int value_ndims = ASRUtils::extract_dimensions_from_ttype(value_type, value_dims);
                    if( !data_only_copy ) {
                        llvm_size = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, 1));
                        data_only_copy = true;
                        for( int i = 0; i < value_ndims; i++ ) {
                            if( value_dims[i].m_length == nullptr ) {
                                data_only_copy = false;
                                break;
                            }
                            this->visit_expr_wrapper(value_dims[i].m_length, true);
                            tmp = builder->CreateSExtOrTrunc(tmp, llvm::Type::getInt32Ty(context));
                            llvm_size = builder->CreateMul(llvm_size, tmp);
                        }
                        if(ASRUtils::is_array_of_strings(value_type)){ // Neglect previous llvm_size
                            llvm_size = llvm_utils->get_stringArray_whole_size(value_type);
                        }
                    }
                } else {
                    llvm::Type* llvm_array_type = llvm_utils->get_type_from_ttype_t_util(x.m_value,
                        ASRUtils::type_get_past_array(
                            ASRUtils::type_get_past_allocatable_pointer(
                                ASRUtils::expr_type(x.m_value))), module.get());
                    llvm::Type* llvm_array_type_ = llvm_utils->get_type_from_ttype_t_util(x.m_value, ASRUtils::expr_type(x.m_value), module.get());
                    value_data =
                            ASRUtils::is_array_of_strings(value_type) ?
                            llvm_utils->get_stringArray_data(value_type, value) :
                            llvm_utils->CreateLoad2(
                                llvm_array_type->getPointerTo(),
                                arr_descr->get_pointer_to_data(llvm_array_type_, value));
                }
                LCOMPILERS_ASSERT(data_only_copy);
                arr_descr->copy_array_data_only(target_data, value_data, module.get(),
                                                target_el_type,
                                                target_type,
                                                llvm_size);
            } else if ( is_target_simd_array ) {
                if (ASR::is_a<ASR::ArraySection_t>(*x.m_value)) {
                    int idx = 1;
                    ASR::ArraySection_t *arr = down_cast<ASR::ArraySection_t>(x.m_value);
                    (void) ASRUtils::extract_value(arr->m_args->m_left, idx);
                    llvm::Type* value_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_value, value_type, module.get());
                    value = llvm_utils->create_gep2(value_llvm_type, value, idx-1);
                    llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, target_type, module.get());
                    target = llvm_utils->create_gep2(target_llvm_type, target, 0);
                    ASR::dimension_t* asr_dims = nullptr;
                    size_t asr_n_dims = ASRUtils::extract_dimensions_from_ttype(target_type, asr_dims);
                    int64_t size = ASRUtils::get_fixed_size_of_array(asr_dims, asr_n_dims);
                    llvm::DataLayout data_layout(module->getDataLayout());
                    uint64_t data_size = data_layout.getTypeAllocSize(target_el_type);
                    llvm::Value* llvm_size = llvm::ConstantInt::get(context, llvm::APInt(32, size));
                    llvm_size = builder->CreateMul(llvm_size,
                        llvm::ConstantInt::get(context, llvm::APInt(32, data_size)));
                    builder->CreateMemCpy(target, llvm::MaybeAlign(), value, llvm::MaybeAlign(), llvm_size);
                } else {
                    builder->CreateStore(value, target);
                }
            } else {
                if( LLVM::is_llvm_pointer(*target_type) ) {
                    llvm::Type* ptr_load_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, ASRUtils::type_get_past_allocatable_pointer(target_type), module.get());
                    target = llvm_utils->CreateLoad2(ptr_load_type->getPointerTo(), target);
                }
                ASR::ttype_t* target_llvm_type_asr = ASRUtils::expr_type(target_expr_for_type);
                llvm::Type* target_array_type = llvm_utils->get_type_from_ttype_t_util(target_expr_for_type,
                        ASRUtils::type_get_past_allocatable_pointer(target_llvm_type_asr), module.get());
                llvm::Type* source_array_type = llvm_utils->get_type_from_ttype_t_util(x.m_value,
                        ASRUtils::type_get_past_allocatable_pointer(value_type), module.get());
                if (x.m_move_allocation) {
                    // For `move_alloc`, the lower bounds of the source must be
                    // preserved. For move-assignments synthesized by the
                    // subroutine_from_function pass (function returning an
                    // allocatable), Fortran intrinsic-assignment semantics
                    // require the destination's lower bound to be reset to 1
                    // in every dimension; that pass therefore also sets
                    // m_realloc_lhs=true to distinguish the two cases.
                    arr_descr->copy_array_move_allocation(source_array_type, value, target_array_type, target, module.get(), x.m_target, target_type, x.m_realloc_lhs);
                } else if (ASRUtils::is_pointer(target_type)) {
                    // Pointer targets may be associated with non-contiguous
                    // array sections (e.g. output(seq, 1:4) in a 3x4 array has
                    // stride 3). Use element-wise copy that respects the target
                    // descriptor's strides instead of a flat memcpy.
                    llvm::Value* src_data = llvm_utils->CreateLoad2(
                        target_el_type->getPointerTo(),
                        arr_descr->get_pointer_to_data(source_array_type, value));
                    int rank = ASRUtils::extract_n_dims_from_ttype(
                        ASRUtils::type_get_past_allocatable_pointer(target_type));
                    arr_descr->copy_contiguous_data_to_descriptor(
                        src_data, target_array_type, target,
                        target_el_type, rank, module.get());
                } else {
                    arr_descr->copy_array(source_array_type, value, target_array_type, target, module.get(), x.m_target, target_type, false);
                }
            }
        } else if( ASR::is_a<ASR::DictItem_t>(*x.m_target) ) {
            ASR::DictItem_t* dict_item_t = ASR::down_cast<ASR::DictItem_t>(x.m_target);
            ASR::Dict_t* dict_type = ASR::down_cast<ASR::Dict_t>(
                                    ASRUtils::expr_type(dict_item_t->m_a));
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr(*dict_item_t->m_a);
            llvm::Value* pdict = tmp;

            ptr_loads = !LLVM::is_llvm_struct(dict_type->m_key_type);
            this->visit_expr_wrapper(dict_item_t->m_key, true);
            llvm::Value *key = tmp;
            ptr_loads = ptr_loads_copy;

            llvm_utils->set_dict_api(dict_type);
            // Note - The value is fully loaded to an LLVM value (not at all a pointer)
            // as opposed to DictInsert where LLVM values are loaded depending upon
            // the ASR type of value. Might give issues here.
            llvm_utils->dict_api->write_item(dict_item_t->m_a, pdict, key, value, module.get(),
                                dict_type->m_key_type,
                                dict_type->m_value_type);
        } else if (ASRUtils::is_allocatable(target_type)) {
            ASR::ttype_t* asr_type = ASRUtils::type_get_past_pointer(
                ASRUtils::type_get_past_allocatable(
                ASRUtils::expr_type(x.m_target)));
            if (!LLVM::is_llvm_pointer(*ASRUtils::expr_type(x.m_value))) {
                if (ASR::is_a<ASR::Integer_t>(*asr_type) ||
                    ASR::is_a<ASR::Real_t>(*asr_type) ||
                    ASR::is_a<ASR::Complex_t>(*asr_type) ||
                    ASR::is_a<ASR::Logical_t>(*asr_type)) {
                    check_and_allocate_scalar(x.m_target);
                }
                llvm::Type* target_ptr_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, ASRUtils::expr_type(x.m_target), module.get());
                target = llvm_utils->CreateLoad2(target_ptr_type, target);
            } else if (!value->getType()->isPointerTy()) {
                llvm::Type* target_ptr_type = llvm_utils->get_type_from_ttype_t_util(x.m_target, ASRUtils::expr_type(x.m_target), module.get());
                target = llvm_utils->CreateLoad2(target_ptr_type, target);
            }
            builder->CreateStore(value, target);
        } else {
            builder->CreateStore(value, target);
        }
    }

    void generate_binop_check(ASR::expr_t* left, ASR::expr_t* right) {
        ASR::ttype_t *type32 = ASRUtils::TYPE(ASR::make_Integer_t(al, left->base.loc, 4));

        ASR::ttype_t* left_type = ASRUtils::expr_type(left);
        ASR::ttype_t* right_type = ASRUtils::expr_type(right);
        ASR::dimension_t* m_dims = nullptr;
        size_t lrank = ASRUtils::extract_dimensions_from_ttype(left_type, m_dims);
        ASR::dimension_t* n_dims = nullptr;
        size_t rrank = ASRUtils::extract_dimensions_from_ttype(right_type, n_dims);

        if (lrank == 0 || rrank == 0 || lrank != rrank) {
            return;
        }

        for (size_t dim = 0; dim < lrank; dim++) {
            ASR::expr_t* dim_asr = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, m_dims[dim].loc, dim + 1,
                        ASRUtils::TYPE(ASR::make_Integer_t(al, m_dims[dim].loc, 4))));

            ASR::expr_t* left_size = ASRUtils::EXPR(ASR::make_ArraySize_t(al, left->base.loc,
                left, dim_asr, type32, nullptr));
            visit_expr(*left_size);
            llvm::Value* left_llvm_size = tmp;

            ASR::expr_t* right_size = ASRUtils::EXPR(ASR::make_ArraySize_t(al, right->base.loc,
                right, dim_asr, type32, nullptr));
            visit_expr(*right_size);
            llvm::Value* right_llvm_size = tmp;

            ASR::Variable_t* left_var = ASRUtils::expr_to_variable_or_null(left);
            ASR::Variable_t* right_var = ASRUtils::expr_to_variable_or_null(right);

            llvm::Value* dim_llvm = llvm::ConstantInt::get(context, llvm::APInt(32, dim + 1));
            if (left_var && right_var) {
                llvm::Value *left_name = LCompilers::create_global_string_ptr(context, *module, *builder, left_var->m_name);
                llvm::Value *right_name = LCompilers::create_global_string_ptr(context, *module, *builder, right_var->m_name);
                llvm_utils->generate_runtime_error(builder->CreateICmpNE(right_llvm_size, left_llvm_size),
                                                    "Array shape mismatch in binary operation with operands '%s' and '%s'. Tried to match size %d of dimension %d of '%s' with size %d of dimension %d of '%s'.",
                                                    {LLVMUtils::RuntimeLabel("LHS size is %d", {left->base.loc}, {left_llvm_size}),
                                                    LLVMUtils::RuntimeLabel("RHS size is %d", {right->base.loc}, {right_llvm_size})},
                                                    infile,
                                                    location_manager,
                                                    left_name,
                                                    right_name,
                                                    left_llvm_size,
                                                    dim_llvm,
                                                    left_name,
                                                    right_llvm_size,
                                                    dim_llvm,
                                                    right_name);
            } else {
                llvm_utils->generate_runtime_error(builder->CreateICmpNE(right_llvm_size, left_llvm_size),
                                                    "Array shape mismatch in binary operation. Tried to match size %d of dimension %d of one argument with size %d of dimension %d of the other.",
                                                    {LLVMUtils::RuntimeLabel("LHS size is %d", {left->base.loc}, {left_llvm_size}),
                                                    LLVMUtils::RuntimeLabel("RHS size is %d", {right->base.loc}, {right_llvm_size})},
                                                    infile,
                                                    location_manager,
                                                    left_llvm_size,
                                                    dim_llvm,
                                                    right_llvm_size,
                                                    dim_llvm);
            }
        }
    }

    void generate_binop_checks(ASR::expr_t** components, size_t n) {
        for (size_t i = 0; i < n - 1; i++) {
            generate_binop_check(components[i], components[i + 1]);
        }
    }

    void visit_DebugCheckArrayBounds(const ASR::DebugCheckArrayBounds_t &x) {
        if (compiler_options.po.bounds_checking) {
            // Check for errors in array operations in the RHS of the assignment
            generate_binop_checks(x.m_components, x.n_components);

            ASR::ttype_t *type32 = ASRUtils::TYPE(ASR::make_Integer_t(al, x.m_components[0]->base.loc, 4));

            ASR::ttype_t* target_type = ASRUtils::expr_type(x.m_target);
            bool is_allocatable_descriptor_target = ASRUtils::is_array(target_type) &&
                ASRUtils::is_allocatable(target_type) &&
                ASRUtils::extract_physical_type(target_type) == ASR::array_physical_typeType::DescriptorArray;
            if (is_allocatable_descriptor_target && !x.m_move_allocation) {
                llvm::Value* is_not_allocated = expr_is_unallocated(x.m_target);
                generate_unallocated_array_runtime_error(is_not_allocated, x.m_target);
            }
            ASR::dimension_t* m_dims = nullptr;
            size_t rank = ASRUtils::extract_dimensions_from_ttype(target_type, m_dims);
            for (size_t dim = 0; dim < rank; dim++) {
                ASR::expr_t* dim_asr = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, m_dims[dim].loc, dim + 1,
                            ASRUtils::TYPE(ASR::make_Integer_t(al, m_dims[dim].loc, 4))));
                llvm::Value* dim_llvm = llvm::ConstantInt::get(context, llvm::APInt(32, dim + 1));

                ASR::expr_t* target_size_asr = ASRUtils::EXPR(ASR::make_ArraySize_t(al, x.m_target->base.loc,
                    x.m_target, dim_asr, type32, nullptr));
                visit_expr(*target_size_asr);
                llvm::Value* target_size = tmp;

                ASR::expr_t* x_m_components_0_size = ASRUtils::EXPR(ASR::make_ArraySize_t(al, x.m_components[0]->base.loc,
                    x.m_components[0], dim_asr, type32, nullptr));
                visit_expr(*x_m_components_0_size);
                llvm::Value* value_size = tmp;

                ASR::Variable_t* target_variable = ASRUtils::expr_to_variable_or_null(x.m_target);
                if (target_variable) {
                    ASR::expr_t* v = ASRUtils::EXPR(ASR::make_Var_t(al, x.base.base.loc, (ASR::symbol_t *)target_variable));
                    if (ASRUtils::is_array(target_variable->m_type) &&
                        ASRUtils::is_allocatable(target_variable->m_type) &&
                        ASRUtils::extract_physical_type(target_variable->m_type) == ASR::array_physical_typeType::DescriptorArray) {
                        llvm::Value* is_not_allocated = expr_is_unallocated(v);
                        llvm::Function *fn = builder->GetInsertBlock()->getParent();
                        llvm::BasicBlock *thenBB = nullptr;
                        llvm::BasicBlock *mergeBB = nullptr;
                        thenBB = llvm::BasicBlock::Create(context, "then", fn);
                        mergeBB = llvm::BasicBlock::Create(context, "ifcont");

                        builder->CreateCondBr(is_not_allocated, mergeBB, thenBB);
                        builder->SetInsertPoint(thenBB); {
                            llvm_utils->generate_runtime_error(builder->CreateICmpNE(value_size, target_size),
                                                                "Array shape mismatch in assignment to '%s'. Tried to match size %d of dimension %d of LHS with size %d of dimension %d of RHS. Use '--realloc-lhs-arrays' option to reallocate LHS automatically.",
                                                                {LLVMUtils::RuntimeLabel("LHS size is %d", {x.m_target->base.loc}, {target_size}),
                                                                LLVMUtils::RuntimeLabel("RHS size is %d", {x.m_components[0]->base.loc}, {value_size})},
                                                                infile,
                                                                location_manager,
                                                                LCompilers::create_global_string_ptr(context, *module, *builder, target_variable->m_name),
                                                                target_size,
                                                                dim_llvm,
                                                                value_size,
                                                                dim_llvm);
                        }
                        builder->CreateBr(mergeBB);

                        start_new_block(mergeBB);
                    } else {
                        llvm_utils->generate_runtime_error(builder->CreateICmpNE(value_size, target_size),
                                                            "Array shape mismatch in assignment to '%s'. Tried to match size %d of dimension %d of LHS with size %d of dimension %d of RHS.",
                                                     {LLVMUtils::RuntimeLabel("LHS size is %d", {x.m_target->base.loc}, {target_size}),
                                                         LLVMUtils::RuntimeLabel("RHS size is %d", {x.m_components[0]->base.loc}, {value_size})},
                                                          infile,
                                                        location_manager,
                                                            LCompilers::create_global_string_ptr(context, *module, *builder, target_variable->m_name),
                                                            target_size,
                                                            dim_llvm,
                                                            value_size,
                                                            dim_llvm);
                    }
                } else {
                    llvm_utils->generate_runtime_error(builder->CreateICmpNE(value_size, target_size),
                                                        "Array shape mismatch in assignment. Tried to match size %d of dimension %d of LHS with size %d of dimension %d of RHS.",
                                                   {LLVMUtils::RuntimeLabel("LHS size is %d", {x.m_target->base.loc}, {target_size}),
                                                       LLVMUtils::RuntimeLabel("RHS size is %d", {x.m_components[0]->base.loc}, {value_size})},
                                                        infile,
                                                        location_manager,
                                                        target_size,
                                                        dim_llvm,
                                                        value_size,
                                                        dim_llvm);
                }
            }
        }
    }

    // Checks if target_expr is allocated and if not then allocate
    void check_and_allocate_scalar(ASR::expr_t *target_expr, ASR::expr_t *value_expr = nullptr,
            ASR::ttype_t *value_struct_type = nullptr, bool is_assignment = false) {
        ASR::ttype_t *asr_ttype =ASRUtils::expr_type(target_expr);
        ASR::ttype_t *asr_type = ASRUtils::type_get_past_pointer(
            ASRUtils::type_get_past_allocatable(asr_ttype));
        if (ASRUtils::is_array(asr_ttype)) {
            throw CodeGenError("Target is an array, it should be scalar");
        }
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr_wrapper(target_expr, false);
        ptr_loads = ptr_loads_copy;
        llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(target_expr, asr_type, module.get());
        llvm::Value* llvm_cond = nullptr;

        if (ASR::is_a<ASR::Allocatable_t>(*asr_ttype) &&
            ASRUtils::is_class_type(ASRUtils::type_get_past_allocatable(asr_ttype))) {
            llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(target_expr, asr_ttype, module.get());
            ASR::ttype_t* wrapped_struct_type = ASRUtils::make_StructType_t_util(al, asr_ttype->base.loc,
                            ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(target_expr)), true);
            if (value_struct_type == nullptr) {
                value_struct_type = wrapped_struct_type;
            }
            // llvm::Type* wrapper_struct_llvm_type = llvm_utils->get_type_from_ttype_t_util(target_expr, wrapped_struct_type, module.get());
            llvm::Value* target_struct_ptr = tmp;
            target_struct_ptr = llvm_utils->CreateLoad2(target_llvm_type, target_struct_ptr);
            llvm::Value* null_cond = builder->CreateICmpEQ(
                        target_struct_ptr,
                        llvm::ConstantPointerNull::get(llvm::cast<llvm::PointerType>(target_llvm_type)));
            
            if (is_assignment && 
                    !ASRUtils::is_class_type(ASRUtils::extract_type(value_struct_type))) {
                // If lhs is class(), rhs is type() then we must reallocate always
                Vec<ASR::expr_t*> del_args; del_args.reserve(al, 1);
                del_args.push_back(al, target_expr);
                ASR::stmt_t* delete_stmt = ASRUtils::STMT(ASR::make_ImplicitDeallocate_t(
                    al, target_expr->base.loc, del_args.p, del_args.n));
                this->visit_stmt(*delete_stmt);
                null_cond = llvm::ConstantInt::get(context, llvm::APInt(1, 1));
            }

            // Reallocate when target struct ptr is null
            llvm_cond = null_cond;
        } else {
            if (ASR::is_a<ASR::StructType_t>(*
                    ASRUtils::type_get_past_allocatable_pointer(asr_ttype)) && (value_struct_type == nullptr)) {
                value_struct_type = ASRUtils::make_StructType_t_util(al, asr_type->base.loc,
                        ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(target_expr)), true);
            }
            llvm_cond = builder->CreateICmpEQ(
                        builder->CreateLoad(llvm_type->getPointerTo(), tmp),
                        llvm::ConstantPointerNull::get(llvm_type->getPointerTo()));
        }
        llvm_utils->create_if_else(
            llvm_cond,
            [&]() {
                    // For Pointer class types, only allocate the class wrapper
                    // (not the data payload). The data pointer will be set by
                    // the callee or by pointer association. Allocating data here
                    // would leak because the callee typically nullifies the out
                    // parameter before setting it to the actual target.
                    if (ASR::is_a<ASR::Pointer_t>(*asr_ttype) &&
                        ASRUtils::is_class_type(asr_type)) {
                        llvm::DataLayout DL(module->getDataLayout());
                        llvm::Value* wrapper_size = llvm::ConstantInt::get(
                            context, llvm::APInt(32, DL.getTypeAllocSize(llvm_type)));
                        llvm::Value* wrapper_ptr = allocate_class_wrapper_storage(
                            target_expr, llvm_type, wrapper_size);
                        builder->CreateStore(wrapper_ptr, tmp);
                        return;
                    }
                    Vec<ASR::alloc_arg_t> alloc_args; alloc_args.reserve(al, 1);
                    ASR::alloc_arg_t alloc_arg;
                    alloc_arg.loc = target_expr->base.loc;
                    alloc_arg.m_a = target_expr;
                    alloc_arg.m_dims = nullptr;
                    alloc_arg.n_dims = 0;
                    alloc_arg.m_sym_subclass = nullptr;
                    // if the assignment target is a class type variable and the value is not a struct type,
                    // then set the ttype of the alloc_arg to the class type
                    if (ASRUtils::is_class_type(asr_type) &&
                        !ASR::is_a<ASR::StructType_t>(*
                            ASRUtils::type_get_past_allocatable_pointer(value_struct_type))) {
                        alloc_arg.m_type = ASRUtils::make_StructType_t_util(al, asr_type->base.loc,
                                ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(target_expr)), true);
                        alloc_arg.m_sym_subclass = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(target_expr));
                    } else {
                        if (value_struct_type) {
                            value_struct_type = ASRUtils::type_get_past_allocatable_pointer(value_struct_type);
                        }
                        alloc_arg.m_type = value_struct_type;
                        if (value_expr == nullptr) {
                            value_expr = target_expr;
                        }
                        alloc_arg.m_sym_subclass = ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(value_expr));
                    }
                    alloc_arg.m_len_expr = nullptr;
                    alloc_args.push_back(al, alloc_arg);
                    ASR::stmt_t *alloc_stmt =
                        ASRUtils::STMT(
                            ASR::make_Allocate_t(al, target_expr->base.loc, alloc_args.p, 1, nullptr, nullptr, nullptr));

                    this->visit_stmt(*alloc_stmt);
            },
            []() {});
    }

    static constexpr int POLYMORPHIC_BOX_DATA_FIELD = 1;

    bool is_polymorphic_to_concrete_cast(ASR::ttype_t* src, ASR::ttype_t* dst) {
        return ASRUtils::is_unlimited_polymorphic_type(src) &&
               !ASRUtils::is_unlimited_polymorphic_type(dst);
    }

    llvm::Value* unwrap_polymorphic_box_data(llvm::Type* box_type, llvm::Value* box_ptr) {
        llvm::Value* data_field = llvm_utils->create_gep2(box_type, box_ptr, POLYMORPHIC_BOX_DATA_FIELD);
        return llvm_utils->CreateLoad2(llvm_utils->i8_ptr, data_field);
    }

    llvm::Value* get_typed_array_data_ptr(
            ASR::ttype_t* src_type, ASR::ttype_t* dst_type,
            llvm::Type* src_el_type, llvm::Type* dst_el_type,
            llvm::Value* first_el_ptr) {
        if (is_polymorphic_to_concrete_cast(src_type, dst_type) &&
            llvm::isa<llvm::StructType>(src_el_type)) {
            llvm::Value* unwrapped = unwrap_polymorphic_box_data(src_el_type, first_el_ptr);
            return builder->CreateBitCast(unwrapped, dst_el_type->getPointerTo());
        }
        return builder->CreateBitCast(first_el_ptr, dst_el_type->getPointerTo());
    }

    // Map ASR element type to CFI type code constant
    int8_t get_cfi_type_code(ASR::ttype_t* elem_type) {
        if (ASR::is_a<ASR::Integer_t>(*elem_type)) {
            int kind = ASRUtils::extract_kind_from_ttype_t(elem_type);
            switch (kind) {
                case 1: return 7;   // CFI_type_int8_t
                case 2: return 8;   // CFI_type_int16_t
                case 4: return 9;   // CFI_type_int32_t
                case 8: return 10;  // CFI_type_int64_t
            }
        } else if (ASR::is_a<ASR::Real_t>(*elem_type)) {
            int kind = ASRUtils::extract_kind_from_ttype_t(elem_type);
            switch (kind) {
                case 4: return 27;  // CFI_type_float
                case 8: return 28;  // CFI_type_double
            }
        } else if (ASR::is_a<ASR::Complex_t>(*elem_type)) {
            int kind = ASRUtils::extract_kind_from_ttype_t(elem_type);
            switch (kind) {
                case 4: return 34;  // CFI_type_float_Complex
                case 8: return 35;  // CFI_type_double_Complex
            }
        } else if (ASR::is_a<ASR::Logical_t>(*elem_type)) {
            return 39; // CFI_type_Bool
        } else if (ASR::is_a<ASR::String_t>(*elem_type)) {
            return 40; // CFI_type_char
        } else if (ASR::is_a<ASR::CPtr_t>(*elem_type)) {
            return 41; // CFI_type_cptr
        } else if (ASR::is_a<ASR::StructType_t>(*elem_type)) {
            return 42; // CFI_type_struct
        }
        return -1; // CFI_type_other
    }

    bool get_struct_flat_layout(ASR::ttype_t* elem_asr_type,
            llvm::Type* elem_llvm_type,
            ASR::expr_t* actual_expr,
            std::vector<FlatCopyback::Member>& layout,
            uint64_t& flat_size) {
        if (!ASR::is_a<ASR::StructType_t>(*elem_asr_type)) return false;
        if (!actual_expr) return false;
        ASR::symbol_t* dt_sym = ASRUtils::get_struct_sym_from_struct_expr(actual_expr);
        if (!dt_sym) return false;
        dt_sym = ASRUtils::symbol_get_past_external(dt_sym);
        if (!ASR::is_a<ASR::Struct_t>(*dt_sym)) return false;
        ASR::Struct_t* dt = ASR::down_cast<ASR::Struct_t>(dt_sym);
        if (dt->m_abi == ASR::abiType::BindC || dt->m_is_sequence) return false;

        llvm::DataLayout data_layout(module->getDataLayout());
        llvm::StructType* st = llvm::cast<llvm::StructType>(elem_llvm_type);
        bool has_char = false;
        layout.clear();
        std::vector<llvm::Type*> flat_types;
        std::vector<std::pair<int, bool>> member_info;
        for (size_t i = 0; i < dt->n_members; i++) {
            ASR::Variable_t* mvar = ASR::down_cast<ASR::Variable_t>(
                dt->m_symtab->get_symbol(dt->m_members[i]));
            ASR::ttype_t* mt = mvar->m_type;
            int mem_idx = (int)i;
            bool is_char_mem = false;
            int64_t slen = -1;
            if (!ASR::is_a<ASR::Pointer_t>(*mt) &&
                !ASR::is_a<ASR::Allocatable_t>(*mt) &&
                !ASR::is_a<ASR::Array_t>(*mt) &&
                ASR::is_a<ASR::String_t>(*ASRUtils::type_get_past_array(mt))) {
                ASR::String_t* str = ASR::down_cast<ASR::String_t>(
                    ASRUtils::type_get_past_array(mt));
                if (str->m_len && ASRUtils::extract_value(str->m_len, slen) && slen > 0) {
                    is_char_mem = true;
                    has_char = true;
                }
            }
            if (is_char_mem) {
                flat_types.push_back(llvm::ArrayType::get(
                    llvm::Type::getInt8Ty(context), slen));
                member_info.push_back({mem_idx, true});
            } else {
                flat_types.push_back(st->getElementType(mem_idx));
                member_info.push_back({mem_idx, false});
            }
        }
        if (!has_char) return false;
        llvm::StructType* flat_st = llvm::StructType::get(context, flat_types);
        const llvm::StructLayout* sl = data_layout.getStructLayout(flat_st);
        for (size_t i = 0; i < member_info.size(); i++) {
            uint64_t off = sl->getElementOffset(i);
            uint64_t sz = data_layout.getTypeAllocSize(flat_types[i]);
            layout.push_back({off, sz, member_info[i].first,
                member_info[i].second});
        }
        flat_size = sl->getSizeInBytes();
        return true;
    }

    llvm::Value* create_flat_copy_from_struct(llvm::Value* struct_ptr,
            llvm::Type* struct_llvm_type,
            const std::vector<FlatCopyback::Member>& layout,
            uint64_t flat_size) {
        llvm::Value* flat_buf = builder->CreateAlloca(
            llvm::ArrayType::get(llvm::Type::getInt8Ty(context), flat_size),
            nullptr, "flat_struct");
        llvm::Value* flat_i8 = builder->CreateBitCast(
            flat_buf, llvm::Type::getInt8Ty(context)->getPointerTo());
        for (auto& fm : layout) {
            llvm::Value* dst_gep = builder->CreateGEP(
                llvm::Type::getInt8Ty(context), flat_i8,
                llvm::ConstantInt::get(context, llvm::APInt(64, fm.flat_off)));
            llvm::Value* src_gep = llvm_utils->create_gep2(
                struct_llvm_type, struct_ptr, fm.member_idx);
            if (fm.is_char) {
                llvm::Value* desc_data = builder->CreateLoad(
                    llvm::Type::getInt8Ty(context)->getPointerTo(),
                    llvm_utils->create_gep2(string_descriptor, src_gep, 0));
                builder->CreateMemCpy(dst_gep, llvm::MaybeAlign(),
                    desc_data, llvm::MaybeAlign(),
                    llvm::ConstantInt::get(context, llvm::APInt(64, fm.size)));
            } else {
                llvm::Value* src_i8 = builder->CreateBitCast(
                    src_gep, llvm::Type::getInt8Ty(context)->getPointerTo());
                builder->CreateMemCpy(dst_gep, llvm::MaybeAlign(),
                    src_i8, llvm::MaybeAlign(),
                    llvm::ConstantInt::get(context, llvm::APInt(64, fm.size)));
            }
        }
        return flat_buf;
    }

    void copy_flat_back_to_struct(llvm::Value* flat_buf,
            llvm::Value* struct_ptr,
            llvm::Type* struct_llvm_type,
            const std::vector<FlatCopyback::Member>& layout) {
        llvm::Value* flat_i8 = builder->CreateBitCast(
            flat_buf, llvm::Type::getInt8Ty(context)->getPointerTo());
        for (auto& fm : layout) {
            llvm::Value* src_gep = builder->CreateGEP(
                llvm::Type::getInt8Ty(context), flat_i8,
                llvm::ConstantInt::get(context, llvm::APInt(64, fm.flat_off)));
            llvm::Value* dst_gep = llvm_utils->create_gep2(
                struct_llvm_type, struct_ptr, fm.member_idx);
            if (fm.is_char) {
                llvm::Value* desc_data = builder->CreateLoad(
                    llvm::Type::getInt8Ty(context)->getPointerTo(),
                    llvm_utils->create_gep2(string_descriptor, dst_gep, 0));
                builder->CreateMemCpy(desc_data, llvm::MaybeAlign(),
                    src_gep, llvm::MaybeAlign(),
                    llvm::ConstantInt::get(context, llvm::APInt(64, fm.size)));
            } else {
                llvm::Value* dst_i8 = builder->CreateBitCast(
                    dst_gep, llvm::Type::getInt8Ty(context)->getPointerTo());
                builder->CreateMemCpy(dst_i8, llvm::MaybeAlign(),
                    src_gep, llvm::MaybeAlign(),
                    llvm::ConstantInt::get(context, llvm::APInt(64, fm.size)));
            }
        }
    }

    // Set CFI descriptor fields (elem_len, type, attribute) in descriptor
    void set_cfi_descriptor_fields(llvm::Type* desc_type, llvm::Value* desc,
            ASR::ttype_t* elem_asr_type, uint64_t elem_size,
            ASR::ttype_t* full_type) {
        // elem_len (field 1)
        builder->CreateStore(
            llvm::ConstantInt::get(context, llvm::APInt(64, elem_size)),
            llvm_utils->create_gep2(desc_type, desc, 1));
        // type code (field 4, i8)
        builder->CreateStore(
            llvm::ConstantInt::get(llvm::Type::getInt8Ty(context),
                get_cfi_type_code(elem_asr_type)),
            llvm_utils->create_gep2(desc_type, desc, 4));
        // attribute (field 5, i8): allocatable=2, pointer=1, other=0
        int cfi_attr = 0;
        if (full_type && ASRUtils::is_allocatable(full_type)) {
            cfi_attr = 2;
        } else if (full_type && ASRUtils::is_pointer(full_type)) {
            cfi_attr = 1;
        }
        builder->CreateStore(
            llvm::ConstantInt::get(llvm::Type::getInt8Ty(context), cfi_attr),
            llvm_utils->create_gep2(desc_type, desc, 5));
    }

    void PointerToData_to_Descriptor(ASR::expr_t* expr, ASR::ttype_t* m_type, ASR::ttype_t* m_type_for_dimensions) {
        llvm::Type* target_type = llvm_utils->get_type_from_ttype_t_util(expr,
            ASRUtils::type_get_past_allocatable(
                ASRUtils::type_get_past_pointer(m_type)), module.get());
        llvm::Value *target = arr_descr->create_descriptor_alloca(
            target_type,
            "array_descriptor");
        ASR::ttype_t* array_ttype = ASRUtils::type_get_past_allocatable(
            ASRUtils::type_get_past_pointer(m_type));
        llvm::Type* llvm_data_type = llvm_utils->get_el_type(
            expr, ASRUtils::extract_type(array_ttype), module.get());
        llvm::Type* expected_data_ptr_type = llvm_data_type->getPointerTo();
        if (llvm::StructType* desc_struct = llvm::dyn_cast<llvm::StructType>(target_type)) {
            if (desc_struct->getNumElements() > 0 &&
                desc_struct->getElementType(0)->isPointerTy()) {
                expected_data_ptr_type = desc_struct->getElementType(0);
            }
        }
        llvm::Value* data_ptr = arr_descr->get_pointer_to_data(target_type, target);
        llvm::Value* tmp_cast = tmp;
        if (tmp_cast->getType() != expected_data_ptr_type) {
            tmp_cast = builder->CreateBitCast(tmp_cast, expected_data_ptr_type);
        }
        builder->CreateStore(tmp_cast, data_ptr);
        ASR::dimension_t* m_dims = nullptr;
        int n_dims = ASRUtils::extract_dimensions_from_ttype(m_type_for_dimensions, m_dims);
        llvm::Type* llvm_typ = llvm_utils->get_type_from_ttype_t_util(expr,
            ASRUtils::type_get_past_allocatable(m_type), module.get());
        fill_array_details(llvm_typ, target, llvm_data_type, m_dims, n_dims, false, false);
        // Set CFI descriptor fields (elem_len, type, attribute)
        ASR::ttype_t* elem_asr_type = ASRUtils::extract_type(array_ttype);
        llvm::DataLayout data_layout(module->getDataLayout());
        uint64_t elem_size = data_layout.getTypeAllocSize(llvm_data_type);
        set_cfi_descriptor_fields(target_type, target, elem_asr_type, elem_size, m_type);
        if( LLVM::is_llvm_pointer(*m_type) ) {
            llvm::AllocaInst* target_ptr = llvm_utils->CreateAlloca(
                target_type->getPointerTo(), nullptr, "array_descriptor_ptr");
            builder->CreateStore(target, target_ptr);
            target = target_ptr;
        }
        tmp = target;
    }

    void visit_ArrayPhysicalCastUtil(llvm::Value* arg, ASR::expr_t* m_arg,
        ASR::ttype_t* m_type, ASR::ttype_t* m_type_for_dimensions,
        ASR::array_physical_typeType m_old, ASR::array_physical_typeType m_new) {

        if( m_old == m_new &&
            m_old != ASR::array_physical_typeType::DescriptorArray ) {
            return ;
        }

        llvm::Type *data_type = llvm_utils->get_el_type(
            m_arg, ASRUtils::extract_type(ASRUtils::expr_type(m_arg)), module.get());
        llvm::Type *arr_type = llvm_utils->get_type_from_ttype_t_util(m_arg,
            ASRUtils::type_get_past_allocatable(
            ASRUtils::type_get_past_pointer(ASRUtils::expr_type(m_arg))),
            module.get());
        llvm::Type* m_arg_llvm_type = llvm_utils->get_type_from_ttype_t_util(m_arg, ASRUtils::expr_type(m_arg), module.get());
        ASR::expr_t* m_arg_value = ASRUtils::expr_value(m_arg);
        bool is_array_constructor_value = m_arg_value &&
            ASR::is_a<ASR::ArrayConstructor_t>(*m_arg_value);
        if( m_new == ASR::array_physical_typeType::PointerArray &&
            m_old == ASR::array_physical_typeType::DescriptorArray ) {
            if( ASR::is_a<ASR::StructInstanceMember_t>(*m_arg) ) {
                arg = llvm_utils->CreateLoad2(m_arg_llvm_type, arg);
            }
            ASR::ttype_t* src_asr_type = ASRUtils::expr_type(m_arg);
            ASR::ttype_t* dst_asr_type = m_type;
            llvm::Type* dst_el_type = llvm_utils->get_el_type(
                m_arg, ASRUtils::extract_type(ASRUtils::type_get_past_allocatable_pointer(dst_asr_type)), module.get());
            llvm::Value* src_data_ptr = llvm_utils->CreateLoad2(data_type->getPointerTo(),
                arr_descr->get_pointer_to_data(m_arg, src_asr_type, arg, module.get()));
            llvm::Value* src_first_el_ptr = llvm_utils->create_ptr_gep2(data_type, src_data_ptr,
                arr_descr->get_offset(arr_type, arg));
            tmp = get_typed_array_data_ptr(src_asr_type, dst_asr_type, data_type, dst_el_type, src_first_el_ptr);
        } else if( m_new == ASR::array_physical_typeType::UnboundedPointerArray &&
            m_old == ASR::array_physical_typeType::DescriptorArray ) {
            if( ASR::is_a<ASR::StructInstanceMember_t>(*m_arg) ) {
                arg = llvm_utils->CreateLoad2(m_arg_llvm_type, arg);
            }
            ASR::ttype_t* src_asr_type = ASRUtils::expr_type(m_arg);
            ASR::ttype_t* dst_asr_type = m_type;
            llvm::Type* dst_el_type = llvm_utils->get_el_type(
                m_arg, ASRUtils::extract_type(ASRUtils::type_get_past_allocatable_pointer(dst_asr_type)), module.get());
            llvm::Value* src_data_ptr = llvm_utils->CreateLoad2(data_type->getPointerTo(),
                arr_descr->get_pointer_to_data(m_arg, src_asr_type, arg, module.get()));
            llvm::Value* src_first_el_ptr = llvm_utils->create_ptr_gep2(data_type, src_data_ptr,
                arr_descr->get_offset(arr_type, arg));
            tmp = get_typed_array_data_ptr(src_asr_type, dst_asr_type, data_type, dst_el_type, src_first_el_ptr);
        } else if(
            m_new == ASR::array_physical_typeType::PointerArray &&
            m_old == ASR::array_physical_typeType::UnboundedPointerArray) {
            // Both are pointer-to-data representations, just pass through
        } else if(
            m_new == ASR::array_physical_typeType::UnboundedPointerArray &&
            m_old == ASR::array_physical_typeType::PointerArray) {
            // Both are pointer-to-data representations, just pass through
        } else if(
            m_new == ASR::array_physical_typeType::PointerArray &&
            m_old == ASR::array_physical_typeType::FixedSizeArray) {
            if( ((m_arg_value &&
                !ASR::is_a<ASR::ArrayConstant_t>(*m_arg_value) &&
                !is_array_constructor_value) ||
                m_arg_value == nullptr ) &&
                !ASR::is_a<ASR::ArrayConstructor_t>(*m_arg) ) {
                tmp = llvm_utils->CreateGEP2(m_arg_llvm_type, tmp, 0);
            }
        } else if(
            m_new == ASR::array_physical_typeType::UnboundedPointerArray &&
            m_old == ASR::array_physical_typeType::FixedSizeArray) {
            if( ((m_arg_value &&
                !ASR::is_a<ASR::ArrayConstant_t>(*m_arg_value) &&
                !is_array_constructor_value) ||
                m_arg_value == nullptr) &&
                !ASR::is_a<ASR::ArrayConstructor_t>(*m_arg) ) {
                tmp = llvm_utils->create_gep2(arr_type, tmp, 0);
            }
        } else if (
            m_new == ASR::array_physical_typeType::SIMDArray &&
            m_old == ASR::array_physical_typeType::FixedSizeArray) {
            // pass
        } else if (
            m_new == ASR::array_physical_typeType::DescriptorArray &&
            m_old == ASR::array_physical_typeType::SIMDArray) {
            tmp = llvm_utils->CreateLoad2(m_arg_llvm_type, arg);
        } else if(
            m_new == ASR::array_physical_typeType::DescriptorArray &&
            m_old == ASR::array_physical_typeType::FixedSizeArray) {
            if( ((m_arg_value &&
                !ASR::is_a<ASR::ArrayConstant_t>(*m_arg_value) &&
                !is_array_constructor_value) ||
                m_arg_value == nullptr) &&
                !ASR::is_a<ASR::ArrayConstructor_t>(*m_arg) ) {
                tmp = llvm_utils->create_gep2(arr_type, tmp, 0);
            }
            PointerToData_to_Descriptor(m_arg, m_type, m_type_for_dimensions);
        } else if(
            m_new == ASR::array_physical_typeType::DescriptorArray &&
            m_old == ASR::array_physical_typeType::PointerArray) {
            PointerToData_to_Descriptor(m_arg, m_type, m_type_for_dimensions);
        } else if(
            m_new == ASR::array_physical_typeType::DescriptorArray &&
            m_old == ASR::array_physical_typeType::UnboundedPointerArray) {
            PointerToData_to_Descriptor(m_arg, m_type, m_type_for_dimensions);
        } else if(
            m_new == ASR::array_physical_typeType::FixedSizeArray &&
            m_old == ASR::array_physical_typeType::DescriptorArray) {
            if( ASR::is_a<ASR::StructInstanceMember_t>(*m_arg) ) {
                tmp = llvm_utils->CreateLoad2(m_arg_llvm_type, tmp);
            }
            ASR::ttype_t* src_asr_type = ASRUtils::expr_type(m_arg);
            tmp = llvm_utils->CreateLoad2(data_type->getPointerTo(), arr_descr->get_pointer_to_data(m_arg, src_asr_type, tmp, module.get()));
            llvm::Type* target_type = llvm_utils->get_type_from_ttype_t_util(m_arg, m_type, module.get())->getPointerTo();
            tmp = builder->CreateBitCast(tmp, target_type);
        } else if(
            m_new == ASR::array_physical_typeType::DescriptorArray &&
            m_old == ASR::array_physical_typeType::DescriptorArray) {
            // Check if this is a rank-only change for allocatable/pointer args
            // (e.g., for assumed-rank targets). In that case, a pointer bitcast
            // suffices since the callee's select rank checks the rank field.
            ASR::ttype_t* src_asr_type = ASRUtils::expr_type(m_arg);
            int src_n_dims = ASRUtils::extract_n_dims_from_ttype(src_asr_type);
            int dst_n_dims = ASRUtils::extract_n_dims_from_ttype(m_type);
            if (dst_n_dims > src_n_dims &&
                (ASR::is_a<ASR::Allocatable_t>(*src_asr_type) ||
                 ASR::is_a<ASR::Pointer_t>(*src_asr_type)) &&
                (ASR::is_a<ASR::Allocatable_t>(*m_type) ||
                 ASR::is_a<ASR::Pointer_t>(*m_type))) {
                llvm::Type* target_desc_type = llvm_utils->get_type_from_ttype_t_util(
                    m_arg,
                    ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(m_type)),
                    module.get());
                // arg is %array.N** (pointer to allocatable descriptor pointer)
                // with ptr_loads=0. Bitcast to %array.15** so the callee sees
                // the right type. This is safe because the callee's select rank
                // checks the rank field and only accesses valid dim slots.
                tmp = builder->CreateBitCast(arg, target_desc_type->getPointerTo()->getPointerTo());
            } else {
            // General descriptor conversion: create a new descriptor with the
            // target element type/rank and copy metadata from the source.
            // Handles polymorphic-to-concrete casts, rank expansion for
            // non-allocatable args, and other type-only conversions.
            if( ASR::is_a<ASR::StructInstanceMember_t>(*m_arg) ) {
                arg = llvm_utils->CreateLoad2(m_arg_llvm_type, arg);
            }
            llvm::Type* target_desc_type = llvm_utils->get_type_from_ttype_t_util(
                m_arg,
                ASRUtils::type_get_past_allocatable(
                    ASRUtils::type_get_past_pointer(m_type)),
                module.get());
            llvm::Value *target_desc = arr_descr->create_descriptor_alloca(
                target_desc_type,
                "array_descriptor");

            llvm::Value* source_desc = arg;
            llvm::Value* source_desc_as_target = builder->CreateBitCast(
                source_desc, target_desc_type->getPointerTo());

            ASR::ttype_t* src_asr_type2 = ASRUtils::expr_type(m_arg);
            ASR::ttype_t* dst_asr_type = m_type;
            ASR::ttype_t* src_arr_asr_type = ASRUtils::type_get_past_allocatable_pointer(src_asr_type2);
            ASR::ttype_t* dst_arr_asr_type = ASRUtils::type_get_past_allocatable_pointer(dst_asr_type);

            llvm::Type* src_el_type = llvm_utils->get_el_type(
                m_arg, ASRUtils::extract_type(src_arr_asr_type), module.get());
            llvm::Type* dst_el_type = llvm_utils->get_el_type(
                m_arg, ASRUtils::extract_type(dst_arr_asr_type), module.get());

            llvm::Value* src_data_ptr = llvm_utils->CreateLoad2(
                src_el_type->getPointerTo(),
                arr_descr->get_pointer_to_data(m_arg, src_asr_type2, source_desc, module.get()));
            llvm::Value* src_offset = arr_descr->get_offset(target_desc_type, source_desc_as_target);
            llvm::Value* src_first_el_ptr = llvm_utils->create_ptr_gep2(src_el_type, src_data_ptr, src_offset);

            llvm::Value* dst_first_el_ptr = get_typed_array_data_ptr(
                src_asr_type2, dst_asr_type, src_el_type, dst_el_type, src_first_el_ptr);

            // Character arrays from class(*): the data is per-element string_descriptors,
            // but character array layout expects a single descriptor with flat buffer.
            ASR::ttype_t* dst_base_type = ASRUtils::extract_type(dst_arr_asr_type);
            if (is_polymorphic_to_concrete_cast(src_asr_type2, dst_asr_type) &&
                    ASR::is_a<ASR::String_t>(*dst_base_type)) {
                llvm::Value* n_elems = arr_descr->get_array_size(
                    target_desc_type, source_desc_as_target, nullptr, 4);
                llvm::Value* n_elems_i64 = builder->CreateSExt(
                    n_elems, llvm::Type::getInt64Ty(context));
                dst_first_el_ptr = llvm_utils->consolidate_char_descriptors(
                    dst_first_el_ptr, n_elems_i64);
                llvm::Value* consolidated_flat_buf = llvm_utils->CreateLoad2(
                    character_type,
                    llvm_utils->create_gep2(string_descriptor, dst_first_el_ptr, 0));
                heap_fixed_size_arrays.push_back(al, consolidated_flat_buf);
            }

            builder->CreateStore(dst_first_el_ptr,
                arr_descr->get_pointer_to_data(target_desc_type, target_desc));

            int n_dims = ASRUtils::extract_n_dims_from_ttype(m_type_for_dimensions);
            arr_descr->reset_array_details(target_desc_type, target_desc, target_desc_type, source_desc_as_target, n_dims);
            tmp = target_desc;
            }
        } else if (
            m_new == ASR::array_physical_typeType::PointerArray &&
            m_old == ASR::array_physical_typeType::StringArraySinglePointer) {
        //
            if (ASRUtils::is_fixed_size_array(m_type)) {
                if( ((m_arg_value &&
                    !ASR::is_a<ASR::ArrayConstant_t>(*m_arg_value) &&
                    !is_array_constructor_value) ||
                    m_arg_value == nullptr) &&
                    !ASR::is_a<ASR::ArrayConstructor_t>(*m_arg) ) {
                    tmp = llvm_utils->create_gep2(arr_type, tmp, 0);
                }
            } else {
                tmp = llvm_utils->CreateLoad2(data_type->getPointerTo(),arr_descr->get_pointer_to_data(arr_type, tmp));
            }
        } else if (
            m_new == ASR::array_physical_typeType::StringArraySinglePointer &&
            m_old == ASR::array_physical_typeType::DescriptorArray) {
            auto const arr_data_loaded = llvm_utils->CreateLoad2(data_type->getPointerTo(), arr_descr->get_pointer_to_data(arr_type, tmp));
            tmp = llvm_utils->get_string_data(ASRUtils::get_string_type(m_type), arr_data_loaded); //StringArraySinglePointer = `char*`
        } else if (
            m_new == ASR::array_physical_typeType::StringArraySinglePointer &&
            m_old == ASR::array_physical_typeType::PointerArray) {
            if (ASRUtils::is_character(*ASRUtils::extract_type(ASRUtils::expr_type(m_arg)))) {
                // For character arrays in bind(c) context
                ASR::ttype_t* old_ttype = ASRUtils::extract_type(ASRUtils::expr_type(m_arg));
                llvm::Type* str_desc = llvm_utils->get_type_from_ttype_t_util(m_arg, old_ttype, module.get());
                tmp = llvm_utils->create_gep2(str_desc, tmp, 0);
                tmp = llvm_utils->CreateLoad2(llvm::Type::getInt8Ty(context)->getPointerTo(),tmp);
            }
        } else if (
            m_new == ASR::array_physical_typeType::DescriptorArray &&
            m_old == ASR::array_physical_typeType::AssumedRankArray) {

            if (!ASRUtils::is_array(m_type)) {
                // rank(0): extract scalar from the assumed-rank descriptor.
                llvm::Value* data_ptr = llvm_utils->CreateLoad2(
                    data_type->getPointerTo(),
                    arr_descr->get_pointer_to_data(arr_type, arg));
                if (ASRUtils::is_struct(*m_type) || ptr_loads == 0) {
                    tmp = data_ptr;
                } else {
                    tmp = llvm_utils->CreateLoad2(data_type, data_ptr);
                }
            } else {

            llvm::Type* target_desc_type = llvm_utils->get_type_from_ttype_t_util(
                m_arg,
                ASRUtils::type_get_past_allocatable(
                    ASRUtils::type_get_past_pointer(m_type)),
                module.get());
            llvm::Value *target_desc = arr_descr->create_descriptor_alloca(
                target_desc_type,
                "array_descriptor");

            llvm::Value* source_desc = arg;
            // `reset_array_details()` expects `source_arr` to be indexed using the same
            // descriptor type as the target. The only difference between the source and
            // destination descriptor types here is the element pointer type, so a bitcast
            // is safe for copying descriptor metadata (dims/offset/rank).
            llvm::Value* source_desc_as_target = builder->CreateBitCast(
                source_desc, target_desc_type->getPointerTo());

            ASR::ttype_t* src_asr_type = ASRUtils::expr_type(m_arg);
            ASR::ttype_t* dst_asr_type = m_type;
            ASR::ttype_t* src_arr_asr_type = ASRUtils::type_get_past_allocatable_pointer(src_asr_type);
            ASR::ttype_t* dst_arr_asr_type = ASRUtils::type_get_past_allocatable_pointer(dst_asr_type);

            llvm::Type* src_el_type = llvm_utils->get_el_type(
                m_arg, ASRUtils::extract_type(src_arr_asr_type), module.get());
            llvm::Type* dst_el_type = llvm_utils->get_el_type(
                m_arg, ASRUtils::extract_type(dst_arr_asr_type), module.get());

            llvm::Value* src_data_ptr = llvm_utils->CreateLoad2(
                src_el_type->getPointerTo(),
                arr_descr->get_pointer_to_data(m_arg, src_asr_type, source_desc, module.get()));
            llvm::Value* src_offset = arr_descr->get_offset(target_desc_type, source_desc_as_target);
            llvm::Value* src_first_el_ptr = llvm_utils->create_ptr_gep2(src_el_type, src_data_ptr, src_offset);

            llvm::Value* dst_first_el_ptr = get_typed_array_data_ptr(
                src_asr_type, dst_asr_type, src_el_type, dst_el_type, src_first_el_ptr);

            builder->CreateStore(dst_first_el_ptr,
                arr_descr->get_pointer_to_data(target_desc_type, target_desc));

            int n_dims = ASRUtils::extract_n_dims_from_ttype(m_type_for_dimensions);
            arr_descr->reset_array_details(target_desc_type, target_desc, target_desc_type, source_desc_as_target, n_dims);
            if( LLVM::is_llvm_pointer(*m_type) ) {
                llvm::AllocaInst* target_ptr = llvm_utils->CreateAlloca(
                    target_desc_type->getPointerTo(), nullptr, "array_descriptor_ptr");
                builder->CreateStore(target_desc, target_ptr);
                tmp = target_ptr;
            } else {
                tmp = target_desc;
            }
            } // end else (rank >= 1)
        } else if (
            m_new == ASR::array_physical_typeType::PointerArray &&
            m_old == ASR::array_physical_typeType::AssumedRankArray) {
            
            tmp = llvm_utils->CreateLoad2(data_type->getPointerTo(), arr_descr->get_pointer_to_data(arr_type, arg));
            tmp = llvm_utils->create_ptr_gep2(data_type, tmp, arr_descr->get_offset(arr_type, arg));
        } else if (
            m_new == ASR::array_physical_typeType::FixedSizeArray &&
            m_old == ASR::array_physical_typeType::PointerArray) {
            llvm::Type* target_type = llvm_utils->get_type_from_ttype_t_util(
                m_arg, m_type, module.get())->getPointerTo();
            tmp = builder->CreateBitCast(tmp, target_type);
        } else {
            LCOMPILERS_ASSERT(false);
        }
    }

    void visit_ArrayPhysicalCast(const ASR::ArrayPhysicalCast_t& x) {
        if( x.m_old != ASR::array_physical_typeType::DescriptorArray ) {
            LCOMPILERS_ASSERT(x.m_new != x.m_old);
        }
        ASR::ttype_t* src_asr_type = ASRUtils::expr_type(x.m_arg);
        int target_n_dims = ASRUtils::extract_n_dims_from_ttype(x.m_type);
        int src_n_dims = ASRUtils::extract_n_dims_from_ttype(src_asr_type);
        bool is_src_alloc_or_ptr = ASR::is_a<ASR::Allocatable_t>(*src_asr_type) ||
                               ASR::is_a<ASR::Pointer_t>(*src_asr_type);
        bool is_dst_alloc_or_ptr = ASR::is_a<ASR::Allocatable_t>(*x.m_type) ||
                                   ASR::is_a<ASR::Pointer_t>(*x.m_type);
        // For allocatable/pointer assumed-rank casts (DescriptorArray→DescriptorArray
        // with rank expansion), where both source AND target are allocatable/pointer,
        // we need the unloaded pointer (%array.N**) so we can
        // bitcast it to %array.15** without losing the pointer-to-pointer semantics.
        bool is_alloc_rank_expand = (x.m_old == ASR::array_physical_typeType::DescriptorArray &&
                                     x.m_new == ASR::array_physical_typeType::DescriptorArray &&
                                     target_n_dims > src_n_dims &&
                                     is_src_alloc_or_ptr && is_dst_alloc_or_ptr);
        int64_t ptr_loads_copy = ptr_loads;
        if (is_alloc_rank_expand) {
            ptr_loads = 0;
        } else {
            ptr_loads = 2 - LLVM::is_llvm_pointer(*src_asr_type);
        }
        this->visit_expr_wrapper(x.m_arg, false);
        ptr_loads = ptr_loads_copy;
        // For assumed-rank targets (rank-15 descriptor), use the argument's
        // type for dimension info since the target dims are empty placeholders.
        ASR::ttype_t* m_type_for_dims = x.m_type;
        if (target_n_dims > src_n_dims && src_n_dims > 0) {
            m_type_for_dims = src_asr_type;
        }
        visit_ArrayPhysicalCastUtil(tmp, x.m_arg, x.m_type, m_type_for_dims, x.m_old, x.m_new);
    }

    void visit_AssociateBlockCall(const ASR::AssociateBlockCall_t& x) {
        LCOMPILERS_ASSERT(ASR::is_a<ASR::AssociateBlock_t>(*x.m_m));
        ASR::AssociateBlock_t* associate_block = ASR::down_cast<ASR::AssociateBlock_t>(x.m_m);
        
        llvm::BasicBlock* start_BB = llvm::BasicBlock::Create(context, std::string(associate_block->m_name) + "_start");
        start_new_block(start_BB);

        // Save stack pointer to reclaim alloca space at block exit.
        // Associate-block-scoped variables (descriptors, temporaries)
        // are allocated via alloca each time the block executes; without
        // stackrestore, an associate block inside a loop leaks stack
        // every iteration.
#if LLVM_VERSION_MAJOR >= 18
        llvm::Value *saved_stack = builder->CreateStackSave();
#else
        llvm::Function *stacksave_fn = llvm::Intrinsic::getDeclaration(
            module.get(), llvm::Intrinsic::stacksave);
        llvm::Value *saved_stack = builder->CreateCall(stacksave_fn);
#endif

        size_t heap_arrays_before = heap_fixed_size_arrays.n;
        in_block_context = true;
        declare_vars(*associate_block);
        in_block_context = false;

        // For expression-valued associates (e.g., nv => v * 2.0),
        // the variable is Pointer-typed but declare_vars only
        // null-initializes it.  Allocate backing storage so the
        // body assignment can store through the pointer.
        // Simple associations (e.g., v => tensor%v) appear as
        // Associate nodes in the body and set the pointer correctly;
        // those are skipped.
        {
            // Collect variables that are targets of Associate nodes
            // (simple associations that will be set up correctly).
            std::set<uint32_t> simple_assoc_vars;
            for (size_t i = 0; i < associate_block->n_body; i++) {
                if (!ASR::is_a<ASR::Associate_t>(
                        *associate_block->m_body[i]))
                    continue;
                ASR::Associate_t *assoc = ASR::down_cast<
                    ASR::Associate_t>(associate_block->m_body[i]);
                if (!ASR::is_a<ASR::Var_t>(*assoc->m_target)) continue;
                ASR::symbol_t *sym = ASR::down_cast<ASR::Var_t>(
                    assoc->m_target)->m_v;
                simple_assoc_vars.insert(
                    get_hash((ASR::asr_t*)ASRUtils::symbol_get_past_external(sym)));
            }
            // Allocate backing storage for Pointer-typed variables
            // NOT covered by Associate nodes (expression associates).
            for (auto &item : associate_block->m_symtab->get_scope()) {
                if (!ASR::is_a<ASR::Variable_t>(*item.second)) continue;
                ASR::Variable_t *v = ASR::down_cast<ASR::Variable_t>(
                    item.second);
                if (!ASR::is_a<ASR::Pointer_t>(*v->m_type)) continue;
                ASR::ttype_t *pointee_type =
                    ASR::down_cast<ASR::Pointer_t>(v->m_type)->m_type;
                if (ASR::is_a<ASR::FunctionType_t>(*pointee_type)) continue;
                if (ASRUtils::is_array(pointee_type)) continue;
                uint32_t h = get_hash((ASR::asr_t*)v);
                if (simple_assoc_vars.count(h)) continue;
                if (llvm_symtab.find(h) == llvm_symtab.end()) continue;
                llvm::Value *ptr_var = llvm_symtab[h];
                llvm::AllocaInst *alloca_inst =
                    llvm::dyn_cast<llvm::AllocaInst>(ptr_var);
                if (!alloca_inst) continue;
                llvm::Type *ptr_type = alloca_inst->getAllocatedType();
                if (!ptr_type->isPointerTy()) continue;
#if LLVM_VERSION_MAJOR >= 15
                llvm::Type *elem_type =
                    llvm_utils->get_type_from_ttype_t_util(
                        nullptr, pointee_type, module.get());
#else
                llvm::Type *elem_type =
                    ptr_type->getPointerElementType();
#endif
                llvm::Value *backing = builder->CreateAlloca(
                    elem_type, nullptr,
                    std::string(v->m_name) + ".tmp");
                builder->CreateStore(backing, ptr_var);
            }
        }

        for (size_t i = 0; i < associate_block->n_body; i++) {
            this->visit_stmt(*(associate_block->m_body[i]));
        }

        llvm::BasicBlock* end_BB = llvm::BasicBlock::Create(context, std::string(associate_block->m_name) + "_end");
        start_new_block(end_BB);
        llvm_symtab_finalizer.finalize_symtab(associate_block->m_symtab);

        // Free associate-block-local heap arrays
        for (size_t i = heap_arrays_before; i < heap_fixed_size_arrays.n; i++) {
            llvm_utils->lfortran_free(heap_fixed_size_arrays[i]);
        }
        heap_fixed_size_arrays.n = heap_arrays_before;

        // Restore stack pointer to reclaim block-scoped alloca space
#if LLVM_VERSION_MAJOR >= 18
        builder->CreateStackRestore(saved_stack);
#else
        llvm::Function *stackrestore_fn = llvm::Intrinsic::getDeclaration(
            module.get(), llvm::Intrinsic::stackrestore);
        builder->CreateCall(stackrestore_fn, {saved_stack});
#endif
    }

    void visit_BlockCall(const ASR::BlockCall_t& x) {
        if( x.m_label != -1 ) {
            if( llvm_goto_targets.find(x.m_label) == llvm_goto_targets.end() ) {
                llvm::BasicBlock *new_target = llvm::BasicBlock::Create(context, "goto_target");
                llvm_goto_targets[x.m_label] = new_target;
            }
            start_new_block(llvm_goto_targets[x.m_label]);
        }
        LCOMPILERS_ASSERT(ASR::is_a<ASR::Block_t>(*x.m_m));
        ASR::Block_t* block = ASR::down_cast<ASR::Block_t>(x.m_m);
        std::string block_name;
        if (block->m_name) {
            block_name = std::string(block->m_name);
        } else {
            block_name = "block";
        }
        std::string blockstart_name = block_name + ".start";
        std::string blockend_name = block_name + ".end";
        llvm::BasicBlock* const blockstart = llvm::BasicBlock::Create(context, blockstart_name);
        llvm::BasicBlock* const blockend   = llvm::BasicBlock::Create(context, blockend_name);
        start_new_block(blockstart);
        SymbolTable* current_scope_copy = current_scope;
        current_scope = block->m_symtab;

        // Save stack pointer to reclaim alloca space at block exit.
        // Block-scoped variables (descriptors, temporaries, loop vars)
        // are allocated via alloca each time the block executes; without
        // stackrestore, a block inside a loop leaks stack every iteration.
#if LLVM_VERSION_MAJOR >= 18
        llvm::Value *saved_stack = builder->CreateStackSave();
#else
        llvm::Function *stacksave_fn = llvm::Intrinsic::getDeclaration(
            module.get(), llvm::Intrinsic::stacksave);
        llvm::Value *saved_stack = builder->CreateCall(stacksave_fn);
#endif

        // BLOCK arrays always use heap allocation (can be in loops)
        // Track allocations separately for cleanup at BLOCK exit
        size_t heap_arrays_before = heap_fixed_size_arrays.n;
        size_t wb_before = pending_char_writebacks.size();
        in_block_context = true;
        declare_vars(*block);
        in_block_context = false;
        loop_or_block_end.push_back(blockend);
        loop_or_block_end_names.push_back(blockend_name);
        for (size_t i = 0; i < block->n_body; i++) {
            this->visit_stmt(*(block->m_body[i]));
        }

        // Write back consolidated character data to original polymorphic
        // per-element string descriptors (select type char array fix).
        for (size_t i = wb_before; i < pending_char_writebacks.size(); i++) {
            auto& wb = pending_char_writebacks[i];
            llvm_utils->writeback_char_to_polymorphic_descriptors(
                wb.original_descs_i8, wb.consolidated_desc, wb.n_elems_i64);
        }
        pending_char_writebacks.resize(wb_before);

        start_new_block(blockend);
        // Finalize struct members before freeing the backing array memory
        llvm_symtab_finalizer.finalize_symtab(block->m_symtab);

        // Free BLOCK-local heap arrays after finalizing (important for loops)
        for (size_t i = heap_arrays_before; i < heap_fixed_size_arrays.n; i++) {
            llvm_utils->lfortran_free(heap_fixed_size_arrays[i]);
        }
        heap_fixed_size_arrays.n = heap_arrays_before;

        // Restore stack pointer to reclaim block-scoped alloca space
#if LLVM_VERSION_MAJOR >= 18
        builder->CreateStackRestore(saved_stack);
#else
        llvm::Function *stackrestore_fn = llvm::Intrinsic::getDeclaration(
            module.get(), llvm::Intrinsic::stackrestore);
        builder->CreateCall(stackrestore_fn, {saved_stack});
#endif

        loop_or_block_end.pop_back();
        loop_or_block_end_names.pop_back();
        current_scope = current_scope_copy;
    }

    inline void visit_expr_wrapper(ASR::expr_t* x, bool load_ref=false, bool is_volatile = false) {
        // Check if *x is nullptr.
        if( x == nullptr ) {
            throw CodeGenError("Internal error: x is nullptr");
        }

        this->visit_expr(*x);

        if (load_ref &&
               ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(ASRUtils::expr_type(x))) &&
                ASR::is_a<ASR::StructInstanceMember_t>(*x)) {
            llvm::Type* x_llvm_type = llvm_utils->get_type_from_ttype_t_util(x, ASRUtils::expr_type(x), module.get());
            tmp = llvm_utils->CreateLoad2(x_llvm_type, tmp, is_volatile);
            return;
        }

        if (load_ref && LLVM::is_llvm_pointer(*ASRUtils::expr_type(x)) &&
                (ASRUtils::is_unlimited_polymorphic_type(x) || ASR::is_a<ASR::Cast_t>(*x))) {
            llvm::Type* x_llvm_type = llvm_utils->get_type_from_ttype_t_util(x, ASRUtils::expr_type(x), module.get());
            tmp = llvm_utils->CreateLoad2(x_llvm_type, tmp, is_volatile);
            return;
        }

        if( x->type == ASR::exprType::ArrayItem ||
            x->type == ASR::exprType::ArraySection ||
            x->type == ASR::exprType::StructInstanceMember ) {
            if( load_ref &&
                !ASRUtils::is_value_constant(ASRUtils::expr_value(x)) &&
                (ASRUtils::is_array(expr_type(x)) || !ASRUtils::is_character(*expr_type(x)))) {
                tmp = logical_load_val(tmp, x, is_volatile);
            }
        }
    }
    /*
        Visits the expression with the desired pointer loads
        and reverting back the original load.
    */
    void visit_expr_load_wrapper(ASR::expr_t* x, int desired_ptr_load, bool load_ref=false){
        int ptr_loads_copy = ptr_loads;
        ptr_loads = desired_ptr_load;
        if(load_ref){
            this->visit_expr_wrapper(x, true);
        } else {
            this->visit_expr(*x);
        }
        ptr_loads = ptr_loads_copy;
    }

    void fill_type_stmt(const ASR::SelectType_t& x,
        std::vector<ASR::type_stmt_t*>& type_stmt_order,
        ASR::type_stmtType type_stmt_type) {
        for( size_t i = 0; i < x.n_body; i++ ) {
            if( x.m_body[i]->type == type_stmt_type ) {
                type_stmt_order.push_back(x.m_body[i]);
            }
        }
    }

    void fill_rank_stmt(const ASR::SelectRank_t& x, std::vector<ASR::rank_stmt_t*>& rank_stmt_order,
        ASR::rank_stmtType rank_stmt_type) {
        for( size_t i = 0; i < x.n_body; i++ ) {
            if( x.m_body[i]->type == rank_stmt_type ) {
                rank_stmt_order.push_back(x.m_body[i]);
            }
        }
    }

    void visit_SelectRank(const ASR::SelectRank_t& x) {
        LCOMPILERS_ASSERT(ASR::is_a<ASR::Var_t>(*x.m_selector));
        std::vector<ASR::rank_stmt_t*> rank_stmts;
        fill_rank_stmt(x, rank_stmts, ASR::rank_stmtType::RankExpr);
        LCOMPILERS_ASSERT(x.n_body == rank_stmts.size());
        ASR::Var_t* selector_var = ASR::down_cast<ASR::Var_t>(x.m_selector);
        std::string selector_var_name = ASRUtils::symbol_name(selector_var->m_v);
        uint64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        visit_Var(*selector_var);
        ptr_loads = ptr_loads_copy;
        llvm::Value* llvm_selector = tmp;
        ASR::ttype_t* selector_asr_type = ASRUtils::expr_type(x.m_selector);
        llvm::Type* llvm_selector_type_ = llvm_utils->get_type_from_ttype_t_util(x.m_selector, selector_asr_type, module.get());
        if (LLVM::is_llvm_pointer(*selector_asr_type)) {
            llvm_selector = llvm_utils->CreateLoad2(llvm_selector_type_, llvm_selector);
            selector_asr_type = ASRUtils::type_get_past_allocatable_pointer(selector_asr_type);
            llvm_selector_type_ = llvm_utils->get_type_from_ttype_t_util(x.m_selector, selector_asr_type, module.get());
        }
        std::string merge_name = "ifcont";
        if (x.m_name) {
            merge_name = std::string(x.m_name) + ".end";
        }
        llvm::BasicBlock *mergeBB = llvm::BasicBlock::Create(context, merge_name);

        if (x.m_name) {
            loop_or_block_end.push_back(mergeBB);
            loop_or_block_end_names.push_back(merge_name);
        }

        llvm::Value* rank = arr_descr->get_rank(llvm_selector_type_, llvm_selector);
        for( size_t i = 0; i < rank_stmts.size(); i++ ) {
            llvm::Function* fn = builder->GetInsertBlock()->getParent();
            llvm::BasicBlock *thenBB = llvm::BasicBlock::Create(context, "then", fn);
            llvm::BasicBlock *elseBB = llvm::BasicBlock::Create(context, "else");

            llvm::Value* cond = nullptr;
            ASR::stmt_t** rank_block = nullptr;
            size_t n_rank_block = 0;
            switch( rank_stmts[i]->type ) {
                case ASR::rank_stmtType::RankExpr: {
                    ASR::RankExpr_t* rank_expr = ASR::down_cast<ASR::RankExpr_t>(rank_stmts[i]);
                    llvm::Value* rank_value = nullptr;
                    this->visit_expr_wrapper(rank_expr->m_rank, false);
                    rank_value = tmp;
                    cond = builder->CreateICmpEQ(rank, rank_value);

                    rank_block = rank_expr->m_body;
                    n_rank_block = rank_expr->n_body;
                    break;
                }
                default: {
                    throw CodeGenError("ASR::rank_stmtType, " +
                                       std::to_string(x.m_body[i]->type) +
                                       " is not yet supported.");
                }
            }
            builder->CreateCondBr(cond, thenBB, elseBB);
            builder->SetInsertPoint(thenBB);
            {
                if ( n_rank_block == 1 && ASR::is_a<ASR::BlockCall_t>(*rank_block[0])) {
                    visit_BlockCall(*ASR::down_cast<ASR::BlockCall_t>(rank_block[0]));
                }
            }
            builder->CreateBr(mergeBB);
            start_new_block(elseBB);

        }
        if (x.n_default > 0) {
            for( size_t i = 0; i < x.n_default; i++ ) {
                this->visit_stmt(*x.m_default[i]);
            }
        }
        if (x.m_name) {
            loop_or_block_end.pop_back();
            loop_or_block_end_names.pop_back();
        }
        start_new_block(mergeBB);
    }

    void visit_SelectType(const ASR::SelectType_t& x) {
        // Process TypeStmtName first, then ClassStmt
        std::vector<ASR::type_stmt_t*> select_type_stmts;
        fill_type_stmt(x, select_type_stmts, ASR::type_stmtType::TypeStmtName);
        fill_type_stmt(x, select_type_stmts, ASR::type_stmtType::TypeStmtType);
        fill_type_stmt(x, select_type_stmts, ASR::type_stmtType::ClassStmt);
        LCOMPILERS_ASSERT(x.n_body == select_type_stmts.size());
        uint64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        this->visit_expr(*x.m_selector);
        ptr_loads = ptr_loads_copy;
        llvm::Value* llvm_selector = tmp;
        llvm::Type* llvm_selector_type_ = llvm_utils->get_type_from_ttype_t_util(x.m_selector, ASRUtils::expr_type(x.m_selector), module.get());
        llvm::BasicBlock *mergeBB = llvm::BasicBlock::Create(context, "ifcont");
        for( size_t i = 0; i < select_type_stmts.size(); i++ ) {
            llvm::Function *fn = builder->GetInsertBlock()->getParent();

            llvm::BasicBlock *thenBB = llvm::BasicBlock::Create(context, "then", fn);
            llvm::BasicBlock *elseBB = llvm::BasicBlock::Create(context, "else");

            llvm::Value* cond = nullptr;
            cond = llvm_utils->CreateAlloca(*builder, llvm::Type::getInt1Ty(context));
            ASR::stmt_t** type_block = nullptr;
            size_t n_type_block = 0;
            switch( select_type_stmts[i]->type ) {
                case ASR::type_stmtType::TypeStmtName: {
                    ASR::TypeStmtName_t* type_stmt_name = ASR::down_cast<ASR::TypeStmtName_t>(select_type_stmts[i]);
                    ASR::symbol_t* type_sym = ASRUtils::symbol_get_past_external(type_stmt_name->m_sym);
                    LCOMPILERS_ASSERT(ASR::is_a<ASR::Struct_t>(*type_sym));
                    llvm::Value* static_ptr = llvm_selector;
                    llvm::Type* static_ptr_type = llvm_selector_type_;
                    ASR::ttype_t* selector_var_type = ASRUtils::expr_type(x.m_selector);
                    // If selector is a pointer type, load it first
                    if (LLVM::is_llvm_pointer(*selector_var_type)) {
                        static_ptr = llvm_utils->CreateLoad2(llvm_selector_type_, static_ptr);
                        selector_var_type = ASRUtils::type_get_past_pointer(selector_var_type);
                        static_ptr_type = llvm_utils->get_type_from_ttype_t_util(x.m_selector, selector_var_type, module.get());
                    }
                    if (ASRUtils::is_array(selector_var_type) &&
                            (ASRUtils::extract_physical_type(selector_var_type) == ASR::array_physical_typeType::DescriptorArray ||
                             ASRUtils::extract_physical_type(selector_var_type) == ASR::array_physical_typeType::AssumedRankArray)) {
                        static_ptr_type = llvm_utils->get_type_from_ttype_t_util(
                            x.m_selector, ASRUtils::type_get_past_allocatable_pointer(selector_var_type), module.get());
                        static_ptr = arr_descr->get_pointer_to_data(static_ptr_type, static_ptr);
                        llvm::Type* el_type = llvm_utils->get_type_from_ttype_t_util(
                            x.m_selector, ASRUtils::extract_type(selector_var_type), module.get());
                        static_ptr = llvm_utils->CreateLoad2(
                            el_type->getPointerTo(),
                            static_ptr);
                    }

                    if (struct_api->newclass2vtab.find(type_sym) == struct_api->newclass2vtab.end()) {
                        struct_api->create_new_vtable_for_struct_type(type_sym, module.get());
                    }

                    // Use `lfortran_dynamic_cast()` to compare runtime type-info of a class
                    // variable passed as selector variable. We need to compare the exact type
                    // instead of inheritance here.
                    if (ASRUtils::is_unlimited_polymorphic_type(x.m_selector)) {
                        llvm::Value* val = lfortran_dynamic_cast(
                                static_ptr, struct_api->newclass2typeinfo.at(type_sym), true);
                        builder->CreateStore(builder->CreateICmpNE(
                            val, llvm::ConstantPointerNull::get(llvm_utils->i8_ptr)), cond);
                    } else {
                        ASR::symbol_t* selector_sym = ASRUtils::symbol_get_past_external(
                            ASRUtils::get_struct_sym_from_struct_expr(x.m_selector));
                        llvm::Value* is_selector_null = builder->CreateICmpEQ(
                            builder->CreatePtrToInt(static_ptr, llvm::Type::getInt64Ty(context)),
                            llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), llvm::APInt(64, 0)));
                        llvm_utils->create_if_else(is_selector_null, [=]() {
                            llvm::Value* ptr_to_method = struct_api->get_pointer_to_method(selector_sym, module.get());
                            llvm::Value* vptr = llvm_utils->CreateAlloca(*builder, llvm_utils->vptr_type);
                            builder->CreateStore(ptr_to_method, vptr);
                            llvm::Value* val = lfortran_dynamic_cast(
                                vptr, struct_api->newclass2typeinfo.at(type_sym), true);
                            builder->CreateStore(builder->CreateICmpNE(
                                val, llvm::ConstantPointerNull::get(llvm_utils->i8_ptr)), cond);
                        }, [=](){
                            llvm::Value* val = lfortran_dynamic_cast(
                                static_ptr, struct_api->newclass2typeinfo.at(type_sym), true);
                            builder->CreateStore(builder->CreateICmpNE(
                                val, llvm::ConstantPointerNull::get(llvm_utils->i8_ptr)), cond);
                        });
                    }
                    type_block = type_stmt_name->m_body;
                    n_type_block = type_stmt_name->n_body;
                    break ;
                }
                case ASR::type_stmtType::ClassStmt: {
                    ASR::ClassStmt_t* class_stmt = ASR::down_cast<ASR::ClassStmt_t>(select_type_stmts[i]);
                    ASR::symbol_t* class_sym = ASRUtils::symbol_get_past_external(class_stmt->m_sym);
                    LCOMPILERS_ASSERT(ASR::is_a<ASR::Struct_t>(*class_sym));
                    llvm::Value* static_ptr = llvm_selector;
                    if (LLVM::is_llvm_pointer(*ASRUtils::expr_type(x.m_selector))) {
                        static_ptr = llvm_utils->CreateLoad2(llvm_selector_type_, static_ptr);
                    }
                    if (ASRUtils::is_array(ASRUtils::expr_type(x.m_selector))) {
                        static_ptr = arr_descr->get_pointer_to_data(llvm_selector_type_, static_ptr);
                        static_ptr = llvm_utils->CreateLoad2(
                            llvm_utils->get_el_type(
                                x.m_selector,
                                ASRUtils::extract_type(ASRUtils::expr_type(x.m_selector)),
                                module.get())->getPointerTo(),
                            static_ptr);
                    }

                    if (struct_api->newclass2vtab.find(class_sym) == struct_api->newclass2vtab.end()) {
                        struct_api->create_new_vtable_for_struct_type(class_sym, module.get());
                    }

                    if (ASRUtils::is_unlimited_polymorphic_type(x.m_selector)) {
                        llvm::Value* val = lfortran_dynamic_cast(
                                static_ptr, struct_api->newclass2typeinfo.at(class_sym), false);
                        builder->CreateStore(builder->CreateICmpNE(
                            val, llvm::ConstantPointerNull::get(llvm_utils->i8_ptr)), cond);
                    } else {
                        ASR::symbol_t* selector_sym = ASRUtils::symbol_get_past_external(
                            ASRUtils::get_struct_sym_from_struct_expr(x.m_selector));
                        llvm::Value* is_selector_null = builder->CreateICmpEQ(
                            builder->CreatePtrToInt(static_ptr, llvm::Type::getInt64Ty(context)),
                            llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), llvm::APInt(64, 0)));
                        llvm_utils->create_if_else(is_selector_null, [=]() {
                            llvm::Value* ptr_to_method = struct_api->get_pointer_to_method(selector_sym, module.get());
                            llvm::Value* vptr = llvm_utils->CreateAlloca(*builder, llvm_utils->vptr_type);
                            builder->CreateStore(ptr_to_method, vptr);
                            llvm::Value* val = lfortran_dynamic_cast(
                                vptr, struct_api->newclass2typeinfo.at(class_sym), false);
                            builder->CreateStore(builder->CreateICmpNE(
                                val, llvm::ConstantPointerNull::get(llvm_utils->i8_ptr)), cond);
                        }, [=](){
                            llvm::Value* val = lfortran_dynamic_cast(
                                static_ptr, struct_api->newclass2typeinfo.at(class_sym), false);
                            builder->CreateStore(builder->CreateICmpNE(
                                val, llvm::ConstantPointerNull::get(llvm_utils->i8_ptr)), cond);
                        });
                    }
                    type_block = class_stmt->m_body;
                    n_type_block = class_stmt->n_body;
                    break ;
                }
                case ASR::type_stmtType::TypeStmtType: {
                    ASR::ttype_t* selector_var_type = ASRUtils::expr_type(x.m_selector);
                    ASR::TypeStmtType_t* type_stmt_type_t = ASR::down_cast<ASR::TypeStmtType_t>(select_type_stmts[i]);
                    ASR::ttype_t* type_stmt_type = type_stmt_type_t->m_type;
                    int kind = ASRUtils::extract_kind_from_ttype_t(type_stmt_type);
                    // If the intrinsic type's type-info does not exist, create it on the fly.
                    if (struct_api->intrinsic_type_info.find(
                            ASRUtils::intrinsic_type_to_str_with_kind(type_stmt_type, kind))
                        == struct_api->intrinsic_type_info.end()) {
                        struct_api->create_type_info_for_intrinsic_type(type_stmt_type, kind, module.get());
                    }
                    llvm::Value* static_ptr = llvm_selector;
                    llvm::Type* static_ptr_type = llvm_selector_type_;
                    // If selector is a pointer/allocatable type, load it first
                    if (LLVM::is_llvm_pointer(*selector_var_type)) {
                        static_ptr = llvm_utils->CreateLoad2(llvm_selector_type_, static_ptr);
                        selector_var_type = ASRUtils::type_get_past_allocatable_pointer(selector_var_type);
                        static_ptr_type = llvm_utils->get_type_from_ttype_t_util(x.m_selector, selector_var_type, module.get());
                    }
                    if (ASRUtils::is_array(selector_var_type)) {
                        static_ptr = arr_descr->get_pointer_to_data(static_ptr_type, static_ptr);
                        static_ptr = llvm_utils->CreateLoad2(
                            llvm_utils->get_el_type(
                                x.m_selector,
                                ASRUtils::type_get_past_array(selector_var_type),
                                module.get())->getPointerTo(),
                            static_ptr);
                    }
                    llvm::Value* val = lfortran_dynamic_cast(
                        static_ptr,
                        struct_api->intrinsic_type_info.at(
                            ASRUtils::intrinsic_type_to_str_with_kind(type_stmt_type, kind)),
                        true);
                    builder->CreateStore(builder->CreateICmpNE(
                        val, llvm::ConstantPointerNull::get(llvm_utils->i8_ptr)), cond);
                    type_block = type_stmt_type_t->m_body;
                    n_type_block = type_stmt_type_t->n_body;
                    break;
                }
                default: {
                    throw CodeGenError("ASR::type_stmtType, " +
                                       std::to_string(x.m_body[i]->type) +
                                       " is not yet supported.");
                }
            }
            builder->CreateCondBr(llvm_utils->CreateLoad2(
                llvm::Type::getInt1Ty(context), cond), thenBB, elseBB);
            builder->SetInsertPoint(thenBB);
            {
                if( n_type_block == 1 && ASR::is_a<ASR::BlockCall_t>(*type_block[0]) ) {
                    ASR::BlockCall_t* block_call = ASR::down_cast<ASR::BlockCall_t>(type_block[0]);
                    visit_BlockCall(*block_call);
                }
            }
            builder->CreateBr(mergeBB);

            start_new_block(elseBB);
        }
        if( x.n_default > 0 ) {
            for( size_t i = 0; i < x.n_default; i++ ) {
                this->visit_stmt(*x.m_default[i]);
            }
        }
        start_new_block(mergeBB);
    }

    void visit_IntegerCompare(const ASR::IntegerCompare_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_load_wrapper(x.m_left,
            LLVM::is_llvm_pointer(*expr_type(x.m_left)) ? 2 : 1,
            true);
        llvm::Value *left = tmp;
        this->visit_expr_load_wrapper(x.m_right,
            LLVM::is_llvm_pointer(*expr_type(x.m_right)) ? 2 : 1,
            true);
        llvm::Value *right = tmp;
        load_non_array_non_character_pointers(x.m_left, ASRUtils::expr_type(x.m_left), left);
        load_non_array_non_character_pointers(x.m_right, ASRUtils::expr_type(x.m_right), right);
        switch (x.m_op) {
            case (ASR::cmpopType::Eq) : {
                tmp = builder->CreateICmpEQ(left, right);
                break;
            }
            case (ASR::cmpopType::Gt) : {
                tmp = builder->CreateICmpSGT(left, right);
                break;
            }
            case (ASR::cmpopType::GtE) : {
                tmp = builder->CreateICmpSGE(left, right);
                break;
            }
            case (ASR::cmpopType::Lt) : {
                tmp = builder->CreateICmpSLT(left, right);
                break;
            }
            case (ASR::cmpopType::LtE) : {
                tmp = builder->CreateICmpSLE(left, right);
                break;
            }
            case (ASR::cmpopType::NotEq) : {
                tmp = builder->CreateICmpNE(left, right);
                break;
            }
            default : {
                throw CodeGenError("Comparison operator not implemented",
                        x.base.base.loc);
            }
        }
    }

    void visit_UnsignedIntegerCompare(const ASR::UnsignedIntegerCompare_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_load_wrapper(x.m_left,
            LLVM::is_llvm_pointer(*expr_type(x.m_left)) ? 2 : 1,
            true);
        llvm::Value *left = tmp;
        this->visit_expr_load_wrapper(x.m_right,
            LLVM::is_llvm_pointer(*expr_type(x.m_right)) ? 2 : 1,
            true);
        llvm::Value *right = tmp;
        load_non_array_non_character_pointers(x.m_left, ASRUtils::expr_type(x.m_left), left);
        load_non_array_non_character_pointers(x.m_right, ASRUtils::expr_type(x.m_right), right);
        switch (x.m_op) {
            case (ASR::cmpopType::Eq) : {
                tmp = builder->CreateICmpEQ(left, right);
                break;
            }
            case (ASR::cmpopType::Gt) : {
                tmp = builder->CreateICmpUGT(left, right);
                break;
            }
            case (ASR::cmpopType::GtE) : {
                tmp = builder->CreateICmpUGE(left, right);
                break;
            }
            case (ASR::cmpopType::Lt) : {
                tmp = builder->CreateICmpULT(left, right);
                break;
            }
            case (ASR::cmpopType::LtE) : {
                tmp = builder->CreateICmpULE(left, right);
                break;
            }
            case (ASR::cmpopType::NotEq) : {
                tmp = builder->CreateICmpNE(left, right);
                break;
            }
            default : {
                throw CodeGenError("Comparison operator not implemented",
                        x.base.base.loc);
            }
        }
    }

    void visit_CPtrCompare(const ASR::CPtrCompare_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_wrapper(x.m_left, true);
        llvm::Value *left = tmp;
        left = builder->CreatePtrToInt(left, llvm_utils->getIntType(8, false));
        this->visit_expr_wrapper(x.m_right, true);
        llvm::Value *right = tmp;
        right = builder->CreatePtrToInt(right, llvm_utils->getIntType(8, false));
        switch (x.m_op) {
            case (ASR::cmpopType::Eq) : {
                tmp = builder->CreateICmpEQ(left, right);
                break;
            }
            case (ASR::cmpopType::Gt) : {
                tmp = builder->CreateICmpSGT(left, right);
                break;
            }
            case (ASR::cmpopType::GtE) : {
                tmp = builder->CreateICmpSGE(left, right);
                break;
            }
            case (ASR::cmpopType::Lt) : {
                tmp = builder->CreateICmpSLT(left, right);
                break;
            }
            case (ASR::cmpopType::LtE) : {
                tmp = builder->CreateICmpSLE(left, right);
                break;
            }
            case (ASR::cmpopType::NotEq) : {
                tmp = builder->CreateICmpNE(left, right);
                break;
            }
            default : {
                throw CodeGenError("Comparison operator not implemented",
                        x.base.base.loc);
            }
        }
    }

    void visit_RealCompare(const ASR::RealCompare_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_load_wrapper(x.m_left,
            LLVM::is_llvm_pointer(*expr_type(x.m_left)) ? 2 : 1,
            true);
        llvm::Value *left = tmp;
        this->visit_expr_load_wrapper(x.m_right,
            LLVM::is_llvm_pointer(*expr_type(x.m_right)) ? 2 : 1,
            true);
        llvm::Value *right = tmp;
        load_non_array_non_character_pointers(x.m_left, ASRUtils::expr_type(x.m_left), left);
        load_non_array_non_character_pointers(x.m_right, ASRUtils::expr_type(x.m_right), right);
        switch (x.m_op) {
            case (ASR::cmpopType::Eq) : {
                tmp = builder->CreateFCmpOEQ(left, right);
                break;
            }
            case (ASR::cmpopType::Gt) : {
                tmp = builder->CreateFCmpOGT(left, right);
                break;
            }
            case (ASR::cmpopType::GtE) : {
                tmp = builder->CreateFCmpOGE(left, right);
                break;
            }
            case (ASR::cmpopType::Lt) : {
                tmp = builder->CreateFCmpOLT(left, right);
                break;
            }
            case (ASR::cmpopType::LtE) : {
                tmp = builder->CreateFCmpOLE(left, right);
                break;
            }
            case (ASR::cmpopType::NotEq) : {
                tmp = builder->CreateFCmpUNE(left, right);
                break;
            }
            default : {
                throw CodeGenError("Comparison operator not implemented",
                        x.base.base.loc);
            }
        }
    }

    void visit_ComplexCompare(const ASR::ComplexCompare_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_load_wrapper(x.m_left,
            LLVM::is_llvm_pointer(*expr_type(x.m_left)) ? 2 : 1,
            true);
        llvm::Value *left = tmp;
        this->visit_expr_load_wrapper(x.m_right,
            LLVM::is_llvm_pointer(*expr_type(x.m_right)) ? 2 : 1,
            true);
        llvm::Value *right = tmp;
        load_non_array_non_character_pointers(x.m_left, ASRUtils::expr_type(x.m_left), left);
        load_non_array_non_character_pointers(x.m_right, ASRUtils::expr_type(x.m_right), right);
        llvm::Value* real_left = complex_re(left, left->getType());
        llvm::Value* real_right = complex_re(right, right->getType());
        llvm::Value* img_left = complex_im(left, left->getType());
        llvm::Value* img_right = complex_im(right, right->getType());
        llvm::Value *real_res, *img_res;
        switch (x.m_op) {
            case (ASR::cmpopType::Eq) : {
                real_res = builder->CreateFCmpOEQ(real_left, real_right);
                img_res = builder->CreateFCmpOEQ(img_left, img_right);
                tmp = builder->CreateAnd(real_res, img_res);
                break;
            }
            case (ASR::cmpopType::NotEq) : {
                real_res = builder->CreateFCmpONE(real_left, real_right);
                img_res = builder->CreateFCmpONE(img_left, img_right);
                tmp = builder->CreateOr(real_res, img_res);
                break;
            }
            default : {
                throw CodeGenError("Comparison operator not implemented",
                        x.base.base.loc);
            }
        }
    }

    void visit_StringCompare(const ASR::StringCompare_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        llvm::Value *left, *left_len;
        llvm::Value *right , *right_len;
        std::tie(left, left_len) = get_string_data_and_length(x.m_left);
        std::tie(right, right_len) = get_string_data_and_length(x.m_right);

        bool is_single_char;
        { // Set `is_single_char` flag
            ASR::String_t* left_str = ASRUtils::get_string_type(x.m_left);
            ASR::String_t* right_str = ASRUtils::get_string_type(x.m_right);
            int64_t l_str_len, r_str_len;
            if( ASRUtils::is_value_constant(left_str->m_len, l_str_len) &&
                ASRUtils::is_value_constant(right_str->m_len, r_str_len)){// Check + Fetch len
                is_single_char = (l_str_len == 1) && (r_str_len == 1);
            } else {
                is_single_char = false;
            }
        }
        if( is_single_char ) {
            left = llvm_utils->CreateLoad2(llvm::Type::getInt8Ty(context), left);
            right = llvm_utils->CreateLoad2(llvm::Type::getInt8Ty(context), right);
        } else {
            tmp = lfortran_str_cmp(left, left_len, right, right_len);
        }
        switch (x.m_op) {
            case (ASR::cmpopType::Eq) : {
                if( is_single_char ) {
                    tmp = builder->CreateICmpEQ(left, right);
                } else {
                    tmp = builder->CreateICmpEQ(tmp, llvm::ConstantInt::get(context, llvm::APInt(32,0)));
                }
                break;
            }
            case (ASR::cmpopType::NotEq) : {
                if( is_single_char ) {
                    tmp = builder->CreateICmpNE(left, right);
                } else {
                    tmp = builder->CreateICmpNE(tmp, llvm::ConstantInt::get(context, llvm::APInt(32,0)));
                }
                break;
            }
            case (ASR::cmpopType::Gt) : {
                if( is_single_char ) {
                    tmp = builder->CreateICmpSGT(left, right);
                } else {
                    tmp = builder->CreateICmpSGT(tmp, llvm::ConstantInt::get(context, llvm::APInt(32,0)));
                }
                break;
            }
            case (ASR::cmpopType::GtE) : {
                if( is_single_char ) {
                    tmp = builder->CreateICmpSGE(left, right);
                } else {
                    tmp = builder->CreateICmpSGE(tmp, llvm::ConstantInt::get(context, llvm::APInt(32,0)));
                }
                break;
            }
            case (ASR::cmpopType::Lt) : {
                if( is_single_char ) {
                    tmp = builder->CreateICmpSLT(left, right);
                } else {
                    tmp = builder->CreateICmpSLT(tmp, llvm::ConstantInt::get(context, llvm::APInt(32,0)));
                }
                break;
            }
            case (ASR::cmpopType::LtE) : {
                if( is_single_char ) {
                    tmp = builder->CreateICmpSLE(left, right);
                } else {
                    tmp = builder->CreateICmpSLE(tmp, llvm::ConstantInt::get(context, llvm::APInt(32,0)));
                }
                break;
            }
            default : {
                throw CodeGenError("Comparison operator not implemented",
                        x.base.base.loc);
            }
        }
    }

    void visit_LogicalCompare(const ASR::LogicalCompare_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_load_wrapper(x.m_left,
            LLVM::is_llvm_pointer(*expr_type(x.m_left)) ? 2 : 1,
            true);
        llvm::Value *left = tmp;
        this->visit_expr_load_wrapper(x.m_right,
            LLVM::is_llvm_pointer(*expr_type(x.m_right)) ? 2 : 1,
            true);
        llvm::Value *right = tmp;
        load_non_array_non_character_pointers(x.m_left, ASRUtils::expr_type(x.m_left), left);
        load_non_array_non_character_pointers(x.m_right, ASRUtils::expr_type(x.m_right), right);
        // Match widths if logical kinds differ
        if (left->getType() != right->getType()) {
            unsigned left_bits = left->getType()->getIntegerBitWidth();
            unsigned right_bits = right->getType()->getIntegerBitWidth();
            if (left_bits < right_bits) {
                left = builder->CreateZExt(left, right->getType());
            } else {
                right = builder->CreateZExt(right, left->getType());
            }
        }
        switch (x.m_op) {
            case (ASR::cmpopType::Eq) : {
                tmp = builder->CreateICmpEQ(left, right);
                break;
            }
            case (ASR::cmpopType::NotEq) : {
                tmp = builder->CreateICmpNE(left, right);
                break;
            }
            case (ASR::cmpopType::Gt) : {
                tmp = builder->CreateICmpUGT(left, right);
                break;
            }
            case (ASR::cmpopType::GtE) : {
                tmp = builder->CreateICmpUGE(left, right);
                break;
            }
            case (ASR::cmpopType::Lt) : {
                tmp = builder->CreateICmpULT(left, right);
                break;
            }
            case (ASR::cmpopType::LtE) : {
                tmp = builder->CreateICmpULE(left, right);
                break;
            }
            default : {
                throw CodeGenError("Comparison operator not implemented",
                        x.base.base.loc);
            }
        }
    }

    void visit_OverloadedCompare(const ASR::OverloadedCompare_t &x) {
        this->visit_expr(*x.m_overloaded);
    }

    void visit_If(const ASR::If_t &x) {
        llvm::Value **strings_to_be_deallocated_copy = strings_to_be_deallocated.p;
        size_t n = strings_to_be_deallocated.n;
        strings_to_be_deallocated.reserve(al, 1);
        this->visit_expr_wrapper(x.m_test, true);
        {
            ASR::ttype_t* test_type = ASRUtils::expr_type(x.m_test);
            if (LLVM::is_llvm_pointer(*test_type) &&
                    !ASRUtils::is_array(test_type) &&
                    !ASRUtils::is_character(*test_type) &&
                    tmp->getType()->isPointerTy()) {
                ASR::ttype_t* inner_type =
                    ASRUtils::type_get_past_allocatable_pointer(test_type);
                if (ASR::is_a<ASR::Logical_t>(*inner_type)) {
                    llvm::Type* inner_llvm =
                        llvm_utils->get_type_from_ttype_t_util(
                            x.m_test, inner_type, module.get());
                    tmp = llvm_utils->CreateLoad2(inner_llvm, tmp);
                }
            }
        }
        llvm_utils->create_if_else(tmp, [&]() {
            for (size_t i=0; i<x.n_body; i++) {
                this->visit_stmt(*x.m_body[i]);
            }
        }, [&]() {
            for (size_t i=0; i<x.n_orelse; i++) {
                this->visit_stmt(*x.m_orelse[i]);
            }
        }, x.m_name, loop_or_block_end, loop_or_block_end_names);
        strings_to_be_deallocated.reserve(al, n);
        strings_to_be_deallocated.n = n;
        strings_to_be_deallocated.p = strings_to_be_deallocated_copy;
    }

    void visit_IfExp(const ASR::IfExp_t &x) {
        // IfExp(expr test, expr body, expr orelse, ttype type, expr? value)
        this->visit_expr_wrapper(x.m_test, true);
        llvm::Value *cond = tmp;
        llvm::Type* _type = llvm_utils->get_type_from_ttype_t_util(x.m_test, x.m_type, module.get());
        llvm::Value* ifexp_res = llvm_utils->CreateAlloca(_type, nullptr, "");
        llvm_utils->create_if_else(cond, [&]() {
            this->visit_expr_wrapper(x.m_body, true);
            builder->CreateStore(tmp, ifexp_res);
        }, [&]() {
            this->visit_expr_wrapper(x.m_orelse, true);
            builder->CreateStore(tmp, ifexp_res);
        });
        tmp = llvm_utils->CreateLoad2(_type, ifexp_res);
    }

    // TODO: Implement visit_DooLoop
    //void visit_DoLoop(const ASR::DoLoop_t &x) {
    //}

    void visit_WhileLoop(const ASR::WhileLoop_t &x) {
        llvm::Value **strings_to_be_deallocated_copy = strings_to_be_deallocated.p;
        size_t n = strings_to_be_deallocated.n;
        strings_to_be_deallocated.reserve(al, 1);
        create_loop(x.m_name, [=]() {
            this->visit_expr_wrapper(x.m_test, true);
            return tmp;
        }, [&]() {
            for (size_t i=0; i<x.n_body; i++) {
                this->visit_stmt(*x.m_body[i]);
            }
        });
        strings_to_be_deallocated.reserve(al, n);
        strings_to_be_deallocated.n = n;
        strings_to_be_deallocated.p = strings_to_be_deallocated_copy;
    }

    bool case_insensitive_string_compare(const std::string& str1, const std::string& str2) {
        if (str1.size() != str2.size()) {
            return false;
        }
        for (std::string::const_iterator c1 = str1.begin(), c2 = str2.begin(); c1 != str1.end(); ++c1, ++c2) {
            if (tolower(static_cast<unsigned char>(*c1)) != tolower(static_cast<unsigned char>(*c2))) {
                return false;
            }
        }
        return true;
    }

    void visit_Exit(const ASR::Exit_t &x) {
        if (x.m_stmt_name) {
            std::string stmt_name = std::string(x.m_stmt_name) + ".end";
            std::string if_cont_name = std::string(x.m_stmt_name) + ".ifcont";
            int nested_block_depth = loop_or_block_end_names.size();
            int i = nested_block_depth - 1;
            for (; i >= 0; i--) {
                if (case_insensitive_string_compare(loop_or_block_end_names[i], stmt_name) ||
                    case_insensitive_string_compare(loop_or_block_end_names[i], if_cont_name)) {
                    break;
                }
            }
            if (i >= 0) {
                builder->CreateBr(loop_or_block_end[i]);
            } else {
                throw CodeGenError("Could not find block or loop named " + std::string(x.m_stmt_name) + " in parent scope to exit from.",
                x.base.base.loc);
            }
        } else {
            builder->CreateBr(loop_or_block_end.back());
        }
        llvm::BasicBlock *bb = llvm::BasicBlock::Create(context, "unreachable_after_exit");
        start_new_block(bb);
    }

    void visit_Cycle(const ASR::Cycle_t &x) {
        if (x.m_stmt_name) {
            std::string stmt_name = std::string(x.m_stmt_name) + ".head";
            int nested_block_depth = loop_head_names.size();
            int i = nested_block_depth - 1;
            for (; i >= 0; i--) {
                if (case_insensitive_string_compare(loop_head_names[i], stmt_name)) {
                    break;
                }
            }
            if (i >= 0) {
                builder->CreateBr(loop_head[i]);
            } else {
                throw CodeGenError("Could not find loop named " + std::string(x.m_stmt_name) + " in parent scope to cycle to.",
                x.base.base.loc);
            }
        } else {
            builder->CreateBr(loop_head.back());
        }
        llvm::BasicBlock *bb = llvm::BasicBlock::Create(context, "unreachable_after_cycle");
        start_new_block(bb);
    }

    void visit_Return(const ASR::Return_t & /* x */) {
        builder->CreateBr(proc_return);
        llvm::BasicBlock *bb = llvm::BasicBlock::Create(context, "unreachable_after_return");
        start_new_block(bb);
    }

    void visit_GoTo(const ASR::GoTo_t &x) {
        if (llvm_goto_targets.find(x.m_target_id) == llvm_goto_targets.end()) {
            // If the target does not exist yet, create it
            llvm::BasicBlock *new_target = llvm::BasicBlock::Create(context, "goto_target");
            llvm_goto_targets[x.m_target_id] = new_target;
        }
        llvm::BasicBlock *target = llvm_goto_targets[x.m_target_id];
        builder->CreateBr(target);
        llvm::BasicBlock *bb = llvm::BasicBlock::Create(context, "unreachable_after_goto");
        start_new_block(bb);
    }

    void visit_GoToTarget(const ASR::GoToTarget_t &x) {
        if (llvm_goto_targets.find(x.m_id) == llvm_goto_targets.end()) {
            llvm::BasicBlock *new_target = llvm::BasicBlock::Create(context, "goto_target");
            llvm_goto_targets[x.m_id] = new_target;
        }
        llvm::BasicBlock *target = llvm_goto_targets[x.m_id];
        start_new_block(target); 
    }

    void visit_LogicalBinOp(const ASR::LogicalBinOp_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_load_wrapper(x.m_left,
            LLVM::is_llvm_pointer(*expr_type(x.m_left)) ? 2 : 1,
            true);
        llvm::Value *left_val = tmp;
        this->visit_expr_load_wrapper(x.m_right,
            LLVM::is_llvm_pointer(*expr_type(x.m_right)) ? 2 : 1,
            true);
        llvm::Value *right_val = tmp;
        load_non_array_non_character_pointers(x.m_left, ASRUtils::expr_type(x.m_left), left_val);
        load_non_array_non_character_pointers(x.m_right, ASRUtils::expr_type(x.m_right), right_val);
        llvm::Value *zero, *cond;
        if (ASRUtils::is_integer(*x.m_type)) {
            int a_kind = down_cast<ASR::Integer_t>(x.m_type)->m_kind;
            int init_value_bits = 8*a_kind;
            zero = llvm::ConstantInt::get(context,
                            llvm::APInt(init_value_bits, 0));
            cond = builder->CreateICmpEQ(left_val, zero);
        } else if (ASRUtils::is_real(*x.m_type)) {
            int a_kind = down_cast<ASR::Real_t>(x.m_type)->m_kind;
            int init_value_bits = 8*a_kind;
            if (init_value_bits == 32) {
                zero = llvm::ConstantFP::get(context,
                                    llvm::APFloat((float)0));
            } else {
                zero = llvm::ConstantFP::get(context,
                                    llvm::APFloat((double)0));
            }
            cond = builder->CreateFCmpUEQ(left_val, zero);
        } else if (ASRUtils::is_character(*x.m_type)) {
            zero = llvm::Constant::getNullValue(character_type);
            llvm::Value* comp_res =lfortran_str_cmp(left_val, get_string_length(x.m_left),
                zero, llvm::ConstantInt::get(context, llvm::APInt(64, 0)));
            cond = builder->CreateICmpEQ(
                comp_res,
                llvm::ConstantInt::get(context, llvm::APInt(32, 0)));
        } else if (ASRUtils::is_logical(*x.m_type)) {
            // Use direct iW bitwise ops for logical types.
            // Operands may be i1 (from comparisons) or iW (from variables).
            // Unify to the wider type before the bitwise op.
            if (left_val->getType() != right_val->getType()) {
                unsigned lw = left_val->getType()->getIntegerBitWidth();
                unsigned rw = right_val->getType()->getIntegerBitWidth();
                if (lw < rw) {
                    left_val = builder->CreateZExt(left_val, right_val->getType());
                } else {
                    right_val = builder->CreateZExt(right_val, left_val->getType());
                }
            }
            switch (x.m_op) {
                case ASR::logicalbinopType::And: {
                    tmp = builder->CreateAnd(left_val, right_val);
                    break;
                };
                case ASR::logicalbinopType::Or: {
                    tmp = builder->CreateOr(left_val, right_val);
                    break;
                };
                case ASR::logicalbinopType::Xor: {
                    tmp = builder->CreateXor(left_val, right_val);
                    break;
                };
                case ASR::logicalbinopType::NEqv: {
                    tmp = builder->CreateXor(left_val, right_val);
                    break;
                };
                case ASR::logicalbinopType::Eqv: {
                    llvm::Value *one = llvm::ConstantInt::get(left_val->getType(), 1);
                    tmp = builder->CreateXor(builder->CreateXor(left_val, right_val), one);
                    break;
                };
            }
            return;
        } else {
            throw CodeGenError("Only Integer, Real, Strings and Logical types are supported "
            "in logical binary operation.", x.base.base.loc);
        }
        switch (x.m_op) {
            case ASR::logicalbinopType::And: {
                tmp = builder->CreateSelect(cond, left_val, right_val);
                break;
            };
            case ASR::logicalbinopType::Or: {
                tmp = builder->CreateSelect(cond, right_val, left_val);
                break;
            };
            case ASR::logicalbinopType::Xor: {
                tmp = builder->CreateXor(left_val, right_val);
                break;
            };
            case ASR::logicalbinopType::NEqv: {
                tmp = builder->CreateXor(left_val, right_val);
                break;
            };
            case ASR::logicalbinopType::Eqv: {
                llvm::Value *one = llvm::ConstantInt::get(left_val->getType(), 1);
                tmp = builder->CreateXor(builder->CreateXor(left_val, right_val), one);
                break;
            };
        }
    }

    void visit_StringRepeat(const ASR::StringRepeat_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        llvm::Value* left_val, *left_len;
        std::tie(left_val, left_len) = get_string_data_and_length(x.m_left);
        this->visit_expr_wrapper(x.m_right, true);
        llvm::Value *right_val = tmp;
        tmp = lfortran_strrepeat(left_val, right_val);
        right_val = llvm_utils->convert_kind(right_val, llvm::Type::getInt64Ty(context));
        tmp = llvm_utils->create_string_descriptor(tmp,
            builder->CreateMul(left_len, right_val), "strRepeat_desc");
        strings_to_be_deallocated.push_back(al, tmp);
    }

    void visit_StringConcat(const ASR::StringConcat_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        llvm::Value* left_val {}, *left_len {};
        llvm::Value* right_val {}, *right_len {};
        std::tie(left_val, left_len) = get_string_data_and_length(x.m_left);
        std::tie(right_val, right_len) = get_string_data_and_length(x.m_right);
        tmp = lfortran_strConcat(left_val, left_len, right_val, right_len);
        tmp = llvm_utils->create_string_descriptor(tmp,
            builder->CreateAdd(left_len, right_len), "strConcat_desc");
        strings_to_be_deallocated.push_back(al, tmp);
    }

    void visit_StringLen(const ASR::StringLen_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        tmp = get_string_length(x.m_arg); // `int64` value.
        tmp = llvm_utils->convert_kind(tmp,
            llvm::Type::getIntNTy(context, ASRUtils::extract_kind_from_ttype_t(x.m_type) * 8));
    }

    void visit_StringOrd(const ASR::StringOrd_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        tmp = get_string_data(x.m_arg);
        llvm::AllocaInst *parg = llvm_utils->CreateAlloca(*builder, character_type);
        builder->CreateStore(tmp, parg);
        tmp = lfortran_str_ord(parg);
    }

    void visit_StringChr(const ASR::StringChr_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_load_wrapper(x.m_arg, 1);
        tmp = lfortran_str_chr(tmp);
        tmp = llvm_utils->create_string_descriptor(tmp,
            llvm::ConstantInt::get(context, llvm::APInt(64, 1)), "stringChr_desc");
    }

    void visit_StringItem(const ASR::StringItem_t& x) {
        if (x.m_value) { return this->visit_expr_wrapper(x.m_value, true); }

        /*
            Create A StringView Over The Specified StringItem From Original String
        */

        /* Visit String + Visit Index */
        llvm::Value *idx {};
        llvm::Value *str {};
        this->visit_expr_load_wrapper(x.m_idx, LLVM::is_llvm_pointer(*expr_type(x.m_idx)) ? 2 : 1, true);
        idx = tmp;
        this->visit_expr_load_wrapper(x.m_arg, 0, true);
        str = tmp;

        /* Get StringItem */
        llvm::Value *str_item {};
        {
            llvm::Value* str_data /*  i8*  */ = llvm_utils->get_string_data(ASRUtils::get_string_type(x.m_arg), str);
            llvm::Value* idx_INT64 = llvm_utils->convert_kind(idx, llvm::Type::getInt64Ty(context));
            llvm::Value* idx_zero_based /* 0-based */ = builder->CreateSub(
                                                idx_INT64,
                                                llvm::ConstantInt::get(context, llvm::APInt(64, 1)));
            int kind = ASRUtils::get_string_type(x.m_arg)->m_kind;
            if (kind > 1) {
                llvm::Value* kind_val = llvm::ConstantInt::get(context, llvm::APInt(64, kind));
                idx_zero_based = builder->CreateMul(idx_zero_based, kind_val);
            }
            str_item = builder->CreateGEP(llvm::Type::getInt8Ty(context), str_data, idx_zero_based);
        }

        /* Create StringView */
        tmp = llvm_utils->create_stringView(
            ASRUtils::get_string_type(x.m_type),
            str_item,
            llvm::ConstantInt::get(context, llvm::APInt(64, 1)),
            "StringItem");
    }

    void stringSection_helper_python(const ASR::StringSection_t &x){
        this->visit_expr_load_wrapper(x.m_arg, 0);
        llvm::Value *str = tmp;
        llvm::Value *left, *right, *step;
        llvm::Value *left_present, *right_present;
        if (x.m_start) {
            this->visit_expr_load_wrapper(x.m_start, 1);
            left = tmp;
            left_present = llvm::ConstantInt::get(context,
                llvm::APInt(1, 1));
        } else {
            left = llvm::Constant::getNullValue(llvm::Type::getInt32Ty(context));
            left_present = llvm::ConstantInt::get(context,
                llvm::APInt(1, 0));
        }
        if (x.m_end) {
            this->visit_expr_load_wrapper(x.m_end, 1);
            right = tmp;
            right_present = llvm::ConstantInt::get(context,
                llvm::APInt(1, 1));
        } else {
            right = llvm::Constant::getNullValue(llvm::Type::getInt32Ty(context));
            right_present = llvm::ConstantInt::get(context,
                llvm::APInt(1, 0));
        }
        if (x.m_step) {
            this->visit_expr_load_wrapper(x.m_step, 1);
            step = tmp;
        } else {
            step = llvm::ConstantInt::get(context,
                llvm::APInt(32, 1));
        }
        if(!x.m_start && !x.m_end && !x.m_step){
            tmp = str; // no need for slicing
        } else {
            llvm::Value* str_data{}, *str_len{};
            std::tie(str_data, str_len) = llvm_utils->get_string_length_data(ASRUtils::get_string_type(x.m_arg), str);
            tmp = lfortran_str_slice(str_data, str_len, left, right, step, left_present, right_present);
            llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(x.m_arg, ASRUtils::expr_type(x.m_arg), module.get());
            tmp = llvm_utils->create_string_descriptor(
                tmp, lfortran_str_len(type, tmp, false), "stringSection_desc");
        }
    }
    void stringSection_helper_fortran(const ASR::StringSection_t &x){
        /* Assertions */
        LCOMPILERS_ASSERT( x.m_step )
        LCOMPILERS_ASSERT( x.m_start )
        LCOMPILERS_ASSERT( x.m_end )
        LCOMPILERS_ASSERT(ASR::is_a<ASR::IntegerConstant_t>(*x.m_step))
        LCOMPILERS_ASSERT(ASR::down_cast<ASR::IntegerConstant_t>(x.m_step)->m_n == 1 /*Fortran only has step of 1*/)

        /* Evaluate String */
        llvm::Value *str {};
        this->visit_expr_load_wrapper(x.m_arg, 0);
        str = tmp;
        
        /* Evaluate Start + End */
        llvm::Value *start {};
        llvm::Value *end   {};
        {
            const int start_load = LLVM::is_llvm_pointer(*expr_type(x.m_start)) ? 2 : 1;
            this->visit_expr_load_wrapper(x.m_start, start_load, true);
            start = tmp;

            const int end_load = LLVM::is_llvm_pointer(*expr_type(x.m_end)) ? 2 : 1;
            this->visit_expr_load_wrapper(x.m_end, end_load, true);
            end = tmp;
        }
        
        /* Calculate Resulting Length (clamped to 0 per F2018 9.4.1) */
        llvm::Value* str_section_len {};
        {
            llvm::Value* start_INT64 = llvm_utils->convert_kind(start, llvm::Type::getInt64Ty(context));
            llvm::Value* end_INT64   = llvm_utils->convert_kind(end, llvm::Type::getInt64Ty(context));
            llvm::Value* raw_len = builder->CreateAdd(
                                            builder->CreateSub(end_INT64, start_INT64),
                                            llvm::ConstantInt::get(context, llvm::APInt(64, 1)));
            llvm::Value* zero = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
            llvm::Value* is_negative = builder->CreateICmpSLT(raw_len, zero);
            str_section_len = builder->CreateSelect(is_negative, zero, raw_len);
        }

        /* Get Start-String Ptr (GEP) */
        llvm::Value* str_data {}; // Shifted from Original by value = start
        {
            int kind = ASRUtils::get_string_type(x.m_arg)->m_kind;
            llvm::Value* str_data_orig = llvm_utils->get_string_data(ASRUtils::get_string_type(x.m_arg), str);
            llvm::Value* start_INT64 = llvm_utils->convert_kind(start, llvm::Type::getInt64Ty(context));
            llvm::Value* start = builder->CreateSub(start_INT64, llvm::ConstantInt::get(context, llvm::APInt(64, 1)));
            
            if (kind > 1) {
                llvm::Value* kind_val = llvm::ConstantInt::get(context, llvm::APInt(64, kind));
                start = builder->CreateMul(start, kind_val);
            }
            str_data = builder->CreateGEP(llvm::Type::getInt8Ty(context), str_data_orig, start, "StrSliceGEP");
        }

        /* Create StringView */
        tmp = llvm_utils->create_stringView(ASRUtils::get_string_type(x.m_type), str_data, str_section_len, "StrSlice_StrView");
    }

    void visit_StringSection(const ASR::StringSection_t& x) {
        // Don't use compile-time constant value when this is an assignment target
        // For assignment targets, we need the actual mutable location in the string,
        // not a constant value that would cause writes to read-only constant memory
        if (x.m_value && !is_assignment_target) {
            return this->visit_expr_wrapper(x.m_value, true);
        }

        // Compute the actual substring location for reading or writing
        // TODO : Find some way to use the helper functions based on the frontend,
        // We are using fortran only for now.
        if(true){ // Fortran
            stringSection_helper_fortran(x);
        } else { // Python
            stringSection_helper_python(x);
        }
    }

    void visit_StringPhysicalCast(const ASR::StringPhysicalCast_t &x){
        if( x.m_old == ASR::string_physical_typeType::DescriptorString &&
            x.m_new == ASR::string_physical_typeType::CChar){
            // CChar -> is represented as `char*` in LLVM backend.
            this->visit_expr_load_wrapper(x.m_arg, 0);
            bool is_alloc = ASRUtils::is_allocatable(x.m_type);
            tmp = llvm_utils->get_string_data(ASRUtils::get_string_type(x.m_arg), tmp, is_alloc);
        } else if (x.m_old == ASR::string_physical_typeType::CChar &&
            x.m_new == ASR::string_physical_typeType::DescriptorString){
            this->visit_expr_load_wrapper(x.m_arg, 0);// Typically a bind-C-function return

            int len = -1;
            bool is_const_len = ASRUtils::extract_value(
                ASRUtils::get_string_type(x.m_arg)->m_len, len);
            if(!is_const_len) throw LCompilersException("Unhandled CChar string physical type with non constant length");

            tmp = llvm_utils->create_string_descriptor(tmp,
                llvm::ConstantInt::get(context, llvm::APInt(64, len)),"stringCast_desc");
        } else {
            LCOMPILERS_ASSERT(false);
        }
    }

    void visit_RealCopySign(const ASR::RealCopySign_t& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_load_wrapper(x.m_target,
            LLVM::is_llvm_pointer(*expr_type(x.m_target)) ? 2 : 1,
            true);
        llvm::Value* target = tmp;

        this->visit_expr_load_wrapper(x.m_source,
            LLVM::is_llvm_pointer(*expr_type(x.m_source)) ? 2 : 1,
            true);
        llvm::Value* source = tmp;

        llvm::Type *type;
        int a_kind;
        a_kind = down_cast<ASR::Real_t>(ASRUtils::type_get_past_pointer(x.m_type))->m_kind;
        type = llvm_utils->getFPType(a_kind);
        llvm::Value *ftarget = target;
        llvm::Value *fsource = source;
        if (ftarget->getType() != type) {
            if (ftarget->getType()->isIntegerTy()) ftarget = builder->CreateSIToFP(ftarget, type);
            else if (ftarget->getType()->isFloatingPointTy()) ftarget = builder->CreateFPCast(ftarget, type);
        }
        if (fsource->getType() != type) {
            if (fsource->getType()->isIntegerTy()) fsource = builder->CreateSIToFP(fsource, type);
            else if (fsource->getType()->isFloatingPointTy()) fsource = builder->CreateFPCast(fsource, type);
        }
        std::string func_name = a_kind == 4 ? "llvm.copysign.f32" : "llvm.copysign.f64";
        llvm::Function *fn_copysign = module->getFunction(func_name);
        if (!fn_copysign) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    type, { type, type}, false);
            fn_copysign = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, func_name,
                    module.get());
        }
        tmp = builder->CreateCall(fn_copysign, {ftarget, fsource});
    }

    template <typename T>
    void handle_SU_IntegerBinOp(const T &x, bool signed_int) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_load_wrapper(x.m_left, LLVM::is_llvm_pointer(*expr_type(x.m_left)) ? 2 : 1, true);
        llvm::Value *left_val = tmp;
        this->visit_expr_load_wrapper(x.m_right, LLVM::is_llvm_pointer(*expr_type(x.m_right)) ? 2 : 1, true);
        llvm::Value *right_val = tmp;
        load_non_array_non_character_pointers(x.m_left, ASRUtils::expr_type(x.m_left), left_val);
        load_non_array_non_character_pointers(x.m_right, ASRUtils::expr_type(x.m_right), right_val);
        LCOMPILERS_ASSERT(ASRUtils::is_integer(*x.m_type) ||
            ASRUtils::is_unsigned_integer(*x.m_type))
        switch (x.m_op) {
            case ASR::binopType::Add: {
                tmp = builder->CreateAdd(left_val, right_val);
                break;
            };
            case ASR::binopType::Sub: {
                tmp = builder->CreateSub(left_val, right_val);
                break;
            };
            case ASR::binopType::Mul: {
                tmp = builder->CreateMul(left_val, right_val);
                break;
            };
            case ASR::binopType::Div: {
                if (signed_int) {
                    tmp = builder->CreateSDiv(left_val, right_val);
                } else {
                    tmp = builder->CreateUDiv(left_val, right_val);
                }
                break;
            };
            case ASR::binopType::Pow: {
                int64_t exponent_const =  INT64_MAX;
                ASRUtils::extract_value(x.m_right, exponent_const);
                // Handle simple-common exponent cases for faster computation.
                if (exponent_const == 0) {
                    // x^0 = 1
                    tmp = llvm::ConstantInt::get(left_val->getType(), 1);
                } else if (exponent_const == 1) {
                    // x^1 = x
                    tmp = left_val;
                } else if (exponent_const == 2) {
                    // x^2 = x * x
                    tmp = builder->CreateMul(left_val, left_val, "simplified_pow_operation");
                } else if (exponent_const == 3) {
                    tmp = builder->CreateMul(
                            left_val,
                            builder->CreateMul(
                                left_val,
                                left_val,
                                "simplified_pow_operation"),
                            "simplified_pow_operation");
                } else if (exponent_const == 4) {
                    // x^4 = (x*x) * (x*x)
                    llvm::Value* x2 = builder->CreateMul(left_val, left_val, "simplified_pow_operation");
                    tmp = builder->CreateMul(x2, x2, "simplified_pow_operation");
                } else if (exponent_const == 5) {
                    // x^5 = x^4 * x = (x*x)*(x*x) * x
                    llvm::Value* x2 = builder->CreateMul(left_val, left_val, "simplified_pow_operation");
                    llvm::Value* x4 = builder->CreateMul(x2, x2, "simplified_pow_operation");
                    tmp = builder->CreateMul(x4, left_val, "simplified_pow_operation");
                } else if (exponent_const == 6) {
                    // x^6 = x^4 * x^2 = (x*x)*(x*x) * (x*x)
                    llvm::Value* x2 = builder->CreateMul(left_val, left_val, "simplified_pow_operation");
                    llvm::Value* x4 = builder->CreateMul(x2, x2, "simplified_pow_operation");
                    tmp = builder->CreateMul(x4, x2, "simplified_pow_operation");
                } else if (exponent_const == 7) {
                    // x^7 = x^4 * x^2 * x
                    llvm::Value* x2 = builder->CreateMul(left_val, left_val, "simplified_pow_operation");
                    llvm::Value* x4 = builder->CreateMul(x2, x2, "simplified_pow_operation");
                    llvm::Value* x6 = builder->CreateMul(x4, x2, "simplified_pow_operation");
                    tmp = builder->CreateMul(x6, left_val, "simplified_pow_operation");
                } else if (exponent_const == 8) {
                    // x^8 = (x^4)^2 = ((x*x)*(x*x))^2
                    llvm::Value* x2 = builder->CreateMul(left_val, left_val, "simplified_pow_operation");
                    llvm::Value* x4 = builder->CreateMul(x2, x2, "simplified_pow_operation");
                    tmp = builder->CreateMul(x4, x4, "simplified_pow_operation");
                } else { // Use `pow` function for large or negative exponents
                    llvm::Type* const i64_ty = llvm::Type::getInt64Ty(context);
                    llvm::Value* _right = llvm_utils->convert_kind(right_val, i64_ty);
                    llvm::Value* _left = llvm_utils->convert_kind(left_val, i64_ty);
                    const std::string func_name = "_lfortran_integer_pow_64";
                    llvm::Function *fn_pow = module->getFunction(func_name);
                    if (!fn_pow) {
                        llvm::FunctionType *function_type = llvm::FunctionType::get(
                                i64_ty, {i64_ty, i64_ty}, false);
                        fn_pow = llvm::Function::Create(function_type,
                                llvm::Function::ExternalLinkage, func_name,
                                module.get());
                    }
                    tmp = builder->CreateCall(fn_pow, {_left, _right});
                    llvm::Type* const return_type =
                        llvm_utils->getIntType(
                            ASRUtils::extract_kind_from_ttype_t(x.m_type)); // returnType of the expression.
                    tmp = llvm_utils->convert_kind(tmp, return_type);
                }
                break;
            };
            case ASR::binopType::BitOr: {
                tmp = builder->CreateOr(left_val, right_val);
                break;
            }
            case ASR::binopType::BitAnd: {
                tmp = builder->CreateAnd(left_val, right_val);
                break;
            }
            case ASR::binopType::BitXor: {
                tmp = builder->CreateXor(left_val, right_val);
                break;
            }
            case ASR::binopType::BitLShift: {
                tmp = builder->CreateShl(left_val, right_val);
                break;
            }
            case ASR::binopType::BitRShift: {
                tmp = builder->CreateAShr(left_val, right_val);
                break;
            }
            case ASR::binopType::LBitRShift: {
                tmp = builder->CreateLShr(left_val, right_val);
                break;
            }
        }
    }

    void visit_IntegerBinOp(const ASR::IntegerBinOp_t &x) {
        handle_SU_IntegerBinOp(x, true);
    }

    void visit_UnsignedIntegerBinOp(const ASR::UnsignedIntegerBinOp_t &x) {
        handle_SU_IntegerBinOp(x, false);
    }

    void visit_RealBinOp(const ASR::RealBinOp_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        lookup_enum_value_for_nonints = true;
        this->visit_expr_load_wrapper(x.m_left,
            LLVM::is_llvm_pointer(*expr_type(x.m_left)) ? 2 : 1,
            true);
        llvm::Value *left_val = tmp;
        this->visit_expr_load_wrapper(x.m_right,
            LLVM::is_llvm_pointer(*expr_type(x.m_right)) ? 2 : 1,
            true);
        llvm::Value *right_val = tmp;
        lookup_enum_value_for_nonints = false;
        load_non_array_non_character_pointers(x.m_left, ASRUtils::expr_type(x.m_left), left_val);
        load_non_array_non_character_pointers(x.m_right, ASRUtils::expr_type(x.m_right), right_val);
        LCOMPILERS_ASSERT(ASRUtils::is_real(*x.m_type))
        if (ASRUtils::is_simd_array(x.m_right) && is_a<ASR::Var_t>(*x.m_right)) {
            llvm::Type *right_type = llvm_utils->get_type_from_ttype_t_util(x.m_right, ASRUtils::expr_type(x.m_right), module.get());
            right_val = llvm_utils->CreateLoad2(right_type, right_val);
        }
        if (ASRUtils::is_simd_array(x.m_left) && is_a<ASR::Var_t>(*x.m_left)) {
            llvm::Type *left_type = llvm_utils->get_type_from_ttype_t_util(x.m_left, ASRUtils::expr_type(x.m_left), module.get());
            left_val = llvm_utils->CreateLoad2(left_type, left_val);
        }
        switch (x.m_op) {
            case ASR::binopType::Add: {
                tmp = builder->CreateFAdd(left_val, right_val);
                break;
            };
            case ASR::binopType::Sub: {
                tmp = builder->CreateFSub(left_val, right_val);
                break;
            };
            case ASR::binopType::Mul: {
                tmp = builder->CreateFMul(left_val, right_val);
                break;
            };
            case ASR::binopType::Div: {
                tmp = builder->CreateFDiv(left_val, right_val);
                break;
            };
            case ASR::binopType::Pow: {
                int64_t exponent_const =  INT64_MAX;
                ASRUtils::extract_value(x.m_right, exponent_const);
                // Handle simple-common exponent cases for faster computation.
                if (exponent_const == 0) {
                    tmp = llvm::ConstantFP::get(left_val->getType(), 1.0);
                } else if (exponent_const == 1) {
                    tmp = left_val;
                } else if (exponent_const == 2) {
                    tmp = builder->CreateFMul(left_val, left_val, "simplified_pow_operation");
                } else if (exponent_const == 3) {
                    tmp = builder->CreateFMul(
                            left_val,
                            builder->CreateFMul(
                                left_val,
                                left_val,
                                "simplified_pow_operation"),
                            "simplified_pow_operation");
                } else if (exponent_const == 4) {
                    llvm::Value* x2 = builder->CreateFMul(left_val, left_val, "simplified_pow_operation");
                    tmp = builder->CreateFMul(x2, x2, "simplified_pow_operation");
                } else if (exponent_const == 5) {
                    llvm::Value* x2 = builder->CreateFMul(left_val, left_val, "simplified_pow_operation");
                    llvm::Value* x4 = builder->CreateFMul(x2, x2, "simplified_pow_operation");
                    tmp = builder->CreateFMul(x4, left_val, "simplified_pow_operation");
                } else if (exponent_const == 6) {
                    llvm::Value* x2 = builder->CreateFMul(left_val, left_val, "simplified_pow_operation");
                    llvm::Value* x4 = builder->CreateFMul(x2, x2, "simplified_pow_operation");
                    tmp = builder->CreateFMul(x4, x2, "simplified_pow_operation");
                } else if (exponent_const == 7) {
                    llvm::Value* x2 = builder->CreateFMul(left_val, left_val, "simplified_pow_operation");
                    llvm::Value* x4 = builder->CreateFMul(x2, x2, "simplified_pow_operation");
                    llvm::Value* x6 = builder->CreateFMul(x4, x2, "simplified_pow_operation");
                    tmp = builder->CreateFMul(x6, left_val, "simplified_pow_operation");
                } else if (exponent_const == 8) {
                    llvm::Value* x2 = builder->CreateFMul(left_val, left_val, "simplified_pow_operation");
                    llvm::Value* x4 = builder->CreateFMul(x2, x2, "simplified_pow_operation");
                    tmp = builder->CreateFMul(x4, x4, "simplified_pow_operation");
                } else { // Use `pow` function
                    const int return_kind = down_cast<ASR::Real_t>(ASRUtils::extract_type(x.m_type))->m_kind;
                    llvm::Type* const base_type = llvm_utils->getFPType(return_kind);
                    llvm::Type *exponent_type = nullptr;
                    std::string func_name;
                    // Choose the appropriate llvm_pow* intrinsic function + Set the exponent type.
                    // For real(16) we cannot use llvm.pow{,i}.f128: on platforms where
                    // `long double` != fp128 (Apple ARM64), LLVM lowers those to
                    // `powl`, which silently truncates to 64-bit precision and returns
                    // garbage. Route to our portable runtime instead.
                    if(ASRUtils::is_integer(*ASRUtils::expr_type(x.m_right))) {
                        if (return_kind == 16) {
                            // No integer-exponent fp128 runtime; promote the
                            // integer exponent to fp128 (sitofp) and call
                            // lf_powq (SIMD-ABI shim around lf_f128_pow).
                            func_name = "lf_powq";
                            right_val = builder->CreateSIToFP(right_val, base_type);
                            exponent_type = base_type;
                        } else {
#if LLVM_VERSION_MAJOR <= 12
                            func_name = (return_kind == 4) ? "llvm.powi.f32" : "llvm.powi.f64";
#else
                            func_name = (return_kind == 4) ? "llvm.powi.f32.i32" : "llvm.powi.f64.i32";
#endif
                            right_val = llvm_utils->convert_kind(right_val, llvm::Type::getInt32Ty(context)); // `llvm.powi` only has `i32` exponent.
                            exponent_type = llvm::Type::getInt32Ty(context);
                        }
                    } else if (ASRUtils::is_real(*ASRUtils::expr_type(x.m_right))) {
                        func_name = (return_kind == 4) ? "llvm.pow.f32" :
                                     (return_kind == 8) ? "llvm.pow.f64" : "lf_powq";
                        right_val = llvm_utils->convert_kind(right_val, base_type); // `llvm.pow` exponent and base kinds have to match.
                        exponent_type = base_type;
                    } else {
                        LCOMPILERS_ASSERT_MSG(false, "Exponent in RealBinOp should either be [Integer or Real] only.")
                    }

                    llvm::Function *fn_pow = module->getFunction(func_name);
                    if (!fn_pow) {
                        llvm::FunctionType *function_type = llvm::FunctionType::get(
                                base_type, { base_type, exponent_type }, false);
                        fn_pow = llvm::Function::Create(function_type,
                                llvm::Function::ExternalLinkage, func_name,
                                module.get());
                    }
                    tmp = builder->CreateCall(fn_pow, {left_val, right_val});
                }
                break;
            };
            default: {
                throw CodeGenError("Binary operator '" + ASRUtils::binop_to_str_python(x.m_op) + "' not supported",
                    x.base.base.loc);
            }
        }
    }

    void visit_ComplexBinOp(const ASR::ComplexBinOp_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_wrapper(x.m_left, true);
        llvm::Value *left_val = tmp;
        this->visit_expr_wrapper(x.m_right, true);
        llvm::Value *right_val = tmp;
        LCOMPILERS_ASSERT(ASRUtils::is_complex(*x.m_type));
        llvm::Type *type;
        int a_kind;
        a_kind = ASR::down_cast<ASR::Complex_t>(
            ASRUtils::type_get_past_array(
                ASRUtils::type_get_past_pointer(x.m_type)))->m_kind;
        type = llvm_utils->getComplexType(a_kind);
        if( left_val->getType()->isPointerTy() ) {
            llvm::Type *left_type = llvm_utils->get_type_from_ttype_t_util(x.m_left, ASRUtils::expr_type(x.m_left), module.get());
            left_val = llvm_utils->CreateLoad2(left_type, left_val);
        }
        if( right_val->getType()->isPointerTy() ) {
            llvm::Type *right_type = llvm_utils->get_type_from_ttype_t_util(x.m_right, ASRUtils::expr_type(x.m_right), module.get());
            right_val = llvm_utils->CreateLoad2(right_type, right_val);
        }
        std::string fn_name;
        switch (x.m_op) {
            case ASR::binopType::Add: {
                if (a_kind == 4) {
                    fn_name = "_lfortran_complex_add_32";
                } else {
                    fn_name = "_lfortran_complex_add_64";
                }
                break;
            };
            case ASR::binopType::Sub: {
                if (a_kind == 4) {
                    fn_name = "_lfortran_complex_sub_32";
                } else {
                    fn_name = "_lfortran_complex_sub_64";
                }
                break;
            };
            case ASR::binopType::Mul: {
                if (a_kind == 4) {
                    fn_name = "_lfortran_complex_mul_32";
                } else {
                    fn_name = "_lfortran_complex_mul_64";
                }
                break;
            };
            case ASR::binopType::Div: {
                if (a_kind == 4) {
                    fn_name = "_lfortran_complex_div_32";
                } else {
                    fn_name = "_lfortran_complex_div_64";
                }
                break;
            };
            case ASR::binopType::Pow: {
                if (a_kind == 4) {
                    fn_name = "_lfortran_complex_pow_32";
                } else {
                    fn_name = "_lfortran_complex_pow_64";
                }
                break;
            };
            default: {
                throw CodeGenError("Binary operator '" + ASRUtils::binop_to_str_python(x.m_op) + "' not supported",
                    x.base.base.loc);
            }
        }
        tmp = lfortran_complex_bin_op(left_val, right_val, fn_name, type);
    }

    void visit_OverloadedBinOp(const ASR::OverloadedBinOp_t &x) {
        this->visit_expr(*x.m_overloaded);
    }

    void visit_OverloadedBoolOp(const ASR::OverloadedBoolOp_t& x) {
        this->visit_expr(*x.m_overloaded);
    }

    void visit_OverloadedUnaryMinus(const ASR::OverloadedUnaryMinus_t &x) {
        this->visit_expr(*x.m_overloaded);
    }

    void visit_IntegerBitNot(const ASR::IntegerBitNot_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_wrapper(x.m_arg, true);
        tmp = builder->CreateNot(tmp);
    }

    void visit_UnsignedIntegerBitNot(const ASR::UnsignedIntegerBitNot_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_wrapper(x.m_arg, true);
        tmp = builder->CreateNot(tmp);
    }

    template <typename T>
    void handle_SU_IntegerUnaryMinus(const T& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_wrapper(x.m_arg, true);
        llvm::Value *zero = llvm::ConstantInt::get(context,
            llvm::APInt(ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(x.m_arg)) * 8, 0));
        tmp = builder->CreateSub(zero, tmp);
    }

    void visit_IntegerUnaryMinus(const ASR::IntegerUnaryMinus_t &x) {
        handle_SU_IntegerUnaryMinus(x);
    }

    void visit_UnsignedIntegerUnaryMinus(const ASR::UnsignedIntegerUnaryMinus_t &x) {
        handle_SU_IntegerUnaryMinus(x);
    }

    void visit_RealUnaryMinus(const ASR::RealUnaryMinus_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_wrapper(x.m_arg, true);
        tmp = builder->CreateFNeg(tmp);
    }

    void visit_ComplexUnaryMinus(const ASR::ComplexUnaryMinus_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_wrapper(x.m_arg, true);
        llvm::Type *type = tmp->getType();
        llvm::Value *re = complex_re(tmp, type);
        llvm::Value *im = complex_im(tmp, type);
        re = builder->CreateFNeg(re);
        im = builder->CreateFNeg(im);
        tmp = complex_from_floats(re, im, type);
    }

    template <typename T>
    void handle_SU_IntegerConstant(const T &x) {
        int64_t val = x.m_n;
        int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
        switch( a_kind ) {
            case 1: {
                tmp = llvm::ConstantInt::get(context, llvm::APInt(8, static_cast<int8_t>(val), true));
                break ;
            }
            case 2: {
                tmp = llvm::ConstantInt::get(context, llvm::APInt(16, static_cast<int16_t>(val), true));
                break ;
            }
            case 4 : {
                tmp = llvm::ConstantInt::get(context, llvm::APInt(32, static_cast<int32_t>(val), true));
                break;
            }
            case 8 : {
                tmp = llvm::ConstantInt::get(context, llvm::APInt(64, val, true));
                break;
            }
            default : {
                throw CodeGenError("Constant integers of " + std::to_string(a_kind)
                                    + " bytes aren't supported yet.");
            }

        }
    }

    void visit_IntegerConstant(const ASR::IntegerConstant_t &x) {
        handle_SU_IntegerConstant(x);
    }

    void visit_UnsignedIntegerConstant(const ASR::UnsignedIntegerConstant_t &x) {
        handle_SU_IntegerConstant(x);
    }

    void visit_RealConstant(const ASR::RealConstant_t &x) {
        int a_kind = ((ASR::Real_t*)(&(x.m_type->base)))->m_kind;
        // For kind 4/8, x.m_r is the IEEE-754 double approximation/value.
        // For kind 16, x.m_r is a pointer-encoded payload; we MUST go through
        // real_constant_get_r16_bytes() rather than reading it as a double.
        switch( a_kind ) {

            case 4 : {
                tmp = llvm::ConstantFP::get(context, llvm::APFloat((float)x.m_r));
                break;
            }
            case 8 : {
                tmp = llvm::ConstantFP::get(context, llvm::APFloat(x.m_r));
                break;
            }
            case 16 : {
                const uint8_t* bytes = ASRUtils::real_constant_get_r16_bytes(&x);
                uint64_t lo, hi;
                std::memcpy(&lo, bytes,     sizeof(lo));
                std::memcpy(&hi, bytes + 8, sizeof(hi));
                // APInt(numBits, ArrayRef<uint64_t>{lo, hi}) — low word first.
                uint64_t words[2] = { lo, hi };
                llvm::APInt bits(128, llvm::ArrayRef<uint64_t>(words, 2));
                llvm::APFloat apf(llvm::APFloat::IEEEquad(), bits);
                tmp = llvm::ConstantFP::get(context, apf);
                break;
            }
            default : {
                break;
            }

        }

    }

    template <typename T>
    void visit_ArrayConstructorUtil(const T& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }

        llvm::Type* el_type = nullptr;
        ASR::ttype_t* x_m_type = ASRUtils::type_get_past_array(x.m_type);
        if (ASR::is_a<ASR::Integer_t>(*x_m_type)) {
            el_type = llvm_utils->getIntType(ASR::down_cast<ASR::Integer_t>(x_m_type)->m_kind);
        } else if (ASR::is_a<ASR::Real_t>(*x_m_type)) {
            switch (ASR::down_cast<ASR::Real_t>(x_m_type)->m_kind) {
                case (4) :
                    el_type = llvm::Type::getFloatTy(context); break;
                case (8) :
                    el_type = llvm::Type::getDoubleTy(context); break;
                case (16) :
                    el_type = llvm::Type::getFP128Ty(context); break;
                default :
                    throw CodeGenError("ConstArray real kind not supported yet");
            }
        } else if (ASR::is_a<ASR::Logical_t>(*x_m_type)) {
            int a_kind = ASR::down_cast<ASR::Logical_t>(x_m_type)->m_kind;
            el_type = llvm_utils->getIntType(a_kind);
        } else if (ASR::is_a<ASR::String_t>(*x_m_type)) {
            el_type = llvm_utils->get_StringType(x_m_type);
        } else if (ASR::is_a<ASR::Complex_t>(*x_m_type)) {
            int complex_kind = ASR::down_cast<ASR::Complex_t>(x_m_type)->m_kind;
            if( complex_kind == 4 ) {
                el_type = llvm_utils->complex_type_4;
            } else if( complex_kind == 8 ) {
                el_type = llvm_utils->complex_type_8;
            } else {
                LCOMPILERS_ASSERT(false);
            }
        } else {
            throw CodeGenError("ConstArray type not supported yet");
        }
        // Create <n x float> type, where `n` is the length of the `x` constant array
        int64_t simd_n = ASRUtils::get_fixed_size_of_array(x.m_type);
        llvm::Type* type_fxn = nullptr;
        if (simd_n <= 0) {
            type_fxn = llvm::ArrayType::get(el_type, 0);
        } else {
            type_fxn = FIXED_VECTOR_TYPE::get(el_type, simd_n);
        }
        // Create a pointer <n x float>* to a stack allocated <n x float>
        llvm::AllocaInst *p_fxn = llvm_utils->CreateAlloca(*builder, type_fxn);
        // Assign the array elements to `p_fxn`.
        for (size_t i=0; i < x.n_args; i++) {
            llvm::Value *llvm_el = llvm_utils->create_gep2(type_fxn, p_fxn, i);
            ASR::expr_t *el = x.m_args[i];
            int64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 2;
            this->visit_expr_wrapper(el, true);
            ptr_loads = ptr_loads_copy;
            if (ASRUtils::is_logical(*x_m_type) && tmp->getType()->isIntegerTy(1)) {
                tmp = builder->CreateZExt(tmp, llvm::Type::getInt8Ty(context));
            }
            builder->CreateStore(tmp, llvm_el);
        }
        // Return the vector as float* type:
        tmp = llvm_utils->create_gep2(type_fxn ,p_fxn, 0);
    }

    void visit_ArrayConstantUtil(const ASR::ArrayConstant_t &x) {
        llvm::Type* el_type = nullptr;
        ASR::ttype_t* x_m_type = ASRUtils::type_get_past_array(x.m_type);
        if (ASR::is_a<ASR::Integer_t>(*x_m_type)) {
            el_type = llvm_utils->getIntType(ASR::down_cast<ASR::Integer_t>(x_m_type)->m_kind);
        } else if (ASR::is_a<ASR::Real_t>(*x_m_type)) {
            switch (ASR::down_cast<ASR::Real_t>(x_m_type)->m_kind) {
                case (4) :
                    el_type = llvm::Type::getFloatTy(context); break;
                case (8) :
                    el_type = llvm::Type::getDoubleTy(context); break;
                case (16) :
                    el_type = llvm::Type::getFP128Ty(context); break;
                default :
                    throw CodeGenError("ConstArray real kind not supported yet");
            }
        } else if (ASR::is_a<ASR::Logical_t>(*x_m_type)) {
            int a_kind = ASR::down_cast<ASR::Logical_t>(x_m_type)->m_kind;
            el_type = llvm_utils->getIntType(a_kind);
        } else if (ASR::is_a<ASR::String_t>(*x_m_type)) {
            el_type = llvm_utils->get_StringType(x_m_type);
        } else if (ASR::is_a<ASR::Complex_t>(*x_m_type)) {
            int complex_kind = ASR::down_cast<ASR::Complex_t>(x_m_type)->m_kind;
            if( complex_kind == 4 ) {
                el_type = llvm_utils->complex_type_4;
            } else if( complex_kind == 8 ) {
                el_type = llvm_utils->complex_type_8;
            } else {
                LCOMPILERS_ASSERT(false);
            }
        } else {
            throw CodeGenError("ConstArray type not supported yet");
        }

        // Declaring array constant as global constant and directly using it
        // instead of storing each element using CreateStore
        int64_t arr_size = ASRUtils::get_fixed_size_of_array(x.m_type);
        llvm::Type *Int32Ty = llvm::Type::getInt32Ty(context);
        llvm::ArrayType * arr_type = llvm::ArrayType::get(el_type, arr_size);
        std::vector<llvm::Constant *> values;

        if (ASRUtils::is_integer(*x_m_type)) {
            for (size_t i=0; i < (size_t) arr_size; i++) {
                ASR::expr_t *el = ASRUtils::fetch_ArrayConstant_value(al, x, i);
                values.push_back(llvm::ConstantInt::get(el_type, down_cast<ASR::IntegerConstant_t>(el)->m_n));
            }
        } else if (ASRUtils::is_real(*x_m_type)) {
            for (size_t i=0; i < (size_t) arr_size; i++) {
                ASR::expr_t *el = ASRUtils::fetch_ArrayConstant_value(al, x, i);
                values.push_back(llvm::ConstantFP::get(el_type, down_cast<ASR::RealConstant_t>(el)->m_r));
            }
        } else if (ASRUtils::is_logical(*x_m_type)) {
            int a_kind = ASRUtils::extract_kind_from_ttype_t(x_m_type);
            for (size_t i=0; i < (size_t) arr_size; i++) {
                ASR::expr_t *el = ASRUtils::fetch_ArrayConstant_value(al, x, i);
                values.push_back(llvm::ConstantInt::get(
                    el_type, llvm::APInt(a_kind * 8, down_cast<ASR::LogicalConstant_t>(el)->m_value ? 1 : 0)));
            }
        } else if (ASRUtils::is_complex(*x_m_type)) {
            for (size_t i=0; i < (size_t) arr_size; i++) {
                ASR::expr_t *el = ASRUtils::fetch_ArrayConstant_value(al, x, i);
                ASR::ComplexConstant_t *comp_const = down_cast<ASR::ComplexConstant_t>(el);
                if (ASRUtils::extract_kind_from_ttype_t(comp_const->m_type) == 4) {
                    values.push_back(llvm::ConstantStruct::get(llvm_utils->complex_type_4,
                        {llvm::ConstantFP::get(llvm::Type::getFloatTy(context), comp_const->m_re),
                        llvm::ConstantFP::get(llvm::Type::getFloatTy(context), comp_const->m_im)}));
                } else {
                    values.push_back(llvm::ConstantStruct::get(llvm_utils->complex_type_8,
                        {llvm::ConstantFP::get(llvm::Type::getDoubleTy(context), comp_const->m_re),
                        llvm::ConstantFP::get(llvm::Type::getDoubleTy(context), comp_const->m_im)}));
                }
            }
        } else if (ASRUtils::is_character(*x_m_type)) { // Sepcial Case.
            tmp = llvm_utils->declare_constant_stringArray(al, &x);
            return;
        }

        llvm::Constant *ConstArray = llvm::ConstantArray::get(arr_type, values);
        llvm::GlobalVariable *global_var = new llvm::GlobalVariable(*module, arr_type, true,
            llvm::GlobalValue::PrivateLinkage, ConstArray, "global_array_" + std::to_string(global_array_count++));
        tmp = builder->CreateGEP(
            arr_type, global_var, {llvm::ConstantInt::get(Int32Ty, 0), llvm::ConstantInt::get(Int32Ty, 0)});
    }

    void visit_ArrayConstructor(const ASR::ArrayConstructor_t &x) {
        visit_ArrayConstructorUtil(x);
    }

    void visit_ArrayConstant(const ASR::ArrayConstant_t &x) {
        visit_ArrayConstantUtil(x);
    }

    void visit_Assert(const ASR::Assert_t &x) {
        if (compiler_options.emit_debug_info) debug_emit_loc(x);
        this->visit_expr_wrapper(x.m_test, true);
        llvm_utils->create_if_else(tmp, []() {}, [=]() {
            if (compiler_options.emit_debug_info) {
                llvm::Value *fmt_ptr = LCompilers::create_global_string_ptr(context, *module, *builder, infile);
                llvm::Value *fmt_ptr1 = llvm::ConstantInt::get(context, llvm::APInt(
                    1, compiler_options.use_colors));
                call_print_stacktrace_addresses(context, *module, *builder,
                    {fmt_ptr, fmt_ptr1});
            }
            if (x.m_msg) {
                std::vector<std::string> fmt;
                std::vector<llvm::Value *> args;
                fmt.push_back("%s");
                args.push_back(LCompilers::create_global_string_ptr(context, *module, *builder, "AssertionError: "));
                compute_fmt_specifier_and_arg(fmt, args, x.m_msg, x.base.base.loc);
                fmt.push_back("%s");
                args.push_back(LCompilers::create_global_string_ptr(context, *module, *builder, "\n"));
                std::string fmt_str;
                for (size_t i=0; i<fmt.size(); i++) {
                    fmt_str += fmt[i];
                }
                llvm::Value *fmt_ptr = LCompilers::create_global_string_ptr(context, *module, *builder, fmt_str);
                std::vector<llvm::Value *> print_error_args;
                print_error_args.push_back(fmt_ptr);
                print_error_args.insert(print_error_args.end(), args.begin(), args.end());
                print_error(context, *module, *builder, print_error_args);
            } else {
                llvm::Value *fmt_ptr = LCompilers::create_global_string_ptr(context, *module, *builder, "AssertionError\n");
                print_error(context, *module, *builder, {fmt_ptr});
            }
            int exit_code_int = 1;
            llvm::Value *exit_code = llvm::ConstantInt::get(context,
                    llvm::APInt(32, exit_code_int));
            exit(context, *module, *builder, exit_code);
        });
    }

    void visit_ComplexConstructor(const ASR::ComplexConstructor_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_wrapper(x.m_re, true);
        llvm::Value *re_val = tmp;

        this->visit_expr_wrapper(x.m_im, true);
        llvm::Value *im_val = tmp;

        int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);

        llvm::Value *re2, *im2;
        llvm::Type *type;
        switch( a_kind ) {
            case 4: {
                re2 = builder->CreateFPTrunc(re_val, llvm::Type::getFloatTy(context));
                im2 = builder->CreateFPTrunc(im_val, llvm::Type::getFloatTy(context));
                type = complex_type_4;
                break;
            }
            case 8: {
                re2 = builder->CreateFPExt(re_val, llvm::Type::getDoubleTy(context));
                im2 = builder->CreateFPExt(im_val, llvm::Type::getDoubleTy(context));
                type = complex_type_8;
                break;
            }
            default: {
                throw CodeGenError("kind type is not supported");
            }
        }
        tmp = complex_from_floats(re2, im2, type);
    }

    void visit_ComplexConstant(const ASR::ComplexConstant_t &x) {
        double re = x.m_re;
        double im = x.m_im;
        int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
        llvm::Constant *re2, *im2;
        llvm::Type *type;
        switch( a_kind ) {
            case 4: {
                re2 = llvm::ConstantFP::get(context, llvm::APFloat((float)re));
                im2 = llvm::ConstantFP::get(context, llvm::APFloat((float)im));
                type = complex_type_4;
                break;
            }
            case 8: {
                re2 = llvm::ConstantFP::get(context, llvm::APFloat(re));
                im2 = llvm::ConstantFP::get(context, llvm::APFloat(im));
                type = complex_type_8;
                break;
            }
            default: {
                throw CodeGenError("kind type is not supported");
            }
        }
        // Create a compile-time constant struct for ComplexConstant
        // Complex numbers are represented as {real, imaginary} structs
        std::vector<llvm::Constant*> elements = {re2, im2};
        llvm::StructType* struct_type = llvm::cast<llvm::StructType>(type);
        tmp = llvm::ConstantStruct::get(struct_type, elements);
    }

    void visit_LogicalConstant(const ASR::LogicalConstant_t &x) {
        int val;
        if (x.m_value == true) {
            val = 1;
        } else {
            val = 0;
        }
        int kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
        tmp = llvm::ConstantInt::get(context, llvm::APInt(kind * 8, val));
    }

    void visit_LogicalNot(const ASR::LogicalNot_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        this->visit_expr_wrapper(x.m_arg, true);
        llvm::Value *arg = tmp;
        llvm::Value *one = llvm::ConstantInt::get(arg->getType(), 1);
        tmp = builder->CreateXor(arg, one);
    }

    void visit_StringConstant(const ASR::StringConstant_t &x) {
        tmp = llvm_utils->declare_string_constant(&x);
    }

    void visit_CompilerOptions(const ASR::CompilerOptions_t &x) {
        // Use the compiler options string stored in the ASR node
        std::string options_str = std::string(x.m_compiler_options_str);
        tmp = llvm_utils->declare_global_string(
            ASRUtils::get_string_type(x.m_type),
            options_str, true, "compiler_options_const");
    }

    inline void fetch_ptr(ASR::Variable_t* x) {
        uint32_t x_h = get_hash((ASR::asr_t*)x);
        LCOMPILERS_ASSERT(llvm_symtab.find(x_h) != llvm_symtab.end());
        llvm::Value* x_v = llvm_symtab[x_h];
        int64_t ptr_loads_copy = ptr_loads;
        tmp = x_v;
        llvm::Type* type_req = nullptr;
        int loads = 0;
        while( ptr_loads_copy-- ) {
            if( loads == 0 ) {
                type_req =  llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                    al, x->base.base.loc, &x->base)),
                    x->m_type, module.get(), x->m_abi);

            } else {
                type_req =  llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                    al, x->base.base.loc, &x->base)),
                    ASRUtils::type_get_past_allocatable_pointer(x->m_type),
                    module.get(), x->m_abi);
            }
            tmp = llvm_utils->CreateLoad2(type_req, tmp);
            loads++;
        }
    }

    inline bool is_cptr_dummy_passed_by_value(const ASR::Variable_t* x) const {
        return ASR::is_a<ASR::CPtr_t>(*x->m_type) &&
            ASRUtils::is_arg_dummy(x->m_intent) &&
            !(x->m_intent == ASR::intentType::Out ||
              x->m_intent == ASR::intentType::InOut ||
              (x->m_intent == ASR::intentType::Unspecified && !x->m_value_attr));
    }

    inline void fetch_val(ASR::Variable_t* x) {
        uint32_t x_h = get_hash((ASR::asr_t*)x);
        llvm::Value* x_v;
        LCOMPILERS_ASSERT(llvm_symtab.find(x_h) != llvm_symtab.end());
        x_v = llvm_symtab[x_h];
        if (x->m_abi == ASR::abiType::BindC && x->m_value_attr) {
            // Already a value, such as value argument to bind(c)
            tmp = x_v;
            return;
        }
        if (is_cptr_dummy_passed_by_value(x)) {
            // type(c_ptr) dummy arguments that are passed by value
            // are already the pointer value and must not be loaded.
            tmp = x_v;
            return;
        }
        if( ASRUtils::is_array(x->m_type) ) {
            tmp = x_v;
        } else {
            tmp = x_v;
            // Load only once since its a value
            if( ptr_loads > 0 ) {
                llvm::Type* x_llvm_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                    al, x->base.base.loc, &x->base)), x->m_type, module.get());
                tmp = llvm_utils->CreateLoad2(x_llvm_type, tmp);
            }
        }
    }

    inline void fetch_var(ASR::Variable_t* x) {
        // Only do for constant variables
        if (x->m_value && x->m_storage == ASR::storage_typeType::Parameter) {
            this->visit_expr_wrapper(x->m_value, true);
            return;
        }
        ASR::ttype_t *t2_ = ASRUtils::type_get_past_array(x->m_type);
        switch( t2_->type ) {
            case ASR::ttypeType::Pointer:
            case ASR::ttypeType::Allocatable: {
                ASR::ttype_t *t2 = ASRUtils::extract_type(x->m_type);
                switch (t2->type) {
                    case ASR::ttypeType::Integer:
                    case ASR::ttypeType::UnsignedInteger:
                    case ASR::ttypeType::Real:
                    case ASR::ttypeType::Complex:
                    case ASR::ttypeType::StructType:
                    case ASR::ttypeType::String:
                    case ASR::ttypeType::Logical:
                    case ASR::ttypeType::CPtr:{
                        if( t2->type == ASR::ttypeType::StructType ) {
                            current_der_type_name = get_type_key(
                                ASRUtils::get_struct_sym_from_struct_expr(ASRUtils::EXPR(ASR::make_Var_t(al, x->base.base.loc, (ASR::symbol_t*) x))));
                        }
                        if (ASRUtils::is_unlimited_polymorphic_type(ASRUtils::EXPR(ASR::make_Var_t(al, x->base.base.loc, (ASR::symbol_t*)x)))
                                && !ASRUtils::is_array(x->m_type)) {
                            uint32_t h = get_hash((ASR::asr_t*)x);
                            if( llvm_symtab.find(h) != llvm_symtab.end() ) {
                                tmp = llvm_symtab[h];
                            }
                        } else {
                            ASR::expr_t* var_expr = ASRUtils::EXPR(ASR::make_Var_t(al, x->base.base.loc, (ASR::symbol_t*)x));
                            ASR::ttype_t* value_type = ASRUtils::expr_type(var_expr);
                            // Throw runtime error for trying to get value of unallocated allocatable scalars
                            if (ptr_loads == 2 && // If ptr_loads == 2 then we are trying to read the value
                                compiler_options.po.bounds_checking &&
                                ASRUtils::is_allocatable(value_type) && !ASRUtils::is_array(value_type) &&
                                !ASRUtils::is_string_only(value_type) && !ASR::is_a<ASR::StructType_t>(*ASRUtils::type_get_past_allocatable(value_type))) {
                                llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(var_expr, ASRUtils::expr_type(var_expr), module.get());
                                int ptr_loads_copy = ptr_loads;
                                ptr_loads = 1;
                                fetch_ptr(x);
                                ptr_loads = ptr_loads_copy;
                                llvm::Value* cond = builder->CreateICmpEQ(
                                    builder->CreatePtrToInt(tmp,
                                        llvm::Type::getInt64Ty(context)),
                                    builder->CreatePtrToInt(llvm::ConstantPointerNull::get(type->getPointerTo()),
                                        llvm::Type::getInt64Ty(context)));
                                // TODO: Set location here
                                llvm_utils->generate_runtime_error(cond,
                                    "Variable '%s' is not allocated.",
                                    {LLVMUtils::RuntimeLabel("", {x->base.base.loc})},
                                    infile,
                                    location_manager,
                                    LCompilers::create_global_string_ptr(context, *module, *builder, x->m_name));
                            }
                            fetch_ptr(x);
                        }
                        break;
                    }
                    case ASR::ttypeType::FunctionType:{
                        fetch_ptr(x);
                        break;
                    }
                    default:
                        break;
                }
                break;
            }
            case ASR::ttypeType::StructType: {
                ASR::Struct_t* der_type = ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(x->m_type_declaration));
                current_der_type_name = get_type_key(der_type);
                uint32_t h = get_hash((ASR::asr_t*)x);
                if( llvm_symtab.find(h) != llvm_symtab.end() ) {
                    tmp = llvm_symtab[h];
                }
                break;
            }
            case ASR::ttypeType::UnionType: {
                ASR::Union_t* der_type = ASR::down_cast<ASR::Union_t>(
                    ASRUtils::symbol_get_past_external(x->m_type_declaration));
                current_der_type_name = get_type_key(&der_type->base);
                uint32_t h = get_hash((ASR::asr_t*)x);
                if( llvm_symtab.find(h) != llvm_symtab.end() ) {
                    tmp = llvm_symtab[h];
                }
                break;
            }
            case ASR::ttypeType::FunctionType: {
                // break;
                uint32_t h = get_hash((ASR::asr_t*)x);
                uint32_t x_h = get_hash((ASR::asr_t*)x);
                if ( llvm_symtab_fn_arg.find(h) != llvm_symtab_fn_arg.end() ) {
                    tmp = llvm_symtab_fn_arg[h];
                } else if ( llvm_symtab.find(x_h) != llvm_symtab.end() ) {
                    tmp = llvm_symtab[x_h];
                } else if (llvm_symtab_fn.find(h) != llvm_symtab_fn.end()) {
                    tmp = llvm_symtab_fn[h];
                    tmp = llvm_utils->CreateLoad2(tmp->getType()->getPointerTo(), tmp);
                } else {
                    throw CodeGenError("Function type not supported yet");
                }
                if (x->m_abi == ASR::abiType::BindC && x->m_value_attr) {
                    // Already a value, such as value argument to bind(c)
                    break;
                }
                if( ASRUtils::is_array(x->m_type) ) {
                    break;
                } else {
                    // Load only once since its a value
                    if( ptr_loads > 0 ) {
                        llvm::Type* x_llvm_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                            al, x->base.base.loc, &x->base)), x->m_type, module.get());
                        tmp = llvm_utils->CreateLoad2(x_llvm_type, tmp);
                    }
                }
                break;
            } default: {
                fetch_val(x);
                break;
            }
        }
    }

    void visit_Var(const ASR::Var_t &x) {
        ASR::symbol_t* x_m_v = ASRUtils::symbol_get_past_external(x.m_v);
        switch( x_m_v->type ) {
            case ASR::symbolType::Variable: {
                ASR::Variable_t *v = ASR::down_cast<ASR::Variable_t>(x_m_v);
                fetch_var(v);
            break;
            }
            case ASR::symbolType::Function: {
                const uint32_t h = get_hash((ASR::asr_t*)x_m_v);
                if(llvm_symtab_fn_arg.find(h) != llvm_symtab_fn_arg.end()){ // Callback fn arg.
                    tmp = llvm_symtab_fn_arg[h];
                } else if( llvm_symtab_fn.find(h) != llvm_symtab_fn.end() ) {
                    tmp = llvm_symtab_fn[h];
                } else {
                    throw CodeGenError(std::string("Can't resolve var to Function '") + ASRUtils::symbol_name(x_m_v) + "'");
                }
            break;
            }
            default: {
                throw CodeGenError("Only function and variables supported so far");
            }
        }
    }

    inline ASR::ttype_t* extract_ttype_t_from_expr(ASR::expr_t* expr) {
        return ASRUtils::expr_type(expr);
    }

    void extract_kinds(const ASR::Cast_t& x,
                       int& arg_kind, int& dest_kind)
    {
        dest_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
        ASR::ttype_t* curr_type = extract_ttype_t_from_expr(x.m_arg);
        LCOMPILERS_ASSERT(curr_type != nullptr)
        arg_kind = ASRUtils::extract_kind_from_ttype_t(curr_type);
    }

    template <typename T>
    void handle_arr_for_complex_im_re(const T& t) {
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 2 - LLVM::is_llvm_pointer(*ASRUtils::expr_type(t.m_arg));
        this->visit_expr_wrapper(t.m_arg, false);
        ptr_loads = ptr_loads_copy;
        llvm::Value* des_complex_arr = tmp;
        llvm::Type* des_complex_type = llvm_utils->get_type_from_ttype_t_util(t.m_arg,
            ASRUtils::extract_type(ASRUtils::expr_type(t.m_arg)), module.get());
        llvm::Type* des_complex_type_ = llvm_utils->get_type_from_ttype_t_util(
            t.m_arg, ASRUtils::expr_type(t.m_arg), module.get());
        tmp = llvm_utils->CreateLoad2(des_complex_type->getPointerTo(), arr_descr->get_pointer_to_data(des_complex_type_, des_complex_arr));
        int kind = ASRUtils::extract_kind_from_ttype_t(t.m_type);
        llvm::Type* pointer_cast_type = nullptr;
        if (kind == 4) {
            pointer_cast_type = llvm::Type::getFloatTy(context)->getPointerTo();
        } else {
            pointer_cast_type = llvm::Type::getDoubleTy(context)->getPointerTo();
        }
        tmp = builder->CreateBitCast(tmp, pointer_cast_type);
        PointerToData_to_Descriptor(t.m_arg, t.m_type, t.m_type);
        llvm::Value* des_real_arr = tmp;
        llvm::Type* des_real_type = llvm_utils->get_type_from_ttype_t_util(t.m_arg, t.m_type, module.get());
        llvm::Value* arr_data = llvm_utils->CreateLoad2(
            des_complex_type->getPointerTo(), arr_descr->get_pointer_to_data(des_complex_type_, des_complex_arr));
        tmp = builder->CreateBitCast(arr_data, pointer_cast_type);
        builder->CreateStore(tmp, arr_descr->get_pointer_to_data(des_real_type,  des_real_arr));
        llvm::Type* idx_type = arr_descr->get_index_type();
        unsigned idx_bits = idx_type->getIntegerBitWidth();
        if (std::is_same<T, ASR::ComplexIm_t>::value) {
            llvm::Value* incremented_offset = builder->CreateAdd(
                arr_descr->get_offset(des_real_type, des_real_arr, true),
                llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 1)));
            builder->CreateStore(incremented_offset, arr_descr->get_offset(des_real_type, des_real_arr, false));
        }
        int n_dims = ASRUtils::extract_n_dims_from_ttype(t.m_type);
        llvm::Value* dim_des_real_arr = arr_descr->get_pointer_to_dimension_descriptor_array(des_real_type, des_real_arr, true);
        for (int i = 0; i < n_dims; i++) {
            llvm::Value* dim_idx = llvm::ConstantInt::get(context, llvm::APInt(32, i));
            llvm::Value* dim_des_real_arr_idx = arr_descr->get_pointer_to_dimension_descriptor(dim_des_real_arr, dim_idx);
            llvm::Value* doubled_stride = builder->CreateMul(
                arr_descr->get_stride(dim_des_real_arr_idx, true),
                llvm::ConstantInt::get(context, llvm::APInt(idx_bits, 2)));
            builder->CreateStore(doubled_stride, arr_descr->get_stride(dim_des_real_arr_idx, false));
        }
        tmp = des_real_arr;
    }

    void visit_ComplexRe(const ASR::ComplexRe_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        if (ASRUtils::is_array(x.m_type) && ASRUtils::is_array(ASRUtils::expr_type(x.m_arg))) {
            handle_arr_for_complex_im_re(x);
            return;
        }
        this->visit_expr_wrapper(x.m_arg, true);
        ASR::ttype_t* curr_type = extract_ttype_t_from_expr(x.m_arg);
        int arg_kind = ASRUtils::extract_kind_from_ttype_t(curr_type);
        llvm::Value *re;
        if (arg_kind == 4) {
            re = complex_re(tmp, complex_type_4);
        } else if (arg_kind == 8) {
            re = complex_re(tmp, complex_type_8);
        } else {
            std::string msg = "Complex kind " + std::to_string(arg_kind) + " not implemented yet.";
            throw CodeGenError(msg);
        }
        tmp = re;
    }

    void visit_ComplexIm(const ASR::ComplexIm_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        if (ASRUtils::is_array(x.m_type) && ASRUtils::is_array(ASRUtils::expr_type(x.m_arg))) {
            handle_arr_for_complex_im_re(x);
            return;
        }
        this->visit_expr_wrapper(x.m_arg, true);
        ASR::ttype_t* curr_type = extract_ttype_t_from_expr(x.m_arg);
        int arg_kind = ASRUtils::extract_kind_from_ttype_t(curr_type);
        llvm::Value *im;
        if (arg_kind == 4) {
            im = complex_im(tmp, complex_type_4);
        } else if (arg_kind == 8) {
            im = complex_im(tmp, complex_type_8);
        } else {
            std::string msg = "Complex kind " + std::to_string(arg_kind) + " not implemented yet.";
            throw CodeGenError(msg);
        }
        tmp = im;
    }

    void visit_BitCast(const ASR::BitCast_t& x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        llvm::Value* source{};
        int64_t p_load = 0 ;
        if(ASRUtils::is_character(*expr_type(x.m_source))){
            const bool source_is_descriptor_array_= ASR::is_a<ASR::Array_t>(*ASRUtils::type_get_past_allocatable_pointer(expr_type(x.m_source)))
                                                    && ASRUtils::extract_physical_type(expr_type(x.m_source)) == ASR::DescriptorArray;
            p_load = source_is_descriptor_array_? 1 : 0;
        } else {
            p_load = ptr_loads;
        }
        this->visit_expr_load_wrapper(x.m_source, p_load, true);
        source = tmp;
        llvm::Type* source_type = llvm_utils->get_type_from_ttype_t_util(x.m_source, ASRUtils::expr_type(x.m_source), module.get());
        llvm::Value* source_ptr;
        bool is_array = ASRUtils::is_array(ASRUtils::expr_type(x.m_source));
        ASR::ttype_t* source_arr_type = ASRUtils::type_get_past_allocatable_pointer(
            ASRUtils::expr_type(x.m_source));
        if (ASRUtils::is_character(*expr_type(x.m_source))) {
            tmp = source_ptr = ASRUtils::is_array_of_strings(expr_type(x.m_source)) ?
                        llvm_utils->get_stringArray_data(expr_type(x.m_source), tmp) :
                        llvm_utils->get_string_data(ASRUtils::get_string_type(x.m_source), tmp);
        } else if(source->getType()->isPointerTy()){//Case: [n x i8]* type Arrays and ptr %
            if (is_array && ASR::is_a<ASR::Array_t>(*source_arr_type) &&
                ASR::down_cast<ASR::Array_t>(source_arr_type)->m_physical_type ==
                    ASR::array_physical_typeType::DescriptorArray) {
                llvm::Type* llvm_source_arr_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_source, source_arr_type, module.get());
                llvm::Type* llvm_source_elem_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_source, ASRUtils::extract_type(source_arr_type), module.get());
                llvm::Value* data_ptr_ptr = arr_descr->get_pointer_to_data(
                    x.m_source, ASRUtils::expr_type(x.m_source), source, module.get());
                source_ptr = llvm_utils->CreateLoad2(
                    llvm_source_elem_type->getPointerTo(), data_ptr_ptr);
                llvm::Value* offset = arr_descr->get_offset(llvm_source_arr_type, source);
                source_ptr = llvm_utils->create_ptr_gep2(
                    llvm_source_elem_type, source_ptr, offset);
            } else if (ASR::is_a<ASR::CPtr_t>(
                    *ASRUtils::type_get_past_allocatable_pointer(expr_type(x.m_source)))) {
                source_ptr = llvm_utils->CreateAlloca(
                    source->getType(), nullptr, "bitcast_cptr_source");
                builder->CreateStore(source, source_ptr);
            } else {
                source_ptr = source;
            }
        } else if(source_type->isArrayTy()){
            // Source is a loaded array value (e.g. from struct member access),
            // store into alloca to obtain a pointer for bitcast
            source_ptr = llvm_utils->CreateAlloca(source->getType(), nullptr, "bitcast_source");
            builder->CreateStore(source, source_ptr);
        } else if (is_array) {
            source_type = source->getType();
            source_ptr = llvm_utils->CreateAlloca(source_type, nullptr, "bitcast_source");
            builder->CreateStore(source, source_ptr);
            ASR::Array_t* arr = ASR::down_cast<ASR::Array_t>(
                ASRUtils::type_get_past_allocatable_pointer(ASRUtils::expr_type(x.m_source)));
            llvm::Type* source_type_ = llvm_utils->get_type_from_ttype_t_util(x.m_source,
                ASRUtils::extract_type(ASRUtils::expr_type(x.m_source)), module.get());
            llvm::Type* llvm_source_type_ = llvm_utils->get_type_from_ttype_t_util(x.m_source,
                ASRUtils::type_get_past_allocatable_pointer(ASRUtils::expr_type(x.m_source)), module.get());
            if (arr->m_physical_type == ASR::array_physical_typeType::DescriptorArray) {
                source_ptr = llvm_utils->create_gep2(llvm_source_type_, source_ptr, 0);
                source_ptr = builder->CreateLoad(source_type_->getPointerTo(), source_ptr);
            } else {      // For PointerArray source itself is a pointer to data
                source_ptr = source;
            }
        } else {
            source_ptr = llvm_utils->CreateAlloca(source_type, nullptr, "bitcast_source");
            builder->CreateStore(source, source_ptr);
        }

        /* Handle The Return Of The Expression (String, Array, Integer_8, etc.) */
        ASR::ttype_t* mold_type_unwrapped = ASRUtils::type_get_past_allocatable_pointer(expr_type(x.m_mold));
        switch(mold_type_unwrapped->type){
            case(ASR::Array) : {// Array result from transfer(source, array_mold)
                // Calculate number of elements = source_size / element_size
                ASR::Array_t* array_mold = ASR::down_cast<ASR::Array_t>(mold_type_unwrapped);
                ASR::ttype_t* element_type = array_mold->m_type;

                if (ASRUtils::is_character(*element_type)) {
                    // For character arrays: num_elements = source_bytes / element_bytes
                    // where element_bytes = kind * length for character(kind, len)
                    llvm::Value* source_length = nullptr;
                    if (ASRUtils::is_character(*ASRUtils::expr_type(x.m_source))) {
                        source_length = get_string_length(x.m_source);
                        if (!source_length->getType()->isIntegerTy(64)) {
                            source_length = builder->CreateZExt(source_length, llvm::Type::getInt64Ty(context));
                        }
                    } else {
                        llvm::DataLayout dl(module->getDataLayout());
                        if (ASRUtils::is_array(ASRUtils::expr_type(x.m_source))) {
                            ASR::ttype_t* src_arr_type = ASRUtils::type_get_past_allocatable_pointer(ASRUtils::expr_type(x.m_source));
                            llvm::Type* llvm_src_type = llvm_utils->get_type_from_ttype_t_util(x.m_source, src_arr_type, module.get());
                            llvm::Value* num_elems = llvm_utils->get_array_size(source, llvm_src_type, src_arr_type, this);
                            ASR::ttype_t* src_el_type = ASRUtils::extract_type(ASRUtils::expr_type(x.m_source));
                            llvm::Type* src_el_llvm = llvm_utils->get_type_from_ttype_t_util(x.m_source, src_el_type, module.get());
                            uint64_t el_sz = dl.getTypeAllocSize(src_el_llvm);
                            llvm::Value* elem_size = llvm::ConstantInt::get(num_elems->getType(), el_sz);
                            source_length = builder->CreateMul(num_elems, elem_size);
                        } else {
                            llvm::Type* src_type = llvm_utils->get_type_from_ttype_t_util(x.m_source, ASRUtils::expr_type(x.m_source), module.get());
                            uint64_t sz = dl.getTypeAllocSize(src_type);
                            source_length = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), sz);
                        }
                    }

                    // Note: source_length is the total number of bytes in the source
                    // The ASR phase has already calculated the correct array dimension
                    // based on: num_elements = source_bytes / (kind * len)
                    // This string view just carries the raw byte data
                    llvm::Value* const casted_to_i8 = builder->CreateBitCast(source_ptr,
                        llvm::Type::getInt8Ty(context)->getPointerTo());

                    // Create a string view representing the whole source byte data
                    // The assignment handler will extract element_length bytes per iteration
                    llvm::Value* const str_view = llvm_utils->create_stringView(
                        ASR::down_cast<ASR::String_t>(ASRUtils::extract_type(element_type)),
                        casted_to_i8, source_length, "bit_cast_expr_return");
                    tmp = str_view;
                }
            break;
            }
            case(ASR::String) : {// Create StringView : data = bitcasted source, length = mold's length
                llvm::Value* const casted_to_i8 /* i8* */  = builder->CreateBitCast(source_ptr, llvm::Type::getInt8Ty(context)->getPointerTo());
                llvm::Value* const str_view /*non-owning*/ = llvm_utils->create_stringView(ASRUtils::get_string_type(x.m_mold), 
                                                                casted_to_i8, get_string_length(x.m_mold), "bit_cast_expr_return");
                tmp = str_view;
            break;
            } default : {
                // Do nothing for now.
            }
        }


        llvm::Type* target_base_type = llvm_utils->get_type_from_ttype_t_util(const_cast<ASR::expr_t*>(&x.base), ASRUtils::type_get_past_array(x.m_type), module.get());
        llvm::Type* target_llvm_type = target_base_type->getPointerTo();
        bool is_string_to_int8 = ASR::is_a<ASR::String_t>(*ASRUtils::extract_type(ASRUtils::expr_type(x.m_source))) &&
                                 ASRUtils::is_integer(*ASRUtils::extract_type(x.m_type)) &&
                                 ASR::down_cast<ASR::Integer_t>(ASRUtils::extract_type(x.m_type))->m_kind == 1;
        // For string→int8 with array result, skip load (assignment handles it)
        // For string→int8 with scalar result, do load (element-wise transfer case)
        bool skip_string_to_int8_load = is_string_to_int8 && ASRUtils::is_array(x.m_type);
        bool is_fixed_size_array_result = ASRUtils::is_array(x.m_type) &&
            ASRUtils::extract_physical_type(x.m_type) == ASR::array_physical_typeType::FixedSizeArray;
        if ( !ASRUtils::types_equal(ASRUtils::extract_type(ASRUtils::expr_type(x.m_source)), ASRUtils::extract_type(x.m_type),
             x.m_source, const_cast<ASR::expr_t*>(&x.base), false) && !ASRUtils::is_string_only(expr_type(x.m_mold)) &&
                !skip_string_to_int8_load ) {
            bool need_pointer_for_memcpy = false;
            if (is_fixed_size_array_result) {
                // Only return a pointer (for memcpy) when source and target
                // element sizes differ; otherwise load the scalar value.
                ASR::ttype_t* source_elem_asr = ASRUtils::extract_type(
                    ASRUtils::expr_type(x.m_source));
                llvm::Type* source_elem_llvm =
                    llvm_utils->get_type_from_ttype_t_util(
                        x.m_source, source_elem_asr, module.get());
                llvm::DataLayout data_layout(module->getDataLayout());
                need_pointer_for_memcpy =
                    data_layout.getTypeAllocSize(source_elem_llvm) !=
                    data_layout.getTypeAllocSize(target_base_type);
            }
            if (need_pointer_for_memcpy) {
                // Source and target element sizes differ: return a bitcasted
                // pointer so the assignment handler can memcpy from it.
                tmp = builder->CreateBitCast(source_ptr, target_llvm_type);
            } else {
                tmp = llvm_utils->CreateLoad2(target_base_type, builder->CreateBitCast(source_ptr, target_llvm_type));
            }
        }
    }

    llvm::Value* get_pointer_to_variable(ASR::expr_t* var) {
        if (llvm_symtab.find((uint64_t)var) != llvm_symtab.end()) {
            return llvm_symtab[(uint64_t)var];
        }
        this->visit_expr_wrapper(var, true);
        llvm::Value *val = tmp;
        llvm::AllocaInst *alloc = builder->CreateAlloca(val->getType(), nullptr);
        builder->CreateStore(val, alloc);
        llvm_symtab[(uint64_t)var] = alloc;
        return alloc;
    }

    void visit_Cast(const ASR::Cast_t &x) {
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return;
        }
        // Visit with appropriate load
        if (ASRUtils::is_string_only(expr_type(x.m_arg))) {
            this->visit_expr_load_wrapper(x.m_arg, 0);
        } else if(ASRUtils::is_pointer(expr_type(x.m_arg))){
            this->visit_expr_load_wrapper(x.m_arg, 2, true);
        } else {
            this->visit_expr_load_wrapper(x.m_arg, ptr_loads, true);
        }
        switch (x.m_kind) {
            case (ASR::cast_kindType::IntegerToReal) : {
                int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
                tmp = builder->CreateSIToFP(tmp, llvm_utils->getFPType(a_kind, false));
                break;
            }
            case (ASR::cast_kindType::UnsignedIntegerToReal) : {
                int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
                tmp = builder->CreateSIToFP(tmp, llvm_utils->getFPType(a_kind, false));
                break;
            }
            case (ASR::cast_kindType::LogicalToReal) : {
                int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
                tmp = builder->CreateUIToFP(tmp, llvm_utils->getFPType(a_kind, false));
                break;
            }
            case (ASR::cast_kindType::StringToString) : {
                break;
            }
            case (ASR::cast_kindType::RealToInteger) : {
                llvm::Type *target_type;
                int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
                target_type = llvm_utils->getIntType(a_kind);
                tmp = builder->CreateFPToSI(tmp, target_type);
                break;
            }
            case (ASR::cast_kindType::RealToUnsignedInteger) : {
                llvm::Type *target_type;
                int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
                target_type = llvm_utils->getIntType(a_kind);
                tmp = builder->CreateFPToSI(tmp, target_type);
                break;
            }
            case (ASR::cast_kindType::RealToComplex) : {
                llvm::Type *target_type;
                llvm::Value *zero;
                int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
                switch(a_kind)
                {
                    case 4:
                        target_type = complex_type_4;
                        tmp = builder->CreateFPTrunc(tmp, llvm::Type::getFloatTy(context));
                        zero = llvm::ConstantFP::get(context, llvm::APFloat((float)0.0));
                        break;
                    case 8:
                        target_type = complex_type_8;
                        tmp = builder->CreateFPExt(tmp, llvm::Type::getDoubleTy(context));
                        zero = llvm::ConstantFP::get(context, llvm::APFloat(0.0));
                        break;
                    default:
                        throw CodeGenError("Only 32 and 64 bits real kinds are supported.");
                }
                tmp = complex_from_floats(tmp, zero, target_type);
                break;
            }
            case (ASR::cast_kindType::IntegerToComplex) : {
                int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
                llvm::Type *target_type;
                llvm::Type *complex_type;
                llvm::Value *zero;
                switch(a_kind)
                {
                    case 4:
                        target_type = llvm::Type::getFloatTy(context);
                        complex_type = complex_type_4;
                        zero = llvm::ConstantFP::get(context, llvm::APFloat((float)0.0));
                        break;
                    case 8:
                        target_type = llvm::Type::getDoubleTy(context);
                        complex_type = complex_type_8;
                        zero = llvm::ConstantFP::get(context, llvm::APFloat(0.0));
                        break;
                    default:
                        throw CodeGenError("Only 32 and 64 bits real kinds are supported.");
                }
                tmp = builder->CreateSIToFP(tmp, target_type);
                tmp = complex_from_floats(tmp, zero, complex_type);
                break;
            }
            case (ASR::cast_kindType::IntegerToLogical) :
            case (ASR::cast_kindType::UnsignedIntegerToLogical) : {
                ASR::ttype_t* curr_type = extract_ttype_t_from_expr(x.m_arg);
                LCOMPILERS_ASSERT(curr_type != nullptr)
                int a_kind = ASRUtils::extract_kind_from_ttype_t(curr_type);
                switch (a_kind) {
                    case 1:
                        tmp = builder->CreateICmpNE(tmp, builder->getInt8(0));
                        break;
                    case 2:
                        tmp = builder->CreateICmpNE(tmp, builder->getInt16(0));
                        break;
                    case 4:
                        tmp = builder->CreateICmpNE(tmp, builder->getInt32(0));
                        break;
                    case 8:
                        tmp = builder->CreateICmpNE(tmp, builder->getInt64(0));
                        break;
                }
                break;
            }
            case (ASR::cast_kindType::RealToLogical) : {
                llvm::Value *zero;
                ASR::ttype_t* curr_type = extract_ttype_t_from_expr(x.m_arg);
                LCOMPILERS_ASSERT(curr_type != nullptr)
                int a_kind = ASRUtils::extract_kind_from_ttype_t(curr_type);
                if (a_kind == 4) {
                    zero = llvm::ConstantFP::get(context, llvm::APFloat((float)0.0));
                } else {
                    zero = llvm::ConstantFP::get(context, llvm::APFloat(0.0));
                }
                tmp = builder->CreateFCmpUNE(tmp, zero);
                break;
            }
            case (ASR::cast_kindType::StringToLogical) : {
                tmp = builder->CreateICmpNE(get_string_length(x.m_arg), builder->getInt32(0));
                break;
            }
            case (ASR::cast_kindType::StringToInteger) : {
                llvm::AllocaInst *parg = llvm_utils->CreateAlloca(*builder, character_type);
                builder->CreateStore(tmp, parg);
                tmp = lfortran_str_to_int(parg);
                break;
            }
            case (ASR::cast_kindType::ComplexToLogical) : {
                // !(c.real == 0.0 && c.imag == 0.0)
                llvm::Value *zero;
                ASR::ttype_t* curr_type = extract_ttype_t_from_expr(x.m_arg);
                LCOMPILERS_ASSERT(curr_type != nullptr)
                int a_kind = ASRUtils::extract_kind_from_ttype_t(curr_type);
                if (a_kind == 4) {
                    zero = llvm::ConstantFP::get(context, llvm::APFloat((float)0.0));
                } else {
                    zero = llvm::ConstantFP::get(context, llvm::APFloat(0.0));
                }
                llvm::Value *c_real = complex_re(tmp, tmp->getType());
                llvm::Value *real_check = builder->CreateFCmpUEQ(c_real, zero);
                llvm::Value *c_imag = complex_im(tmp, tmp->getType());
                llvm::Value *imag_check = builder->CreateFCmpUEQ(c_imag, zero);
                tmp = builder->CreateAnd(real_check, imag_check);
                tmp = builder->CreateNot(tmp);
                break;
            }
            case (ASR::cast_kindType::LogicalToInteger) : {
                int a_kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
                tmp = builder->CreateZExt(tmp, llvm_utils->getIntType(a_kind));
                break;
            }
            case (ASR::cast_kindType::LogicalToLogical) : {
                int arg_kind = -1, dest_kind = -1;
                extract_kinds(x, arg_kind, dest_kind);
                if (arg_kind > 0 && dest_kind > 0 && arg_kind != dest_kind) {
                    llvm::Type* dest_type = llvm_utils->getIntType(dest_kind);
                    unsigned src_bits = tmp->getType()->getIntegerBitWidth();
                    unsigned dest_bits = dest_type->getIntegerBitWidth();
                    if (src_bits < dest_bits) {
                        tmp = builder->CreateZExt(tmp, dest_type);
                    } else if (src_bits > dest_bits) {
                        tmp = builder->CreateTrunc(tmp, dest_type);
                    }
                }
                break;
            }
            case (ASR::cast_kindType::RealToReal) : {
                int arg_kind = -1, dest_kind = -1;
                extract_kinds(x, arg_kind, dest_kind);
                if( arg_kind > 0 && dest_kind > 0 &&
                    arg_kind != dest_kind )
                {
                    llvm::Type *dest_type = llvm_utils->getFPType(dest_kind, false);
                    if (dest_kind > arg_kind) {
                        tmp = builder->CreateFPExt(tmp, dest_type);
                    } else if (dest_kind < arg_kind) {
                        tmp = builder->CreateFPTrunc(tmp, dest_type);
                    } else {
                        std::string msg = "Conversion from " + std::to_string(arg_kind) +
                                          " to " + std::to_string(dest_kind) + " not implemented yet.";
                        throw CodeGenError(msg);
                    }
                }
                break;
            }
            case (ASR::cast_kindType::IntegerToInteger) : {
                int arg_kind = -1, dest_kind = -1;
                extract_kinds(x, arg_kind, dest_kind);
                if( arg_kind > 0 && dest_kind > 0 &&
                    arg_kind != dest_kind )
                {
                    if (dest_kind > arg_kind) {
                        tmp = builder->CreateSExt(tmp, llvm_utils->getIntType(dest_kind));
                    } else {
                        tmp = builder->CreateTrunc(tmp, llvm_utils->getIntType(dest_kind));
                    }
                }
                break;
            }
            case (ASR::cast_kindType::UnsignedIntegerToUnsignedInteger) : {
                int arg_kind = -1, dest_kind = -1;
                extract_kinds(x, arg_kind, dest_kind);
                if( arg_kind > 0 && dest_kind > 0 &&
                    arg_kind != dest_kind )
                {
                    if (dest_kind > arg_kind) {
                        tmp = builder->CreateZExt(tmp, llvm_utils->getIntType(dest_kind));
                    } else {
                        tmp = builder->CreateTrunc(tmp, llvm_utils->getIntType(dest_kind));
                    }
                }
                break;
            }
            case (ASR::cast_kindType::IntegerToUnsignedInteger) : {
                int arg_kind = -1, dest_kind = -1;
                extract_kinds(x, arg_kind, dest_kind);
                LCOMPILERS_ASSERT(arg_kind != -1 && dest_kind != -1)
                if( arg_kind > 0 && dest_kind > 0 &&
                    arg_kind != dest_kind )
                {
                    if (dest_kind > arg_kind) {
                        tmp = builder->CreateSExt(tmp, llvm_utils->getIntType(dest_kind));
                    } else {
                        tmp = builder->CreateTrunc(tmp, llvm_utils->getIntType(dest_kind));
                    }
                }
                break;
            }
            case (ASR::cast_kindType::UnsignedIntegerToInteger) : {
                int arg_kind = -1, dest_kind = -1;
                extract_kinds(x, arg_kind, dest_kind);
                LCOMPILERS_ASSERT(arg_kind != -1 && dest_kind != -1)
                if( arg_kind > 0 && dest_kind > 0 &&
                    arg_kind != dest_kind )
                {
                    if (dest_kind > arg_kind) {
                        tmp = builder->CreateZExt(tmp, llvm_utils->getIntType(dest_kind));
                    } else {
                        tmp = builder->CreateTrunc(tmp, llvm_utils->getIntType(dest_kind));
                    }
                }
                break;
            }
            case (ASR::cast_kindType::CPtrToUnsignedInteger) : {
                tmp = builder->CreatePtrToInt(tmp, llvm_utils->getIntType(8, false));
                break;
            }
            case (ASR::cast_kindType::UnsignedIntegerToCPtr) : {
                tmp = builder->CreateIntToPtr(tmp, llvm::Type::getVoidTy(context)->getPointerTo());
                break;
            }
            case (ASR::cast_kindType::ComplexToComplex) : {
                llvm::Type *target_type;
                int arg_kind = -1, dest_kind = -1;
                extract_kinds(x, arg_kind, dest_kind);
                llvm::Value *re, *im;
                if( arg_kind > 0 && dest_kind > 0 &&
                    arg_kind != dest_kind )
                {
                    if( arg_kind == 4 && dest_kind == 8 ) {
                        target_type = complex_type_8;
                        re = complex_re(tmp, complex_type_4);
                        re = builder->CreateFPExt(re, llvm::Type::getDoubleTy(context));
                        im = complex_im(tmp, complex_type_4);
                        im = builder->CreateFPExt(im, llvm::Type::getDoubleTy(context));
                    } else if( arg_kind == 8 && dest_kind == 4 ) {
                        target_type = complex_type_4;
                        re = complex_re(tmp, complex_type_8);
                        re = builder->CreateFPTrunc(re, llvm::Type::getFloatTy(context));
                        im = complex_im(tmp, complex_type_8);
                        im = builder->CreateFPTrunc(im, llvm::Type::getFloatTy(context));
                    } else {
                        std::string msg = "Conversion from " + std::to_string(arg_kind) +
                                          " to " + std::to_string(dest_kind) + " not implemented yet.";
                        throw CodeGenError(msg);
                    }
                } else {
                    throw CodeGenError("Negative kinds are not supported.");
                }
                tmp = complex_from_floats(re, im, target_type);
                break;
            }
            case (ASR::cast_kindType::ComplexToReal) : {
                int arg_kind = -1, dest_kind = -1;
                extract_kinds(x, arg_kind, dest_kind);
                llvm::Value *re;
                if( arg_kind > 0 && dest_kind > 0)
                {
                    if( arg_kind == 4 && dest_kind == 4 ) {
                        // complex(4) -> real(4)
                        re = complex_re(tmp, complex_type_4);
                        tmp = re;
                    } else if( arg_kind == 4 && dest_kind == 8 ) {
                        // complex(4) -> real(8)
                        re = complex_re(tmp, complex_type_4);
                        tmp = builder->CreateFPExt(re, llvm::Type::getDoubleTy(context));
                    } else if( arg_kind == 8 && dest_kind == 4 ) {
                        // complex(8) -> real(4)
                        re = complex_re(tmp, complex_type_8);
                        tmp = builder->CreateFPTrunc(re, llvm::Type::getFloatTy(context));
                    } else if( arg_kind == 8 && dest_kind == 8 ) {
                        // complex(8) -> real(8)
                        re = complex_re(tmp, complex_type_8);
                        tmp = re;
                    } else {
                        std::string msg = "Conversion from " + std::to_string(arg_kind) +
                                          " to " + std::to_string(dest_kind) + " not implemented yet.";
                        throw CodeGenError(msg);
                    }
                } else {
                    throw CodeGenError("Negative kinds are not supported.");
                }
                break;
            }
            case (ASR::cast_kindType::ComplexToInteger) : {
                int arg_kind = -1, dest_kind = -1;
                extract_kinds(x, arg_kind, dest_kind);
                llvm::Value *re;
                if (arg_kind > 0 && dest_kind > 0)
                {
                    if (arg_kind == 4) {
                        // complex(4) -> real(8)
                        re = complex_re(tmp, complex_type_4);
                        tmp = re;
                    } else if (arg_kind == 8) {
                        // complex(8) -> real(8)
                        re = complex_re(tmp, complex_type_8);
                        tmp = re;
                    } else {
                        std::string msg = "Unsupported Complex type kind: " + std::to_string(arg_kind);
                        throw CodeGenError(msg);
                    }
                    llvm::Type *target_type;
                    target_type = llvm_utils->getIntType(dest_kind);
                    tmp = builder->CreateFPToSI(tmp, target_type);
                } else {
                    throw CodeGenError("Negative kinds are not supported.");
                }
                break;
             }

            case (ASR::cast_kindType::RealToString) : {
                /* Call Runtime Function `lfortran_float_to_str` */
                llvm::Value* casted_float {}; // float -> string
                { 
                    llvm::Value *arg = tmp;
                    ASR::ttype_t* arg_type = extract_ttype_t_from_expr(x.m_arg);
                    LCOMPILERS_ASSERT(arg_type != nullptr)
                    int arg_kind = ASRUtils::extract_kind_from_ttype_t(arg_type);
                    if (arg->getType()->isPointerTy()) {arg = llvm_utils->CreateLoad2(llvm_utils->getFPType(arg_kind), arg);}

                    casted_float = lfortran_type_to_str(arg, llvm_utils->getFPType(arg_kind), "float", arg_kind); // Returns i8*
                }


                /* Create A String To Hold The Runtime Function Return */
                llvm::Value* str {};
                {
                    str = llvm_utils->create_string(ASRUtils::get_string_type(x.m_type), "FloatToStringCast");
                    setup_string(str, x.m_type);
                    /*
                        Now we have an already set string matching `x.m_type`
                        (allocatable-deferred-len OR Expression-len OR allocatable-nondeferred-len)
                    */ 
                }

                /* Copy Runtime Function Return Into Our Created String*/
                {
                    llvm::Value *lhs_data, *lhs_len;
                    llvm::Value *rhs_data, *rhs_len;
                    std::tie(lhs_data, lhs_len) = llvm_utils->get_string_length_data(ASRUtils::get_string_type(x.m_type), str, true, true);
                    rhs_data = casted_float;
                    llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(x.m_arg, ASRUtils::expr_type(x.m_arg), module.get());
                    rhs_len = lfortran_str_len(type, casted_float);
                    llvm_utils->lfortran_str_copy_with_data(lhs_data, lhs_len, rhs_data, rhs_len, true, true);
                }
                
                /* Free Runtime Function Return */
                {
                    llvm::Value* allocator = llvm_utils->get_allocator(module.get());
                    builder->CreateCall(llvm_utils->_Deallocate(), {allocator, casted_float});
                    casted_float = nullptr;
                }

                tmp = str;
                strings_to_be_deallocated.push_back(al, tmp);
                break;
            }
            case (ASR::cast_kindType::IntegerToString) : {
                /* Call Runtime Function `lfortran_int_to_str` */
                llvm::Value* casted_int {};
                { 
                    llvm::Value *arg = tmp;
                    ASR::ttype_t* arg_type = extract_ttype_t_from_expr(x.m_arg);
                    LCOMPILERS_ASSERT(arg_type != nullptr)
                    int arg_kind = ASRUtils::extract_kind_from_ttype_t(arg_type);
                    if (arg->getType()->isPointerTy()) {arg = llvm_utils->CreateLoad2(llvm_utils->getIntType(arg_kind), arg);}

                    casted_int = lfortran_type_to_str(arg, llvm_utils->getIntType(arg_kind), "int", arg_kind); // Returns i8*
                }


                /* Create A String To Hold The Runtime Function Return */
                llvm::Value* str {};
                {
                    str = llvm_utils->create_string(ASRUtils::get_string_type(x.m_type), "IntegerToStringCast");
                    setup_string(str, x.m_type);
                    /*
                        Now we have an already set string matching `x.m_type`
                        (allocatable-deferred-len OR Expression-len OR allocatable-nondeferred-len)
                    */ 
                }

                /* Copy Runtime Function Return Into Our Created String*/
                {
                    llvm::Value *lhs_data, *lhs_len;
                    llvm::Value *rhs_data, *rhs_len;
                    std::tie(lhs_data, lhs_len) = llvm_utils->get_string_length_data(ASRUtils::get_string_type(x.m_type), str, true, true);
                    rhs_data = casted_int;
                    llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(x.m_arg, ASRUtils::expr_type(x.m_arg), module.get());
                    rhs_len = lfortran_str_len(type, casted_int);
                    llvm_utils->lfortran_str_copy_with_data(lhs_data, lhs_len, rhs_data, rhs_len, true, true);
                }
                
                /* Free Runtime Function Return */
                {
                    llvm::Value* allocator = llvm_utils->get_allocator(module.get());
                    builder->CreateCall(llvm_utils->_Deallocate(), {allocator, casted_int});
                    casted_int = nullptr;
                }

                tmp = str;
                strings_to_be_deallocated.push_back(al, tmp);
                break;
            }
            case (ASR::cast_kindType::LogicalToString) : {
                llvm::Value *cmp = builder->CreateICmpEQ(tmp,
                    llvm::ConstantInt::get(tmp->getType(), 0));
                llvm::Value *zero_str = LCompilers::create_global_string_ptr(context, *module, *builder, "False");
                llvm::Value *one_str = LCompilers::create_global_string_ptr(context, *module, *builder, "True");
                tmp = builder->CreateSelect(cmp, zero_str, one_str);

                if (ASRUtils::is_allocatable(x.m_type)) {
                    llvm::Value* temp_str = builder->CreateAlloca(string_descriptor);
                    llvm_utils->set_string_memory_on_heap(
                        ASR::string_physical_typeType::DescriptorString,
                        temp_str, lfortran_str_len(string_descriptor, tmp),
                        ASRUtils::get_string_type(x.m_type)->m_kind
                    );

                    llvm::Value *lhs_data, *lhs_len;
                    llvm::Value *rhs_data, *rhs_len;

                    std::tie(lhs_data, lhs_len) = llvm_utils->get_string_length_data(
                                                    ASR::down_cast<ASR::String_t>(ASRUtils::TYPE(ASR::make_String_t(
                                                        al, x.base.base.loc, 1, nullptr,
                                                        ASR::string_length_kindType::DeferredLength,
                                                        ASR::string_physical_typeType::DescriptorString))),
                                                    temp_str, true, true);
                    rhs_data = tmp;
                    rhs_len = lfortran_str_len(string_descriptor, tmp);

                    llvm_utils->lfortran_str_copy_with_data(lhs_data, lhs_len, rhs_data, rhs_len, true, true);
                    tmp = temp_str;
                }
                break;
            }
            case (ASR::cast_kindType::ListToArray) : {
                if( !ASR::is_a<ASR::List_t>(*ASRUtils::expr_type(x.m_arg)) ) {
                    throw CodeGenError("The argument of ListToArray cast should "
                        "be a list/std::vector, found, " + ASRUtils::type_to_str_fortran_expr(
                            ASRUtils::expr_type(x.m_arg), x.m_arg));
                }
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr(*x.m_arg);
                ptr_loads = ptr_loads_copy;
                llvm::Type* list_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_arg, ASRUtils::expr_type(x.m_arg), module.get());
                tmp = llvm_utils->CreateLoad2(list_llvm_type, list_api->get_pointer_to_list_data_using_type(list_llvm_type, tmp));
                break;
            }
            case (ASR::cast_kindType::PointerToInteger): {
                llvm::Value *ptr = nullptr;
                if (ASR::is_a<ASR::Var_t>(*x.m_arg)) {
                    ptr = get_pointer_to_variable(x.m_arg);
                } else {
                    this->visit_expr(*x.m_arg);
                    llvm::Value *val = tmp;
                    llvm::AllocaInst *alloc = builder->CreateAlloca(val->getType(), nullptr);
                    builder->CreateStore(val, alloc);
                    ptr = alloc;
                }
                tmp = builder->CreatePtrToInt(ptr, llvm::Type::getInt64Ty(context));
                break;
            }
            case (ASR::cast_kindType::ClassToStruct): {
                ASR::symbol_t* struct_sym = nullptr;
                if (x.m_dest) {
                    struct_sym = ASRUtils::symbol_get_past_external(
                        ASRUtils::get_struct_sym_from_struct_expr(x.m_dest));
                }
                this->visit_expr_load_wrapper(x.m_arg, 0);
                ASR::expr_t* dest_arg = x.m_dest ? x.m_dest : const_cast<ASR::expr_t*>(&x.base);
                tmp = convert_class_to_type(x.m_arg, dest_arg, x.m_type, tmp);
                if (struct_sym) {
                    current_der_type_name = get_type_key(struct_sym);
                }
                break;
            }
            case (ASR::cast_kindType::ClassToClass): {
                ASR::symbol_t* struct_sym = nullptr;
                if (x.m_dest) {
                    struct_sym = ASRUtils::symbol_get_past_external(
                        ASRUtils::get_struct_sym_from_struct_expr(x.m_dest));
                }
                this->visit_expr_load_wrapper(x.m_arg, 0);
                tmp = convert_class_to_class(x.m_arg, tmp, x.m_type, struct_sym);
                if (struct_sym) {
                    current_der_type_name = get_type_key(struct_sym);
                }
                break;
            }
            case (ASR::cast_kindType::ClassToIntrinsic): {
                // Cast from class(*) to an intrinsic type (integer, real, complex, etc.)
                ASR::ttype_t* arg_type = ASRUtils::expr_type(x.m_arg);
                ASR::ttype_t* target_type = x.m_type;

                if (ASRUtils::is_array(arg_type) &&
                        ASRUtils::extract_physical_type(arg_type) == ASR::DescriptorArray) {
                    // Array case: delegate to convert_class_to_type
                    this->visit_expr_load_wrapper(x.m_arg, 0);
                    ASR::expr_t* dest_arg = const_cast<ASR::expr_t*>(&x.base);
                    tmp = convert_class_to_type(x.m_arg, dest_arg, target_type, tmp);
                } else {
                    // Scalar case: extract intrinsic value from polymorphic wrapper
                    this->visit_expr_load_wrapper(x.m_arg, 0);
                    llvm::Value* poly_ptr = tmp;
                    llvm::Type* poly_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_arg, ASRUtils::extract_type(arg_type), module.get());
                    llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                        const_cast<ASR::expr_t*>(&x.base),
                        ASRUtils::extract_type(target_type), module.get());
                    // Handle pointer/allocatable types (extra indirection level)
                    if (LLVM::is_llvm_pointer(*arg_type)) {
                        poly_ptr = llvm_utils->CreateLoad2(poly_llvm_type->getPointerTo(), poly_ptr);
                    }
                    // GEP to data pointer field (index 1 in polymorphic struct)
                    poly_ptr = llvm_utils->create_gep2(poly_llvm_type, poly_ptr, 1);
                    // Load the i8* data pointer
                    poly_ptr = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, poly_ptr);
                    // BitCast to target type pointer
                    poly_ptr = builder->CreateBitCast(poly_ptr, target_llvm_type->getPointerTo());
                    // Load the value if ptr_loads > 0 (caller wants value, not pointer)
                    // For ptr_loads == 0 (e.g., character passed to function), return pointer
                    if (ptr_loads > 0) {
                        tmp = llvm_utils->CreateLoad2(target_llvm_type, poly_ptr);
                    } else {
                        tmp = poly_ptr;
                    }
                }
                break;
            }
            case ASR::StringToArray :
                cast_string_to_array(&x);
            break;
            default : throw CodeGenError("Cast kind not implemented");
        }
    }

    void cast_string_to_array(const ASR::Cast_t* const cast){
        LCOMPILERS_ASSERT(cast->m_kind == ASR::StringToArray)
        LCOMPILERS_ASSERT(ASRUtils::is_array_of_strings(cast->m_type))

        visit_expr_load_wrapper(cast->m_arg, 0);
        llvm::Value* const string_llvm = tmp;
        tmp = nullptr; 

        ASR::Array_t* const array_ty = ASR::down_cast<ASR::Array_t>(ASRUtils::type_get_past_allocatable_pointer(cast->m_type));
        llvm::Type* const array_llvm_ty = llvm_utils->get_type_from_ttype_t_util(nullptr, cast->m_type, module.get());

        switch(array_ty->m_physical_type){
            case ASR::StringArraySinglePointer:
                tmp = llvm_utils->get_string_data(ASRUtils::get_string_type(cast->m_arg), string_llvm);
            break;
            case ASR::UnboundedPointerArray:
            case ASR::PointerArray:
                // Note: Under new_classes, unlimited polymorphic variables inside
                // select type blocks are either Cast-wrapped (ClassToIntrinsic) or
                // type-rewritten, so is_unlimited_polymorphic_type is always false.
                // The else branch is kept for the !new_classes legacy path.
                if (!ASRUtils::is_unlimited_polymorphic_type(cast->m_arg)) {
                    LCOMPILERS_ASSERT(ASRUtils::is_character_phsyical_types_matched(
                              ASRUtils::extract_type(expr_type(cast->m_arg))
                            , ASRUtils::extract_type(cast->m_type)))
                    tmp = string_llvm;
                } else {
                    tmp = string_llvm;
                }
            break;
            case ASR::DescriptorArray:{
                LCOMPILERS_ASSERT_MSG(array_ty->n_dims, "Array in StringToArray is expected to have dimensions")
                llvm::Value* const array_llvm = arr_descr->create_descriptor_alloca(
                    array_llvm_ty);
                fill_array_details(array_llvm_ty, array_llvm 
                    , llvm_utils->get_StringType(array_ty->m_type)
                    , array_ty->m_dims, array_ty->n_dims,
                    false, false);
                builder->CreateStore(string_llvm, arr_descr->get_pointer_to_data(array_llvm_ty, array_llvm));
                tmp = array_llvm;
            }
            break;
            case ASR::FixedSizeArray:
                throw LCompilersException("StringToArray Cast : Array of strings can't be of physicalType 'FixedSizeArray`");
            default:
                throw CodeGenError("StringToArray Cast : Casting to array physical type not implemented. Enum : "
                                    + std::to_string(array_ty->m_physical_type));
        }
    }

    std::string get_namelist_var_name(ASR::symbol_t *var_sym, ASR::Variable_t *var) {
        std::string var_name = std::string(var->m_name);
        if (ASR::is_a<ASR::ExternalSymbol_t>(*var_sym)) {
            ASR::ExternalSymbol_t* ext = ASR::down_cast<ASR::ExternalSymbol_t>(var_sym);
            if (ext->m_original_name) {
                var_name = ext->m_original_name;
            }
        }
        return LCompilers::to_lower(var_name);
    }

    // Helper to build namelist descriptor and call runtime function
    llvm::Value* build_namelist_descriptor(ASR::symbol_t* nml_sym) {
        // Unwrap external symbol if needed
        nml_sym = ASRUtils::symbol_get_past_external(nml_sym);
        ASR::Namelist_t* nml = ASR::down_cast<ASR::Namelist_t>(nml_sym);

        // Get group name (lowercase)
        std::string group_name = std::string(nml->m_group_name);
        std::transform(group_name.begin(), group_name.end(), group_name.begin(), ::tolower);

        // Create global constant for group name
        llvm::Value* group_name_ptr = LCompilers::create_global_string_ptr(context, *module, *builder, group_name);

        // Build array of lfortran_nml_item_t structures
        std::vector<llvm::Value*> nml_items;

        // Define item struct type
        llvm::StructType* item_type = llvm::StructType::get(
            character_type,                               // name
            llvm::Type::getInt32Ty(context),             // type
            llvm::Type::getInt32Ty(context),             // rank
            llvm::Type::getInt64Ty(context),             // elem_len
            llvm::Type::getInt64Ty(context),             // stride
            llvm::Type::getInt8Ty(context)->getPointerTo(), // data
            llvm::Type::getInt64Ty(context)->getPointerTo()  // shape
        );

        std::function<void(const std::string&, ASR::ttype_t*, llvm::Value*, ASR::symbol_t*)> add_namelist_item;
        std::function<void(const std::string&, ASR::Struct_t*, llvm::Value*, int32_t,
                           llvm::Value*, llvm::Value*)> add_struct_members;

        add_struct_members = [&](const std::string &prefix, ASR::Struct_t* struct_sym,
                                 llvm::Value* struct_ptr, int32_t rank,
                                 llvm::Value* shape_ptr, llvm::Value* stride_val) {
            ASR::Struct_t* current_struct = struct_sym;
            llvm::Value* current_ptr = struct_ptr;
            while (current_struct) {
                std::string struct_name = get_type_key(current_struct);
                llvm::StructType* llvm_struct_type = name2dertype[struct_name];
                if (current_ptr->getType() != llvm_struct_type->getPointerTo()) {
                    current_ptr = builder->CreateBitCast(current_ptr, llvm_struct_type->getPointerTo());
                }

                for (auto item: current_struct->m_symtab->get_scope()) {
                    if (name2memidx[struct_name].find(item.first) == name2memidx[struct_name].end()) {
                        continue;
                    }
                    ASR::symbol_t* sym = ASRUtils::symbol_get_past_external(item.second);
                    if (ASR::is_a<ASR::StructMethodDeclaration_t>(*sym) ||
                        ASR::is_a<ASR::GenericProcedure_t>(*sym) ||
                        ASR::is_a<ASR::Union_t>(*sym) ||
                        ASR::is_a<ASR::Struct_t>(*sym) ||
                        ASR::is_a<ASR::CustomOperator_t>(*sym)) {
                        continue;
                    }
                    if (!ASR::is_a<ASR::Variable_t>(*sym)) {
                        continue;
                    }
                    ASR::Variable_t* member_var = ASR::down_cast<ASR::Variable_t>(sym);
                    std::string member_name = LCompilers::to_lower(member_var->m_name);
                    int member_idx = name2memidx[struct_name][member_var->m_name];
                    llvm::Value* member_ptr = llvm_utils->create_gep2(llvm_struct_type, current_ptr, member_idx);
                    ASR::ttype_t *member_type = member_var->m_type;
                    if (rank > 0) {
                        member_type = ASRUtils::duplicate_type_with_empty_dims(al, member_type);
                    }
                    add_namelist_item(prefix + "%" + member_name, member_type, member_ptr,
                                      member_var->m_type_declaration);
                    if (rank > 0) {
                        llvm::Value *item = nml_items.back();
                        builder->CreateStore(llvm::ConstantInt::get(
                            llvm::Type::getInt32Ty(context), rank),
                            builder->CreateStructGEP(item_type, item, 2));
                        builder->CreateStore(stride_val,
                            builder->CreateStructGEP(item_type, item, 4));
                        builder->CreateStore(shape_ptr,
                            builder->CreateStructGEP(item_type, item, 6));
                    }
                }

                if (!current_struct->m_parent) {
                    break;
                }
                current_ptr = llvm_utils->create_gep2(llvm_struct_type, current_ptr, 0);
                current_struct = ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(current_struct->m_parent));
            }
        };

        add_namelist_item = [&](const std::string &item_name, ASR::ttype_t* item_type_asr,
                                llvm::Value* data_ptr, ASR::symbol_t* type_decl_sym) {
            // For allocatable/pointer arrays, the symtab value stores a pointer
            // to the array descriptor. Load through it to get the actual descriptor.
            if ((ASR::is_a<ASR::Allocatable_t>(*item_type_asr) ||
                 ASR::is_a<ASR::Pointer_t>(*item_type_asr)) &&
                ASRUtils::is_array(item_type_asr)) {
                ASR::ttype_t* desc_type_asr = ASRUtils::type_get_past_allocatable_pointer(item_type_asr);
                llvm::Type* desc_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    nullptr, desc_type_asr, module.get());
                data_ptr = llvm_utils->CreateLoad2(
                    desc_llvm_type->getPointerTo(), data_ptr);
            }

            // Determine type code
            ASR::ttype_t* var_type = ASRUtils::type_get_past_allocatable_pointer(item_type_asr);
            // For arrays, get the element type
            ASR::ttype_t* elem_type = ASRUtils::type_get_past_array(var_type);

            if (ASR::is_a<ASR::StructType_t>(*elem_type)) {
                if (!type_decl_sym) {
                    throw CodeGenError("Namelist derived type is missing its declaration");
                }
                ASR::Struct_t* struct_sym = ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(type_decl_sym));
                int32_t rank = 0;
                llvm::Value* shape_ptr = llvm::ConstantPointerNull::get(
                    llvm::Type::getInt64Ty(context)->getPointerTo());
                llvm::Value* struct_ptr = data_ptr;
                llvm::Type* struct_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    nullptr, elem_type, module.get());
                llvm::Value* stride_val = llvm::ConstantInt::get(
                    llvm::Type::getInt64Ty(context), module->getDataLayout().getTypeAllocSize(struct_llvm_type));

                if (ASRUtils::is_array(item_type_asr)) {
                    ASR::dimension_t* dims = nullptr;
                    size_t n_dims = ASRUtils::extract_dimensions_from_ttype(item_type_asr, dims);
                    rank = (int32_t)n_dims;

                    std::vector<llvm::Constant*> shape_vals;
                    for (size_t d = 0; d < n_dims; d++) {
                        if (!dims[d].m_length) {
                            throw CodeGenError("Namelist array must have explicit dimensions");
                        }
                        this->visit_expr(*dims[d].m_length);
                        llvm::Value* dim_len = tmp;
                        if (!llvm::isa<llvm::ConstantInt>(dim_len)) {
                            throw CodeGenError("Namelist with non-constant array dimensions not yet supported");
                        }
                        int64_t val = llvm::cast<llvm::ConstantInt>(dim_len)->getSExtValue();
                        shape_vals.push_back(llvm::ConstantInt::get(context, llvm::APInt(64, val, true)));
                    }

                    llvm::ArrayType* shape_arr_type = llvm::ArrayType::get(
                        llvm::Type::getInt64Ty(context), n_dims);
                    llvm::Constant* shape_arr = llvm::ConstantArray::get(shape_arr_type, shape_vals);
                    std::string shape_name = "nml_shape_" + item_name;
                    std::replace(shape_name.begin(), shape_name.end(), '%', '_');
                    llvm::GlobalVariable* shape_global = new llvm::GlobalVariable(
                        *module, shape_arr_type, true, llvm::GlobalValue::PrivateLinkage,
                        shape_arr, shape_name);
                    shape_ptr = builder->CreateBitCast(
                        shape_global, llvm::Type::getInt64Ty(context)->getPointerTo());

                    ASR::ttype_t* past_alloc = ASRUtils::type_get_past_allocatable_pointer(item_type_asr);
                    ASR::Array_t* arr_t = ASR::down_cast<ASR::Array_t>(past_alloc);
                    if (arr_t->m_physical_type != ASR::array_physical_typeType::FixedSizeArray) {
                        throw CodeGenError("Namelist arrays of derived types with non-fixed storage are not supported yet");
                    }
                    llvm::Type* arr_type = llvm_utils->get_type_from_ttype_t_util(
                        nullptr, past_alloc, module.get());
                    if (data_ptr->getType() != arr_type->getPointerTo()) {
                        data_ptr = builder->CreateBitCast(data_ptr, arr_type->getPointerTo());
                    }
                    struct_ptr = llvm_utils->create_gep2(arr_type, data_ptr, 0);
                }
                add_struct_members(LCompilers::to_lower(item_name), struct_sym, struct_ptr,
                                   rank, shape_ptr, stride_val);
                return;
            }

            int32_t type_code = -1;
            llvm::Value* elem_len_val = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
            llvm::Value* stride_val = nullptr;
            llvm::Value* str_desc_ptr = nullptr;

            if (ASR::is_a<ASR::Integer_t>(*elem_type)) {
                int kind = ASRUtils::extract_kind_from_ttype_t(elem_type);
                if (kind == 1) type_code = 0; // LFORTRAN_NML_INT1
                else if (kind == 2) type_code = 1; // LFORTRAN_NML_INT2
                else if (kind == 4) type_code = 2; // LFORTRAN_NML_INT4
                else if (kind == 8) type_code = 3; // LFORTRAN_NML_INT8
            } else if (ASR::is_a<ASR::Real_t>(*elem_type)) {
                int kind = ASRUtils::extract_kind_from_ttype_t(elem_type);
                if (kind == 4) type_code = 4; // LFORTRAN_NML_REAL4
                else if (kind == 8) type_code = 5; // LFORTRAN_NML_REAL8
            } else if (ASR::is_a<ASR::Logical_t>(*elem_type)) {
                // For logicals, we'll determine the type code from the LLVM type
                type_code = -2; // Marker to set it later
            } else if (ASR::is_a<ASR::Complex_t>(*elem_type)) {
                int kind = ASRUtils::extract_kind_from_ttype_t(elem_type);
                if (kind == 4) type_code = 10; // LFORTRAN_NML_COMPLEX4
                else if (kind == 8) type_code = 11; // LFORTRAN_NML_COMPLEX8
            } else if (ASR::is_a<ASR::String_t>(*elem_type)) {
                type_code = 12; // LFORTRAN_NML_CHAR
                ASR::String_t* str_type = ASR::down_cast<ASR::String_t>(elem_type);
                int len_val = 0;
                if (ASRUtils::extract_value(str_type->m_len, len_val)) {
                    elem_len_val = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), len_val);
                } else {
                    // Save the string descriptor pointer to extract length later
                    str_desc_ptr = data_ptr;
                }
            }

            // Get rank and shape
            int32_t rank = 0;
            llvm::Value* shape_ptr = llvm::ConstantPointerNull::get(llvm::Type::getInt64Ty(context)->getPointerTo());

            if (ASRUtils::is_array(item_type_asr)) {
                ASR::dimension_t* dims = nullptr;
                size_t n_dims = ASRUtils::extract_dimensions_from_ttype(item_type_asr, dims);
                rank = (int32_t)n_dims;

                if (rank > 0) {
                    // Check if this is a descriptor array (allocatable/pointer)
                    if (ASRUtils::extract_physical_type(item_type_asr) != ASR::array_physical_typeType::DescriptorArray) {
                        // Build compile-time constant shape array for fixed-size arrays
                        std::vector<llvm::Constant*> shape_vals;
                        for (size_t d = 0; d < n_dims; d++) {
                            if (dims[d].m_length) {
                                this->visit_expr(*dims[d].m_length);
                                llvm::Value* dim_len = tmp;
                                if (llvm::isa<llvm::Constant>(dim_len)) {
                                    llvm::Constant* dim_len_const = llvm::cast<llvm::Constant>(dim_len);
                                    // For integer constants, extract the value and create a new i64 constant
                                    // This avoids ConstantExpr issues in LLVM > 15
                                    if (llvm::isa<llvm::ConstantInt>(dim_len_const)) {
                                        llvm::ConstantInt* ci = llvm::cast<llvm::ConstantInt>(dim_len_const);
                                        int64_t val = ci->getSExtValue();
                                        dim_len_const = llvm::ConstantInt::get(context, llvm::APInt(64, val, true));
                                    } else if (dim_len_const->getType() != llvm::Type::getInt64Ty(context)) {
                                        if (!llvm::isa<llvm::IntegerType>(dim_len_const->getType())) {
                                            throw CodeGenError("Namelist array dimension must be an integer constant");
                                        }
                                        llvm::IntegerType* src_ty = llvm::cast<llvm::IntegerType>(dim_len_const->getType());
                                        unsigned src_bits = src_ty->getBitWidth();
                                        if (src_bits < 64) {
                                            dim_len_const = llvm::ConstantExpr::getCast(
                                                llvm::Instruction::SExt, dim_len_const,
                                                llvm::Type::getInt64Ty(context));
                                        } else if (src_bits > 64) {
                                            dim_len_const = llvm::ConstantExpr::getCast(
                                                llvm::Instruction::Trunc, dim_len_const,
                                                llvm::Type::getInt64Ty(context));
                                        }
                                    }
                                    shape_vals.push_back(dim_len_const);
                                } else {
                                    // Non-constant dimension - not supported in initial version
                                    throw CodeGenError("Namelist with non-constant array dimensions not yet supported");
                                }
                            } else {
                                throw CodeGenError("Namelist array must have explicit dimensions");
                            }
                        }

                        llvm::ArrayType* shape_arr_type = llvm::ArrayType::get(llvm::Type::getInt64Ty(context), n_dims);
                        llvm::Constant* shape_arr = llvm::ConstantArray::get(shape_arr_type, shape_vals);
                        std::string shape_name = "nml_shape_" + item_name;
                        std::replace(shape_name.begin(), shape_name.end(), '%', '_');
                        llvm::GlobalVariable* shape_global = new llvm::GlobalVariable(
                            *module, shape_arr_type, true, llvm::GlobalValue::PrivateLinkage,
                            shape_arr, shape_name);
                        shape_ptr = builder->CreateBitCast(shape_global, llvm::Type::getInt64Ty(context)->getPointerTo());
                    }
                    // For descriptor arrays, shape_ptr will be filled later after getting data_ptr
                }
            }

            if (type_code == -2) {
                llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(nullptr, elem_type, module.get());
                if (llvm::isa<llvm::IntegerType>(llvm_type)) {
                    unsigned bit_width = llvm::cast<llvm::IntegerType>(llvm_type)->getBitWidth();
                    unsigned byte_size = (bit_width + 7) / 8; // Round up to nearest byte
                    if (byte_size == 1) type_code = 6; // LFORTRAN_NML_LOGICAL1
                    else if (byte_size == 2) type_code = 7; // LFORTRAN_NML_LOGICAL2
                    else if (byte_size == 4) type_code = 8; // LFORTRAN_NML_LOGICAL4
                    else if (byte_size == 8) type_code = 9; // LFORTRAN_NML_LOGICAL8
                } else {
                    throw CodeGenError("Unsupported logical LLVM type for namelist");
                }
            }

            // For strings, we need to extract the data pointer from the descriptor
            if (ASR::is_a<ASR::String_t>(*var_type)) {
                ASR::String_t* str_type = ASR::down_cast<ASR::String_t>(var_type);
                llvm::Type* string_desc_type = llvm_utils->get_type_from_ttype_t_util(nullptr, var_type, module.get());
                // Extract the length from the string descriptor for deferred-length strings
                if (str_desc_ptr != nullptr) {
                    elem_len_val = llvm_utils->get_string_length(str_type, str_desc_ptr);
                }
                llvm::Value* str_data_ptr_ptr = llvm_utils->create_gep2(string_desc_type, data_ptr, 0);
                data_ptr = llvm_utils->CreateLoad2(character_type, str_data_ptr_ptr);
            }

            // For arrays, get the data pointer and extract dimensions for descriptor arrays
            if (ASRUtils::is_array(item_type_asr)) {
                ASR::ttype_t* past_alloc = ASRUtils::type_get_past_allocatable_pointer(item_type_asr);
                if (ASR::is_a<ASR::Array_t>(*past_alloc)) {
                    ASR::Array_t* arr_t = ASR::down_cast<ASR::Array_t>(past_alloc);
                    if (arr_t->m_physical_type == ASR::array_physical_typeType::DescriptorArray) {
                        // Get array type for descriptor operations
                        llvm::Type* arr_type = llvm_utils->get_type_from_ttype_t_util(nullptr, past_alloc, module.get());
                        // Get element type for loading data pointer
                        llvm::Type* llvm_elem_type = llvm_utils->get_type_from_ttype_t_util(nullptr, elem_type, module.get());
                        llvm::Value* arr_desc_loaded = llvm_utils->get_array_descriptor_ptr(
                            data_ptr, arr_type, ASRUtils::is_character(*item_type_asr));

                        // For descriptor arrays, extract dimensions at runtime if needed
                        if (rank > 0) {
                            ASR::dimension_t* dims = nullptr;
                            size_t n_dims = ASRUtils::extract_dimensions_from_ttype(item_type_asr, dims);
                            // Allocate array to store runtime dimensions
                            llvm::ArrayType* shape_arr_type = llvm::ArrayType::get(llvm::Type::getInt64Ty(context), n_dims);
                            llvm::Value* shape_arr = llvm_utils->CreateAlloca(*builder, shape_arr_type);

                            for (size_t d = 0; d < n_dims; d++) {
                                llvm::Value* dim_idx = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), d + 1); // 1-based
                                llvm::Value* dim_size = arr_descr->get_array_size(arr_type, arr_desc_loaded, dim_idx, 8, 4);
                                // Store in shape array
                                llvm::Value* shape_elem_ptr = builder->CreateConstGEP2_32(shape_arr_type, shape_arr, 0, d);
                                builder->CreateStore(dim_size, shape_elem_ptr);
                            }
                            // Get pointer to first element
                            shape_ptr = builder->CreateConstGEP2_32(shape_arr_type, shape_arr, 0, 0);
                        }

                        // Get data pointer from array descriptor
                        // Get pointer to data field, then load the data pointer
                        llvm::Value* data_ptr_field = arr_descr->get_pointer_to_data(arr_type, arr_desc_loaded);
                        if (ASR::is_a<ASR::String_t>(*elem_type)) {
                            ASR::String_t* str_type = ASR::down_cast<ASR::String_t>(elem_type);
                            llvm::Value* str_desc = llvm_utils->CreateLoad2(
                                llvm_utils->get_StringType(elem_type)->getPointerTo(), data_ptr_field);
                            data_ptr = llvm_utils->get_string_data(str_type, str_desc);
                        } else {
                            data_ptr = llvm_utils->CreateLoad2(llvm_elem_type->getPointerTo(), data_ptr_field);
                        }
                    } else if (arr_t->m_physical_type == ASR::array_physical_typeType::FixedSizeArray) {
                        // For FixedSizeArray, data_ptr points to [N x Type]*, we need Type*
                        // Use GEP with indices [0, 0] to get pointer to first element
                        llvm::Type* arr_type = llvm_utils->get_type_from_ttype_t_util(nullptr, past_alloc, module.get());
                        data_ptr = builder->CreateConstGEP2_32(arr_type, data_ptr, 0, 0);
                    } else if (arr_t->m_physical_type == ASR::array_physical_typeType::PointerArray) {
                        if (ASRUtils::is_array_of_strings(item_type_asr)) {
                            data_ptr = llvm_utils->get_stringArray_data(item_type_asr, data_ptr);
                        }
                    }
                }
            }

            data_ptr = builder->CreateBitCast(data_ptr, llvm::Type::getInt8Ty(context)->getPointerTo());
            if (!stride_val) {
                if (ASR::is_a<ASR::String_t>(*elem_type)) {
                    stride_val = elem_len_val;
                } else {
                    llvm::DataLayout data_layout(module->getDataLayout());
                    llvm::Type* llvm_elem_type = llvm_utils->get_type_from_ttype_t_util(
                        nullptr, elem_type, module.get());
                    stride_val = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context),
                        data_layout.getTypeAllocSize(llvm_elem_type));
                }
            }

            // Create lfortran_nml_item_t struct
            llvm::Value* item = llvm_utils->CreateAlloca(*builder, item_type);
            llvm::Value* item_name_ptr = LCompilers::create_global_string_ptr(
                context, *module, *builder, LCompilers::to_lower(item_name));
            builder->CreateStore(item_name_ptr, builder->CreateStructGEP(item_type, item, 0));
            builder->CreateStore(llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), type_code),
                                 builder->CreateStructGEP(item_type, item, 1));
            builder->CreateStore(llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), rank),
                                 builder->CreateStructGEP(item_type, item, 2));
            builder->CreateStore(elem_len_val,
                                 builder->CreateStructGEP(item_type, item, 3));
            builder->CreateStore(stride_val,
                                 builder->CreateStructGEP(item_type, item, 4));
            builder->CreateStore(data_ptr, builder->CreateStructGEP(item_type, item, 5));
            builder->CreateStore(shape_ptr, builder->CreateStructGEP(item_type, item, 6));

            nml_items.push_back(item);
        };

        for (size_t i = 0; i < nml->n_var_list; i++) {
            ASR::symbol_t* var_sym = nml->m_var_list[i];
            ASR::symbol_t* var_sym_past = ASRUtils::symbol_get_past_external(var_sym);
            ASR::Variable_t* var = ASR::down_cast<ASR::Variable_t>(var_sym_past);

            // Get variable name (lowercase)
            std::string var_name = get_namelist_var_name(var_sym, var);

            uint32_t var_hash = get_hash((ASR::asr_t*)var);
            llvm::Value* data_ptr = llvm_symtab[var_hash];
            if (!data_ptr) {
                throw CodeGenError("Variable " + var_name + " not found in symbol table");
            }

            add_namelist_item(var_name, var->m_type, data_ptr, var->m_type_declaration);
        }

        // Create items array
        llvm::ArrayType* items_array_type = llvm::ArrayType::get(item_type, nml_items.size());
        llvm::Value* items_array = llvm_utils->CreateAlloca(*builder, items_array_type);

        // Copy items into array
        for (size_t i = 0; i < nml_items.size(); i++) {
            llvm::Value* dest_ptr = builder->CreateConstGEP2_32(items_array_type, items_array, 0, i);
            llvm::Value* src_item = builder->CreateLoad(item_type, nml_items[i]);
            builder->CreateStore(src_item, dest_ptr);
        }

        // Get pointer to first element of items array
        llvm::Value* items_ptr = builder->CreateConstGEP2_32(items_array_type, items_array, 0, 0);

        // Create lfortran_nml_group_t struct
        // struct { const char *group_name, int32_t n_items, lfortran_nml_item_t *items }
        llvm::StructType* group_type = llvm::StructType::get(
            character_type,                           // group_name
            llvm::Type::getInt32Ty(context),         // n_items
            item_type->getPointerTo()                // items
        );

        llvm::Value* group = llvm_utils->CreateAlloca(*builder, group_type);
        builder->CreateStore(group_name_ptr, builder->CreateStructGEP(group_type, group, 0));
        builder->CreateStore(llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), nml_items.size()),
                           builder->CreateStructGEP(group_type, group, 1));
        builder->CreateStore(items_ptr, builder->CreateStructGEP(group_type, group, 2));

        return group;
    }

    // All read functions now have a unified signature with iostat as the last parameter.
    // Pass NULL for iostat when not needed (will exit on error), or a valid pointer
    // (will set iostat and return on error).
    llvm::Function* get_read_function(ASR::ttype_t *type) {
        type = ASRUtils::type_get_past_allocatable(
            ASRUtils::type_get_past_pointer(type));
        llvm::Function *fn = nullptr;
        switch (type->type) {
            case (ASR::ttypeType::Integer): {
                std::string runtime_func_name;
                llvm::Type *type_arg;
                int a_kind = ASRUtils::extract_kind_from_ttype_t(type);
                if ( a_kind == 2 ) {
                    runtime_func_name = "_lfortran_read_int16";
                    type_arg = llvm::Type::getInt16Ty(context);
                } else if (a_kind == 4) {
                    runtime_func_name = "_lfortran_read_int32";
                    type_arg = llvm::Type::getInt32Ty(context);
                } else if (a_kind == 8) {
                    runtime_func_name = "_lfortran_read_int64";
                    type_arg = llvm::Type::getInt64Ty(context);
                } else {
                    throw CodeGenError("Read Integer function not implemented "
                        "for integer kind: " + std::to_string(a_kind));
                }
                fn = module->getFunction(runtime_func_name);
                if (!fn) {
                    std::vector<llvm::Type*> args{
                        type_arg->getPointerTo(),
                        llvm::Type::getInt32Ty(context),
                        llvm::Type::getInt32Ty(context)->getPointerTo()
                    };
                    llvm::FunctionType *function_type = llvm::FunctionType::get(
                            llvm::Type::getVoidTy(context), args, false);
                    fn = llvm::Function::Create(function_type,
                            llvm::Function::ExternalLinkage, runtime_func_name, module.get());
                }
                break;
            }
            case (ASR::ttypeType::String): {
                std::string runtime_func_name = "_lfortran_read_char";
                fn = module->getFunction(runtime_func_name);
                if (!fn) {
                    std::vector<llvm::Type*> args{
                        character_type->getPointerTo(), // Str_data
                        llvm::Type::getInt64Ty(context), // Str_len
                        llvm::Type::getInt32Ty(context), // Unit_num
                        llvm::Type::getInt32Ty(context)->getPointerTo() // iostat
                    };
                    llvm::FunctionType *function_type = llvm::FunctionType::get(
                            llvm::Type::getVoidTy(context), args, false);
                    fn = llvm::Function::Create(function_type,
                            llvm::Function::ExternalLinkage, runtime_func_name, module.get());
                }
                break;
            }

            case (ASR::ttypeType::Logical):{
                // All logical kinds use the same runtime function - it reads T/F
                // into a bool. The LLVM type is i1 regardless of Fortran kind.
                std::string runtime_func_name = "_lfortran_read_logical";
                llvm::Type *type_arg = llvm::Type::getInt1Ty(context);

                fn = module->getFunction(runtime_func_name);
                if (!fn) {
                    std::vector<llvm::Type*> args{
                        type_arg->getPointerTo(),
                        llvm::Type::getInt32Ty(context),
                        llvm::Type::getInt32Ty(context)->getPointerTo()
                    };
                    llvm::FunctionType *function_type = llvm::FunctionType::get(
                            llvm::Type::getVoidTy(context), args, false);
                    fn = llvm::Function::Create(function_type,
                            llvm::Function::ExternalLinkage, runtime_func_name, module.get());
                }
                break;
            }
            case (ASR::ttypeType::Real): {
                std::string runtime_func_name;
                llvm::Type *type_arg;
                int a_kind = ASRUtils::extract_kind_from_ttype_t(type);
                if (a_kind == 4) {
                    runtime_func_name = "_lfortran_read_float";
                    type_arg = llvm::Type::getFloatTy(context);
                } else {
                    runtime_func_name = "_lfortran_read_double";
                    type_arg = llvm::Type::getDoubleTy(context);
                }
                fn = module->getFunction(runtime_func_name);
                if (!fn) {
                    std::vector<llvm::Type*> args{
                        type_arg->getPointerTo(),
                        llvm::Type::getInt32Ty(context),
                        llvm::Type::getInt32Ty(context)->getPointerTo()
                    };
                    llvm::FunctionType *function_type = llvm::FunctionType::get(
                            llvm::Type::getVoidTy(context), args, false);
                    fn = llvm::Function::Create(function_type,
                            llvm::Function::ExternalLinkage, runtime_func_name, module.get());
                }
                break;
            }
            case (ASR::ttypeType::Complex): {
                std::string runtime_func_name;
                llvm::Type *type_arg;
                int a_kind = ASRUtils::extract_kind_from_ttype_t(type);
                if (a_kind == 4) {
                    runtime_func_name = "_lfortran_read_complex_float";
                    type_arg = complex_type_4;
                } else if (a_kind == 8) {
                    runtime_func_name = "_lfortran_read_complex_double";
                    type_arg = complex_type_8;
                } else {
                    throw CodeGenError("Read Complex function not implemented "
                        "for complex kind: " + std::to_string(a_kind));
                }
                fn = module->getFunction(runtime_func_name);
                if (!fn) {
                    std::vector<llvm::Type*> args{
                        type_arg->getPointerTo(),
                        llvm::Type::getInt32Ty(context),
                        llvm::Type::getInt32Ty(context)->getPointerTo()
                    };
                    llvm::FunctionType *function_type = llvm::FunctionType::get(
                            llvm::Type::getVoidTy(context), args, false);
                    fn = llvm::Function::Create(function_type,
                            llvm::Function::ExternalLinkage, runtime_func_name, module.get());
                }
                break;
            }
            case (ASR::ttypeType::Array): {
                type = ASRUtils::type_get_past_array(type);
                int a_kind = ASRUtils::extract_kind_from_ttype_t(type);
                std::string runtime_func_name;
                llvm::Type *type_arg;
                if (ASR::is_a<ASR::Integer_t>(*type)) {
                    if (a_kind == 1) {
                        runtime_func_name = "_lfortran_read_array_int8";
                        type_arg = llvm::Type::getInt8Ty(context);
                    } else if ( a_kind == 2) {
                        runtime_func_name = "_lfortran_read_array_int16";
                        type_arg = llvm::Type::getInt16Ty(context);
                    } else if (a_kind == 4) {
                        runtime_func_name = "_lfortran_read_array_int32";
                        type_arg = llvm::Type::getInt32Ty(context);
                    } else if (a_kind == 8) {
                        runtime_func_name = "_lfortran_read_array_int64";
                        type_arg = llvm::Type::getInt64Ty(context);
                    } else {
                        throw CodeGenError("Integer arrays of kind 1 or 4 only supported for now. Found kind: "
                                            + std::to_string(a_kind));
                    }
                } else if (ASR::is_a<ASR::Real_t>(*type)) {
                    if (a_kind == 4) {
                        runtime_func_name = "_lfortran_read_array_float";
                        type_arg = llvm::Type::getFloatTy(context);
                    } else if (a_kind == 8) {
                        runtime_func_name = "_lfortran_read_array_double";
                        type_arg = llvm::Type::getDoubleTy(context);
                    } else {
                        throw CodeGenError("Real arrays of kind 4 or 8 only supported for now. Found kind: "
                                            + std::to_string(a_kind));
                    }
                } else if (ASR::is_a<ASR::Complex_t>(*type)) {
                    if ( a_kind == 4 ) {
                        runtime_func_name = "_lfortran_read_array_complex_float";
                        type_arg = complex_type_4;
                    } else if ( a_kind == 8 ) {
                        runtime_func_name = "_lfortran_read_array_complex_double";
                        type_arg = complex_type_8;
                    } else {
                        throw CodeGenError("Complex arrays of kind 4 or 8 only supported for now. Found kind: "
                                            + std::to_string(a_kind));
                    }
                } else if (ASR::is_a<ASR::String_t>(*type)) {
                    runtime_func_name = "_lfortran_read_array_char";
                    type_arg = llvm::Type::getInt8Ty(context);
                } else if (ASR::is_a<ASR::Logical_t>(*type)) {
                    runtime_func_name = "_lfortran_read_array_logical";
                    type_arg = llvm_utils->getIntType(a_kind);
                } else {
                    throw CodeGenError("Type not supported.");
                }
                fn = module->getFunction(runtime_func_name);
                if (!fn) {
                    std::vector<llvm::Type*> types;
                    if (runtime_func_name == "_lfortran_read_array_char") {
                        // (char *p, int64_t len, int size, int unit, int32_t *iostat)
                        types = {
                            type_arg->getPointerTo(),
                            llvm::Type::getInt64Ty(context),
                            llvm::Type::getInt32Ty(context),
                            llvm::Type::getInt32Ty(context),
                            llvm::Type::getInt32Ty(context)->getPointerTo()
                        };
                    } else if (runtime_func_name == "_lfortran_read_array_logical") {
                        // (void *p, int size, int kind, int stride, int unit, int32_t *iostat)
                        types = {
                            type_arg->getPointerTo(),
                            llvm::Type::getInt32Ty(context),
                            llvm::Type::getInt32Ty(context),
                            llvm::Type::getInt32Ty(context),
                            llvm::Type::getInt32Ty(context),
                            llvm::Type::getInt32Ty(context)->getPointerTo()
                        };
                    } else {
                        // (T *p, int size, int stride, int unit, int32_t *iostat)
                        types = {
                            type_arg->getPointerTo(),
                            llvm::Type::getInt32Ty(context),
                            llvm::Type::getInt32Ty(context),
                            llvm::Type::getInt32Ty(context),
                            llvm::Type::getInt32Ty(context)->getPointerTo()
                        };
                    }
                    llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context), types, false);
                    fn = llvm::Function::Create(function_type,
                            llvm::Function::ExternalLinkage, runtime_func_name, module.get());
                }
                break;
            }
            default: {
                std::string s_type = ASRUtils::type_to_str_fortran_expr(type, nullptr);
                throw CodeGenError("Read function not implemented for: " + s_type);
            }
        }
        return fn;
    }

    void emit_string_read_loop(ASR::ttype_t* elem_type, llvm::Type* llvm_elem_type,
            llvm::Value* section_ptr, llvm::Value* size,
            llvm::Value* str_src_data, llvm::Value* str_src_len,
            llvm::Value* iostat, llvm::Value* str_offset,
            ASR::expr_t* value_expr) {
        std::string rt_name = "_lfortran_string_read_" +
            ASRUtils::type_to_str_python_expr(elem_type, value_expr);
        llvm::Function* str_fn = module->getFunction(rt_name);
        if (!str_fn) {
            llvm::FunctionType *ft = llvm::FunctionType::get(
                llvm::Type::getVoidTy(context),
                { character_type,
                  llvm::Type::getInt64Ty(context),
                  character_type,
                  llvm_elem_type->getPointerTo(),
                  llvm::Type::getInt32Ty(context)->getPointerTo(),
                  llvm::Type::getInt64Ty(context)->getPointerTo()
                }, false);
            str_fn = llvm::Function::Create(ft,
                llvm::Function::ExternalLinkage, rt_name, module.get());
        }
        llvm::Value* fmt = nullptr;
        if (ASR::is_a<ASR::Real_t>(*elem_type)) {
            int a_kind = ASR::down_cast<ASR::Real_t>(elem_type)->m_kind;
            fmt = a_kind == 4 ?
                LCompilers::create_global_string_ptr(context, *module, *builder, "%f") :
                LCompilers::create_global_string_ptr(context, *module, *builder, "%lf");
        } else if (ASR::is_a<ASR::Integer_t>(*elem_type)) {
            int a_kind = ASR::down_cast<ASR::Integer_t>(elem_type)->m_kind;
            fmt = a_kind == 4 ?
                LCompilers::create_global_string_ptr(context, *module, *builder, "%d") :
                LCompilers::create_global_string_ptr(context, *module, *builder, "%ld");
        } else if (ASR::is_a<ASR::Logical_t>(*elem_type)) {
            fmt = LCompilers::create_global_string_ptr(context, *module, *builder, "%d");
        } else if (ASR::is_a<ASR::Complex_t>(*elem_type)) {
            int a_kind = ASR::down_cast<ASR::Complex_t>(elem_type)->m_kind;
            fmt = a_kind == 4 ?
                LCompilers::create_global_string_ptr(context, *module, *builder, " (%f,%f)") :
                LCompilers::create_global_string_ptr(context, *module, *builder, " (%lf,%lf)");
        }
        llvm::Function* parent_fn = builder->GetInsertBlock()->getParent();
        llvm::Value* idx = llvm_utils->CreateAlloca(*builder,
            llvm::Type::getInt32Ty(context), nullptr, "str_read_idx");
        builder->CreateStore(llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0), idx);
        llvm::BasicBlock* loop_cond = llvm::BasicBlock::Create(context, "str_idl.cond", parent_fn);
        llvm::BasicBlock* loop_body = llvm::BasicBlock::Create(context, "str_idl.body", parent_fn);
        llvm::BasicBlock* loop_end = llvm::BasicBlock::Create(context, "str_idl.end", parent_fn);
        builder->CreateBr(loop_cond);
        builder->SetInsertPoint(loop_cond);
        llvm::Value* cur_idx = builder->CreateLoad(llvm::Type::getInt32Ty(context), idx);
        llvm::Value* cond = builder->CreateICmpSLT(cur_idx, size);
        builder->CreateCondBr(cond, loop_body, loop_end);
        builder->SetInsertPoint(loop_body);
        llvm::Value* elem_ptr = llvm_utils->create_ptr_gep2(llvm_elem_type, section_ptr, cur_idx);
        builder->CreateCall(str_fn, {str_src_data, str_src_len, fmt, elem_ptr, iostat, str_offset});
        llvm::Value* next_idx = builder->CreateAdd(cur_idx,
            llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 1));
        builder->CreateStore(next_idx, idx);
        builder->CreateBr(loop_cond);
        builder->SetInsertPoint(loop_end);
    }

    void generate_read_implied_do_loop(ASR::ImpliedDoLoop_t* idl,
            llvm::Value* unit_val, llvm::Value* iostat,
            bool is_string = false,
            llvm::Value* str_src_data = nullptr,
            llvm::Value* str_src_len = nullptr,
            llvm::Value* str_offset = nullptr) {
        ASR::Variable_t* loop_var_sym = ASR::down_cast<ASR::Variable_t>(
            ASR::down_cast<ASR::Var_t>(idl->m_var)->m_v);

        this->visit_expr_wrapper(idl->m_start, true);
        llvm::Value* start_val = tmp;
        this->visit_expr_wrapper(idl->m_end, true);
        llvm::Value* end_val = tmp;
        llvm::Value* inc_val;
        if (idl->m_increment) {
            this->visit_expr_wrapper(idl->m_increment, true);
            inc_val = tmp;
        } else {
            inc_val = llvm::ConstantInt::get(start_val->getType(), 1);
        }

        uint32_t loop_var_hash = get_hash((ASR::asr_t*)loop_var_sym);
        llvm::Value* loop_var_ptr;
        if (llvm_symtab.find(loop_var_hash) == llvm_symtab.end()) {
            loop_var_ptr = builder->CreateAlloca(start_val->getType(), nullptr,
                loop_var_sym->m_name);
            llvm_symtab[loop_var_hash] = loop_var_ptr;
        } else {
            loop_var_ptr = llvm_symtab[loop_var_hash];
        }

        builder->CreateStore(start_val, loop_var_ptr);
        llvm::Function* fn = builder->GetInsertBlock()->getParent();
        llvm::BasicBlock* loop_cond = llvm::BasicBlock::Create(context, "idl.cond", fn);
        llvm::BasicBlock* loop_body = llvm::BasicBlock::Create(context, "idl.body", fn);
        llvm::BasicBlock* loop_inc = llvm::BasicBlock::Create(context, "idl.inc", fn);
        llvm::BasicBlock* loop_end = llvm::BasicBlock::Create(context, "idl.end", fn);

        builder->CreateBr(loop_cond);
        builder->SetInsertPoint(loop_cond);
        llvm::Value* curr_val = builder->CreateLoad(start_val->getType(), loop_var_ptr);
        llvm::Value* cond = builder->CreateICmpSLE(curr_val, end_val);
        builder->CreateCondBr(cond, loop_body, loop_end);
        builder->SetInsertPoint(loop_body);

        for (size_t vi = 0; vi < idl->n_values; vi++) {
            if (ASR::is_a<ASR::ImpliedDoLoop_t>(*idl->m_values[vi])) {
                ASR::ImpliedDoLoop_t* inner_idl = ASR::down_cast<ASR::ImpliedDoLoop_t>(idl->m_values[vi]);
                generate_read_implied_do_loop(inner_idl, unit_val, iostat,
                    is_string, str_src_data, str_src_len, str_offset);
            } else {
                int ptr_loads_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr(*idl->m_values[vi]);
                llvm::Value* elem_ptr = tmp;
                ptr_loads = ptr_loads_copy;

                ASR::ttype_t* elem_type = ASRUtils::expr_type(idl->m_values[vi]);
                if (is_string) {
                    llvm::Type* llvm_elem_type = llvm_utils->get_type_from_ttype_t_util(
                        idl->m_values[vi], elem_type, module.get());
                    llvm::Value* size_one = llvm::ConstantInt::get(
                        llvm::Type::getInt32Ty(context), 1);
                    emit_string_read_loop(elem_type, llvm_elem_type,
                        elem_ptr, size_one, str_src_data, str_src_len,
                        iostat, str_offset, idl->m_values[vi]);
                } else {
                    llvm::Function* read_fn = get_read_function(elem_type);
                    if (ASRUtils::is_logical(*elem_type)) {
                        llvm::Value* tmp_bool = llvm_utils->CreateAlloca(*builder,
                            llvm::Type::getInt1Ty(context));
                        builder->CreateCall(read_fn, {tmp_bool, unit_val, iostat});
                        int kind = ASRUtils::extract_kind_from_ttype_t(elem_type);
                        llvm::Value* loaded = llvm_utils->CreateLoad2(
                            llvm::Type::getInt1Ty(context), tmp_bool);
                        llvm::Value* widened = builder->CreateZExt(loaded,
                            llvm_utils->getIntType(kind));
                        builder->CreateStore(widened, elem_ptr);
                    } else if (ASRUtils::is_character(*elem_type)) {
                        llvm::Value* str_data, *str_len;
                        std::tie(str_data, str_len) = llvm_utils->get_string_length_data(
                            ASRUtils::get_string_type(elem_type), elem_ptr, true);
                        builder->CreateCall(read_fn, {str_data, str_len, unit_val, iostat});
                    } else {
                        builder->CreateCall(read_fn, {elem_ptr, unit_val, iostat});
                    }
                }
            }
        }

        builder->CreateBr(loop_inc);
        builder->SetInsertPoint(loop_inc);
        llvm::Value* next_val = builder->CreateAdd(
            builder->CreateLoad(start_val->getType(), loop_var_ptr), inc_val);
        builder->CreateStore(next_val, loop_var_ptr);
        builder->CreateBr(loop_cond);
        builder->SetInsertPoint(loop_end);
    }

    void visit_FileRead(const ASR::FileRead_t &x) {
        if( x.m_overloaded ) {
            this->visit_stmt(*x.m_overloaded);
            return ;
        }

        // Handle namelist read
        if (x.m_nml) {
            llvm::Value *unit_val, *iostat;
            bool is_string = false;
            bool is_string_array = false;

            if (x.m_unit == nullptr) {
                unit_val = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, -1, true));
            } else {
                ASR::ttype_t* unit_type = expr_type(x.m_unit);
                is_string = ASRUtils::is_character(*unit_type);
                is_string_array = is_string && ASRUtils::is_array(unit_type);
                if (is_string) {
                    if (is_string_array) {
                        // For character arrays, we need the address of the first element
                        this->visit_expr_wrapper(x.m_unit, false);
                        unit_val = tmp;
                    } else {
                        // For scalar strings
                        this->visit_expr_load_wrapper(x.m_unit, 0, true);
                        unit_val = tmp;
                    }
                } else {
                    this->visit_expr_wrapper(x.m_unit, true);
                    unit_val = llvm_utils->convert_kind(tmp, llvm::Type::getInt32Ty(context));
                }
            }

            if (x.m_iostat) {
                int ptr_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr_wrapper(x.m_iostat, false);
                ptr_loads = ptr_copy;
                iostat = tmp;
                builder->CreateStore(
                    llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0),
                    iostat);
            } else {
                iostat = llvm::ConstantPointerNull::get(llvm::Type::getInt32Ty(context)->getPointerTo());
            }

            // Build namelist descriptor and call _lfortran_namelist_read
            llvm::Value* nml_group = build_namelist_descriptor(x.m_nml);

            // Get or create _lfortran_namelist_read function
            std::string func_name;
            if (is_string_array) {
                func_name = "_lfortran_namelist_read_str_array";
            } else if (is_string) {
                func_name = "_lfortran_namelist_read_str";
            } else {
                func_name = "_lfortran_namelist_read";
            }
            llvm::Function* fn = module->getFunction(func_name);
            if (!fn) {
                // Define item struct type (must match build_namelist_descriptor)
                llvm::StructType* item_type = llvm::StructType::get(
                    character_type,                               // name
                    llvm::Type::getInt32Ty(context),             // type
                    llvm::Type::getInt32Ty(context),             // rank
                    llvm::Type::getInt64Ty(context),             // elem_len
                    llvm::Type::getInt64Ty(context),             // stride
                    llvm::Type::getInt8Ty(context)->getPointerTo(), // data
                    llvm::Type::getInt64Ty(context)->getPointerTo()  // shape
                );
                llvm::StructType* group_type = llvm::StructType::get(
                    character_type,
                    llvm::Type::getInt32Ty(context),
                    item_type->getPointerTo()
                );
                std::vector<llvm::Type*> args;
                if (is_string_array) {
                    args = {
                        llvm::Type::getInt8Ty(context)->getPointerTo(), // data
                        llvm::Type::getInt64Ty(context),        // elem_len
                        llvm::Type::getInt64Ty(context),        // n_elems
                        llvm::Type::getInt32Ty(context)->getPointerTo(), // iostat
                        group_type->getPointerTo()              // group
                    };
                } else if (is_string) {
                    args = {
                        character_type,                         // data
                        llvm::Type::getInt64Ty(context),        // data_len
                        llvm::Type::getInt32Ty(context)->getPointerTo(), // iostat
                        group_type->getPointerTo()              // group
                    };
                } else {
                    args = {
                        llvm::Type::getInt32Ty(context),         // unit_num
                        llvm::Type::getInt32Ty(context)->getPointerTo(), // iostat
                        group_type->getPointerTo()              // group
                    };
                }
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), args, false);
                fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, func_name, module.get());
            }

            if (is_string_array) {
                // Get array dimensions and element length
                ASR::ttype_t* unit_type = expr_type(x.m_unit);
                ASR::dimension_t* dims = nullptr;
                size_t n_dims = ASRUtils::extract_dimensions_from_ttype(unit_type, dims);
                LCOMPILERS_ASSERT(n_dims >= 1);

                // Get element length
                ASR::String_t* str_type = ASRUtils::get_string_type(unit_type);
                llvm::Value* elem_len;
                if (str_type->m_len && ASR::is_a<ASR::IntegerConstant_t>(*str_type->m_len)) {
                    ASR::IntegerConstant_t* len_const = ASR::down_cast<ASR::IntegerConstant_t>(str_type->m_len);
                    elem_len = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context),
                        llvm::APInt(64, len_const->m_n));
                } else {
                    // Dynamic length - need to evaluate the expression
                    this->visit_expr_wrapper(str_type->m_len, true);
                    elem_len = tmp;
                    tmp = nullptr;
                }

                // Calculate total number of elements
                int64_t n_elems_val = ASRUtils::get_fixed_size_of_array(dims, n_dims);
                llvm::Value* n_elems = llvm::ConstantInt::get(
                    llvm::Type::getInt64Ty(context), llvm::APInt(64, n_elems_val));

                // Get data pointer from array
                llvm::Value* data_ptr;
                ASR::array_physical_typeType storage_type = ASRUtils::extract_physical_type(unit_type);
                if (storage_type == ASR::PointerArray && ASRUtils::is_array_of_strings(unit_type)) {
                    // For pointer arrays of strings, use get_stringArray_data
                    data_ptr = llvm_utils->get_stringArray_data(unit_type, unit_val);
                } else if (storage_type == ASR::PointerArray) {
                    // For pointer arrays of other types
                    data_ptr = arr_descr->get_pointer_to_data(x.m_unit, unit_type, unit_val, module.get());
                } else {
                    // For fixed arrays, unit_val is already the data pointer
                    data_ptr = unit_val;
                }
                data_ptr = builder->CreateBitCast(data_ptr,
                    llvm::Type::getInt8Ty(context)->getPointerTo());

                builder->CreateCall(fn, {data_ptr, elem_len, n_elems, iostat, nml_group});
            } else if (is_string) {
                llvm::Value *data_ptr, *data_len;
                std::tie(data_ptr, data_len) = llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_unit), unit_val);
                builder->CreateCall(fn, {data_ptr, data_len, iostat, nml_group});
            } else {
                builder->CreateCall(fn, {unit_val, iostat, nml_group});
            }
            return;
        }

        llvm::Value *unit_val, *iostat, *iostat_for_empty_read, *read_size;
        llvm::Value *advance, *advance_length;
        llvm::Value *iostat_user = nullptr; // User's iostat variable 
        int iostat_kind = 4; // kind of iostat variable (default INT32)
        bool is_string = false;
        bool empty_read = (x.n_values == 0 && x.m_iostat == nullptr && x.m_iomsg == nullptr);
        if (empty_read && x.m_unit == nullptr) {
            unit_val = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context),
                                              llvm::APInt(32, -2, true));
        } else if (x.m_unit == nullptr) {
            // Read from stdin
            unit_val = llvm::ConstantInt::get(
                    llvm::Type::getInt32Ty(context), llvm::APInt(32, -1, true));
        } else {
            is_string = ASRUtils::is_character(*expr_type(x.m_unit));
            this->visit_expr_load_wrapper(x.m_unit, (is_string ? 0 : 1), true);
            unit_val = tmp;
            if(ASRUtils::is_integer(*ASRUtils::expr_type(x.m_unit))){
                // Convert the unit to 32 bit integer (We only support unit number up to 1000).
                unit_val = llvm_utils->convert_kind(tmp, llvm::Type::getInt32Ty(context));
            }
            if (empty_read && !is_string) {
                // Unit is provided with no values, no iostat, no iomsg.
                // If unit is 5 (stdin), set `unit_val` = -2.
                // The unit need not be a compile-time constant.
                llvm::Value* is_stdin = builder->CreateICmpEQ(unit_val,
                    llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 5));
                unit_val = builder->CreateSelect(is_stdin,
                    llvm::ConstantInt::get(llvm::Type::getInt32Ty(context),
                                            llvm::APInt(32, -2, true)),
                    unit_val);
            } else {
                empty_read = false;
            }
        }

        if (x.m_iostat) {
            int ptr_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_iostat, false);
            ptr_loads = ptr_copy;
            iostat_kind = ASR::down_cast<ASR::Integer_t>(ASRUtils::expr_type(x.m_iostat))->m_kind;
            iostat_user = tmp;
            llvm::Value* zero_val = llvm::ConstantInt::get(
                llvm::Type::getIntNTy(context, iostat_kind * 8), 0);
            builder->CreateStore(zero_val, iostat_user);
            if (iostat_kind != 4) {
                // use temporary i32 for iostat
                iostat = llvm_utils->CreateAlloca(*builder,
                    llvm::Type::getInt32Ty(context), nullptr, "tmp_iostat");
                builder->CreateStore(
                    llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0),
                    iostat);
            } else {
                iostat = iostat_user;
            }
            iostat_for_empty_read = iostat;
        } else {
            // Pass NULL to read functions so runtime prints specific error messages
            iostat = llvm::ConstantPointerNull::get(
                        llvm::Type::getInt32Ty(context)->getPointerTo());
            // Create internal iostat for empty_read
            iostat_for_empty_read = llvm_utils->CreateAlloca(*builder,
                        llvm::Type::getInt32Ty(context));
            builder->CreateStore(
                llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0),
                iostat_for_empty_read);
        }

        llvm::Value *iomsg_data = nullptr;
        llvm::Value *iomsg_len = nullptr;
        if (x.m_iomsg) {
            std::tie(iomsg_data, iomsg_len) = get_string_data_and_length(x.m_iomsg);
        }

        auto emit_set_read_iomsg = [&]() {
            if (!x.m_iomsg || !x.m_iostat) return;
            llvm::Value* iostat_val = builder->CreateLoad(
                llvm::Type::getInt32Ty(context), iostat);
            std::string func_name = "_lfortran_set_read_iomsg";
            llvm::Function *iomsg_fn = module->getFunction(func_name);
            if (!iomsg_fn) {
                llvm::FunctionType *ft = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        llvm::Type::getInt32Ty(context),
                        character_type,
                        llvm::Type::getInt64Ty(context)
                    }, false);
                iomsg_fn = llvm::Function::Create(ft,
                    llvm::Function::ExternalLinkage, func_name, module.get());
            }
            builder->CreateCall(iomsg_fn, {iostat_val, iomsg_data, iomsg_len});
        };

        if (x.m_advance) {
            this->visit_expr_load_wrapper(x.m_advance, 0, true);
            LCOMPILERS_ASSERT(ASRUtils::is_character(*expr_type(x.m_advance)))
            std::tie(advance, advance_length) = llvm_utils->get_string_length_data(
                                                    ASRUtils::get_string_type(x.m_advance),
                                                    tmp);
        } else {
            std::string yes("yes");
            advance = LCompilers::create_global_string_ptr(context, *module, *builder, yes);
            advance_length = llvm::ConstantInt::get(context, llvm::APInt(64, yes.size()));
        }

        if (x.m_size) {
            int ptr_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_size, false);
            ptr_loads = ptr_copy;
            read_size = tmp;
        } else {
            read_size = llvm_utils->CreateAlloca(*builder,
                        llvm::Type::getInt32Ty(context));
        }

        // Handle pos= specifier: seek to the specified position before reading
        if (x.m_pos && !is_string) {
            this->visit_expr_wrapper(x.m_pos, true);
            llvm::Value* pos_val = tmp;
            // Convert to i64 for file position
            pos_val = builder->CreateIntCast(pos_val, llvm::Type::getInt64Ty(context), true);

            std::string runtime_func_name = "_lfortran_file_seek";
            llvm::Function *fn = module->getFunction(runtime_func_name);
            if (!fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context), {
                            llvm::Type::getInt32Ty(context),  // unit
                            llvm::Type::getInt64Ty(context),  // pos
                            llvm::Type::getInt32Ty(context)->getPointerTo()  // iostat
                        }, false);
                fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, runtime_func_name,
                            module.get());
            }
            builder->CreateCall(fn, {unit_val, pos_val, iostat});
        }

        if (x.m_rec && !is_string) {
            emit_seek_record_from_rec(x.m_rec, unit_val, iostat);
        }

        if (x.m_fmt && !empty_read) {
            emit_formatted_read(x, unit_val, iostat, read_size, advance, advance_length, is_string);
        } else {
            llvm::Value* var_to_read_into = nullptr; // Var expression that we'll read into.
            // For multi-value list-directed internal reads, track position
            llvm::Value *str_offset = nullptr;
            llvm::Value *str_src_data = nullptr, *str_src_len = nullptr;
            if (is_string) {
                str_offset = llvm_utils->CreateAlloca(*builder,
                    llvm::Type::getInt64Ty(context), nullptr, "str_read_offset");
                builder->CreateStore(
                    llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0),
                    str_offset);
                ASR::ttype_t* unit_type = ASRUtils::expr_type(x.m_unit);
                if (ASRUtils::is_array(unit_type)) {
                    ASR::ttype_t* array_type = ASRUtils::type_get_past_allocatable_pointer(unit_type);
                    if (ASRUtils::is_allocatable_or_pointer(unit_type)) {
                        llvm::Type* llvm_array_type = llvm_utils->get_type_from_ttype_t_util(
                            x.m_unit, array_type, module.get());
                        unit_val = llvm_utils->CreateLoad2(llvm_array_type->getPointerTo(), unit_val);
                    }
                    str_src_data = llvm_utils->get_stringArray_data(array_type, unit_val);
                    llvm::Value* elem_len = llvm_utils->get_stringArray_length(array_type, unit_val);
                    ASR::ttype_t* type32 = ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4));
                    ASR::ArraySize_t* array_size = ASR::down_cast2<ASR::ArraySize_t>(
                        ASR::make_ArraySize_t(al, x.base.base.loc, x.m_unit,
                            nullptr, type32, nullptr));
                    visit_ArraySize(*array_size);
                    llvm::Value* n_elems = builder->CreateIntCast(tmp, llvm::Type::getInt64Ty(context), true);
                    tmp = nullptr;
                    str_src_len = builder->CreateMul(elem_len, n_elems);
                } else {
                    std::tie(str_src_data, str_src_len) = llvm_utils->get_string_length_data(
                        ASRUtils::get_string_type(x.m_unit), unit_val);
                }
            }
            for (size_t i=0; i<x.n_values; i++) {
                // Handle ImpliedDoLoop: read(10,*) (vals(j), j=1,n)
                // Transform to reading n elements into vals starting at start index
                if (ASR::is_a<ASR::ImpliedDoLoop_t>(*x.m_values[i])) {
                    ASR::ImpliedDoLoop_t* idl = ASR::down_cast<ASR::ImpliedDoLoop_t>(x.m_values[i]);
                    // Handle simple case: single ArrayItem with loop var as index
                    if (idl->n_values == 1 && ASR::is_a<ASR::ArrayItem_t>(*idl->m_values[0])) {
                        ASR::ArrayItem_t* arr_item = ASR::down_cast<ASR::ArrayItem_t>(idl->m_values[0]);
                        if (arr_item->n_args == 1 && arr_item->m_args[0].m_right &&
                            ASR::is_a<ASR::Var_t>(*arr_item->m_args[0].m_right)) {
                            ASR::Var_t* idx_var = ASR::down_cast<ASR::Var_t>(arr_item->m_args[0].m_right);
                            ASR::Var_t* loop_var = ASR::down_cast<ASR::Var_t>(idl->m_var);
                            if (idx_var->m_v == loop_var->m_v) {
                                // Get array data pointer
                                int ptr_loads_copy = ptr_loads;
                                ptr_loads = 0;
                                this->visit_expr(*arr_item->m_v);
                                llvm::Value* arr_ptr = tmp;
                                ptr_loads = ptr_loads_copy;

                                ASR::ttype_t* arr_type = ASRUtils::expr_type(arr_item->m_v);
                                ASR::ttype_t* elem_type = ASRUtils::type_get_past_array(
                                    ASRUtils::type_get_past_allocatable_pointer(arr_type));
                                llvm::Type* llvm_elem_type = llvm_utils->get_type_from_ttype_t_util(
                                    arr_item->m_v, elem_type, module.get());

                                // Get data pointer - match existing array handling pattern
                                llvm::Value* data_ptr = arr_ptr;
                                ASR::ttype_t* past_alloc_type = ASRUtils::type_get_past_allocatable_pointer(arr_type);
                                ASR::Array_t* arr_t = ASR::down_cast<ASR::Array_t>(past_alloc_type);
                                llvm::Type* llvm_arr_type = llvm_utils->get_type_from_ttype_t_util(
                                    arr_item->m_v, past_alloc_type, module.get());

                                if (arr_t->m_physical_type == ASR::array_physical_typeType::FixedSizeArray) {
                                    // For FixedSizeArray, arr_ptr is [N x Type]*, just use element pointer
                                    // GEP with indices [0, offset] to get element pointer
                                    this->visit_expr_wrapper(idl->m_start, true);
                                    llvm::Value* start_idx = tmp;
                                    llvm::Value* offset = builder->CreateSub(start_idx,
                                        llvm::ConstantInt::get(start_idx->getType(), 1));
                                    llvm::Value* section_ptr = builder->CreateGEP(llvm_arr_type, arr_ptr,
                                        {llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0), offset});

                                    // Compute size: (end - start) / inc + 1
                                    this->visit_expr_wrapper(idl->m_end, true);
                                    llvm::Value* end_idx = tmp;
                                    llvm::Value* size;
                                    if (idl->m_increment) {
                                        this->visit_expr_wrapper(idl->m_increment, true);
                                        llvm::Value* inc = tmp;
                                        llvm::Value* diff = builder->CreateSub(end_idx, start_idx);
                                        llvm::Value* quot = builder->CreateSDiv(diff, inc);
                                        size = builder->CreateAdd(quot,
                                            llvm::ConstantInt::get(quot->getType(), 1));
                                    } else {
                                        llvm::Value* diff = builder->CreateSub(end_idx, start_idx);
                                        size = builder->CreateAdd(diff,
                                            llvm::ConstantInt::get(diff->getType(), 1));
                                    }
                                    size = builder->CreateIntCast(size, llvm::Type::getInt32Ty(context), true);

                                    if (is_string) {
                                        emit_string_read_loop(elem_type, llvm_elem_type,
                                            section_ptr, size, str_src_data, str_src_len,
                                            iostat, str_offset, idl->m_values[0]);
                                    } else {
                                        llvm::Function* fn = get_read_function(arr_type);
                                        llvm::Value* stride_val = llvm::ConstantInt::get(
                                            llvm::Type::getInt32Ty(context), 1);
                                        if (ASR::is_a<ASR::Logical_t>(*elem_type)) {
                                            int a_kind = ASRUtils::extract_kind_from_ttype_t(elem_type);
                                            llvm::Value* kind_val = llvm::ConstantInt::get(context, llvm::APInt(32, a_kind));
                                            builder->CreateCall(fn, {section_ptr, size, kind_val, stride_val, unit_val, iostat});
                                        } else {
                                            builder->CreateCall(fn, {section_ptr, size, stride_val, unit_val, iostat});
                                        }
                                    }
                                    continue;
                                }

                                bool is_pointer_array = (arr_t->m_physical_type == ASR::array_physical_typeType::PointerArray ||
                                    arr_t->m_physical_type == ASR::array_physical_typeType::UnboundedPointerArray);
                                if (is_pointer_array) {
                                    // Check if this is a dummy argument (passed by value as pointer)
                                    // vs local variable (stored as pointer-to-pointer, needs load)
                                    bool is_dummy_arg = false;
                                    if (ASR::is_a<ASR::Var_t>(*arr_item->m_v)) {
                                        ASR::Variable_t* arr_var = ASRUtils::EXPR2VAR(arr_item->m_v);
                                        is_dummy_arg = is_arg_dummy(arr_var->m_intent);
                                    }
                                    if (is_dummy_arg) {
                                        // For dummy arguments, arr_ptr is already Type* (data pointer)
                                        data_ptr = arr_ptr;
                                    } else {
                                        // For local pointer arrays, arr_ptr is Type**, load to get Type*
                                        data_ptr = llvm_utils->CreateLoad2(llvm_elem_type->getPointerTo(), arr_ptr);
                                    }
                                } else {
                                    if (ASRUtils::is_allocatable(arr_type) || ASRUtils::is_pointer(arr_type)) {
                                        arr_ptr = llvm_utils->CreateLoad2(llvm_arr_type->getPointerTo(), arr_ptr);
                                    }
                                    data_ptr = llvm_utils->CreateLoad2(llvm_elem_type->getPointerTo(),
                                        arr_descr->get_pointer_to_data(llvm_arr_type, arr_ptr));
                                }

                                // Compute pointer to start element (1-based indexing)
                                this->visit_expr_wrapper(idl->m_start, true);
                                llvm::Value* start_idx = tmp;
                                llvm::Value* offset = builder->CreateSub(start_idx,
                                    llvm::ConstantInt::get(start_idx->getType(), 1));
                                llvm::Value* section_ptr = llvm_utils->create_ptr_gep2(llvm_elem_type, data_ptr, offset);

                                // Compute size: (end - start) / inc + 1
                                this->visit_expr_wrapper(idl->m_end, true);
                                llvm::Value* end_idx = tmp;
                                llvm::Value* size;
                                if (idl->m_increment) {
                                    this->visit_expr_wrapper(idl->m_increment, true);
                                    llvm::Value* inc = tmp;
                                    llvm::Value* diff = builder->CreateSub(end_idx, start_idx);
                                    llvm::Value* quot = builder->CreateSDiv(diff, inc);
                                    size = builder->CreateAdd(quot,
                                        llvm::ConstantInt::get(quot->getType(), 1));
                                } else {
                                    llvm::Value* diff = builder->CreateSub(end_idx, start_idx);
                                    size = builder->CreateAdd(diff,
                                        llvm::ConstantInt::get(diff->getType(), 1));
                                }
                                // Convert size to i32
                                size = builder->CreateIntCast(size, llvm::Type::getInt32Ty(context), true);

                                // Pointer-array string reads call the special API 
                                // _lfortran_string_read_str_array so that passed input 
                                // strings are copied into the target character array.
                                if (is_string && ASR::is_a<ASR::String_t>(*elem_type)) {
                                    llvm::Type* char_ptr_type = llvm::Type::getInt8Ty(context)->getPointerTo();
                                    llvm::FunctionType* str_arr_ft = llvm::FunctionType::get(
                                        llvm::Type::getVoidTy(context),
                                        { char_ptr_type, llvm::Type::getInt64Ty(context),
                                          char_ptr_type, char_ptr_type, llvm::Type::getInt64Ty(context) }, false);
                                    auto [arr_data, elem_len_llvm] = llvm_utils->get_string_length_data(
                                        ASR::down_cast<ASR::String_t>(elem_type), section_ptr);
                                    llvm::Value* fmt_val = LCompilers::create_global_string_ptr(
                                        context, *module, *builder, "%s");
                                    std::vector<llvm::Value*> args = {str_src_data, str_src_len, fmt_val, arr_data, elem_len_llvm};
                                    builder->CreateCall(module->getOrInsertFunction(
                                            "_lfortran_string_read_str_array", str_arr_ft), args);
                                } else if (is_string) {
                                    emit_string_read_loop(elem_type, llvm_elem_type,
                                        section_ptr, size, str_src_data, str_src_len,
                                        iostat, str_offset, idl->m_values[0]);
                                } else {
                                    llvm::Function* fn = get_read_function(arr_type);
                                    llvm::Value* stride_val = llvm::ConstantInt::get(
                                        llvm::Type::getInt32Ty(context), 1);
                                    if (ASR::is_a<ASR::Logical_t>(*elem_type)) {
                                        int a_kind = ASRUtils::extract_kind_from_ttype_t(elem_type);
                                        llvm::Value* kind_val = llvm::ConstantInt::get(context, llvm::APInt(32, a_kind));
                                        builder->CreateCall(fn, {section_ptr, size, kind_val, stride_val, unit_val, iostat});
                                    } else {
                                        builder->CreateCall(fn, {section_ptr, size, stride_val, unit_val, iostat});
                                    }
                                }
                                continue;
                            }
                        }
                    }
                    // General case: generate a loop to read elements one by one
                    // This handles multi-dimensional arrays like (a(i,j), j=1,n)
                    // and struct member references like (s%spec(j)%num, j=1,n).
                    {
                        bool can_handle = (idl->n_values >= 1);
                        for (size_t vi = 0; vi < idl->n_values && can_handle; vi++) {
                            can_handle =
                                ASR::is_a<ASR::ArrayItem_t>(*idl->m_values[vi]) ||
                                ASR::is_a<ASR::ImpliedDoLoop_t>(*idl->m_values[vi]) ||
                                ASR::is_a<ASR::StructInstanceMember_t>(*idl->m_values[vi]);
                        }
                        if (can_handle) {
                            generate_read_implied_do_loop(idl, unit_val, iostat,
                                is_string, str_src_data, str_src_len, str_offset);
                            continue;
                        }
                    }
                    // Unsupported ImpliedDoLoop pattern - fall through to default handling
                }

                int ptr_loads_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr(*x.m_values[i]);
                var_to_read_into = tmp; tmp =nullptr;
                ptr_loads = ptr_loads_copy;
                ASR::ttype_t* type = ASRUtils::expr_type(x.m_values[i]);
                llvm::Function *fn;
                if (ASR::is_a<ASR::Var_t>(*x.m_values[i]) &&
                    ASR::is_a<ASR::ExternalSymbol_t>(*ASR::down_cast<ASR::Var_t>(x.m_values[i])->m_v)) {
                    ASR::Variable_t *asr_target = EXPR2VAR(x.m_values[i]);
                    uint32_t h = get_hash((ASR::asr_t*)asr_target);
                    var_to_read_into = llvm_symtab[h];
                }
                if (is_string) {
                    std::string runtime_func_name = "_lfortran_string_read_" +
                                            ASRUtils::type_to_str_python_expr(ASRUtils::extract_type(type), x.m_values[i]);
                    if (ASRUtils::is_array(type)) {
                        runtime_func_name += "_array";
                    }
                    llvm::Function* fn = module->getFunction(runtime_func_name);
                    if (!fn) {
                        llvm::FunctionType *function_type;
                        if(ASRUtils::is_string_only(type)){
                            function_type = llvm::FunctionType::get(
                                llvm::Type::getVoidTy(context),
                                {
                                    character_type /*src_data*/,
                                    llvm::Type::getInt64Ty(context) /*src_length*/,
                                    character_type /*dest_data*/,
                                    llvm::Type::getInt64Ty(context) /*dest_length*/,
                                    llvm::Type::getInt64Ty(context)->getPointerTo() /*offset*/
                                },
                                false);
                        } else if (ASRUtils::is_array(type)) {
                            if (ASRUtils::is_array_of_strings(type)) {
                                // String array: flat char* buffer + elem_len
                                function_type = llvm::FunctionType::get(
                                    llvm::Type::getVoidTy(context),
                                    {   character_type /*src_data*/,
                                        llvm::Type::getInt64Ty(context)/*src_length*/,
                                        character_type /*format*/,
                                        character_type /*arr data (flat buffer)*/,
                                        llvm::Type::getInt64Ty(context) /*elem_len*/
                                    },
                                    false);
                            } else {
                                // Numeric array
                                function_type = llvm::FunctionType::get(
                                    llvm::Type::getVoidTy(context),
                                    {   character_type /*src_data*/,
                                        llvm::Type::getInt64Ty(context)/*src_length*/,
                                        character_type,
                                        llvm_utils->get_type_from_ttype_t_util(
                                            x.m_values[i],
                                            ASRUtils::type_get_past_allocatable_pointer(type),
                                            module.get()
                                        )->getPointerTo(),
                                        llvm::Type::getInt64Ty(context) /*array_size*/,
                                        llvm::Type::getInt32Ty(context)->getPointerTo() /*iostat*/
                                    },
                                    false);
                            }
                        } else {
                            function_type = llvm::FunctionType::get(
                                llvm::Type::getVoidTy(context),
                                {   character_type /*src_data*/,
                                    llvm::Type::getInt64Ty(context)/*src_length*/,
                                    character_type,
                                    llvm_utils->get_type_from_ttype_t_util(
                                        x.m_values[i],
                                        ASRUtils::type_get_past_allocatable_pointer(type),
                                        module.get()
                                    )->getPointerTo(),
                                    llvm::Type::getInt32Ty(context)->getPointerTo() /*iostat*/,
                                    llvm::Type::getInt64Ty(context)->getPointerTo() /*offset*/
                                },
                                false);
                        }
                        fn = llvm::Function::Create(function_type,
                                llvm::Function::ExternalLinkage, runtime_func_name, module.get());
                    }
                    llvm::Value *fmt = nullptr;
                    if (ASR::is_a<ASR::Integer_t>(*ASRUtils::type_get_past_array(
                            ASRUtils::type_get_past_allocatable_pointer(type)))) {
                        ASR::Integer_t* int_type = ASR::down_cast<ASR::Integer_t>(ASRUtils::type_get_past_array(
                                ASRUtils::type_get_past_allocatable_pointer(type)));
                        fmt = int_type->m_kind == 4 ? LCompilers::create_global_string_ptr(context, *module, *builder, "%d")
                                                    : LCompilers::create_global_string_ptr(context, *module, *builder, "%ld");
                    } else if (ASR::is_a<ASR::Real_t>(*ASRUtils::type_get_past_array(
                                   ASRUtils::type_get_past_allocatable_pointer(type)))) {
                        ASR::Real_t* real_type = ASR::down_cast<ASR::Real_t>(ASRUtils::type_get_past_array(
                                ASRUtils::type_get_past_allocatable_pointer(type)));
                        fmt = real_type->m_kind == 4 ? LCompilers::create_global_string_ptr(context, *module, *builder, "%f")
                                                     : LCompilers::create_global_string_ptr(context, *module, *builder, "%lf");
                    } else if (ASR::is_a<ASR::String_t>(*ASRUtils::type_get_past_array(
                                   ASRUtils::type_get_past_allocatable_pointer(type)))) {
                        fmt = LCompilers::create_global_string_ptr(context, *module, *builder, "%s");
                    } else if (ASR::is_a<ASR::Logical_t>(*ASRUtils::type_get_past_array(
                                   ASRUtils::type_get_past_allocatable_pointer(type)))) {
                        fmt = LCompilers::create_global_string_ptr(context, *module, *builder, "%d");
                    } else if (ASR::is_a<ASR::Complex_t>(*ASRUtils::type_get_past_array(
                                   ASRUtils::type_get_past_allocatable_pointer(type)))) {
                        ASR::Complex_t* complex_type = ASR::down_cast<ASR::Complex_t>(ASRUtils::type_get_past_array(
                                ASRUtils::type_get_past_allocatable_pointer(type)));
                        fmt = complex_type->m_kind == 4 ? LCompilers::create_global_string_ptr(context, *module, *builder, " (%f,%f)")
                                                       : LCompilers::create_global_string_ptr(context, *module, *builder, " (%lf,%lf)");
                    }

                    if(ASRUtils::is_string_only(type)){
                        llvm::Value* dest_data, *dest_len;
                        std::tie(dest_data, dest_len) = llvm_utils->get_string_length_data(
                            ASRUtils::get_string_type(x.m_values[i]), var_to_read_into);
                        builder->CreateCall(fn, { str_src_data, str_src_len, dest_data, dest_len, str_offset });
                    } else if (ASRUtils::is_array(type)) {
                        // Load descriptor for allocatable/pointer types
                        llvm::Value* arr_desc = var_to_read_into;
                        if (ASR::is_a<ASR::Allocatable_t>(*type) ||
                                ASR::is_a<ASR::Pointer_t>(*type)) {
                            llvm::Type* desc_ptr_type = llvm_utils->get_type_from_ttype_t_util(
                                x.m_values[i],
                                ASRUtils::type_get_past_allocatable_pointer(type),
                                module.get())->getPointerTo();
                            arr_desc = llvm_utils->CreateLoad2(desc_ptr_type, var_to_read_into);
                        }
                        llvm::Value* arr_data;
                        if (ASRUtils::is_array_of_strings(type)) {
                            // For string arrays, get flat data pointer (char*)
                            arr_data = llvm_utils->get_stringArray_data(type, arr_desc);
                        } else {
                            arr_data = arr_desc;
                            if (ASR::is_a<ASR::Allocatable_t>(*type) ||
                                    ASR::is_a<ASR::Pointer_t>(*type)) {
                                llvm::Type *el_type = llvm_utils->get_el_type(
                                    x.m_values[i], ASRUtils::extract_type(type), module.get());
                                ASR::Array_t *arr_tp = ASR::down_cast<ASR::Array_t>(
                                    ASRUtils::type_get_past_allocatable_pointer(type));
                                if (arr_tp->m_physical_type !=
                                        ASR::array_physical_typeType::PointerArray) {
                                    arr_data = arr_descr->get_pointer_to_data(
                                        llvm_utils->get_type_from_ttype_t_util(x.m_values[i],
                                            ASRUtils::type_get_past_allocatable_pointer(type),
                                            module.get()),
                                        arr_data);
                                    arr_data = llvm_utils->CreateLoad2(
                                        el_type->getPointerTo(), arr_data);
                                }
                            }
                            // The runtime entry takes a flat element pointer.
                            // For FixedSizeArray the variable's address is
                            // already a `[N x T]*`, which the LLVM verifier
                            // rejects against the cached function signature.
                            // Bitcast to the declared parameter type so the
                            // call matches regardless of physical layout.
                            llvm::Type* fn_param_type =
                                fn->getFunctionType()->getParamType(3);
                            if (arr_data->getType() != fn_param_type) {
                                arr_data = builder->CreateBitCast(
                                    arr_data, fn_param_type);
                            }
                        }
                        if (x.m_iostat && ASRUtils::is_array_of_strings(type)) {
                            // String array runtime does not return iostat; assume success.
                            int ptr_copy = ptr_loads;
                            ptr_loads = 0;

                            this->visit_expr_wrapper(x.m_iostat);
                            llvm::Value* ios_ptr = tmp;

                            ptr_loads = ptr_copy;

                            builder->CreateStore(
                                llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0),
                                ios_ptr
                            );
                        }
                        if (ASRUtils::is_array_of_strings(type)) {
                            // For string arrays, pass elem_len
                            ASR::String_t* str_type = ASR::down_cast<ASR::String_t>(
                                ASRUtils::type_get_past_array(
                                    ASRUtils::type_get_past_allocatable_pointer(type)));
                            llvm::Value* elem_len_llvm;
                            int64_t elem_len_val;
                            if (ASRUtils::extract_value(str_type->m_len, elem_len_val)) {
                                elem_len_llvm = llvm::ConstantInt::get(
                                    llvm::Type::getInt64Ty(context), elem_len_val);
                            } else if (str_type->m_len) {
                                // Dynamic length with an expression - evaluate at runtime
                                this->visit_expr_wrapper(str_type->m_len, true);
                                elem_len_llvm = builder->CreateIntCast(
                                    tmp, llvm::Type::getInt64Ty(context), true);
                                tmp = nullptr;
                            } else {
                                // Assumed length (character(len=*)) - get length from descriptor
                                elem_len_llvm = llvm_utils->get_stringArray_length(type, arr_desc);
                            }
                            builder->CreateCall(fn, { str_src_data, str_src_len, fmt, arr_data, elem_len_llvm });
                        } else {
                            // Compute array size to detect EOF (fewer values than requested).
                            ASR::ttype_t *type32_local = ASRUtils::TYPE(
                                ASR::make_Integer_t(al, x.base.base.loc, 4));
                            ASR::ArraySize_t* array_size_node =
                                ASR::down_cast2<ASR::ArraySize_t>(ASR::make_ArraySize_t(
                                    al, x.base.base.loc, x.m_values[i],
                                    nullptr, type32_local, nullptr));
                            visit_ArraySize(*array_size_node);
                            llvm::Value* arr_size_i64 = builder->CreateIntCast(
                                tmp, llvm::Type::getInt64Ty(context), true);
                            tmp = nullptr;
                            builder->CreateCall(fn, { str_src_data, str_src_len, fmt,
                                arr_data, arr_size_i64, iostat });
                        }
                    } else {
                        if (ASR::is_a<ASR::Allocatable_t>(*type) ||
                                ASR::is_a<ASR::Pointer_t>(*type)) {
                            llvm::Type* t = llvm_utils->get_type_from_ttype_t_util(
                                x.m_values[i],
                                ASRUtils::type_get_past_allocatable_pointer(type),
                                module.get())->getPointerTo();
                            var_to_read_into = llvm_utils->CreateLoad2(t, var_to_read_into);
                        }
                        builder->CreateCall(fn, { str_src_data, str_src_len, fmt, var_to_read_into, iostat, str_offset });
                    }
                    // Copy temporary i32 iostat back to user's variable
                    if (iostat_user && iostat_kind != 4) {
                        llvm::Value* i32_val = builder->CreateLoad(llvm::Type::getInt32Ty(context), iostat);
                        llvm::Value* extended = builder->CreateIntCast(i32_val,
                            llvm::Type::getIntNTy(context, iostat_kind * 8), true);
                        builder->CreateStore(extended, iostat_user);
                    }
                    emit_set_read_iomsg();
                    continue;
                } else {
                    fn = get_read_function(type);
                }
                if (ASRUtils::is_array(type)) {
                    llvm::Type *el_type = llvm_utils->get_el_type(
                        x.m_values[i], ASRUtils::extract_type(type), module.get());
                    if (ASR::is_a<ASR::Allocatable_t>(*type)
                        || ASR::is_a<ASR::Pointer_t>(*type)) {
                        llvm::Type* t = llvm_utils->get_type_from_ttype_t_util(
                            x.m_values[i],
                            ASRUtils::type_get_past_allocatable_pointer(type),
                            module.get())->getPointerTo();
                        var_to_read_into = llvm_utils->CreateLoad2(t, var_to_read_into);
                    }
                    llvm::Value* original_array_representation = var_to_read_into; // Loaded (if necessary)
                    ASR::Array_t *arr_tp = ASR::down_cast<ASR::Array_t>(
                        ASRUtils::type_get_past_allocatable_pointer(type));
                    llvm::Value* descriptor_stride_i32 = llvm::ConstantInt::get(
                        llvm::Type::getInt32Ty(context), 1);
                    if (arr_tp->m_physical_type == ASR::array_physical_typeType::DescriptorArray) {
                        llvm::Type* llvm_arr_type = llvm_utils->get_type_from_ttype_t_util(
                            x.m_values[i],
                            ASRUtils::type_get_past_allocatable_pointer(type),
                            module.get());
                        llvm::Value* dim_des_arr = arr_descr->get_pointer_to_dimension_descriptor_array(
                            llvm_arr_type, original_array_representation);
                        llvm::Value* dim_zero = llvm::ConstantInt::get(context, llvm::APInt(32, 0));
                        llvm::Value* dim_desc = arr_descr->get_pointer_to_dimension_descriptor(
                            dim_des_arr, dim_zero);
                        llvm::Value* stride_val = arr_descr->get_stride(dim_desc);
                        descriptor_stride_i32 = builder->CreateIntCast(
                            stride_val, llvm::Type::getInt32Ty(context), true);
                    }
                    if (arr_tp->m_physical_type != ASR::array_physical_typeType::PointerArray) {
                        var_to_read_into = arr_descr->get_pointer_to_data(llvm_utils->get_type_from_ttype_t_util(x.m_values[i], ASRUtils::type_get_past_allocatable_pointer(type), module.get()), var_to_read_into);
                    }
                    if (ASR::is_a<ASR::Allocatable_t>(*type)
                        || ASR::is_a<ASR::Pointer_t>(*type)) {
                        var_to_read_into = llvm_utils->CreateLoad2(el_type->getPointerTo(), var_to_read_into);
                    }
                    llvm::Value *arr = var_to_read_into;
                    ASR::ttype_t *type32 = ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4));
                    ASR::ArraySize_t* array_size = ASR::down_cast2<ASR::ArraySize_t>(ASR::make_ArraySize_t(al, x.base.base.loc,
                        x.m_values[i], nullptr, type32, nullptr));
                    visit_ArraySize(*array_size);
                    if(ASRUtils::is_array_of_strings(type)){
                        builder->CreateCall(fn, {
                            llvm_utils->get_stringArray_data(type, original_array_representation),
                            llvm_utils->get_stringArray_length(type, original_array_representation),
                            tmp, unit_val, iostat});
                        tmp = nullptr;
                    } else if (ASR::is_a<ASR::Logical_t>(*ASRUtils::type_get_past_array(
                            ASRUtils::type_get_past_allocatable_pointer(type)))) {
                        int a_kind = ASRUtils::extract_kind_from_ttype_t(
                            ASRUtils::type_get_past_array(
                                ASRUtils::type_get_past_allocatable_pointer(type)));
                        llvm::Value* kind_val = llvm::ConstantInt::get(context, llvm::APInt(32, a_kind));
                        builder->CreateCall(fn, {arr, tmp, kind_val, descriptor_stride_i32, unit_val, iostat});
                        tmp = nullptr;
                    } else {
                        builder->CreateCall(fn, {arr, tmp, descriptor_stride_i32, unit_val, iostat});
                        tmp = nullptr;
                    }
                } else {
                    if(ASRUtils::is_string_only(type)){
                        llvm::Value* str_data, *str_len;
                        std::tie(str_data, str_len) = llvm_utils->get_string_length_data(ASRUtils::get_string_type(type), var_to_read_into, true);
                        builder->CreateCall(fn, {str_data, str_len, unit_val, iostat});
                    } else {
                        if (ASR::is_a<ASR::Allocatable_t>(*type)
                            || ASR::is_a<ASR::Pointer_t>(*type)) {
                            llvm::Type* t = llvm_utils->get_type_from_ttype_t_util(
                                x.m_values[i],
                                ASRUtils::type_get_past_allocatable_pointer(type),
                                module.get())->getPointerTo();
                            var_to_read_into = llvm_utils->CreateLoad2(t, var_to_read_into);
                        }
                        llvm::Value* read_ptr = var_to_read_into;
                        ASR::ttype_t* val_type = ASRUtils::expr_type(x.m_values[i]);
                        if (ASRUtils::is_logical(*ASRUtils::type_get_past_allocatable_pointer(val_type))) {
                            // The runtime _lfortran_read_logical writes a C bool (1 byte),
                            // but the Fortran logical variable is wider (i8/i16/i32/i64).
                            // Use a temporary i1 alloca, call the function,
                            // then widen and store back.
                            // Initialize tmp_bool with the current value so that null
                            // fields in list-directed I/O preserve the existing value.
                            int kind = ASRUtils::extract_kind_from_ttype_t(
                                ASRUtils::type_get_past_allocatable_pointer(val_type));
                            llvm::Value* tmp_bool = llvm_utils->CreateAlloca(*builder,
                                llvm::Type::getInt1Ty(context));
                            llvm::Value* cur_val = llvm_utils->CreateLoad2(
                                llvm_utils->getIntType(kind), var_to_read_into);
                            llvm::Value* cur_bool = builder->CreateTrunc(cur_val,
                                llvm::Type::getInt1Ty(context));
                            builder->CreateStore(cur_bool, tmp_bool);
                            builder->CreateCall(fn, {tmp_bool, unit_val, iostat});
                            llvm::Value* loaded = llvm_utils->CreateLoad2(
                                llvm::Type::getInt1Ty(context), tmp_bool);
                            llvm::Value* widened = builder->CreateZExt(loaded,
                                llvm_utils->getIntType(kind));
                            builder->CreateStore(widened, var_to_read_into);
                        } else {
                            builder->CreateCall(fn, {read_ptr, unit_val, iostat});
                        }
                    }
                }
            }

            // In Fortran, read(u, *) is used to read the entire line. The
            // next read(u, *) function is intended to read the next entire
            // line. Let's take an example: `read(u, *) n`, where n is an
            // integer. The first occurance of the integer value will be
            // read, and anything after that will be skipped.
            // Here, we can use `_lfortran_empty_read` function to move to the
            // pointer to the next line.
            // Skip for internal (string) reads — no file position to advance.
            if (!is_string || empty_read) {
            std::string runtime_func_name = "_lfortran_empty_read";
            llvm::Function *fn = module->getFunction(runtime_func_name);
            if (!fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context), {
                            llvm::Type::getInt32Ty(context),
                            llvm::Type::getInt32Ty(context)->getPointerTo(),
                            llvm::Type::getInt32Ty(context)
                        }, false);
                fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, runtime_func_name,
                            module.get());
            }
            // no_values flag: 1 if no values were read, 0 otherwise
            llvm::Value* no_values_flag = llvm::ConstantInt::get(
                llvm::Type::getInt32Ty(context), x.n_values == 0 ? 1 : 0);
            // When x.m_iostat is provided and values were read (n_values > 0),
            // only call empty_read if no error occurred during value reads.
            // When n_values == 0, no reads happened yet so call unconditionally.
            if (x.m_iostat && x.n_values > 0) {
                llvm::Value* iostat_val = builder->CreateLoad(
                    llvm::Type::getInt32Ty(context), iostat_for_empty_read);
                llvm::Value* iostat_is_zero = builder->CreateICmpEQ(
                    iostat_val, llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0));
                llvm_utils->create_if_else(iostat_is_zero, [&]() {
                    builder->CreateCall(fn, {unit_val, iostat_for_empty_read, no_values_flag});
                }, [](){});
            } else {
                builder->CreateCall(fn, {unit_val, iostat_for_empty_read, no_values_flag});
            }
            }
        }
        // Copy temporary i32 iostat back to user's variable if needed
        if (iostat_user && iostat_kind != 4) {
            llvm::Value* i32_val = builder->CreateLoad(llvm::Type::getInt32Ty(context), iostat);
            llvm::Value* extended = builder->CreateIntCast(i32_val,
                llvm::Type::getIntNTy(context, iostat_kind * 8), true);
            builder->CreateStore(extended, iostat_user);
        }
        emit_set_read_iomsg();
    }

    void add_formatted_read_arg(std::vector<llvm::Value*>& args, ASR::ttype_t* val_type,
            llvm::Value* elem_ptr) {
        constexpr int32_t kChar = 0;
        constexpr int32_t kLogical = 1;
        constexpr int32_t kInt32 = 2;
        constexpr int32_t kInt64 = 3;
        constexpr int32_t kFloat = 4;
        constexpr int32_t kDouble = 5;

        // Scalar arg: prepend is_descriptor_array=0 before type_code
        llvm::Value* is_descriptor_array_false = llvm::ConstantInt::get(context, llvm::APInt(32, 0));
        if (ASR::is_a<ASR::String_t>(*val_type)) {
            args.push_back(is_descriptor_array_false);
            args.push_back(llvm::ConstantInt::get(context, llvm::APInt(32, kChar)));
            auto [str_data, str_len] = llvm_utils->get_string_length_data(
                ASRUtils::get_string_type(val_type), elem_ptr, true);
            args.push_back(str_data);
            args.push_back(str_len);
        } else if (ASR::is_a<ASR::Logical_t>(*val_type)) {
            args.push_back(is_descriptor_array_false);
            args.push_back(llvm::ConstantInt::get(context, llvm::APInt(32, kLogical)));
            args.push_back(elem_ptr);
        } else if (ASR::is_a<ASR::Integer_t>(*val_type)) {
            ASR::Integer_t* int_type = ASR::down_cast<ASR::Integer_t>(val_type);
            int32_t type_code = (int_type->m_kind <= 4) ? kInt32 : kInt64;
            args.push_back(is_descriptor_array_false);
            args.push_back(llvm::ConstantInt::get(context, llvm::APInt(32, type_code)));
            args.push_back(elem_ptr);
        } else if (ASR::is_a<ASR::Real_t>(*val_type)) {
            ASR::Real_t* real_type = ASR::down_cast<ASR::Real_t>(val_type);
            int32_t type_code = (real_type->m_kind == 4) ? kFloat : kDouble;
            args.push_back(is_descriptor_array_false);
            args.push_back(llvm::ConstantInt::get(context, llvm::APInt(32, type_code)));
            args.push_back(elem_ptr);
        } else if (ASR::is_a<ASR::Complex_t>(*val_type)) {
            ASR::Complex_t* complex_type = ASR::down_cast<ASR::Complex_t>(val_type);
            int32_t component_type_code = (complex_type->m_kind == 4) ? kFloat : kDouble;
            llvm::Type* llvm_complex_type = (complex_type->m_kind == 4)
                ? static_cast<llvm::Type*>(complex_type_4)
                : static_cast<llvm::Type*>(complex_type_8);
            llvm::Value* re_ptr = builder->CreateStructGEP(llvm_complex_type, elem_ptr, 0);
            llvm::Value* im_ptr = builder->CreateStructGEP(llvm_complex_type, elem_ptr, 1);
            args.push_back(is_descriptor_array_false);
            args.push_back(llvm::ConstantInt::get(context, llvm::APInt(32, component_type_code)));
            args.push_back(re_ptr);
            args.push_back(is_descriptor_array_false);
            args.push_back(llvm::ConstantInt::get(context, llvm::APInt(32, component_type_code)));
            args.push_back(im_ptr);
        } else {
            throw CodeGenError("Unsupported type in formatted read");
        }
    }

    int64_t get_formatted_read_arg_count(ASR::ttype_t* val_type) {
        return ASR::is_a<ASR::Complex_t>(*val_type) ? 2 : 1;
    }


    void emit_seek_record(llvm::Value* unit_val, llvm::Value* rec_val, llvm::Value* iostat) {
        llvm::Value *unit_i32 = unit_val;
        if (unit_i32->getType()->isPointerTy()) {
            unit_i32 = llvm_utils->CreateLoad2(llvm::Type::getInt32Ty(context), unit_i32);
        }
        unit_i32 = llvm_utils->convert_kind(unit_i32, llvm::Type::getInt32Ty(context));

        std::string seek_name = "_lfortran_seek_record";
        llvm::Function *seek_fn = module->getFunction(seek_name);
        if (!seek_fn) {
            llvm::FunctionType *seek_ft = llvm::FunctionType::get(
                llvm::Type::getVoidTy(context),
                { llvm::Type::getInt32Ty(context), llvm::Type::getInt32Ty(context),
                  llvm::Type::getInt32Ty(context)->getPointerTo() },
                false);
            seek_fn = llvm::Function::Create(seek_ft, llvm::Function::ExternalLinkage, seek_name, module.get());
        }
        builder->CreateCall(seek_fn, { unit_i32, rec_val, iostat });
    }

    void emit_seek_record_from_rec(ASR::expr_t* rec_expr, llvm::Value* unit_val, llvm::Value* iostat) {
        int ptr_copy_rec = ptr_loads;
        ptr_loads = 0;
        this->visit_expr_wrapper(rec_expr, true);
        ptr_loads = ptr_copy_rec;

        llvm::Value *rec_val = tmp;
        if (rec_val->getType()->isPointerTy()) {
            rec_val = llvm_utils->CreateLoad2(llvm::Type::getInt32Ty(context), rec_val);
        }
        rec_val = llvm_utils->convert_kind(rec_val, llvm::Type::getInt32Ty(context));

        emit_seek_record(unit_val, rec_val, iostat);
    }

    void emit_single_formatted_read(ASR::expr_t *val_expr, llvm::Value *unit_val,
                                    llvm::Value *iostat, llvm::Value *read_size,
                                    llvm::Value *advance, llvm::Value *advance_len,
                                    llvm::Value *fmt_data, llvm::Value *fmt_len,
                                    llvm::Value *pad_data, llvm::Value *pad_len,
                                    bool is_string, const ASR::FileRead_t &x) {
        ASR::ttype_t *expr_type_full = ASRUtils::expr_type(val_expr);
        ASR::ttype_t *val_type = ASRUtils::type_get_past_array(
            ASRUtils::type_get_past_allocatable_pointer(expr_type_full));

        int ptr_loads_copy = ptr_loads;
        ptr_loads = 0;
        bool is_assignment_target_copy = is_assignment_target;
        is_assignment_target = true;
        this->visit_expr(*val_expr);
        is_assignment_target = is_assignment_target_copy;
        llvm::Value *var_ptr = tmp;
        ptr_loads = ptr_loads_copy;

        std::vector<llvm::Value *> single_args;
        bool is_string_array = false;
        if (is_string) {
            ASR::ttype_t* unit_type = ASRUtils::expr_type(x.m_unit);
            is_string_array = ASRUtils::is_array(unit_type);
            llvm::Value *src_data, *src_len;
            std::tie(src_data, src_len) = llvm_utils->get_string_length_data(
                ASRUtils::get_string_type(x.m_unit), unit_val);
            single_args.push_back(src_data);
            single_args.push_back(src_len);
            if (is_string_array) {
                ASR::dimension_t* dims = nullptr;
                size_t n_dims = ASRUtils::extract_dimensions_from_ttype(unit_type, dims);
                LCOMPILERS_ASSERT(n_dims >= 1);
                int64_t n_elems_val = ASRUtils::get_fixed_size_of_array(dims, n_dims);
                single_args.push_back(llvm::ConstantInt::get(
                    llvm::Type::getInt64Ty(context), llvm::APInt(64, n_elems_val)));
            }
        } else {
            single_args.push_back(unit_val);
        }
        single_args.push_back(iostat);
        single_args.push_back(read_size);
        single_args.push_back(advance);
        single_args.push_back(advance_len);
        single_args.push_back(fmt_data);
        single_args.push_back(fmt_len);

        if (ASRUtils::is_array(expr_type_full) && ASRUtils::is_fixed_size_array(expr_type_full)) {
            // Fixed-size array: unroll into N scalar args (is_descriptor_array=0 each)
            int64_t array_size = ASRUtils::get_fixed_size_of_array(expr_type_full);
            int64_t single_arg_count = array_size * get_formatted_read_arg_count(val_type);
            single_args.push_back(llvm::ConstantInt::get(context, llvm::APInt(32, single_arg_count)));
            single_args.push_back(pad_data);
            single_args.push_back(pad_len);
            
            ASR::Array_t *arr_type = ASR::down_cast<ASR::Array_t>(
                ASRUtils::type_get_past_allocatable_pointer(expr_type_full));
            llvm::Type *llvm_arr_type = llvm_utils->get_type_from_ttype_t_util(
                val_expr, ASRUtils::type_get_past_allocatable_pointer(expr_type_full), module.get());
            llvm::Type *llvm_elem_type = llvm_utils->get_type_from_ttype_t_util(
                val_expr, val_type, module.get());

            for (int64_t elem_idx = 0; elem_idx < array_size; elem_idx++) {
                llvm::Value *elem_ptr;
                if (arr_type->m_physical_type == ASR::array_physical_typeType::FixedSizeArray) {
                    elem_ptr = builder->CreateGEP(llvm_arr_type, var_ptr,
                                                  {llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0),
                                                   llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), elem_idx)});
                } else if (ASR::is_a<ASR::String_t>(*val_type)) {
                    ASR::String_t* str_type = ASRUtils::get_string_type(val_type);
                    elem_ptr = llvm_utils->get_string_element_in_array(
                        str_type, var_ptr,
                        llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), elem_idx));
                } else {
                    llvm::Value *data_ptr = arr_descr->get_pointer_to_data(llvm_arr_type, var_ptr);
                    data_ptr = llvm_utils->CreateLoad2(llvm_elem_type->getPointerTo(), data_ptr);
                    elem_ptr = builder->CreateGEP(llvm_elem_type, data_ptr,
                                                  llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), elem_idx));
                }
                add_formatted_read_arg(single_args, val_type, elem_ptr);
            }
        } else if (ASRUtils::is_array(expr_type_full)) {
            // DescriptorArray target: push is_descriptor_array=1, elem_tc, data_ptr, n_elems, stride
            single_args.push_back(pad_data);
            single_args.push_back(pad_len);
            arr_descr->push_descriptor_array_args(val_expr, expr_type_full, 
                    val_type, var_ptr, module.get(), single_args);
        } else {
            // Scalar: one arg with is_descriptor_array=0 (pushed inside add_formatted_read_arg)
            single_args.push_back(llvm::ConstantInt::get(context, llvm::APInt(32,
                get_formatted_read_arg_count(val_type))));
            single_args.push_back(pad_data);
            single_args.push_back(pad_len);
            add_formatted_read_arg(single_args, val_type, var_ptr);
        }

        std::string runtime_func_name;
        if (is_string_array) {
            runtime_func_name = "_lfortran_string_array_formatted_read";
        } else if (is_string) {
            runtime_func_name = "_lfortran_string_formatted_read";
        } else {
            runtime_func_name = "_lfortran_formatted_read";
        }
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            std::vector<llvm::Type *> param_types;
            if (is_string_array) {
                param_types.push_back(character_type);
                param_types.push_back(llvm::Type::getInt64Ty(context));
                param_types.push_back(llvm::Type::getInt64Ty(context));
            } else if (is_string) {
                param_types.push_back(character_type);
                param_types.push_back(llvm::Type::getInt64Ty(context));
            } else {
                param_types.push_back(llvm::Type::getInt32Ty(context));
            }
            param_types.push_back(llvm::Type::getInt32Ty(context)->getPointerTo());
            param_types.push_back(llvm::Type::getInt32Ty(context)->getPointerTo());
            param_types.push_back(character_type);
            param_types.push_back(llvm::Type::getInt64Ty(context));
            param_types.push_back(character_type);
            param_types.push_back(llvm::Type::getInt64Ty(context));
            param_types.push_back(llvm::Type::getInt32Ty(context));
            param_types.push_back(character_type);
            param_types.push_back(llvm::Type::getInt64Ty(context));

            llvm::FunctionType *function_type = llvm::FunctionType::get(
                llvm::Type::getVoidTy(context), param_types, true);
            fn = llvm::Function::Create(function_type, llvm::Function::ExternalLinkage,
                                        runtime_func_name, module.get());
        }
        builder->CreateCall(fn, single_args);
    }

    void emit_formatted_read_with_idl(const ASR::FileRead_t &x, llvm::Value *unit_val,
            llvm::Value *iostat, llvm::Value *read_size, llvm::Value *advance,
            llvm::Value *advance_length, llvm::Value *pad_data, llvm::Value *pad_len,
            bool is_string, ASR::expr_t* fmt_expr) {
        (void)iostat;
        (void)advance;
        (void)advance_length;
        ASR::expr_t** values = x.m_values;
        size_t n_values = x.n_values;

        llvm::Value* fmt_data;
        llvm::Value* fmt_len;
        std::tie(fmt_data, fmt_len) = get_string_data_and_length(fmt_expr);

        llvm::Constant *no_advance_const = llvm::ConstantDataArray::getString(context, "NO", false);
        llvm::GlobalVariable *no_advance_global = new llvm::GlobalVariable(
            *module,
            no_advance_const->getType(),
            true,
            llvm::GlobalValue::PrivateLinkage,
            no_advance_const,
            ".str.no_advance_idl"
        );
        no_advance_global->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
        no_advance_global->setAlignment(llvm::Align(1));
        llvm::Value* no_advance_str = builder->CreateBitCast(no_advance_global, character_type);
        llvm::Value* no_advance_len = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 2);

        llvm::AllocaInst* loop_iostat = builder->CreateAlloca(llvm::Type::getInt32Ty(context), nullptr, "loop_iostat");
        builder->CreateStore(llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0), loop_iostat);

        for (size_t i = 0; i < n_values; i++) {
            ASR::expr_t* val_expr = values[i];

            if (ASR::is_a<ASR::ImpliedDoLoop_t>(*val_expr)) {
                ASR::ImpliedDoLoop_t* idl = ASR::down_cast<ASR::ImpliedDoLoop_t>(val_expr);

                int ptr_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr_wrapper(idl->m_start, true);
                ptr_loads = ptr_copy;
                llvm::Value* loop_start = tmp;
                if (loop_start->getType()->isPointerTy()) {
                    ASR::ttype_t* t = ASRUtils::expr_type(idl->m_start);
                    llvm::Type* ty = llvm_utils->get_type_from_ttype_t_util(idl->m_start, t, module.get());
                    loop_start = llvm_utils->CreateLoad2(ty, loop_start);
                }
                loop_start = llvm_utils->convert_kind(loop_start, llvm::Type::getInt64Ty(context));

                ptr_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr_wrapper(idl->m_end, true);
                ptr_loads = ptr_copy;
                llvm::Value* loop_end = tmp;
                if (loop_end->getType()->isPointerTy()) {
                    ASR::ttype_t* t = ASRUtils::expr_type(idl->m_end);
                    llvm::Type* ty = llvm_utils->get_type_from_ttype_t_util(idl->m_end, t, module.get());
                    loop_end = llvm_utils->CreateLoad2(ty, loop_end);
                }
                loop_end = llvm_utils->convert_kind(loop_end, llvm::Type::getInt64Ty(context));

                llvm::Value* loop_step = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 1);
                if (idl->m_increment) {
                    ptr_copy = ptr_loads;
                    ptr_loads = 0;
                    this->visit_expr_wrapper(idl->m_increment, true);
                    ptr_loads = ptr_copy;
                    loop_step = tmp;
                    if (loop_step->getType()->isPointerTy()) {
                        ASR::ttype_t* t = ASRUtils::expr_type(idl->m_increment);
                        llvm::Type* ty = llvm_utils->get_type_from_ttype_t_util(idl->m_increment, t, module.get());
                        loop_step = llvm_utils->CreateLoad2(ty, loop_step);
                    }
                    loop_step = llvm_utils->convert_kind(loop_step, llvm::Type::getInt64Ty(context));
                }

                llvm::AllocaInst* loop_var_alloca = builder->CreateAlloca(
                    llvm::Type::getInt64Ty(context), nullptr);
                builder->CreateStore(loop_start, loop_var_alloca);

                llvm::Function* fn_ptr = builder->GetInsertBlock()->getParent();
                llvm::BasicBlock* loop_cond_bb = llvm::BasicBlock::Create(context, "idl_cond", fn_ptr);
                llvm::BasicBlock* loop_body_bb = llvm::BasicBlock::Create(context, "idl_body", fn_ptr);
                llvm::BasicBlock* loop_end_bb = llvm::BasicBlock::Create(context, "idl_end", fn_ptr);

                builder->CreateBr(loop_cond_bb);
                builder->SetInsertPoint(loop_cond_bb);

                llvm::Value* cur = builder->CreateLoad(llvm::Type::getInt64Ty(context), loop_var_alloca);
                llvm::Value* is_pos = builder->CreateICmpSGT(loop_step, 
                    llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0));
                llvm::Value* cond_pos = builder->CreateICmpSLE(cur, loop_end);
                llvm::Value* cond_neg = builder->CreateICmpSGE(cur, loop_end);
                llvm::Value* cond = builder->CreateSelect(is_pos, cond_pos, cond_neg);
                builder->CreateCondBr(cond, loop_body_bb, loop_end_bb);

                builder->SetInsertPoint(loop_body_bb);

                for (size_t j = 0; j < idl->n_values; ++j) {
                    ASR::expr_t* idl_val = idl->m_values[j];
                    if (ASR::is_a<ASR::ImpliedDoLoop_t>(*idl_val)) {
                        throw CodeGenError("Nested implied-do in formatted READ not yet supported");
                    }

                    llvm::Value* cur_val = builder->CreateLoad(llvm::Type::getInt64Ty(context), loop_var_alloca);
                    llvm::Type* loop_var_type = llvm_utils->get_type_from_ttype_t_util(
                        idl->m_var, ASRUtils::expr_type(idl->m_var), module.get());
                    cur_val = llvm_utils->convert_kind(cur_val, loop_var_type);

                    ASR::symbol_t* loop_var_sym = ASR::down_cast<ASR::Var_t>(idl->m_var)->m_v;
                    llvm::AllocaInst* temp_loop_var = builder->CreateAlloca(loop_var_type, nullptr);
                    builder->CreateStore(cur_val, temp_loop_var);

                    uint64_t var_hash = (uint64_t)loop_var_sym;
                    llvm::Value* prev_symtab_val = nullptr;
                    auto it = llvm_symtab.find(var_hash);
                    if (it != llvm_symtab.end()) {
                        prev_symtab_val = it->second;
                    }
                    llvm_symtab[var_hash] = temp_loop_var;

                    {
                        int ptr_copy_inner = ptr_loads;
                        ptr_loads = 0;
                        bool iat_copy_inner = is_assignment_target;
                        is_assignment_target = true;
                        this->visit_expr(*idl->m_var);
                        llvm::Value* real_var_ptr = tmp;
                        is_assignment_target = iat_copy_inner;
                        ptr_loads = ptr_copy_inner;

                        if (real_var_ptr && real_var_ptr->getType()->isPointerTy()) {
                             builder->CreateStore(cur_val, real_var_ptr);
                        }
                    }

                    emit_single_formatted_read(idl_val, unit_val, loop_iostat, read_size,
                                               no_advance_str, no_advance_len,
                                               fmt_data, fmt_len, pad_data, pad_len,
                                               is_string, x);

                    if (prev_symtab_val) {
                        llvm_symtab[var_hash] = prev_symtab_val;
                    } else {
                        llvm_symtab.erase(var_hash);
                    }
                }

                cur = builder->CreateLoad(llvm::Type::getInt64Ty(context), loop_var_alloca);
                cur = builder->CreateAdd(cur, loop_step);
                builder->CreateStore(cur, loop_var_alloca);
                builder->CreateBr(loop_cond_bb);

                builder->SetInsertPoint(loop_end_bb);
                if (compiler_options.po.use_loop_variable_after_loop) {
                    llvm::Value* cur_val = builder->CreateLoad(
                        llvm::Type::getInt64Ty(context), loop_var_alloca);
                    llvm::Type* loop_var_type = llvm_utils->get_type_from_ttype_t_util(
                        idl->m_var, ASRUtils::expr_type(idl->m_var), module.get());
                    cur_val = llvm_utils->convert_kind(cur_val, loop_var_type);

                    int ptr_copy_inner = ptr_loads;
                    ptr_loads = 0;
                    bool iat_copy_inner = is_assignment_target;
                    is_assignment_target = true;
                    this->visit_expr(*idl->m_var);
                    llvm::Value* real_var_ptr = tmp;
                    is_assignment_target = iat_copy_inner;
                    ptr_loads = ptr_copy_inner;

                    if (real_var_ptr && real_var_ptr->getType()->isPointerTy()) {
                        builder->CreateStore(cur_val, real_var_ptr);
                    }
                }
            } else {
                emit_single_formatted_read(val_expr, unit_val, loop_iostat, read_size,
                                           no_advance_str, no_advance_len,
                                           fmt_data, fmt_len, pad_data, pad_len,
                                           is_string, x);
            }
        }
    }

    void emit_formatted_read(const ASR::FileRead_t &x, llvm::Value *unit_val,
            llvm::Value *iostat, llvm::Value *read_size, llvm::Value *advance,
            llvm::Value *advance_length, bool is_string) {
        ASR::expr_t* fmt_expr = x.m_fmt;
        ASR::expr_t** values = x.m_values;
        size_t n_values = x.n_values;

        llvm::Value* pad_data = llvm::Constant::getNullValue(character_type);
        llvm::Value* pad_len = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        if (x.m_pad) {
            std::tie(pad_data, pad_len) = get_string_data_and_length(x.m_pad);
        }

        if (ASR::is_a<ASR::IntegerConstant_t>(*fmt_expr) &&
                n_values == 1 && ASR::is_a<ASR::StringFormat_t>(*values[0])) {
            ASR::StringFormat_t* sf = ASR::down_cast<ASR::StringFormat_t>(values[0]);
            fmt_expr = sf->m_fmt;
            values = sf->m_args;
            n_values = sf->n_args;
        }

        bool has_implied_do = false;
        for (size_t i = 0; i < n_values; i++) {
            if (ASR::is_a<ASR::ImpliedDoLoop_t>(*values[i])) {
                has_implied_do = true;
                break;
            }
        }

        if (has_implied_do) {
            if (is_string) {
                // For internal string reads, we cannot use the per-iteration IDL
                // approach because each call to _lfortran_string_formatted_read
                // creates a new InputSource that resets the read position to 0.
                // Instead, extract the underlying arrays from IDL patterns like
                // (array(i), i=1,N) and pass them directly through the non-IDL
                // path, which makes a single runtime call.
                Vec<ASR::expr_t*> expanded_values;
                expanded_values.reserve(al, n_values);
                bool can_expand = true;
                for (size_t i = 0; i < n_values; i++) {
                    if (ASR::is_a<ASR::ImpliedDoLoop_t>(*values[i])) {
                        ASR::ImpliedDoLoop_t* idl = ASR::down_cast<ASR::ImpliedDoLoop_t>(values[i]);
                        // Check for simple pattern: (array(i), i=start, end)
                        // where array is a 1D fixed-size array and loop covers
                        // the entire array.
                        if (idl->n_values == 1 && ASR::is_a<ASR::ArrayItem_t>(*idl->m_values[0])) {
                            ASR::ArrayItem_t* arr_item = ASR::down_cast<ASR::ArrayItem_t>(idl->m_values[0]);
                            if (arr_item->n_args == 1) {
                                // Replace with the array variable itself
                                expanded_values.push_back(al, arr_item->m_v);
                                continue;
                            }
                        }
                        can_expand = false;
                        break;
                    } else {
                        expanded_values.push_back(al, values[i]);
                    }
                }
                if (can_expand) {
                    // Emit final loop variable assignments: i = end + step
                    // to match Fortran semantics for implied-do loops.
                    for (size_t i = 0; i < n_values; i++) {
                        if (ASR::is_a<ASR::ImpliedDoLoop_t>(*values[i])) {
                            ASR::ImpliedDoLoop_t* idl = ASR::down_cast<ASR::ImpliedDoLoop_t>(values[i]);
                            // Get pointer to loop variable
                            int ptr_copy = ptr_loads;
                            ptr_loads = 0;
                            this->visit_expr_wrapper(idl->m_var, false);
                            llvm::Value* loop_var_ptr = tmp;
                            ptr_loads = ptr_copy;
                            // Compute end + step
                            this->visit_expr_wrapper(idl->m_end, true);
                            llvm::Value* end_val = tmp;
                            llvm::Value* step_val;
                            if (idl->m_increment) {
                                this->visit_expr_wrapper(idl->m_increment, true);
                                step_val = tmp;
                            } else {
                                step_val = llvm::ConstantInt::get(
                                    end_val->getType(), 1);
                            }
                            llvm::Value* final_val = builder->CreateAdd(end_val, step_val);
                            builder->CreateStore(final_val, loop_var_ptr);
                        }
                    }
                    values = expanded_values.p;
                    n_values = expanded_values.size();
                    has_implied_do = false;
                    // Fall through to the non-IDL path below
                }
            }
            if (has_implied_do) {
                ASR::FileRead_t x2 = x;
                x2.m_values = values;
                x2.n_values = n_values;
                emit_formatted_read_with_idl(x2, unit_val, iostat, read_size, advance, advance_length, pad_data, pad_len, is_string, fmt_expr);
                return;
            }
        }

        size_t total_scalar_values = 0;
        for (size_t i = 0; i < n_values; i++) {
            ASR::ttype_t* expr_type = ASRUtils::expr_type(values[i]);
            ASR::ttype_t* val_type = ASRUtils::type_get_past_array(
                ASRUtils::type_get_past_allocatable_pointer(expr_type));
            int64_t arg_count = get_formatted_read_arg_count(val_type);
            if (ASRUtils::is_array(expr_type) && ASRUtils::is_fixed_size_array(expr_type)) {
                total_scalar_values += ASRUtils::get_fixed_size_of_array(expr_type) * arg_count;
            } else {
                // scalars and descriptor arrays each count as one read target
                total_scalar_values += ASRUtils::is_array(expr_type) ? 1 : arg_count;
            }
        }

        std::vector<llvm::Value*> args;
        args.reserve(8 + 3 * total_scalar_values);
        bool is_string_array = false;
        if (is_string) {
            ASR::ttype_t* unit_type = ASRUtils::expr_type(x.m_unit);
            is_string_array = ASRUtils::is_array(unit_type);
            llvm::Value *src_data, *src_len;
            if (is_string_array) {
                ASR::ttype_t* array_type = ASRUtils::type_get_past_allocatable_pointer(unit_type);
                if (ASRUtils::extract_physical_type(array_type) == ASR::DescriptorArray) {
                    llvm::Type* llvm_array_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_unit, array_type, module.get());
                    if (ASRUtils::is_allocatable_or_pointer(unit_type)) {
                        unit_val = llvm_utils->CreateLoad2(
                            llvm_array_type->getPointerTo(), unit_val);
                    }
                }
                src_data = llvm_utils->get_stringArray_data(array_type, unit_val);
                src_len = llvm_utils->get_stringArray_length(array_type, unit_val);
                args.push_back(src_data);
                args.push_back(src_len);

                ASR::Array_t* array_t = ASR::down_cast<ASR::Array_t>(array_type);
                llvm::Value* n_elems;
                if (array_t->m_physical_type == ASR::array_physical_typeType::DescriptorArray) {
                    llvm::Type* llvm_array_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_unit, array_type, module.get());
                    n_elems = arr_descr->get_array_size(llvm_array_type, unit_val, nullptr, 4);
                    n_elems = llvm_utils->convert_kind(n_elems, llvm::Type::getInt64Ty(context));
                } else {
                    ASR::dimension_t* dims = nullptr;
                    size_t n_dims = ASRUtils::extract_dimensions_from_ttype(array_type, dims);
                    LCOMPILERS_ASSERT(n_dims >= 1);
                    int64_t n_elems_val = ASRUtils::get_fixed_size_of_array(dims, n_dims);
                    n_elems = llvm::ConstantInt::get(
                        llvm::Type::getInt64Ty(context), llvm::APInt(64, n_elems_val));
                }
                args.push_back(n_elems);
            } else {
                std::tie(src_data, src_len) = llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_unit), unit_val);
                args.push_back(src_data);
                args.push_back(src_len);
            }
        } else {
            args.push_back(unit_val);
        }
        args.push_back(iostat);
        args.push_back(read_size);
        args.push_back(advance);
        args.push_back(advance_length);
        llvm::Value* fmt_data;
        llvm::Value* fmt_len;
        std::tie(fmt_data, fmt_len) = get_string_data_and_length(fmt_expr);
        args.push_back(fmt_data);
        args.push_back(fmt_len);

        args.push_back(llvm::ConstantInt::get(context, llvm::APInt(32, total_scalar_values)));
        args.push_back(pad_data);
        args.push_back(pad_len);

        for (size_t i = 0; i < n_values; i++) {
            ASR::expr_t* val_expr = values[i];
            ASR::ttype_t* expr_type_full = ASRUtils::expr_type(val_expr);
            ASR::ttype_t* val_type = ASRUtils::type_get_past_array(
                ASRUtils::type_get_past_allocatable_pointer(expr_type_full));

            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            bool is_assignment_target_copy = is_assignment_target;
            is_assignment_target = true;
            this->visit_expr(*val_expr);
            is_assignment_target = is_assignment_target_copy;
            llvm::Value* var_ptr = tmp;
            ptr_loads = ptr_loads_copy;

            // For equivalenced variables (Pointer-typed Var), var_ptr is a
            // pointer to the pointer. Load it to get the actual data address.
            // Skip this for descriptor arrays (e.g., array section pointers)
            // which are handled by push_descriptor_array_args.
            if (ASR::is_a<ASR::Var_t>(*val_expr) &&
                ASR::is_a<ASR::Pointer_t>(*ASRUtils::type_get_past_array(expr_type_full)) &&
                !(ASRUtils::is_array(expr_type_full) && !ASRUtils::is_fixed_size_array(expr_type_full))) {
                llvm::Type* llvm_val_type = llvm_utils->get_type_from_ttype_t_util(
                    val_expr, val_type, module.get());
                var_ptr = llvm_utils->CreateLoad2(llvm_val_type->getPointerTo(), var_ptr);
            }

            if (ASRUtils::is_array(expr_type_full) && ASRUtils::is_fixed_size_array(expr_type_full)) {
                int64_t array_size = ASRUtils::get_fixed_size_of_array(expr_type_full);
                ASR::Array_t* arr_type = ASR::down_cast<ASR::Array_t>(
                    ASRUtils::type_get_past_allocatable_pointer(expr_type_full));
                llvm::Type* llvm_arr_type = llvm_utils->get_type_from_ttype_t_util(
                    val_expr, ASRUtils::type_get_past_allocatable_pointer(expr_type_full), module.get());
                llvm::Type* llvm_elem_type = llvm_utils->get_type_from_ttype_t_util(
                    val_expr, val_type, module.get());

                for (int64_t elem_idx = 0; elem_idx < array_size; elem_idx++) {
                    llvm::Value* elem_ptr;
                    if (arr_type->m_physical_type == ASR::array_physical_typeType::FixedSizeArray) {
                        elem_ptr = builder->CreateGEP(llvm_arr_type, var_ptr,
                            {llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0),
                             llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), elem_idx)});
                    } else if (ASR::is_a<ASR::String_t>(*val_type)) {
                        ASR::String_t* str_type = ASRUtils::get_string_type(val_type);
                        elem_ptr = llvm_utils->get_string_element_in_array(
                            str_type, var_ptr,
                            llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), elem_idx));
                    } else {
                        llvm::Value* data_ptr = arr_descr->get_pointer_to_data(llvm_arr_type, var_ptr);
                        data_ptr = llvm_utils->CreateLoad2(llvm_elem_type->getPointerTo(), data_ptr);
                        elem_ptr = builder->CreateGEP(llvm_elem_type, data_ptr,
                            llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), elem_idx));
                    }
                    add_formatted_read_arg(args, val_type, elem_ptr);
                }
            } else if (ASRUtils::is_array(expr_type_full)) {
                // DescriptorArray target: push is_descriptor_array=1, elem_tc, 
                // data_ptr, n_elems, stride
                arr_descr->push_descriptor_array_args(val_expr, expr_type_full, 
                            val_type, var_ptr, module.get(), args);
            } else {
                add_formatted_read_arg(args, val_type, var_ptr);
            }
        }

        std::string runtime_func_name;
        llvm::Function *fn;

        if (is_string_array) {
            runtime_func_name = "_lfortran_string_array_formatted_read";
            fn = module->getFunction(runtime_func_name);
            if (!fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context), {
                            character_type,                                  // data
                            llvm::Type::getInt64Ty(context),                 // elem_len
                            llvm::Type::getInt64Ty(context),                 // n_elems
                llvm::Type::getInt32Ty(context)->getPointerTo(), // Iostat
                llvm::Type::getInt32Ty(context)->getPointerTo(), // Chunk
                character_type,                                  // advance
                llvm::Type::getInt64Ty(context),                 // advance_length
                character_type,                                  // fmt
                llvm::Type::getInt64Ty(context),                 // fmt_len
                llvm::Type::getInt32Ty(context),                 // no_of_args
                character_type,                                  // pad_data
                llvm::Type::getInt64Ty(context)                  // pad_len
                }, true);
                fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, runtime_func_name, module.get());
            }
        } else if (is_string) {
            runtime_func_name = "_lfortran_string_formatted_read";
            fn = module->getFunction(runtime_func_name);
            if (!fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context), {
                            character_type,                                  // src_data
                            llvm::Type::getInt64Ty(context),                 // src_len
                llvm::Type::getInt32Ty(context)->getPointerTo(), // Iostat
                llvm::Type::getInt32Ty(context)->getPointerTo(), // Chunk
                character_type,                                  // advance
                llvm::Type::getInt64Ty(context),                 // advance_length
                character_type,                                  // fmt
                llvm::Type::getInt64Ty(context),                 // fmt_len
                llvm::Type::getInt32Ty(context),                 // no_of_args
                character_type,                                  // pad_data
                llvm::Type::getInt64Ty(context)                  // pad_len
                }, true);
                fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, runtime_func_name, module.get());
            }
        } else {
            // File read
            runtime_func_name = "_lfortran_formatted_read";
            fn = module->getFunction(runtime_func_name);
            if (!fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context), {
                            llvm::Type::getInt32Ty(context),                 // Unit
                            llvm::Type::getInt32Ty(context)->getPointerTo(), // Iostat
                            llvm::Type::getInt32Ty(context)->getPointerTo(), // Chunk
                            character_type,                                  // advance
                            llvm::Type::getInt64Ty(context),                 // advance_length
                            character_type,                                  // fmt
                            llvm::Type::getInt64Ty(context),                 // fmt_len
                            llvm::Type::getInt32Ty(context),                 // no_of_args
                            character_type,                                  // pad_data
                            llvm::Type::getInt64Ty(context)                  // pad_len
                        }, true);
                fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, runtime_func_name, module.get());
            }
        }

        builder->CreateCall(fn, args);
    }

    void visit_FileOpen(const ASR::FileOpen_t &x) {
        llvm::Value *unit_val = nullptr;
        llvm::Value* f_name_data{}, *f_name_len{};
        llvm::Value *status_data{}, *status_len{};
        llvm::Value *form_data{},   *form_len{};
        llvm::Value *access_data{}, *access_len{};
        llvm::Value *iomsg_data{},  *iomsg_len{};
        llvm::Value *iostat{};
        llvm::Value *action{}, *action_len{};
        llvm::Value *delim{}, *delim_len{};
        llvm::Value *position{}, *position_len{};
        llvm::Value *blank{}, *blank_len{};
        llvm::Value *recl{};
        llvm::Value *encoding_data{}, *encoding_len{};
        llvm::Value *sign_data{}, *sign_len{};
        llvm::Value *decimal_data{}, *decimal_len{};
        llvm::Value *round_data{}, *round_len{};
        llvm::Value *pad_data{}, *pad_len{};

        this->visit_expr_wrapper(x.m_newunit, true);
        unit_val = llvm_utils->convert_kind(tmp, llvm::Type::getInt32Ty(context));
        int ptr_copy = ptr_loads;
        if (x.m_filename) {
            std::tie(f_name_data, f_name_len) = get_string_data_and_length(x.m_filename);
        } else {
            f_name_data = llvm::Constant::getNullValue(character_type);
            f_name_len = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        }
        if (x.m_status) {
            std::tie(status_data, status_len) = get_string_data_and_length(x.m_status);
        } else {
            status_data = llvm::Constant::getNullValue(character_type);
            status_len = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        }
        if (x.m_form) {
            std::tie(form_data, form_len) = get_string_data_and_length(x.m_form);
        } else {
            form_data = llvm::Constant::getNullValue(character_type);
            form_len = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        }
        if (x.m_access) {
            std::tie(access_data, access_len) = get_string_data_and_length(x.m_access);
        } else {
            access_data = llvm::Constant::getNullValue(character_type);
            access_len = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        }
        if (x.m_iostat) {
            int ptr_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_iostat, false);
            ptr_loads = ptr_copy;
            iostat = tmp;
        } else {
            iostat = llvm::ConstantInt::getNullValue(llvm::Type::getInt32Ty(context)->getPointerTo());
        }
        if (x.m_iomsg) {
            std::tie(iomsg_data, iomsg_len) = get_string_data_and_length(x.m_iomsg);
        } else {
            iomsg_data = llvm::Constant::getNullValue(character_type);
            iomsg_len = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        } if (x.m_action) {
            std::tie(action, action_len) = get_string_data_and_length(x.m_action);
        } else {
            action = llvm::Constant::getNullValue(character_type);
            action_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        } if (x.m_delim) {
            std::tie(delim, delim_len) = get_string_data_and_length(x.m_delim);
        } else {
            delim = llvm::Constant::getNullValue(character_type);
            delim_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        } if (x.m_position) {
            std::tie(position, position_len) = get_string_data_and_length(x.m_position);
        } else {
            position = llvm::Constant::getNullValue(character_type);
            position_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        } if (x.m_blank) {
            std::tie(blank, blank_len) = get_string_data_and_length(x.m_blank);
        } else {
            blank = llvm::Constant::getNullValue(character_type);
            blank_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }
        recl = llvm::ConstantPointerNull::get(
            llvm::Type::getInt32Ty(context)->getPointerTo());

        if (x.m_recl) {                                      
            this->visit_expr_wrapper(x.m_recl, true);       
            llvm::Value *recl_val = llvm_utils->convert_kind(
                tmp, llvm::Type::getInt32Ty(context));
            llvm::AllocaInst *alloc =                        
                builder->CreateAlloca(llvm::Type::getInt32Ty(context));
            builder->CreateStore(recl_val, alloc);            
            recl = alloc;                                  
        } else {
            recl = llvm::ConstantPointerNull::get(
                llvm::Type::getInt32Ty(context)->getPointerTo());
        }
        if (x.m_encoding) {
            std::tie(encoding_data, encoding_len) = get_string_data_and_length(x.m_encoding);
        } else {
            encoding_data = llvm::Constant::getNullValue(character_type);
            encoding_len  = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        }
        if (x.m_sign) {
            std::tie(sign_data, sign_len) = get_string_data_and_length(x.m_sign);
        } else {
            sign_data = llvm::Constant::getNullValue(character_type);
            sign_len  = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        }
        if (x.m_decimal) {
            std::tie(decimal_data, decimal_len) = get_string_data_and_length(x.m_decimal);
        } else {
            decimal_data = llvm::Constant::getNullValue(character_type);
            decimal_len  = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        }
        if (x.m_round) {
            std::tie(round_data, round_len) = get_string_data_and_length(x.m_round);
        } else {
            round_data = llvm::Constant::getNullValue(character_type);
            round_len  = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        }
        if (x.m_pad) {
            std::tie(pad_data, pad_len) = get_string_data_and_length(x.m_pad);
        } else {
            pad_data = llvm::Constant::getNullValue(character_type);
            pad_len  = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        }

        ptr_loads = ptr_copy;
        std::string runtime_func_name = "_lfortran_open";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::Type *i64 = llvm::Type::getInt64Ty(context);
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    i64,
                    {llvm::Type::getInt32Ty(context),
                        character_type, i64, //f_name, f_name_length
                        character_type, i64, //status, status_len
                        character_type, i64, //form, form_length
                        character_type, i64, //access, access_len
                        character_type, i64, //iomsg, iomsg_len
                        llvm::Type::getInt32Ty(context)->getPointerTo(),
                        character_type, i64, // action, action_len
                        character_type, i64, // delim, delim_len
                        character_type, i64, // position, position_len
                        character_type, i64,  // blank, blank_len
                        character_type, i64, //encoding_data, encoding_len
                        llvm::Type::getInt32Ty(context)->getPointerTo(), // recl
                        character_type, i64,  // sign_data, sign_len
                        character_type, i64,  // decimal_data, decimal_len
                        character_type, i64,   // round_data, round_len
                        character_type, i64    // pad_data, pad_len
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        tmp = builder->CreateCall(fn, {unit_val,
            f_name_data, f_name_len,
            status_data, status_len,
            form_data, form_len,
            access_data, access_len,
            iomsg_data, iomsg_len,
            iostat,
            action, action_len,
            delim, delim_len,
            position, position_len,
            blank, blank_len,
            encoding_data, encoding_len,
            recl,
            sign_data, sign_len,
            decimal_data, decimal_len,
            round_data, round_len,
            pad_data, pad_len
        });
    }

    void visit_FileInquire(const ASR::FileInquire_t &x) {
        llvm::Value *exist_val {};
        llvm::Value* f_name_data {}, *f_name_len{};
        llvm::Value *unit {}, *opened_val {}, *size_val {}, *pos_val {};
        llvm::Value *write{}, *write_len{};
        llvm::Value *read{}, *read_len{};
        llvm::Value *readwrite{}, *readwrite_len{};
        llvm::Value *access_val{}, *access_len{};
        llvm::Value *name_val{}, *name_len{};
        llvm::Value *blank_val{}, *blank_len{};
        llvm::Value *delim_val{}, *delim_len{};
        llvm::Value *recl{}, *number{};
        llvm::Value *named{}, *sequential{}, *sequential_len{};
        llvm::Value *direct{}, *direct_len{};
        llvm::Value *form{}, *form_len{};
        llvm::Value *formatted{} , *formatted_len{};
        llvm::Value *unformatted{}, *unformatted_len{};
        llvm::Value *iostat{}, *nextrec{};
        llvm::Value *decimal_val{}, *decimal_len{};
        llvm::Value *sign_val{}, *sign_len{};
        llvm::Value *encoding_val{}, *encoding_len{};
        llvm::Value *stream_val{}, *stream_len{};
        llvm::Value *iomsg_val{}, *iomsg_len{};
        llvm::Value *round_val{}, *round_len{};
        llvm::Value *pad_val{}, *pad_len{};
        llvm::Value *pending_val{};
        llvm::Value *asynchronous_val{}, *asynchronous_len{};
        llvm::Value *action_val{}, *action_len{};
        llvm::Value *position_val{}, *position_len{};

        if (x.m_file) {
            std::tie(f_name_data, f_name_len) = get_string_data_and_length(x.m_file);
        } else {
            f_name_data = llvm::Constant::getNullValue(character_type);
            f_name_len = llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0);
        }
        llvm::Value *exist_actual = nullptr;
        if (x.m_exist) {
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_exist, true);
            exist_actual = tmp;
            ptr_loads = ptr_loads_copy;
        }
        exist_val = llvm_utils->CreateAlloca(*builder,
                        llvm::Type::getInt1Ty(context));

        if (x.m_unit) {
            this->visit_expr_wrapper(x.m_unit, true);
            unit = tmp;
        } else {
            unit = llvm::ConstantInt::get(
                    llvm::Type::getInt32Ty(context), llvm::APInt(32, -1, true));
        }
        llvm::Value *opened_actual = nullptr;
        if (x.m_opened) {
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_opened, true);
            opened_actual = tmp;
            ptr_loads = ptr_loads_copy;
        }
        opened_val = llvm_utils->CreateAlloca(*builder,
                        llvm::Type::getInt1Ty(context));

        llvm::Value *size_orig_ptr = nullptr;
        int size_kind = 4;
        if (x.m_size) {
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_size, false);
            ptr_loads = ptr_loads_copy;
            size_kind = ASRUtils::extract_kind_from_ttype_t(
                ASRUtils::expr_type(x.m_size));
            if (size_kind == 4) {
                size_val = tmp;
            } else {
                size_orig_ptr = tmp;
                size_val = llvm_utils->CreateAlloca(*builder,
                                llvm::Type::getInt32Ty(context));
            }
        } else {
            size_val = llvm_utils->CreateAlloca(*builder,
                            llvm::Type::getInt32Ty(context));
        }

        llvm::Value *pos_orig_ptr = nullptr;
        int pos_kind = 4;
        if (x.m_pos) {
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            // `pos=` is also an output parameter; see `size=` above.
            this->visit_expr_wrapper(x.m_pos, false);
            ptr_loads = ptr_loads_copy;
            pos_kind = ASRUtils::extract_kind_from_ttype_t(
                ASRUtils::expr_type(x.m_pos));
            if (pos_kind == 4) {
                pos_val = tmp;
            } else {
                pos_orig_ptr = tmp;
                pos_val = llvm_utils->CreateAlloca(*builder,
                                llvm::Type::getInt32Ty(context));
            }
        } else {
            pos_val = llvm_utils->CreateAlloca(*builder,
                            llvm::Type::getInt32Ty(context));
        }

        if (x.m_write) {
            this->visit_expr_load_wrapper(x.m_write, 0);
            std::tie(write, write_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_write),
                    tmp);
        } else {
            write = llvm::Constant::getNullValue(character_type);
            write_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_read) {
            this->visit_expr_load_wrapper(x.m_read, 0);
            std::tie(read, read_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_read),
                    tmp);
        } else {
            read = llvm::Constant::getNullValue(character_type);
            read_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_readwrite) {
            this->visit_expr_load_wrapper(x.m_readwrite, 0);
            std::tie(readwrite, readwrite_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_readwrite),
                    tmp);
        } else {
            readwrite = llvm::Constant::getNullValue(character_type);
            readwrite_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_access) {
            this->visit_expr_load_wrapper(x.m_access, 0);
            std::tie(access_val, access_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_access),
                    tmp);
        } else {
            access_val = llvm::Constant::getNullValue(character_type);
            access_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_name) {
            this->visit_expr_load_wrapper(x.m_name, 0);
            std::tie(name_val, name_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_name),
                    tmp);
        } else {
            name_val = llvm::Constant::getNullValue(character_type);
            name_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_blank) {
            this->visit_expr_load_wrapper(x.m_blank, 0);
            std::tie(blank_val, blank_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_blank),
                    tmp);
        } else {
            blank_val = llvm::Constant::getNullValue(character_type);
            blank_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }
        if (x.m_delim) {
            this->visit_expr_load_wrapper(x.m_delim, 0);
            std::tie(delim_val, delim_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_delim),
                    tmp);
        } else {
            delim_val = llvm::Constant::getNullValue(character_type);
            delim_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }
        llvm::Value *recl_orig_ptr = nullptr;
        int recl_kind = 4;
        if (x.m_recl) {
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_recl, false);
            ptr_loads = ptr_loads_copy;
            recl_kind = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(x.m_recl));
            if (recl_kind == 4) {
                recl = tmp;
            } else {
                recl_orig_ptr = tmp;
                recl = llvm_utils->CreateAlloca(*builder,llvm::Type::getInt32Ty(context));
            }
        } else {
            recl = llvm::ConstantPointerNull::get(
                llvm::Type::getInt32Ty(context)->getPointerTo());
        }

        if (x.n_iolength_vars > 0 && x.m_iolength) {
            // INQUIRE(IOLENGTH=iol) var_list
            // Computes the number of file storage units (bytes) required to store
            // the output list using unformatted sequential I/O
            llvm::Value* total_size = llvm::ConstantInt::get(
                llvm::Type::getInt64Ty(context), 0);
            
            for (size_t i = 0; i < x.n_iolength_vars; i++) {
                ASR::expr_t* var_expr = x.m_iolength_vars[i];
                ASR::ImpliedDoLoop_t* idl = nullptr;
                ASR::expr_t* size_expr = var_expr;
                if (ASR::is_a<ASR::ImpliedDoLoop_t>(*var_expr)) {
                    idl = ASR::down_cast<ASR::ImpliedDoLoop_t>(var_expr);
                    LCOMPILERS_ASSERT(idl->n_values > 0);
                    size_expr = idl->m_values[0];
                }
                ASR::ttype_t* var_type = ASRUtils::expr_type(size_expr);
                ASR::ttype_t* var_type_base = ASRUtils::type_get_past_array(
                    ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(var_type)));
                llvm::Value* var_size = nullptr;
                llvm::Value* elem_size = nullptr;

                if (ASR::is_a<ASR::String_t>(*var_type_base)) {
                    ASR::String_t *s = ASR::down_cast<ASR::String_t>(var_type_base);
                    this->visit_expr_wrapper(s->m_len, true);
                    elem_size = builder->CreateZExtOrTrunc(
                        tmp, llvm::Type::getInt64Ty(context));
                } else {
                    int64_t kind = ASRUtils::extract_kind_from_ttype_t(var_type_base);
                    if (ASR::is_a<ASR::Complex_t>(*var_type_base)) {
                        kind *= 2;
                    }
                    elem_size = llvm::ConstantInt::get(
                        llvm::Type::getInt64Ty(context), kind);
                }

                var_size = elem_size;

                if (ASRUtils::is_array(var_type)) {
                    ASR::ttype_t* int_type = ASRUtils::TYPE(
                        ASR::make_Integer_t(al, x.base.base.loc, 4));
                    visit_ArraySizeUtil(size_expr, int_type, nullptr, nullptr);
                    llvm::Value* num_elements = builder->CreateZExtOrTrunc(
                        tmp, llvm::Type::getInt64Ty(context));
                    var_size = builder->CreateMul(elem_size, num_elements);
                }
                if (idl) {
                    llvm::Type* int64_type = llvm::Type::getInt64Ty(context);
                    auto get_int64_value = [&](ASR::expr_t* expr) {
                        int ptr_copy = ptr_loads;
                        ptr_loads = 0;
                        this->visit_expr_wrapper(expr, true);
                        llvm::Value* value = tmp;
                        ptr_loads = ptr_copy;
                        if (value->getType()->isPointerTy()) {
                            ASR::ttype_t* t = ASRUtils::expr_type(expr);
                            llvm::Type* ty = llvm_utils->get_type_from_ttype_t_util(
                                expr, t, module.get());
                            value = llvm_utils->CreateLoad2(ty, value);
                        }
                        return llvm_utils->convert_kind(value, int64_type);
                    };

                    llvm::Value* loop_start = get_int64_value(idl->m_start);
                    llvm::Value* loop_end = get_int64_value(idl->m_end);
                    llvm::Value* loop_step = llvm::ConstantInt::get(int64_type, 1);
                    if (idl->m_increment) {
                        loop_step = get_int64_value(idl->m_increment);
                    }

                    // count = max(0, (end - start) / step + 1)
                    llvm::Value* zero = llvm::ConstantInt::get(int64_type, 0);
                    llvm::Value* one = llvm::ConstantInt::get(int64_type, 1);
                    llvm::Value* count = builder->CreateAdd(
                        builder->CreateSDiv(
                            builder->CreateSub(loop_end, loop_start), loop_step),
                        one);
                    count = builder->CreateSelect(
                        builder->CreateICmpSGT(count, zero), count, zero);

                    var_size = builder->CreateMul(var_size, count);

                    if (compiler_options.po.use_loop_variable_after_loop) {
                        llvm::Value* final_val = builder->CreateAdd(
                            loop_start, builder->CreateMul(count, loop_step));
                        llvm::Type* loop_var_type = llvm_utils->get_type_from_ttype_t_util(
                            idl->m_var, ASRUtils::expr_type(idl->m_var), module.get());
                        final_val = llvm_utils->convert_kind(final_val, loop_var_type);

                        int ptr_copy = ptr_loads;
                        ptr_loads = 0;
                        bool iat_copy = is_assignment_target;
                        is_assignment_target = true;
                        this->visit_expr(*idl->m_var);
                        llvm::Value* real_var_ptr = tmp;
                        is_assignment_target = iat_copy;
                        ptr_loads = ptr_copy;

                        if (real_var_ptr && real_var_ptr->getType()->isPointerTy()) {
                            builder->CreateStore(final_val, real_var_ptr);
                        }
                    }
                }

                total_size = builder->CreateAdd(total_size, var_size);
            }
            
            // Store the result in the iolength variable
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_iolength, false);
            ptr_loads = ptr_loads_copy;
            llvm::Value* iolength_ptr = tmp;
            ASR::ttype_t* iolength_type = ASRUtils::expr_type(x.m_iolength);
            int iolength_kind = ASRUtils::extract_kind_from_ttype_t(iolength_type);
            llvm::Type* iolength_llvm_type = llvm::Type::getIntNTy(
                context, iolength_kind * 8);
            llvm::Value* result = builder->CreateTrunc(total_size, iolength_llvm_type);
            builder->CreateStore(result, iolength_ptr);
            return;
        }

        if (x.m_number) {
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_number, true);
            number = tmp;
            ptr_loads = ptr_loads_copy;
        } else {
            number = llvm::ConstantPointerNull::get(
                llvm::Type::getInt32Ty(context)->getPointerTo());
        }

        llvm::Value *named_actual = nullptr;
        if (x.m_named) {
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_named, true);
            named_actual = tmp;
            ptr_loads = ptr_loads_copy;
        }
        named = llvm_utils->CreateAlloca(*builder,
                    llvm::Type::getInt1Ty(context));

        if (x.m_sequential) {
            this->visit_expr_load_wrapper(x.m_sequential, 0);
            std::tie(sequential, sequential_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_sequential),
                    tmp);
        } else {
            sequential = llvm::Constant::getNullValue(character_type);
            sequential_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_direct) {
            this->visit_expr_load_wrapper(x.m_direct, 0);
            std::tie(direct, direct_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_direct),
                    tmp);
        } else {
            direct = llvm::Constant::getNullValue(character_type);
            direct_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_form) {
            this->visit_expr_load_wrapper(x.m_form, 0);
            std::tie(form, form_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_form),
                    tmp);
        } else {
            form = llvm::Constant::getNullValue(character_type);
            form_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_formatted) {
            this->visit_expr_load_wrapper(x.m_formatted, 0);
            std::tie(formatted, formatted_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_formatted),
                    tmp);
        } else {
            formatted = llvm::Constant::getNullValue(character_type);
            formatted_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_unformatted) {
            this->visit_expr_load_wrapper(x.m_unformatted, 0);
            std::tie(unformatted, unformatted_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_unformatted),
                    tmp);
        } else {
            unformatted = llvm::Constant::getNullValue(character_type);
            unformatted_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_iostat) {
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_iostat, true);
            iostat = tmp;
            ptr_loads = ptr_loads_copy;
        } else {
            iostat = llvm::ConstantPointerNull::get(
                llvm::Type::getInt32Ty(context)->getPointerTo());
        }

        if (x.m_nextrec) {
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_nextrec, true);
            nextrec = tmp;
            ptr_loads = ptr_loads_copy;
        } else {
            nextrec = llvm::ConstantPointerNull::get(
                llvm::Type::getInt32Ty(context)->getPointerTo());
        }

        if (x.m_decimal) {
            this->visit_expr_load_wrapper(x.m_decimal, 0);
            std::tie(decimal_val, decimal_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_decimal),
                    tmp);
        } else {
            decimal_val = llvm::Constant::getNullValue(character_type);
            decimal_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_sign) {
            this->visit_expr_load_wrapper(x.m_sign, 0);
            std::tie(sign_val, sign_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_sign),
                    tmp);
        } else {
            sign_val = llvm::Constant::getNullValue(character_type);
            sign_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_encoding) {
            this->visit_expr_load_wrapper(x.m_encoding, 0);
            std::tie(encoding_val, encoding_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_encoding),
                    tmp);
        } else {
            encoding_val = llvm::Constant::getNullValue(character_type);
            encoding_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_stream) {
            this->visit_expr_load_wrapper(x.m_stream, 0);
            std::tie(stream_val, stream_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_stream),
                    tmp);
        } else {
            stream_val = llvm::Constant::getNullValue(character_type);
            stream_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_iomsg) {
            this->visit_expr_load_wrapper(x.m_iomsg, 0);
            std::tie(iomsg_val, iomsg_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_iomsg),
                    tmp);
        } else {
            iomsg_val = llvm::Constant::getNullValue(character_type);
            iomsg_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        if (x.m_round) {
            this->visit_expr_load_wrapper(x.m_round, 0);
            std::tie(round_val, round_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_round),
                    tmp);
        } else {
            round_val = llvm::Constant::getNullValue(character_type);
            round_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }
        if (x.m_pad) {
            this->visit_expr_load_wrapper(x.m_pad, 0);
            std::tie(pad_val, pad_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_pad),
                    tmp);
        } else {
            pad_val = llvm::Constant::getNullValue(character_type);
            pad_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }
        if (x.m_action) {
            this->visit_expr_load_wrapper(x.m_action, 0);
            std::tie(action_val, action_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_action),
                    tmp);
        } else {
            action_val = llvm::Constant::getNullValue(character_type);
            action_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        llvm::Value *pending_actual = nullptr;
        if (x.m_pending) {
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_pending, true);
            pending_actual = tmp;
            ptr_loads = ptr_loads_copy;
        }
        pending_val = llvm_utils->CreateAlloca(*builder,
                        llvm::Type::getInt1Ty(context));

        if (x.m_asynchronous) {
            this->visit_expr_load_wrapper(x.m_asynchronous, 0);
            std::tie(asynchronous_val, asynchronous_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_asynchronous),
                    tmp);
        } else {
            asynchronous_val = llvm::Constant::getNullValue(character_type);
            asynchronous_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }
        if (x.m_position) {
            this->visit_expr_load_wrapper(x.m_position, 0);
            std::tie(position_val, position_len) =
                llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_position),
                    tmp);
        } else {
            position_val = llvm::Constant::getNullValue(character_type);
            position_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        std::string runtime_func_name = "_lfortran_inquire";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        character_type, llvm::Type::getInt64Ty(context),
                        llvm::Type::getInt1Ty(context)->getPointerTo(),
                        llvm::Type::getInt32Ty(context),
                        llvm::Type::getInt1Ty(context)->getPointerTo(),
                        llvm::Type::getInt32Ty(context)->getPointerTo(),
                        llvm::Type::getInt32Ty(context)->getPointerTo(),
                        character_type, llvm::Type::getInt64Ty(context), // write_data, write_len
                        character_type, llvm::Type::getInt64Ty(context), // read_data, read_len
                        character_type, llvm::Type::getInt64Ty(context), // readwrite_data, readwrite_len
                        character_type, llvm::Type::getInt64Ty(context), // access_data, access_len
                        character_type, llvm::Type::getInt64Ty(context),  // name_data, name_len
                        character_type, llvm::Type::getInt64Ty(context),  // blank_data, blank_len
                        llvm::Type::getInt32Ty(context)->getPointerTo(), // recl
                        llvm::Type::getInt32Ty(context)->getPointerTo(), // number
                        llvm::Type::getInt1Ty(context)->getPointerTo(),  // named
                        character_type, llvm::Type::getInt64Ty(context), // sequential, sequential_len
                        character_type, llvm::Type::getInt64Ty(context), // direct, direct_len
                        character_type, llvm::Type::getInt64Ty(context), // form, form_len
                        character_type, llvm::Type::getInt64Ty(context), // formatted, formatted_len
                        character_type, llvm::Type::getInt64Ty(context), // unformatted, unformatted_len
                        llvm::Type::getInt32Ty(context)->getPointerTo(), // iostat
                        llvm::Type::getInt32Ty(context)->getPointerTo(), // nextrec
                        character_type, llvm::Type::getInt64Ty(context),  // decimal_data, decimal_len
                        character_type, llvm::Type::getInt64Ty(context),   // sign_data, sign_len
                        character_type, llvm::Type::getInt64Ty(context),   // encoding_data, encoding_len
                        character_type, llvm::Type::getInt64Ty(context),   // stream_data, stream_len
                        character_type, llvm::Type::getInt64Ty(context),    // iomsg_data, iomsg_len
                        character_type, llvm::Type::getInt64Ty(context),    // round_data, round_len
                        character_type, llvm::Type::getInt64Ty(context),    // pad_data, pad_len
                        llvm::Type::getInt1Ty(context)->getPointerTo(),    // pending
                        character_type, llvm::Type::getInt64Ty(context),    // asynchronous_data, asynchronous_len
                        character_type, llvm::Type::getInt64Ty(context),   // action_data, action_len
                        character_type, llvm::Type::getInt64Ty(context),    // position_data, position_len
                        character_type, llvm::Type::getInt64Ty(context)    // delim_data, delim_len
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        tmp = builder->CreateCall(fn, {f_name_data, f_name_len,
            exist_val, unit,
            opened_val, size_val,
            pos_val,
            write, write_len,
            read, read_len,
            readwrite, readwrite_len,
            access_val, access_len,
            name_val, name_len,
            blank_val, blank_len,
            recl, number, named,
            sequential, sequential_len,
            direct, direct_len,
            form, form_len,
            formatted, formatted_len,
            unformatted, unformatted_len,
            iostat, nextrec,
            decimal_val, decimal_len,
            sign_val, sign_len,
            encoding_val, encoding_len,
            stream_val, stream_len,
            iomsg_val, iomsg_len,
            round_val, round_len,
            pad_val, pad_len,
            pending_val,
            asynchronous_val, asynchronous_len,
            action_val, action_len,
            position_val, position_len,
            delim_val, delim_len});
        if (exist_actual) {
            llvm::Value *loaded = llvm_utils->CreateLoad2(
                llvm::Type::getInt1Ty(context), exist_val);
            int kind = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(x.m_exist));
            llvm::Value *converted = builder->CreateZExt(loaded, llvm_utils->getIntType(kind));
            builder->CreateStore(converted, exist_actual);
        }
        if (opened_actual) {
            llvm::Value *loaded = llvm_utils->CreateLoad2(
                llvm::Type::getInt1Ty(context), opened_val);
            int kind = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(x.m_opened));
            llvm::Value *converted = builder->CreateZExt(loaded, llvm_utils->getIntType(kind));
            builder->CreateStore(converted, opened_actual);
        }
        if (named_actual) {
            llvm::Value *loaded = llvm_utils->CreateLoad2(
                llvm::Type::getInt1Ty(context), named);
            int kind = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(x.m_named));
            llvm::Value *converted = builder->CreateZExt(loaded, llvm_utils->getIntType(kind));
            builder->CreateStore(converted, named_actual);
        }
        if (size_orig_ptr) {
            llvm::Value *loaded = builder->CreateLoad(
                llvm::Type::getInt32Ty(context), size_val);
            llvm::Value *converted = builder->CreateSExtOrTrunc(loaded,
                llvm_utils->getIntType(size_kind));
            builder->CreateStore(converted, size_orig_ptr);
        }
        if (pos_orig_ptr) {
            llvm::Value *loaded = builder->CreateLoad(
                llvm::Type::getInt32Ty(context), pos_val);
            llvm::Value *converted = builder->CreateSExtOrTrunc(loaded,
                llvm_utils->getIntType(pos_kind));
            builder->CreateStore(converted, pos_orig_ptr);
        }
        if (recl_orig_ptr) {
            llvm::Value *loaded = builder->CreateLoad(llvm::Type::getInt32Ty(context), recl);
            llvm::Value *converted = builder->CreateSExtOrTrunc(loaded,llvm_utils->getIntType(recl_kind));
            builder->CreateStore(converted, recl_orig_ptr);
        }
        if (pending_actual) {
            llvm::Value *loaded = llvm_utils->CreateLoad2(
                llvm::Type::getInt1Ty(context), pending_val);
            int kind = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(x.m_pending));
            llvm::Value *converted = builder->CreateZExt(loaded, llvm_utils->getIntType(kind));
            builder->CreateStore(converted, pending_actual);
        }
    }

    void visit_Flush(const ASR::Flush_t& x) {
        llvm::Value *unit_val = nullptr;
        llvm::Value *iostat{};
        llvm::Value *iomsg{}, *iomsg_len{};

        this->visit_expr_wrapper(x.m_unit, true);
        unit_val = llvm_utils->convert_kind(tmp, llvm::Type::getInt32Ty(context));
        int ptr_copy = ptr_loads;

        if (x.m_iostat) {
            int ptr_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_iostat, false);
            ptr_loads = ptr_copy;
            iostat = tmp;
        } else {
            iostat = llvm::ConstantInt::getNullValue(llvm::Type::getInt32Ty(context)->getPointerTo());
        }

        if (x.m_iomsg) {
            std::tie(iomsg, iomsg_len) = get_string_data_and_length(x.m_iomsg);
        } else {
            iomsg = llvm::Constant::getNullValue(character_type);
            iomsg_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        ptr_loads = ptr_copy;
        std::string runtime_func_name = "_lfortran_flush";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        llvm::Type::getInt32Ty(context),
                        llvm::Type::getInt32Ty(context)->getPointerTo(),
                        character_type, llvm::Type::getInt64Ty(context)
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        tmp = builder->CreateCall(fn, {unit_val, iostat, iomsg, iomsg_len});
    }

    void visit_FileRewind(const ASR::FileRewind_t &x) {
        llvm::Value *unit_val = nullptr;
        llvm::Value *iostat = nullptr;
        llvm::Value *iomsg = nullptr;
        llvm::Value *iomsg_len = nullptr;

        this->visit_expr_wrapper(x.m_unit, true);
        unit_val = llvm_utils->convert_kind(tmp, llvm::Type::getInt32Ty(context));

        if (x.m_iostat) {
            int ptr_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_iostat, false);
            ptr_loads = ptr_copy;
            iostat = tmp;
        } else {
            iostat = llvm::ConstantInt::getNullValue(llvm::Type::getInt32Ty(context)->getPointerTo());
        }

        if (x.m_iomsg) {
            std::tie(iomsg, iomsg_len) = get_string_data_and_length(x.m_iomsg);
        } else {
            iomsg = llvm::Constant::getNullValue(character_type);
            iomsg_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }

        std::string runtime_func_name = "_lfortran_rewind";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        llvm::Type::getInt32Ty(context),
                        llvm::Type::getInt32Ty(context)->getPointerTo(),
                        character_type,
                        llvm::Type::getInt64Ty(context)
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        builder->CreateCall(fn, {unit_val, iostat, iomsg, iomsg_len});
    }

    void visit_FileEndfile(const ASR::FileEndfile_t &x) {
        std::string runtime_func_name = "_lfortran_endfile";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        llvm::Type::getInt32Ty(context)
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        this->visit_expr_wrapper(x.m_unit, true);
        llvm::Value *unit_val = llvm_utils->convert_kind(tmp, llvm::Type::getInt32Ty(context));
        builder->CreateCall(fn, {unit_val});
    }

    void visit_FileBackspace(const ASR::FileBackspace_t &x) {
        std::string runtime_func_name = "_lfortran_backspace";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        llvm::Type::getInt32Ty(context)
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        this->visit_expr_wrapper(x.m_unit, true);
        llvm::Value *unit_val = llvm_utils->convert_kind(tmp, llvm::Type::getInt32Ty(context));
        builder->CreateCall(fn, {unit_val});
    }

    void visit_FileClose(const ASR::FileClose_t &x) {
        llvm::Value *unit_val;
        llvm::Value *status, *status_len;
        llvm::Value *iostat;
        if (ASRUtils::is_character(*expr_type(x.m_unit))) {
            if (x.m_iostat) {
                int ptr_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr_wrapper(x.m_iostat, false);
                ptr_loads = ptr_copy;
                iostat = tmp;
                builder->CreateStore(llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0), iostat);
            }
            return;
        }
        this->visit_expr_wrapper(x.m_unit, true);
        unit_val = llvm_utils->convert_kind(tmp, llvm::Type::getInt32Ty(context));
        if (x.m_status) {
            this->visit_expr_load_wrapper(x.m_status, 0);
            status = tmp;
            std::tie(status, status_len) = llvm_utils->get_string_length_data(
                                            ASRUtils::get_string_type(x.m_status),
                                            tmp);
        } else {
            status = llvm::Constant::getNullValue(character_type);
            status_len = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
        }
        if (x.m_iostat) {
            int ptr_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_iostat, false);
            ptr_loads = ptr_copy;
            iostat = tmp;
        } else {
            iostat = llvm::ConstantInt::getNullValue(llvm::Type::getInt32Ty(context)->getPointerTo());
        }
        std::string runtime_func_name = "_lfortran_close";
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {
            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {
                        llvm::Type::getInt32Ty(context),
                        character_type, llvm::Type::getInt64Ty(context), llvm::Type::getInt32Ty(context)->getPointerTo()
                    }, false);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        tmp = builder->CreateCall(fn, {unit_val, status, status_len, iostat});
    }

    void visit_Print(const ASR::Print_t &x) {
        handle_print(x.m_text, nullptr);
    }

    void visit_FileWrite(const ASR::FileWrite_t &x) {
        if( x.m_overloaded ) {
            // DTIO: set child I/O mode to suppress record terminators in
            // child writes, call the user-defined I/O subroutine, then
            // clear child mode and write the parent record terminator.
            llvm::Value *unit_val;
            if (x.m_unit == nullptr) {
                unit_val = llvm::ConstantInt::get(
                    llvm::Type::getInt32Ty(context), llvm::APInt(32, 6, true));
            } else {
                this->visit_expr_wrapper(x.m_unit, true);
                unit_val = llvm_utils->convert_kind(tmp, llvm::Type::getInt32Ty(context));
            }

            // _lfortran_set_child_io(unit, 1)
            {
                std::string func_name = "_lfortran_set_child_io";
                llvm::Function *fn = module->getFunction(func_name);
                if (!fn) {
                    llvm::FunctionType *ft = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context),
                        {llvm::Type::getInt32Ty(context),
                         llvm::Type::getInt32Ty(context)}, false);
                    fn = llvm::Function::Create(ft,
                        llvm::Function::ExternalLinkage, func_name,
                        module.get());
                }
                llvm::Value *one = llvm::ConstantInt::get(
                    llvm::Type::getInt32Ty(context), llvm::APInt(32, 1, true));
                builder->CreateCall(fn, {unit_val, one});
            }

            this->visit_stmt(*x.m_overloaded);

            // _lfortran_set_child_io(unit, 0)
            {
                std::string func_name = "_lfortran_set_child_io";
                llvm::Function *fn = module->getFunction(func_name);
                llvm::Value *zero = llvm::ConstantInt::get(
                    llvm::Type::getInt32Ty(context), llvm::APInt(32, 0, true));
                builder->CreateCall(fn, {unit_val, zero});
            }

            // Write parent record terminator
            {
                std::string func_name = "_lfortran_file_write_newline";
                llvm::Function *fn = module->getFunction(func_name);
                if (!fn) {
                    llvm::FunctionType *ft = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context),
                        {llvm::Type::getInt32Ty(context)}, false);
                    fn = llvm::Function::Create(ft,
                        llvm::Function::ExternalLinkage, func_name,
                        module.get());
                }
                builder->CreateCall(fn, {unit_val});
            }

            return ;
        }

        // Handle namelist write
        if (x.m_nml) {
            llvm::Value *unit_val, *iostat;
            bool is_string_array = false;

            if (x.m_unit == nullptr) {
                unit_val = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, 6, true));
            } else {
                ASR::ttype_t* unit_type = expr_type(x.m_unit);
                is_string_array = ASRUtils::is_character(*unit_type) && ASRUtils::is_array(unit_type);

                if (is_string_array) {
                    this->visit_expr_wrapper(x.m_unit, false);
                    unit_val = tmp;
                } else {
                    this->visit_expr_wrapper(x.m_unit, true);
                    unit_val = llvm_utils->convert_kind(tmp, llvm::Type::getInt32Ty(context));
                }
            }

            if (x.m_iostat) {
                int ptr_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr_wrapper(x.m_iostat, false);
                ptr_loads = ptr_copy;
                iostat = tmp;
            } else {
                iostat = llvm::ConstantPointerNull::get(llvm::Type::getInt32Ty(context)->getPointerTo());
            }

            // Build namelist descriptor and call _lfortran_namelist_write
            llvm::Value* nml_group = build_namelist_descriptor(x.m_nml);

            std::string func_name;
            if (is_string_array) {
                func_name = "_lfortran_namelist_write_str_array";
            } else {
                func_name = "_lfortran_namelist_write";
            }
            llvm::Function* fn = module->getFunction(func_name);

            if (!fn) {
                llvm::StructType* item_type = llvm::StructType::get(
                    character_type,
                    llvm::Type::getInt32Ty(context),
                    llvm::Type::getInt32Ty(context),
                    llvm::Type::getInt64Ty(context),
                    llvm::Type::getInt64Ty(context),
                    llvm::Type::getInt8Ty(context)->getPointerTo(),
                    llvm::Type::getInt64Ty(context)->getPointerTo()
                );

                llvm::StructType* group_type = llvm::StructType::get(
                    character_type,
                    llvm::Type::getInt32Ty(context),
                    item_type->getPointerTo()
                );

                std::vector<llvm::Type*> args;
                if (is_string_array) {
                    args = {
                        llvm::Type::getInt8Ty(context)->getPointerTo(),
                        llvm::Type::getInt64Ty(context),
                        llvm::Type::getInt64Ty(context),
                        llvm::Type::getInt32Ty(context)->getPointerTo(),
                        group_type->getPointerTo()
                    };
                } else {
                    args = {
                        llvm::Type::getInt32Ty(context),
                        llvm::Type::getInt32Ty(context)->getPointerTo(),
                        group_type->getPointerTo()
                    };
                }

                llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), args, false
                );

                fn = llvm::Function::Create(
                    function_type,
                    llvm::Function::ExternalLinkage,
                    func_name,
                    module.get()
                );
            }

            if (is_string_array) {
                llvm::Value* data_ptr = builder->CreateStructGEP(string_descriptor, unit_val, 0);
                data_ptr = builder->CreateLoad(
                    llvm::Type::getInt8Ty(context)->getPointerTo(),
                    data_ptr
                );

                llvm::Value* len_ptr = builder->CreateStructGEP(string_descriptor, unit_val, 1);
                llvm::Value* elem_len = builder->CreateLoad(
                    llvm::Type::getInt64Ty(context),
                    len_ptr
                );

                ASR::ttype_t* unit_type = ASRUtils::expr_type(x.m_unit);
                ASR::dimension_t* dims = nullptr;
                size_t n_dims = ASRUtils::extract_dimensions_from_ttype(unit_type, dims);
                LCOMPILERS_ASSERT(n_dims >= 1);

                int64_t n_elems_val = ASRUtils::get_fixed_size_of_array(dims, n_dims);

                llvm::Value* n_elems = llvm::ConstantInt::get(
                    llvm::Type::getInt64Ty(context),
                    llvm::APInt(64, n_elems_val)
                );

                builder->CreateCall(fn, {data_ptr, elem_len, n_elems, iostat, nml_group});
            } else {
                builder->CreateCall(fn, {unit_val, iostat, nml_group});
            }

            return;
        }

        if (x.m_unit == nullptr) {
            if(x.n_values  == 0){ // TODO : We should remove any function that creates a `FileWrite` with no args
                llvm::Value *fmt_ptr = LCompilers::create_global_string_ptr(context, *module, *builder, "%s%s");
                llvm::Value *str_data, *str_len;
                // For empty output, use a space (like old behavior)
                str_data = LCompilers::create_global_string_ptr(context, *module, *builder, " ");
                str_len = llvm::ConstantInt::get(context, llvm::APInt(32, 1));
                llvm::Value *end_data, *end_len;
                if (x.m_end) {
                    std::tie(end_data, end_len) = get_string_data_and_length(x.m_end);
                    end_len = builder->CreateTrunc(end_len, llvm::Type::getInt32Ty(context));
                } else {
                    end_data = LCompilers::create_global_string_ptr(context, *module, *builder, "\n");
                    end_len = llvm::ConstantInt::get(context, llvm::APInt(32, 1));
                }
                printf(context, *module, *builder, { fmt_ptr, str_data, str_len, end_data, end_len });
            } else if (x.n_values == 1){
                handle_print(x.m_values[0], x.m_end);
            } else {
                throw CodeGenError("File write should have single argument of type character)", x.base.base.loc);
            }
            return;
        }
        std::vector<llvm::Value *> args;
        std::vector<llvm::Type *> args_type;
        std::vector<std::string> fmt;
        llvm::Value *sep_data {}, *sep_len {};
        llvm::Value *end_data {}, *end_len {};
        llvm::Value *unit = nullptr;
        llvm::Value *iostat = nullptr;
        std::string runtime_func_name;
        bool is_string = ASRUtils::is_character(*expr_type(x.m_unit));

        int ptr_loads_copy = ptr_loads;
        bool is_string_array_unit = false;
        llvm::Value* string_array_size = nullptr;
        if ( is_string ) {
            is_string_array_unit = ASRUtils::is_array(ASRUtils::expr_type(x.m_unit));
            ptr_loads = LLVM::is_llvm_pointer(*ASRUtils::expr_type(x.m_unit));
            runtime_func_name = "_lfortran_string_write";
            args_type.push_back(llvm::Type::getInt8Ty(context)->getPointerTo()); // allocator
            args_type.push_back(character_type->getPointerTo());// str_holder
            args_type.push_back(llvm::Type::getInt8Ty(context)); // is_allocatable
            args_type.push_back(llvm::Type::getInt8Ty(context)); // is_deferred
            args_type.push_back(llvm::Type::getInt8Ty(context)); // is_array_unit
            args_type.push_back(llvm::Type::getInt64Ty(context)); // array_size
            args_type.push_back(llvm::Type::getInt64Ty(context)->getPointerTo()); //len
            args_type.push_back(llvm::Type::getInt32Ty(context)->getPointerTo()); //iostat
            args_type.push_back(llvm::Type::getInt8Ty(context)->getPointerTo()); //format_data
            args_type.push_back(llvm::Type::getInt64Ty(context)); // format_len
        } else if ( ASRUtils::is_integer(*expr_type(x.m_unit)) ) {
            ptr_loads = 1;
            runtime_func_name = "_lfortran_file_write";
            args_type.push_back(llvm::Type::getInt32Ty(context)); //unit_num
            args_type.push_back(llvm::Type::getInt32Ty(context)->getPointerTo()); //iostat
            args_type.push_back(llvm::Type::getInt8Ty(context)->getPointerTo()); //format_data
            args_type.push_back(llvm::Type::getInt64Ty(context));//format_len

        } else {
            throw CodeGenError("Unsupported type for `unit` in write(..)");
        }
        // Don't use constant value optimization for assignments to string-sections
        if (is_string && ASR::is_a<ASR::StringSection_t>(*x.m_unit)) {
            bool is_assignment_target_temp = is_assignment_target;
            is_assignment_target = true;
            this->visit_expr_wrapper(x.m_unit);
            is_assignment_target = is_assignment_target_temp;
        } else {
            this->visit_expr_wrapper(x.m_unit);
        }
        ptr_loads = ptr_loads_copy;

        llvm::Value* string_len;
        if(!is_string){
            unit = tmp;
            if (unit->getType()->isPointerTy()) {
                unit = llvm_utils->CreateLoad2(llvm::Type::getInt32Ty(context), unit);
            } else {
                unit = llvm_utils->convert_kind(unit, llvm::Type::getInt32Ty(context));
            }
        } else { // String Write
            if (is_string_array_unit &&
                ASRUtils::extract_physical_type(ASRUtils::expr_type(x.m_unit))
                    == ASR::array_physical_typeType::DescriptorArray &&
                !ASR::is_a<ASR::ArraySection_t>(*x.m_unit)) {
                // For DescriptorArray (e.g. EQUIVALENCE), extract the data
                // pointer from the array descriptor to get to string descriptors
                ASR::ttype_t* unit_type = ASRUtils::expr_type(x.m_unit);
                llvm::Type* arr_desc_type = llvm_utils->get_type_from_ttype_t_util(
                    x.m_unit, ASRUtils::type_get_past_allocatable_pointer(unit_type),
                    module.get());
                llvm::Value* str_desc = llvm_utils->CreateLoad2(
                    llvm_utils->get_StringType(ASRUtils::extract_type(unit_type))->getPointerTo(),
                    arr_descr->get_pointer_to_data(arr_desc_type, tmp));
                std::tie(unit, string_len) = llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_unit), str_desc, true, true);
            } else {
                std::tie(unit, string_len) = llvm_utils->get_string_length_data(
                    ASRUtils::get_string_type(x.m_unit), tmp, true, true);
            }
            if (is_string_array_unit) {
                if (ASR::is_a<ASR::ArraySection_t>(*x.m_unit)) {
                    ASR::ArraySection_t* section = ASR::down_cast<ASR::ArraySection_t>(x.m_unit);
                    auto get_i64_value = [&](ASR::expr_t* expr) {
                        int ptr_copy = ptr_loads;
                        ptr_loads = 0;
                        this->visit_expr_wrapper(expr, true);
                        ptr_loads = ptr_copy;
                        return llvm_utils->convert_kind(tmp, llvm::Type::getInt64Ty(context));
                    };
                    string_array_size = llvm::ConstantInt::get(context, llvm::APInt(64, 1));
                    for (size_t i = 0; i < section->n_args; i++) {
                        if (!section->m_args[i].m_left) {
                            continue;
                        }
                        llvm::Value* left = get_i64_value(section->m_args[i].m_left);
                        llvm::Value* right = get_i64_value(section->m_args[i].m_right);
                        llvm::Value* step = section->m_args[i].m_step ?
                            get_i64_value(section->m_args[i].m_step) :
                            llvm::ConstantInt::get(context, llvm::APInt(64, 1));
                        llvm::Value* extent = builder->CreateAdd(
                            builder->CreateSDiv(builder->CreateSub(right, left), step),
                            llvm::ConstantInt::get(context, llvm::APInt(64, 1)));
                        llvm::Value* zero = llvm::ConstantInt::get(context, llvm::APInt(64, 0));
                        extent = builder->CreateSelect(
                            builder->CreateICmpSLT(extent, zero), zero, extent);
                        string_array_size = builder->CreateMul(string_array_size, extent);
                    }
                } else {
                    ASR::ttype_t *type32 = ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4));
                    ASR::ArraySize_t* array_size = ASR::down_cast2<ASR::ArraySize_t>(
                        ASR::make_ArraySize_t(al, x.base.base.loc, x.m_unit, nullptr, type32, nullptr));
                    visit_ArraySize(*array_size);
                    string_array_size = builder->CreateSExt(tmp, llvm::Type::getInt64Ty(context));
                }
            } else {
                string_array_size = llvm::ConstantInt::get(context, llvm::APInt(64, 1));
            }
        }
        ptr_loads = ptr_loads_copy;

        if (x.m_iostat) {
            int ptr_copy = ptr_loads;
            ptr_loads = 0;
            this->visit_expr_wrapper(x.m_iostat, false);
            ptr_loads = ptr_copy;
            iostat = tmp;
        } else {
            iostat = llvm_utils->CreateAlloca(*builder,
                        llvm::Type::getInt32Ty(context)->getPointerTo());
            builder->CreateStore(llvm::ConstantInt::getNullValue(
                llvm::Type::getInt32Ty(context)->getPointerTo()), iostat);
            iostat = llvm_utils->CreateLoad2(llvm::Type::getInt32Ty(context)->getPointerTo(), iostat);
        }

        if (!is_string) {
            std::string func_name = "_lfortran_get_decimal_mode";
            llvm::Function *fn = module->getFunction(func_name);
            if (!fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getInt32Ty(context), {
                            llvm::Type::getInt32Ty(context)
                        }, false);
                fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, func_name, module.get());
            }
            this->current_decimal_mode = builder->CreateCall(fn, {unit});

            std::string sign_func_name = "_lfortran_get_sign_mode";
            llvm::Function *sign_fn = module->getFunction(sign_func_name);
            if (!sign_fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getInt32Ty(context), {
                            llvm::Type::getInt32Ty(context)
                        }, false);
                sign_fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, sign_func_name, module.get());
            }
            this->current_sign_mode = builder->CreateCall(sign_fn, {unit});

            std::string round_func_name = "_lfortran_get_round_mode";
            llvm::Function *round_fn = module->getFunction(round_func_name);
            if (!round_fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getInt32Ty(context), {
                            llvm::Type::getInt32Ty(context)
                        }, false);
                round_fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, round_func_name, module.get());
            }
            this->current_round_mode = builder->CreateCall(round_fn, {unit});
        }
        
        if (x.m_rec && !is_string) {
            emit_seek_record_from_rec(x.m_rec, unit, iostat);
        }

        if (x.m_pos && !is_string) {
            this->visit_expr_wrapper(x.m_pos, true);
            llvm::Value* pos_val = tmp;
            pos_val = builder->CreateIntCast(pos_val, llvm::Type::getInt64Ty(context), true);

            std::string runtime_func_name = "_lfortran_file_seek";
            llvm::Function *fn = module->getFunction(runtime_func_name);
            if (!fn) {
                llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context), {
                            llvm::Type::getInt32Ty(context),
                            llvm::Type::getInt64Ty(context),
                            llvm::Type::getInt32Ty(context)->getPointerTo()
                        }, false);
                fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, runtime_func_name,
                            module.get());
            }
            builder->CreateCall(fn, {unit, pos_val, iostat});
        }

        if (x.m_separator) {
            std::tie(sep_data, sep_len) = get_string_data_and_length(x.m_separator);
        } else {
            sep_data = LCompilers::create_global_string_ptr(context, *module, *builder, " ");
            sep_len = llvm::ConstantInt::get(context, llvm::APInt(64, 1));
        }
        if (x.m_end) {
            std::tie(end_data, end_len) = get_string_data_and_length(x.m_end);
        } else {
            end_data = LCompilers::create_global_string_ptr(context, *module, *builder, "\n");
            end_len = llvm::ConstantInt::get(context, llvm::APInt(64, 1));
        }
        size_t n_values = x.n_values; ASR::expr_t **m_values = x.m_values;
        for (size_t i=0; i<n_values; i++) {
            if (i != 0 && !is_string && x.m_is_formatted) {
                fmt.push_back("%s");
                args.push_back(sep_data);
                args.push_back(sep_len);
            }
            if (!x.m_is_formatted) {
                ASR::ttype_t* value_type = ASRUtils::extract_type(ASRUtils::expr_type(m_values[i]));
                ASR::ttype_t* value_type_base = ASRUtils::type_get_past_array(value_type);
                int kind = ASRUtils::extract_kind_from_ttype_t(value_type_base);
                if (ASR::is_a<ASR::Complex_t>(*value_type_base)) {
                    kind *= 2;
                }
                llvm::Value* kind_val = llvm::ConstantInt::get(context, llvm::APInt(32, kind, true));
                ASR::ttype_t *type32 = ASRUtils::TYPE(ASR::make_Integer_t(al, x.base.base.loc, 4));
                if (ASRUtils::is_array(ASRUtils::expr_type(m_values[i])) &&
                    ASR::is_a<ASR::Logical_t>(*value_type_base)) {
                    // Logical arrays are already stored with their proper kind width,
                    // so we can write them directly like integer arrays.
                    ASR::ArraySize_t* array_size_node = ASR::down_cast2<ASR::ArraySize_t>(
                        ASR::make_ArraySize_t(al, m_values[i]->base.loc,
                            m_values[i], nullptr, type32, nullptr));
                    visit_ArraySize(*array_size_node);
                    llvm::Value* total_size = builder->CreateMul(kind_val, tmp);
                    args.push_back(total_size);
                    {
                        int64_t ptr_loads_save = ptr_loads;
                        int reduce_loads = 0;
                        ptr_loads = 2;
                        if (ASR::is_a<ASR::Var_t>(*m_values[i])) {
                            ASR::Variable_t* var = ASRUtils::EXPR2VAR(m_values[i]);
                            reduce_loads = var->m_intent == ASRUtils::intent_in;
                            if (LLVM::is_llvm_pointer(*var->m_type)) {
                                ptr_loads = 1;
                            }
                        }
                        ptr_loads = ptr_loads - reduce_loads;
                        ASR::ttype_t *arr_t = ASRUtils::expr_type(m_values[i]);
                        this->visit_expr_load_wrapper(m_values[i], (ASRUtils::is_character(*arr_t) ? 0 : ptr_loads), true);
                        ptr_loads = ptr_loads_save;
                        load_non_array_non_character_pointers(m_values[i], arr_t, tmp);
                        if (ASRUtils::is_array(arr_t) && ASRUtils::is_allocatable(arr_t)) {
                            llvm::Type *llvm_type = llvm_utils->get_type_from_ttype_t_util(m_values[i],
                                ASRUtils::extract_type(arr_t), module.get());
                            tmp = llvm_utils->CreateLoad2(llvm_type->getPointerTo(), tmp);
                        } else {
                            ASR::array_physical_typeType phys = ASRUtils::extract_physical_type(arr_t);
                            if (phys == ASR::array_physical_typeType::DescriptorArray) {
                                llvm::Value* data_ptr = arr_descr->get_pointer_to_data(
                                    m_values[i], ASRUtils::type_get_past_allocatable_pointer(arr_t),
                                    tmp, module.get());
                                llvm::Type* el_type = llvm_utils->get_type_from_ttype_t_util(
                                    m_values[i], ASRUtils::extract_type(arr_t), module.get());
                                tmp = llvm_utils->CreateLoad2(el_type->getPointerTo(), data_ptr);
                            }
                        }
                    }
                    args.push_back(tmp);
                    continue;
                } else if (ASRUtils::is_array(ASRUtils::expr_type(m_values[i]))) {
                    ASR::ArraySize_t* array_size = ASR::down_cast2<ASR::ArraySize_t>(ASR::make_ArraySize_t(al, m_values[i]->base.loc,
                        m_values[i], nullptr, type32, nullptr));
                    visit_ArraySize(*array_size);
                    llvm::Value* total_size = builder->CreateMul(kind_val, tmp);
                    if (ASR::is_a<ASR::String_t>(*value_type_base)) {
                        llvm::Value* array_size_val = tmp;
                        ASR::StringLen_t* str_len = ASR::down_cast2<ASR::StringLen_t>(ASR::make_StringLen_t(al,
                            m_values[i]->base.loc, m_values[i], type32, nullptr));
                        visit_StringLen(*str_len);
                        total_size = builder->CreateMul(array_size_val, tmp);
                    }
                    args.push_back(total_size);
                    {
                        int64_t ptr_loads_save = ptr_loads;
                        int reduce_loads = 0;
                        ptr_loads = 2;
                        if (ASR::is_a<ASR::Var_t>(*m_values[i])) {
                            ASR::Variable_t* var = ASRUtils::EXPR2VAR(m_values[i]);
                            reduce_loads = var->m_intent == ASRUtils::intent_in;
                            if (LLVM::is_llvm_pointer(*var->m_type)) {
                                ptr_loads = 1;
                            }
                        }
                        ptr_loads = ptr_loads - reduce_loads;
                        ASR::ttype_t *arr_t = ASRUtils::expr_type(m_values[i]);
                        this->visit_expr_load_wrapper(m_values[i], ptr_loads, true);
                        ptr_loads = ptr_loads_save;
                        load_non_array_non_character_pointers(m_values[i], arr_t, tmp);
                        if (ASRUtils::is_array(arr_t) && ASRUtils::is_allocatable(arr_t)) {
                            llvm::Type *llvm_type = llvm_utils->get_type_from_ttype_t_util(m_values[i],
                                ASRUtils::extract_type(arr_t), module.get());
                            tmp = llvm_utils->CreateLoad2(llvm_type->getPointerTo(), tmp);
                        } else {
                            ASR::array_physical_typeType phys = ASRUtils::extract_physical_type(arr_t);
                            if (phys == ASR::array_physical_typeType::DescriptorArray) {
                                llvm::Value* data_ptr = arr_descr->get_pointer_to_data(
                                    m_values[i], ASRUtils::type_get_past_allocatable_pointer(arr_t),
                                    tmp, module.get());
                                llvm::Type* el_type = llvm_utils->get_type_from_ttype_t_util(
                                    m_values[i], ASRUtils::extract_type(arr_t), module.get());
                                tmp = llvm_utils->CreateLoad2(el_type->getPointerTo(), data_ptr);
                            }
                        }
                    }
                    if (ASR::is_a<ASR::String_t>(*value_type_base)) {
                        tmp = llvm_utils->get_string_data(ASRUtils::get_string_type(m_values[i]), tmp);
                    }
                    args.push_back(tmp);
                    continue;
                } else if (ASRUtils::is_character(*ASRUtils::expr_type(m_values[i]))) {
                    ASR::StringLen_t * strlen = ASR::down_cast2<ASR::StringLen_t>(ASR::make_StringLen_t(al,
                        m_values[i]->base.loc, m_values[i], type32, nullptr));
                    visit_StringLen(*strlen);
                    args.push_back(builder->CreateMul(kind_val, tmp));
                    ptr_loads = 0;
                    this->visit_expr_wrapper(m_values[i], true);
                    ptr_loads = ptr_loads_copy;
                    llvm::Value* str_data = llvm_utils->get_string_data(ASRUtils::get_string_type(m_values[i]), tmp);
                    args.push_back(str_data);
                    continue;
                } else if (ASR::is_a<ASR::Logical_t>(*value_type_base)) {
                    args.push_back(kind_val);
                    this->visit_expr_wrapper(m_values[i], true);
                    llvm::Value* logical_val = tmp;
                    if (logical_val->getType()->isPointerTy()) {
                        llvm::Type* logical_type = llvm_utils->getIntType(
                            ASRUtils::extract_kind_from_ttype_t(value_type_base));
                        logical_val = llvm_utils->CreateLoad2(logical_type, logical_val);
                    }
                    llvm::Type* int_type = llvm::Type::getIntNTy(context, kind * 8);
                    llvm::Value* logical_ext = llvm_utils->convert_kind(logical_val, int_type);
                    llvm::Value* logical_ptr = llvm_utils->CreateAlloca(*builder, int_type);
                    builder->CreateStore(logical_ext, logical_ptr);
                    args.push_back(logical_ptr);
                    continue;
                } else {
                    args.push_back(kind_val);
                    this->visit_expr_wrapper(m_values[i], true);
                    llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(m_values[i], ASRUtils::expr_type(m_values[i]), module.get());
                    llvm::Value* llvm_arg = llvm_utils->CreateAlloca(*builder, type);
                    builder->CreateStore(tmp, llvm_arg);
                    args.push_back(llvm_arg);
                    continue;

                }
            }
            ASR::ttype_t* val_type_fw = ASRUtils::expr_type(m_values[i]);
            if (ASRUtils::is_array(val_type_fw) &&
                ASRUtils::is_character(*ASRUtils::extract_type(val_type_fw))) {
                int ptr_copy = ptr_loads;
                ptr_loads = 0;
                this->visit_expr_wrapper(m_values[i], true);
                ptr_loads = ptr_copy;
                llvm::Value* arr_val = tmp;

                ASR::ttype_t* type_past_alloc = ASRUtils::type_get_past_allocatable_pointer(val_type_fw);
                llvm::Type* arr_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                    m_values[i], type_past_alloc, module.get());

                if (LLVM::is_llvm_pointer(*val_type_fw)) {
                    arr_val = llvm_utils->CreateLoad2(
                        arr_llvm_type->getPointerTo(), arr_val);
                }

                llvm::Value* str_data = llvm_utils->get_stringArray_data(
                    val_type_fw, arr_val);
                llvm::Value* elem_len = llvm_utils->get_stringArray_length(
                    val_type_fw, arr_val);

                llvm::Value* arr_size = arr_descr->get_array_size(
                    arr_llvm_type, arr_val, nullptr, 4);
                llvm::Value* arr_size_i64 = builder->CreateSExt(
                    arr_size, llvm::Type::getInt64Ty(context));
                llvm::Value* total_len = builder->CreateMul(
                    arr_size_i64, elem_len);

                fmt.push_back("%s");
                args.push_back(str_data);
                if (x.m_is_formatted) {
                    args.push_back(total_len);
                }
                continue;
            }
            compute_fmt_specifier_and_arg(fmt, args, m_values[i],
                x.base.base.loc); // return of visited expression `m_values[i]` stored in `tmp`

            // Push the length of the string to args
            ASR::ttype_t *t = ASRUtils::extract_type(ASRUtils::expr_type(m_values[i]));
            if (x.m_is_formatted && ASRUtils::is_character(*t)) {
                llvm::Value *str_len = llvm_utils->get_string_length(ASRUtils::get_string_type(t), tmp);
                args.push_back(str_len);
            }
        }
        if (!x.m_is_formatted) {  // give -1 argument for end of arguments
            llvm::Value* minus_one = llvm::ConstantInt::get(context, llvm::APInt(32, -1, true));
            args.push_back(minus_one);
        } else if (!is_string) {
            fmt.push_back("%s");
            args.push_back(end_data);
            args.push_back(end_len);
        }
        std::string fmt_str {};
        if(x.m_is_formatted){
            for (size_t i=0; i<fmt.size(); i++) {
                fmt_str += fmt[i];
            }
        } else {
            // Don't provide format.
        }
        llvm::Value *fmt_data = LCompilers::create_global_string_ptr(context, *module, *builder, fmt_str);
        llvm::Value *fmt_len = llvm::ConstantInt::get(context, llvm::APInt(64, fmt_str.length()));


        std::vector<llvm::Value *> printf_args;
        if(is_string){
            printf_args.push_back(llvm_utils->get_allocator(module.get()));
        }
        printf_args.push_back(unit);
        if(is_string){
            llvm::Value* is_allocatable = llvm::ConstantInt::get(context, llvm::APInt(8,
                ASRUtils::is_allocatable(ASRUtils::expr_type(x.m_unit))));
            llvm::Value* is_deferred = llvm::ConstantInt::get(context, llvm::APInt(8,
                ASRUtils::is_deferredLength_string(ASRUtils::expr_type(x.m_unit))));
            llvm::Value* is_array_unit = llvm::ConstantInt::get(context, llvm::APInt(8,
                is_string_array_unit));
            printf_args.push_back(is_allocatable);
            printf_args.push_back(is_deferred);
            printf_args.push_back(is_array_unit);
            printf_args.push_back(string_array_size);
            printf_args.push_back(string_len);
        }
        printf_args.push_back(iostat);
        printf_args.push_back(fmt_data);
        printf_args.push_back(fmt_len);
        printf_args.insert(printf_args.end(), args.begin(), args.end());
        llvm::Function *fn = module->getFunction(runtime_func_name);
        if (!fn) {

            llvm::FunctionType *function_type = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), args_type, true);
            fn = llvm::Function::Create(function_type,
                    llvm::Function::ExternalLinkage, runtime_func_name, module.get());
        }
        tmp = builder->CreateCall(fn, printf_args);
        llvm_utils->stringFormat_return.free();

        this->current_decimal_mode = nullptr;
        this->current_sign_mode = nullptr;
        this->current_round_mode = nullptr;
    }

    std::string serialize_structType_symbols(ASR::symbol_t* sym){
        std::string res {};
        ASR::Struct_t* StructSymbol = ASR::down_cast<ASR::Struct_t>(sym);
        for(size_t i=0; i < StructSymbol->n_members; i++){
            ASR::symbol_t* StructMember = StructSymbol->m_symtab->
                                            get_symbol(StructSymbol->m_members[i]);
            res += SerializeType(ASRUtils::get_expr_from_sym(al, StructMember), ASRUtils::symbol_type(StructMember), true);
            if(i < StructSymbol->n_members-1){
                res += ",";
            }
        }
        return res;
    }

    /*
        [ ] --> Array
        ( ) --> Struct
        I   --> Integer
        R   --> Real
        S-PhysicalType-len? --> Character [S-DESC-10, S-CCHAR-1, S-DESC]
        L   --> Logical
        { } --> Struct for COMPLEX
        The serialization corresponds to how these arguments are represented in LLVM backend;
        So, Complex type results in `{R, R}` as it's a struct of floating types in the backend.
    */

    // Serialize `type` using symbols above.
    std::string SerializeType(ASR::expr_t* expr, ASR::ttype_t* type, bool in_struct) {
        std::string res {};
        type = ASRUtils::type_get_past_allocatable(
                ASRUtils::type_get_past_pointer(type));
        if (ASR::is_a<ASR::Integer_t>(*type)) {
            res += "I";
            res += std::to_string(ASRUtils::extract_kind_from_ttype_t(type));
        } else if (ASR::is_a<ASR::UnsignedInteger_t>(*type)) {
            res += "U";
            res += std::to_string(ASRUtils::extract_kind_from_ttype_t(type));
        } else if (ASR::is_a<ASR::Real_t>(*type)) {
            res += "R";
            res += std::to_string(ASRUtils::extract_kind_from_ttype_t(type));
        } else if (ASR::is_a<ASR::String_t>(*type)) {
            res += "S-";
            ASR::String_t* str_type = ASR::down_cast<ASR::String_t>(type);
            if(str_type->m_physical_type == ASR::DescriptorString) res += "DESC";
            else if(str_type->m_physical_type == ASR::CChar) res +="CCHAR";
            else throw LCompilersException("Unhandled string physical type");
            int len;
            if(ASRUtils::extract_value(str_type->m_len, len)){res += "-" + std::to_string(len);}
        } else if (ASR::is_a<ASR::Complex_t>(*type)){
            res += "{R" + std::to_string(ASRUtils::extract_kind_from_ttype_t(type))+
                    ",R" + std::to_string(ASRUtils::extract_kind_from_ttype_t(type))+
                    "}";
        } else if (ASR::is_a<ASR::Array_t>(*type)) {
            // push array size only if it's not a struct member.
            if(in_struct && ASRUtils::is_fixed_size_array(type)){
                res += std::to_string(ASRUtils::get_fixed_size_of_array(type));
            } else if (in_struct && !ASRUtils::is_fixed_size_array(type)){
                // TODO : Throw a semantic error instead.
                throw CodeGenError("Can't print type variable with dynamic array member");
            }
            res += "[";
            res += SerializeType(expr, ASR::down_cast<ASR::Array_t>(type)->m_type, in_struct);
            res += "]";
        } else if (ASR::is_a<ASR::StructType_t>(*type)) {
            res += "(";
            res += serialize_structType_symbols(ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(expr)));
            res += ")";
        } else if (ASR::is_a<ASR::Logical_t>(*type)) {
            int a_kind = ASR::down_cast<ASR::Logical_t>(type)->m_kind;
            res += "L" + std::to_string(a_kind * 8);
        } else if(ASR::is_a<ASR::CPtr_t>(*type)){
            res += "CPtr";
        } else {
            throw CodeGenError("Printing support is not available for `" +
                ASRUtils::type_to_str_fortran_expr(type, expr) + "` type.");
        }
        return res;
    }
    // Serializes the types of a list of expressions.
    llvm::Value* SerializeExprTypes(ASR::expr_t** args, size_t n_args){
        std::string serialization_res = "";
        for (size_t i=0; i<n_args; i++) {
            serialization_res += SerializeType(args[i], ASRUtils::expr_type(args[i]), false);
            if(i != n_args-1){
                serialization_res += ",";
            }
        }
        return LCompilers::create_global_string_ptr(context, *module, *builder, serialization_res, "serialization_info");
    }

    void compute_fmt_specifier_and_arg(std::vector<std::string> &fmt,
        std::vector<llvm::Value *> &args, ASR::expr_t *v, const Location &loc) {
        int64_t ptr_loads_copy = ptr_loads;
        int reduce_loads = 0;
        ptr_loads = 2;
        if( ASR::is_a<ASR::Var_t>(*v) ) {
            ASR::Variable_t* var = ASRUtils::EXPR2VAR(v);
            reduce_loads = var->m_intent == ASRUtils::intent_in;
            if( LLVM::is_llvm_pointer(*var->m_type) ) {
                ptr_loads = 1;
            }
        }

        ptr_loads = ptr_loads - reduce_loads;
        lookup_enum_value_for_nonints = true;
        ASR::ttype_t *t = ASRUtils::expr_type(v);
        this->visit_expr_load_wrapper(v, (ASRUtils::is_character(*t) ? 0 : ptr_loads), true);
        lookup_enum_value_for_nonints = false;
        ptr_loads = ptr_loads_copy;

        if ((t->type == ASR::ttypeType::CPtr && !ASRUtils::is_array(t)) ||
            (t->type == ASR::ttypeType::Pointer &&
                (ASR::is_a<ASR::Var_t>(*v) || ASR::is_a<ASR::GetPointer_t>(*v))
                && !ASRUtils::is_array(t) && !ASRUtils::is_character(*t))
            ) {
            fmt.push_back("%lld");
            llvm::Value* d = builder->CreatePtrToInt(tmp, llvm_utils->getIntType(8, false));
            args.push_back(d);
            return ;
        }

        load_non_array_non_character_pointers(v, ASRUtils::expr_type(v), tmp);
        if (ASRUtils::is_array(t) && ASRUtils::is_allocatable(t)) {
            llvm::Type *llvm_type = llvm_utils->get_type_from_ttype_t_util(v,
                ASRUtils::extract_type(t), module.get());
            tmp = llvm_utils->CreateLoad2(llvm_type->getPointerTo(), tmp);
        }
        t = ASRUtils::extract_type(t);
        int a_kind = ASRUtils::extract_kind_from_ttype_t(t);

        if (ASRUtils::is_integer(*t)) {
            switch( a_kind ) {
                case 1 : {
                    fmt.push_back("%hhi");
                    break;
                }
                case 2 : {
                    fmt.push_back("%hi");
                    break;
                }
                case 4 : {
                    fmt.push_back("%d");
                    break;
                }
                case 8 : {
                    fmt.push_back("%lld");
                    break;
                }
                default: {
                    throw CodeGenError(R"""(Printing support is available only
                                        for 8, 16, 32, and 64 bit integer kinds.)""",
                                        loc);
                }
            }
            args.push_back(tmp);
        } else if (ASRUtils::is_unsigned_integer(*t)) {
            switch( a_kind ) {
                case 1 : {
                    fmt.push_back("%hhu");
                    break;
                }
                case 2 : {
                    fmt.push_back("%hu");
                    break;
                }
                case 4 : {
                    fmt.push_back("%u");
                    break;
                }
                case 8 : {
                    fmt.push_back("%llu");
                    break;
                }
                default: {
                    throw CodeGenError(R"""(Printing support is available only
                                        for 8, 16, 32, and 64 bit unsigned integer kinds.)""",
                                        loc);
                }
            }
            args.push_back(tmp);
        } else if (ASRUtils::is_real(*t)) {
            switch( a_kind ) {
                case 4 : {
                        fmt.push_back("%13.8e");
                    break;
                }
                case 8 : {
                        fmt.push_back("%23.17e");
                    break;
                }
                default: {
                    throw CodeGenError(R"""(Printing support is available only
                                        for 32, and 64 bit real kinds.)""",
                                        loc);
                }
            }
            args.push_back(tmp);
        } else if (t->type == ASR::ttypeType::String) {
            fmt.push_back("%s");
            args.push_back(llvm_utils->get_string_data(ASRUtils::get_string_type(t), tmp));
        } else if (ASRUtils::is_logical(*t)) {
            fmt.push_back("%s");
            args.push_back(tmp);
        } else if (ASRUtils::is_complex(*t)) {
            switch( a_kind ) {
                case 4 : {
                    fmt.push_back("(%f,%f)");
                    break;
                }
                case 8 : {
                    fmt.push_back("(%lf,%lf)");
                    break;
                }
                default: {
                    throw CodeGenError(R"""(Printing support is available only
                                        for 32, and 64 bit complex kinds.)""",
                                        loc);
                }
            }
            args.push_back(tmp);
        } else if (t->type == ASR::ttypeType::CPtr) {
            fmt.push_back("%lld");
            args.push_back(tmp);
        } else if (t->type == ASR::ttypeType::EnumType) {
            // TODO: Use recursion to generalise for any underlying type in enum
            fmt.push_back("%d");
            args.push_back(tmp);
        } else {
            throw CodeGenError("Printing support is not available for `" +
                ASRUtils::type_to_str_fortran_expr(t, v) + "` type.", loc);
        }
    }

    void handle_print(ASR::expr_t* arg, ASR::expr_t* end_expr)
    {
        ASR::ttype_t* t = ASRUtils::expr_type(arg);
        llvm::Value* main_data = nullptr;
        llvm::Value* main_len = nullptr;

        // End string (always computed once)
        llvm::Value *end_data, *end_len;
        if (end_expr == nullptr) {
            end_data = LCompilers::create_global_string_ptr(context, *module, *builder, "\n");
            end_len = llvm::ConstantInt::get(context, llvm::APInt(32, 1));
        } else {
            std::tie(end_data, end_len) = get_string_data_and_length(end_expr);
            end_len = builder->CreateTrunc(end_len, llvm::Type::getInt32Ty(context));
        }

        std::string fmt_str;

        if (ASRUtils::is_character(*t)) {
            // --- String path ---
            std::tie(main_data, main_len) = get_string_data_and_length(arg);
            main_len = builder->CreateTrunc(main_len, llvm::Type::getInt32Ty(context));
            fmt_str = "%s";
        } else {
            // --- Non-string path ---
            // Evaluate once
            this->visit_expr_wrapper(arg, true);
            llvm::Value* val = tmp;  // integer/real/etc value
            main_len = llvm::ConstantInt::get(context, llvm::APInt(32, 0));  // no length for non-string

            // Determine correct printf specifier
            std::vector<std::string> fmt_parts;
            std::vector<llvm::Value*> dummy_args;
            dummy_args.push_back(nullptr);
            compute_fmt_specifier_and_arg(fmt_parts, dummy_args, arg, arg->base.loc);

            fmt_str.clear();
            for (auto& f : fmt_parts)
                fmt_str += f;
            llvm::Value* as_i32 = builder->CreateSExtOrBitCast(val, llvm::Type::getInt32Ty(context));
            main_data = builder->CreateIntToPtr(as_i32, llvm::PointerType::getUnqual(llvm::Type::getInt8Ty(context)));
        }
        fmt_str += "%s";
        llvm::Value* fmt_ptr = LCompilers::create_global_string_ptr(context, *module, *builder, fmt_str);
        printf(context, *module, *builder, { fmt_ptr, main_data, main_len, end_data, end_len });
        llvm_utils->stringFormat_return.free();
    }

    void handle_bitcast_assignment_char(const ASR::Assignment_t &x) {
        // Handler for BitCast (transfer intrinsic) to character arrays
        ASR::ttype_t* target_type = ASRUtils::expr_type(x.m_target);
        ASR::ttype_t* elem_type = target_type;
        bool is_array_target = false;
        if (ASRUtils::is_array(target_type)) {
            elem_type = ASRUtils::extract_type(target_type);
            is_array_target = (x.m_target->type == ASR::exprType::Var || x.m_target->type == ASR::exprType::ArraySection);
        }
        
        auto with_value_semantics = [&](auto func) {
            int64_t saved = ptr_loads;
            ptr_loads = 0;
            func();
            ptr_loads = saved;
        };
        llvm::Value* target_ptr;
        if (x.m_target->type == ASR::exprType::Var) {
            uint32_t h = get_hash((ASR::asr_t*)ASRUtils::EXPR2VAR(x.m_target));
            target_ptr = llvm_symtab[h];
            if (ASRUtils::is_allocatable(target_type) || ASRUtils::is_pointer(target_type)) {
                llvm::Type* target_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_target,
                    ASRUtils::type_get_past_allocatable(ASRUtils::type_get_past_pointer(target_type)), module.get());
                target_ptr = llvm_utils->CreateLoad2(target_llvm_type->getPointerTo(), target_ptr);
            }
        } else {
            is_assignment_target = true;
            visit_expr(*x.m_target);
            is_assignment_target = false;
            target_ptr = tmp;
        }

        llvm::Value* zero_based_idx;
        if (x.m_target->type == ASR::exprType::ArrayItem) {
            ASR::ArrayItem_t* array_item = ASR::down_cast<ASR::ArrayItem_t>(x.m_target);
            visit_expr_wrapper(array_item->m_args[0].m_right, true);
            llvm::Value* array_index = tmp;
            zero_based_idx = builder->CreateSub(array_index,llvm::ConstantInt::get(array_index->getType(), 1));
        } else {
            zero_based_idx = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0);
        }
        
        ASR::String_t* src_str_type = ASR::down_cast<ASR::String_t>(
            ASRUtils::type_get_past_array(ASRUtils::expr_type(x.m_value)));
        ASR::String_t* dest_str_type = ASR::down_cast<ASR::String_t>(elem_type);

        llvm::Value* element_length_val;
        if (dest_str_type->m_len && ASR::is_a<ASR::IntegerConstant_t>(*dest_str_type->m_len)) {
            ASR::IntegerConstant_t* len = ASR::down_cast<ASR::IntegerConstant_t>(dest_str_type->m_len);
            element_length_val = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), len->m_n);
        } else {
            element_length_val = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 1);
        }

        llvm::Value* bitcast_descriptor;
        with_value_semantics([&]() { visit_expr_wrapper(x.m_value, true); bitcast_descriptor = tmp; });

        llvm::Value* src_data_ptr;
        if (ASRUtils::is_array(ASRUtils::expr_type(x.m_value))) {
            src_data_ptr = llvm_utils->get_stringArray_data(ASRUtils::expr_type(x.m_value), bitcast_descriptor);
        } else {
            src_data_ptr = llvm_utils->get_string_data(src_str_type, bitcast_descriptor);
        }

        llvm::Value* byte_offset = builder->CreateMul(zero_based_idx, element_length_val);
        int dest_kind = dest_str_type->m_kind;
        if (dest_kind > 1) {
            llvm::Value* kind_val = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), dest_kind);
            byte_offset = builder->CreateMul(byte_offset, kind_val);
        }
        llvm::Value* byte_ptr = builder->CreateGEP(llvm::Type::getInt8Ty(context),
            src_data_ptr, byte_offset);

        llvm::Value* dest_data_ptr;
        if (is_array_target) {
            dest_data_ptr = llvm_utils->get_stringArray_data(target_type, target_ptr);
        } else {
            dest_data_ptr = llvm_utils->get_string_data(dest_str_type, target_ptr);
        }
        llvm::Value* copy_length = element_length_val;
        if (is_array_target) {
            llvm::Type* arr_type_llvm = llvm_utils->get_type_from_ttype_t_util(x.m_target, ASRUtils::type_get_past_allocatable(ASRUtils::type_get_past_pointer(target_type)), module.get());
            llvm::Value* num_elems = nullptr;
            if (ASRUtils::is_fixed_size_array(target_type)) {
                num_elems = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), ASRUtils::get_fixed_size_of_array(target_type));
            } else {
                num_elems = arr_descr->get_array_size(arr_type_llvm, target_ptr, nullptr, 4);
            }
            copy_length = builder->CreateMul(copy_length, num_elems);
        }
        
        copy_length = builder->CreateSExtOrTrunc(copy_length, llvm::Type::getInt64Ty(context));
        
        builder->CreateMemCpy(dest_data_ptr, llvm::MaybeAlign(), byte_ptr, llvm::MaybeAlign(), copy_length);
        tmp = nullptr;
        return;
    }

    void construct_stop(llvm::Value* exit_code, std::string stop_msg, ASR::expr_t* stop_code, Location /*loc*/) {
        std::string fmt {};
        std::vector<llvm::Value*> args;
        args.push_back(nullptr); // reserve space for fmt_str

        /* STOP MSG ("ERROR STOP") */
        {
            llvm::Value* STOP_MSG {};
            STOP_MSG = module->getNamedGlobal(stop_msg);
            if(!STOP_MSG) {STOP_MSG = builder->CreateGlobalString(stop_msg, stop_msg);} 
            STOP_MSG = builder->CreateBitCast(STOP_MSG, llvm::Type::getInt8Ty(context)->getPointerTo());
            fmt += "%s";
            args.push_back(STOP_MSG); // Null-Terminated String
        }

        /* SPACE + STOP CODE */
        if (stop_code && ASR::is_a<ASR::Integer_t>(*expr_type(stop_code))) {
            if(ASRUtils::extract_kind_from_ttype_t(expr_type(stop_code)) != 4) throw LCompilersException("Kind in Stop code should be = 4");
            fmt += " %d";
            visit_expr(*stop_code);
            llvm::Value* stop_code_int = tmp; tmp = nullptr;
            args.push_back(stop_code_int);
        } else if(stop_code && ASRUtils::is_string_only(expr_type(stop_code))){
            fmt += " %.*s";
            visit_expr_load_wrapper(stop_code, 0);
            llvm::Value* stop_code_str = tmp; tmp = nullptr;
            args.push_back(llvm_utils->get_string_length(ASRUtils::get_string_type(stop_code), stop_code_str));
            args.push_back(llvm_utils->get_string_data  (ASRUtils::get_string_type(stop_code), stop_code_str));
        } else if (stop_code){
            throw LCompilersException(
                "Stop Code should be of type [String, Integer].\n"
                "CurrentType : "+ASRUtils::type_to_str_fortran_expr(expr_type(stop_code), stop_code));
        } else if(!stop_code){
            // Do Nothing. Stop With No Error Message.
        }
        
        /* NEWLINE */
        {
            llvm::Value* NEWLINE = LCompilers::create_global_string_ptr(context, *module, *builder, "\n");
            fmt += "%s";
            args.push_back(NEWLINE); // Null-Terminated
        }

        /* PRINT ERROR */
        llvm::Value *fmt_ptr {};
        fmt_ptr = module->getNamedGlobal(fmt);
        if(!fmt_ptr) {fmt_ptr = builder->CreateGlobalString(fmt);}
        fmt_ptr = builder->CreateBitCast(fmt_ptr, llvm::Type::getInt8Ty(context)->getPointerTo());

        args[0] = fmt_ptr;
        print_error(context, *module, *builder, args);

        /* EXIT WITH CODE */
        {
            llvm::Function *fn_finalize = module->getFunction(
                "_lfortran_internal_alloc_finalize");
            if (!fn_finalize) {
                llvm::FunctionType *ft = llvm::FunctionType::get(
                    llvm::Type::getVoidTy(context), {}, false);
                fn_finalize = llvm::Function::Create(ft,
                    llvm::Function::ExternalLinkage,
                    "_lfortran_internal_alloc_finalize", module.get());
            }
            builder->CreateCall(fn_finalize, {});
        }
        if (stop_code && is_a<ASR::Integer_t>(*ASRUtils::expr_type(stop_code))) {
            this->visit_expr(*stop_code);
            exit_code = tmp;
        }
        exit(context, *module, *builder, exit_code);
    }

    void visit_Stop(const ASR::Stop_t &x) {
        if (compiler_options.emit_debug_info) {
            debug_emit_loc(x);
            if (x.m_code && is_a<ASR::Integer_t>(*ASRUtils::expr_type(x.m_code))) {
                llvm::Value *fmt_ptr = LCompilers::create_global_string_ptr(context, *module, *builder, infile);
                llvm::Value *fmt_ptr1 = llvm::ConstantInt::get(context, llvm::APInt(
                    1, compiler_options.use_colors));
                this->visit_expr(*x.m_code);
                llvm::Value *test = builder->CreateICmpNE(tmp, builder->getInt32(0));
                llvm_utils->create_if_else(test, [=]() {
                    call_print_stacktrace_addresses(context, *module, *builder,
                        {fmt_ptr, fmt_ptr1});
                }, [](){});
            }
        }

        int exit_code_int = 0;
        llvm::Value *exit_code = llvm::ConstantInt::get(context,
                llvm::APInt(32, exit_code_int));
        construct_stop(exit_code, "STOP", x.m_code, x.base.base.loc);
    }

    void visit_ErrorStop(const ASR::ErrorStop_t &x) {
        if (compiler_options.emit_debug_info) {
            debug_emit_loc(x);
            llvm::Value *fmt_ptr = LCompilers::create_global_string_ptr(context, *module, *builder, infile);
            llvm::Value *fmt_ptr1 = llvm::ConstantInt::get(context, llvm::APInt(
                1, compiler_options.use_colors));
            call_print_stacktrace_addresses(context, *module, *builder,
                {fmt_ptr, fmt_ptr1});
        }

        int exit_code_int = 1;
        llvm::Value *exit_code = llvm::ConstantInt::get(context,
                llvm::APInt(32, exit_code_int));
        construct_stop(exit_code, "ERROR STOP", x.m_code, x.base.base.loc);
    }

    void visit_SyncAll(const ASR::SyncAll_t & /* x */) {
        // No-op: coarray sync all is not yet supported at runtime
    }

    void visit_SyncMemory(const ASR::SyncMemory_t & /* x */) {
        // No-op: coarray sync memory is not yet supported at runtime
    }

    void visit_GpuKernelFunction(const ASR::GpuKernelFunction_t & /* x */) {
        // GPU kernel functions are not lowered to LLVM IR.
        // They are emitted as Metal/CUDA source by the device emitter.
    }

    llvm::Function* get_gpu_runtime_func(const std::string &name,
            llvm::FunctionType *ftype) {
        llvm::Function *fn = module->getFunction(name);
        if (!fn) {
            fn = llvm::Function::Create(ftype,
                llvm::Function::ExternalLinkage, name, module.get());
        }
        return fn;
    }

    void visit_GpuKernelLaunch(const ASR::GpuKernelLaunch_t &x) {
        llvm::Type *i8_ptr = llvm::PointerType::getUnqual(
            llvm::Type::getInt8Ty(context));
        llvm::Type *i32 = llvm::Type::getInt32Ty(context);
        llvm::Type *i64 = llvm::Type::getInt64Ty(context);
        llvm::Type *void_type = llvm::Type::getVoidTy(context);

        // 1. lfortran_gpu_init() -> ctx
        llvm::FunctionType *init_ft = llvm::FunctionType::get(i8_ptr, {}, false);
        llvm::Function *init_fn = get_gpu_runtime_func("lfortran_gpu_init", init_ft);
        llvm::Value *gpu_ctx = builder->CreateCall(init_fn, {});

        // 2. Get the GPU source string and kernel name
        ASR::GpuKernelFunction_t *kernel_func =
            ASR::down_cast<ASR::GpuKernelFunction_t>(x.m_kernel);
        std::string kernel_name(kernel_func->m_name);

        // The GPU source is passed via compiler_options
        std::string global_name = "__lfortran_gpu_source_" + kernel_name;
        llvm::Value *gpu_src;
        if (!compiler_options.gpu_metal_source.empty()) {
#if LLVM_VERSION_MAJOR >= 20
            gpu_src = builder->CreateGlobalString(
                compiler_options.gpu_metal_source, global_name);
#else
            gpu_src = builder->CreateGlobalStringPtr(
                compiler_options.gpu_metal_source, global_name);
#endif
        } else if (!compiler_options.gpu_cuda_source.empty()) {
            // CUDA kernels are pre-compiled; pass empty string
#if LLVM_VERSION_MAJOR >= 20
            gpu_src = builder->CreateGlobalString("", global_name);
        } else {
            gpu_src = builder->CreateGlobalString("", global_name);
        }

        llvm::Value *entry_name = builder->CreateGlobalString(kernel_name);
#else
            gpu_src = builder->CreateGlobalStringPtr("", global_name);
        } else {
            gpu_src = builder->CreateGlobalStringPtr("", global_name);
        }

        llvm::Value *entry_name = builder->CreateGlobalStringPtr(kernel_name);
#endif

        // 3. lfortran_gpu_load_kernel(ctx, source, entry_point) -> kernel
        llvm::FunctionType *load_ft = llvm::FunctionType::get(
            i8_ptr, {i8_ptr, i8_ptr, i8_ptr}, false);
        llvm::Function *load_fn = get_gpu_runtime_func("lfortran_gpu_load_kernel", load_ft);
        llvm::Value *gpu_kernel = builder->CreateCall(load_fn,
            {gpu_ctx, gpu_src, entry_name});

        // 4. Set arguments
        llvm::FunctionType *set_buffer_ft = llvm::FunctionType::get(
            void_type, {i8_ptr, i32, i8_ptr, i64}, false);
        llvm::Function *set_buffer_fn = get_gpu_runtime_func(
            "lfortran_gpu_set_buffer_arg", set_buffer_ft);

        llvm::FunctionType *set_scalar_ft = llvm::FunctionType::get(
            void_type, {i8_ptr, i32, i8_ptr, i64}, false);
        llvm::Function *set_scalar_fn = get_gpu_runtime_func(
            "lfortran_gpu_set_scalar_arg", set_scalar_ft);

        // Save scalar arg LLVM values for VLA workspace size computation
        std::vector<llvm::Value*> call_arg_values(x.n_args, nullptr);

        // Track scalar args to pack into a single struct buffer
        struct ScalarArgInfo {
            llvm::Value *value;
            llvm::Type *type;
        };
        std::vector<ScalarArgInfo> scalar_arg_infos;

        bool packed_mode = gpu_kernel_needs_buffer_packing(*kernel_func);

        // In packed mode, collect buffer arg info for deferred packing
        struct BufferArgInfo {
            llvm::Value *data_ptr;
            llvm::Value *byte_size;
        };
        std::vector<BufferArgInfo> buffer_arg_infos;

        // Track decomposed struct member buffers for write-back
        // after kernel launch.
        struct StructMemberWriteBack {
            llvm::Value *flat_buf;     // flat data buffer
            std::vector<llvm::Value*> data_ptrs; // original member data ptrs
            std::vector<llvm::Value*> sizes;     // per-element sizes
            int elem_byte_size;        // size of one element in bytes
            int64_t total_elements;    // number of struct array elements
            bool vla_sized;            // true if sized from VLA analysis
            // Descriptor info for allocating unallocated members
            llvm::Type *desc_type;
            llvm::Type *arr_llvm;
            llvm::Type *struct_llvm;
            llvm::Value *arg_val;
            int field_idx;
        };
        std::vector<StructMemberWriteBack> struct_writebacks;
        // Map "arr.member" -> first element's size (LLVM value)
        // for VLA workspace allocation of function-call result temps.
        std::map<std::string, llvm::Value*> struct_member_first_sizes;

        int buffer_idx = 0;

        // Pre-compute VLA workspace analysis and struct member write sizes
        // so that unallocated struct members can be sized correctly.
        std::vector<GpuVlaWorkspace> gpu_vla_workspaces =
            analyze_gpu_vla_workspaces(*kernel_func);
        std::map<std::string, int64_t> struct_member_vla_sizes =
            find_struct_member_vla_write_sizes(
                *kernel_func, gpu_vla_workspaces);
        std::map<std::string, std::string> struct_member_runtime_sources =
            find_struct_member_vla_runtime_sources(*kernel_func);

        for (size_t i = 0; i < x.n_args; i++) {
            ASR::expr_t *arg_expr = x.m_args[i].m_value;
            if (!arg_expr) continue;

            ASR::ttype_t *arg_type = ASRUtils::expr_type(arg_expr);
            bool is_allocatable_array = ASR::is_a<ASR::Allocatable_t>(*arg_type)
                && ASRUtils::is_array(arg_type);
            bool is_struct_member_alloc = is_allocatable_array
                && ASR::is_a<ASR::StructInstanceMember_t>(*arg_expr);

            // Check for assumed-shape (descriptor) arrays with null dims
            bool is_descriptor_array = false;
            if (ASRUtils::is_array(arg_type) && !is_allocatable_array) {
                ASR::ttype_t *past_alloc =
                    ASRUtils::type_get_past_allocatable(arg_type);
                if (ASR::is_a<ASR::Array_t>(*past_alloc)) {
                    ASR::Array_t *arr_early =
                        ASR::down_cast<ASR::Array_t>(past_alloc);
                    for (size_t d = 0; d < arr_early->n_dims; d++) {
                        if (!arr_early->m_dims[d].m_length) {
                            is_descriptor_array = true;
                            break;
                        }
                    }
                }
            }

            // For allocatable arrays and assumed-shape descriptor arrays,
            // we need the descriptor pointer (ptr_loads=1).
            // For StructInstanceMember of an allocatable array,
            // visit_StructInstanceMember returns a pointer to the
            // allocatable member field (T**); we load once afterward
            // to get T* (the descriptor pointer).
            int64_t ptr_loads_copy = ptr_loads;
            if (is_allocatable_array || is_descriptor_array) {
                ptr_loads = 1;
            }
            this->visit_expr(*arg_expr);
            ptr_loads = ptr_loads_copy;
            llvm::Value *arg_val = tmp;

            // StructInstanceMember for an allocatable array returns
            // a pointer to the allocatable-pointer field within the
            // struct (i.e., descriptor**). Load once to get the
            // descriptor pointer that get_pointer_to_data expects.
            if (is_struct_member_alloc) {
                ASR::ttype_t *desc_asr_type =
                    ASRUtils::type_get_past_allocatable(arg_type);
                llvm::Type *desc_type =
                    llvm_utils->get_type_from_ttype_t_util(
                        arg_expr, desc_asr_type, module.get());
                arg_val = llvm_utils->CreateLoad2(
                    desc_type->getPointerTo(), arg_val);
            }

            if (ASRUtils::is_array(arg_type)) {
                // For allocatable/descriptor arrays, extract data pointer
                // from the descriptor
                llvm::Value *data_ptr;
                if (is_allocatable_array || is_descriptor_array) {
                    ASR::ttype_t *desc_asr_type =
                        ASRUtils::type_get_past_allocatable(arg_type);
                    llvm::Type *desc_type =
                        llvm_utils->get_type_from_ttype_t_util(
                            arg_expr, desc_asr_type, module.get());
                    llvm::Value *data_ptr_ptr =
                        arr_descr->get_pointer_to_data(desc_type, arg_val);
                    data_ptr = llvm_utils->CreateLoad2(
                        llvm::PointerType::getUnqual(llvm::Type::getInt8Ty(context)), data_ptr_ptr);
                    data_ptr = builder->CreatePointerCast(data_ptr, i8_ptr);
                } else {
                    data_ptr = builder->CreatePointerCast(arg_val, i8_ptr);
                }
                // Compute size
                ASR::Array_t *arr = ASR::down_cast<ASR::Array_t>(
                    ASRUtils::type_get_past_allocatable(arg_type));
                int elem_size = 4; // default float/int
                if (arr->m_type->type == ASR::ttypeType::Real) {
                    int kind = ASR::down_cast<ASR::Real_t>(arr->m_type)->m_kind;
                    elem_size = kind;
                } else if (arr->m_type->type == ASR::ttypeType::Integer) {
                    int kind = ASR::down_cast<ASR::Integer_t>(arr->m_type)->m_kind;
                    elem_size = kind;
                } else if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(arr->m_type))
                        && ASR::is_a<ASR::Var_t>(*arg_expr)) {
                    ASR::Variable_t *sv = ASR::down_cast<ASR::Variable_t>(
                        ASRUtils::symbol_get_past_external(
                            ASR::down_cast<ASR::Var_t>(arg_expr)->m_v));
                    if (sv->m_type_declaration) {
                        ASR::symbol_t *ss = ASRUtils::symbol_get_past_external(
                            sv->m_type_declaration);
                        if (ASR::is_a<ASR::Struct_t>(*ss)) {
                            llvm::Type *elem_llvm_type =
                                llvm_utils->getStructType(
                                    ASR::down_cast<ASR::Struct_t>(ss),
                                    module.get());
                            elem_size = module->getDataLayout().getTypeAllocSize(
                                elem_llvm_type);
                        }
                    }
                }
                // Get the total number of elements from the array dimension
                int64_t total_elements = 1;
                bool all_constant = true;
                bool has_null_dim = false;
                for (size_t d = 0; d < arr->n_dims; d++) {
                    if (arr->m_dims[d].m_length &&
                        ASR::is_a<ASR::IntegerConstant_t>(*arr->m_dims[d].m_length)) {
                        total_elements *= ASR::down_cast<ASR::IntegerConstant_t>(
                            arr->m_dims[d].m_length)->m_n;
                    } else {
                        all_constant = false;
                        if (!arr->m_dims[d].m_length) {
                            has_null_dim = true;
                        }
                    }
                }
                llvm::Value *byte_size;
                if (all_constant) {
                    byte_size = llvm::ConstantInt::get(i64, total_elements * elem_size);
                } else if (has_null_dim &&
                        (is_allocatable_array || is_descriptor_array)) {
                    // Allocatable/descriptor arrays: get size from the
                    // descriptor
                    ASR::ttype_t *desc_asr_type =
                        ASRUtils::type_get_past_allocatable(arg_type);
                    llvm::Type *desc_type =
                        llvm_utils->get_type_from_ttype_t_util(
                            arg_expr, desc_asr_type, module.get());
                    llvm::Value *n_elems = arr_descr->get_array_size(
                        desc_type, arg_val, nullptr, 4);
                    llvm::Value *n_elems_64 =
                        builder->CreateSExtOrTrunc(n_elems, i64);
                    byte_size = builder->CreateMul(n_elems_64,
                        llvm::ConstantInt::get(i64, elem_size));
                } else {
                    // Compute array size at runtime from dimension expressions
                    llvm::Value *total_val = llvm::ConstantInt::get(i64, 1);
                    int64_t ptr_loads_copy2 = ptr_loads;
                    ptr_loads = 2;
                    for (size_t d = 0; d < arr->n_dims; d++) {
                        if (arr->m_dims[d].m_length) {
                            this->visit_expr(*arr->m_dims[d].m_length);
                            llvm::Value *dim_len = tmp;
                            if (dim_len->getType()->isPointerTy()) {
                                dim_len = llvm_utils->CreateLoad2(
                                    llvm::Type::getInt32Ty(context), dim_len);
                            }
                            dim_len = builder->CreateSExtOrTrunc(dim_len, i64);
                            total_val = builder->CreateMul(total_val, dim_len);
                        }
                    }
                    ptr_loads = ptr_loads_copy2;
                    byte_size = builder->CreateMul(total_val,
                        llvm::ConstantInt::get(i64, elem_size));
                }
                if (packed_mode) {
                    buffer_arg_infos.push_back({data_ptr, byte_size});
                } else {
                    llvm::Value *idx = llvm::ConstantInt::get(i32, buffer_idx++);
                    builder->CreateCall(set_buffer_fn, {gpu_kernel, idx, data_ptr, byte_size});
                }

                // For struct arrays with allocatable members, pass
                // flattened data and offsets as additional buffers.
                // In packed mode these go into buffer_arg_infos;
                // otherwise they become individual Metal buffers.
                auto emit_gpu_buffer = [&](llvm::Value *ptr,
                        llvm::Value *sz) {
                    if (packed_mode) {
                        buffer_arg_infos.push_back({ptr, sz});
                    } else {
                        llvm::Value *bi = llvm::ConstantInt::get(
                            i32, buffer_idx++);
                        builder->CreateCall(set_buffer_fn,
                            {gpu_kernel, bi, ptr, sz});
                    }
                };
                if (all_constant &&
                            ASR::is_a<ASR::StructType_t>(
                                *ASRUtils::extract_type(arr->m_type)) &&
                            ASR::is_a<ASR::Var_t>(*arg_expr)) {
                        ASR::Variable_t *arr_var =
                            ASR::down_cast<ASR::Variable_t>(
                                ASRUtils::symbol_get_past_external(
                                    ASR::down_cast<ASR::Var_t>(
                                        arg_expr)->m_v));
                        ASR::Struct_t *st = nullptr;
                        if (arr_var->m_type_declaration) {
                            ASR::symbol_t *s =
                                ASRUtils::symbol_get_past_external(
                                    arr_var->m_type_declaration);
                            if (ASR::is_a<ASR::Struct_t>(*s)) {
                                st = ASR::down_cast<ASR::Struct_t>(s);
                            }
                        }
                        if (st) {
                            llvm::Type *struct_llvm =
                                llvm_utils->getStructType(
                                    st, module.get());
                            llvm::Type *arr_llvm =
                                llvm::ArrayType::get(struct_llvm,
                                    total_elements);
                            llvm::Value *i32_0 =
                                llvm::ConstantInt::get(i32, 0);
                            int field_idx = 0;
                            for (size_t m = 0; m < st->n_members;
                                    m++) {
                                ASR::symbol_t *mem =
                                    st->m_symtab->get_symbol(
                                        st->m_members[m]);
                                if (!mem ||
                                        !ASR::is_a<ASR::Variable_t>(
                                            *mem)) {
                                    field_idx++;
                                    continue;
                                }
                                ASR::Variable_t *mv =
                                    ASR::down_cast<ASR::Variable_t>(
                                        mem);
                                if (!ASRUtils::is_allocatable(
                                        mv->m_type)) {
                                    field_idx++;
                                    continue;
                                }
                                ASR::ttype_t *inner =
                                    ASRUtils::type_get_past_allocatable(
                                        mv->m_type);
                                if (!ASR::is_a<ASR::Array_t>(*inner)) {
                                    field_idx++;
                                    continue;
                                }
                                ASR::Array_t *mem_arr =
                                    ASR::down_cast<ASR::Array_t>(inner);
                                // Get LLVM descriptor type for the
                                // allocatable member's array
                                llvm::Type *mem_el_llvm = nullptr;
                                if (ASR::is_a<ASR::StructType_t>(
                                        *ASRUtils::extract_type(
                                            mem_arr->m_type)) &&
                                        mv->m_type_declaration) {
                                    ASR::symbol_t *es =
                                        ASRUtils::symbol_get_past_external(
                                            mv->m_type_declaration);
                                    if (ASR::is_a<ASR::Struct_t>(*es)) {
                                        mem_el_llvm =
                                            llvm_utils->getStructType(
                                                ASR::down_cast<ASR::Struct_t>(es),
                                                module.get());
                                    }
                                }
                                if (!mem_el_llvm) {
                                    mem_el_llvm = llvm_utils->get_el_type(
                                        nullptr, mem_arr->m_type,
                                        module.get());
                                }
                                llvm::Type *desc_type =
                                    arr_descr->get_array_type(
                                        nullptr, inner,
                                        mem_el_llvm, false);
                                int me_size = 4;
                                if (mem_arr->m_type->type ==
                                        ASR::ttypeType::Real) {
                                    me_size =
                                        ASR::down_cast<ASR::Real_t>(
                                            mem_arr->m_type)->m_kind;
                                } else if (mem_arr->m_type->type ==
                                        ASR::ttypeType::Integer) {
                                    me_size =
                                        ASR::down_cast<ASR::Integer_t>(
                                            mem_arr->m_type)->m_kind;
                                }
                                // Collect data ptrs and sizes.
                                // Look up VLA write size for this member
                                // (non-zero when the kernel writes to an
                                // unallocated allocatable member).
                                std::string arr_var_name(
                                    arr_var->m_name);
                                std::string sm_key =
                                    arr_var_name + "."
                                    + st->m_members[m];
                                int64_t vla_member_sz = 0;
                                {
                                    auto vit =
                                        struct_member_vla_sizes.find(
                                            sm_key);
                                    if (vit !=
                                            struct_member_vla_sizes.end())
                                        vla_member_sz = vit->second;
                                }
                                // Check for runtime-sourced allocation
                                // (target member sized from another
                                // struct's member at runtime).
                                bool runtime_sourced = false;
                                std::string runtime_src_key;
                                if (vla_member_sz == 0) {
                                    auto rit =
                                        struct_member_runtime_sources
                                            .find(sm_key);
                                    if (rit !=
                                            struct_member_runtime_sources
                                                .end()) {
                                        runtime_src_key = rit->second;
                                        runtime_sourced = true;
                                    }
                                }
                                std::vector<llvm::Value*> dps, szs;
                                llvm::Value *tot =
                                    llvm::ConstantInt::get(i64, 0);
                                for (int64_t k = 0;
                                        k < total_elements; k++) {
                                    llvm::Value *ep =
                                        builder->CreateGEP(arr_llvm,
                                            arg_val,
                                            {i32_0,
                                             llvm::ConstantInt::get(
                                                 i32, k)});
                                    llvm::Value *fp =
                                        builder->CreateGEP(
                                            struct_llvm, ep,
                                            {i32_0,
                                             llvm::ConstantInt::get(
                                                 i32, field_idx)});
                                    llvm::Value *dp =
                                        llvm_utils->CreateLoad2(
                                            desc_type->getPointerTo(),
                                            fp);
                                    llvm::Value *dpp =
                                        arr_descr->get_pointer_to_data(
                                            desc_type, dp);
                                    llvm::Value *raw =
                                        llvm_utils->CreateLoad2(
                                            llvm::PointerType::getUnqual(
                                                llvm::Type::getInt8Ty(
                                                    context)),
                                            dpp);
                                    raw = builder->CreatePointerCast(
                                        raw, i8_ptr);
                                    if (vla_member_sz > 0) {
                                        // Kernel writes to this member.
                                        // The descriptor pointer in the
                                        // struct may be garbage (member
                                        // not yet allocated). Allocate
                                        // a fresh descriptor and data
                                        // buffer, and update the struct
                                        // field so the descriptor is
                                        // valid.
                                        llvm::Value *ne64 =
                                            llvm::ConstantInt::get(
                                                i64, vla_member_sz);
                                        szs.push_back(ne64);
                                        // Allocate descriptor on heap
                                        uint64_t desc_sz =
                                            module->getDataLayout()
                                                .getTypeAllocSize(
                                                    desc_type);
                                        llvm::FunctionType *mft2 =
                                            llvm::FunctionType::get(
                                                i8_ptr, {i64}, false);
                                        llvm::Function *mfn2 =
                                            get_gpu_runtime_func(
                                                "malloc", mft2);
                                        llvm::Value *desc_mem =
                                            builder->CreateCall(mfn2,
                                                {llvm::ConstantInt::get(
                                                    i64, desc_sz)});
                                        llvm::Value *new_desc =
                                            builder->CreatePointerCast(
                                                desc_mem,
                                                desc_type
                                                    ->getPointerTo());
                                        // Store descriptor ptr in
                                        // struct field
                                        builder->CreateStore(
                                            new_desc, fp);
                                        // Initialize descriptor
                                        arr_descr
                                            ->fill_dimension_descriptor(
                                                desc_type, new_desc, 1);
                                        // Allocate data buffer
                                        llvm::Value *alloc_bytes =
                                            llvm::ConstantInt::get(
                                                i64,
                                                vla_member_sz * me_size);
                                        llvm::Value *new_dp =
                                            builder->CreateCall(
                                                mfn2, {alloc_bytes});
                                        // Set data pointer in
                                        // descriptor
                                        llvm::Value *new_dpp =
                                            arr_descr
                                                ->get_pointer_to_data(
                                                    desc_type,
                                                    new_desc);
                                        builder->CreateStore(
                                            builder->CreatePointerCast(
                                                new_dp,
                                                mem_el_llvm
                                                    ->getPointerTo()),
                                            new_dpp);
                                        // Set dimension extent
                                        llvm::Value *dim_des_arr =
                                            arr_descr
                                                ->get_pointer_to_dimension_descriptor_array(
                                                    desc_type,
                                                    new_desc);
                                        llvm::Value *dim0 =
                                            arr_descr
                                                ->get_pointer_to_dimension_descriptor(
                                                    dim_des_arr,
                                                    llvm::ConstantInt::get(
                                                        i32, 0));
                                        llvm::Value *extent_ptr =
                                            arr_descr
                                                ->get_dimension_size(
                                                    dim0, false);
                                        builder->CreateStore(
                                            builder->CreateSExtOrTrunc(
                                                ne64,
                                                llvm::Type::getInt64Ty(
                                                    context)),
                                            extent_ptr);
                                        dps.push_back(new_dp);
                                    } else if (runtime_sourced) {
                                        // Size determined at runtime from
                                        // another struct member (e.g.,
                                        // b(i)%v sized from a(i)%v).
                                        auto sit =
                                            struct_member_first_sizes
                                                .find(runtime_src_key);
                                        llvm::Value *ne64;
                                        if (sit !=
                                                struct_member_first_sizes
                                                    .end()) {
                                            ne64 =
                                                builder->CreateSExtOrTrunc(
                                                    sit->second, i64);
                                        } else {
                                            ne64 =
                                                llvm::ConstantInt::get(
                                                    i64, 1);
                                        }
                                        szs.push_back(ne64);
                                        uint64_t desc_sz =
                                            module->getDataLayout()
                                                .getTypeAllocSize(
                                                    desc_type);
                                        llvm::FunctionType *mft2 =
                                            llvm::FunctionType::get(
                                                i8_ptr, {i64}, false);
                                        llvm::Function *mfn2 =
                                            get_gpu_runtime_func(
                                                "malloc", mft2);
                                        llvm::Value *desc_mem =
                                            builder->CreateCall(mfn2,
                                                {llvm::ConstantInt::get(
                                                    i64, desc_sz)});
                                        llvm::Value *new_desc =
                                            builder->CreatePointerCast(
                                                desc_mem,
                                                desc_type
                                                    ->getPointerTo());
                                        builder->CreateStore(
                                            new_desc, fp);
                                        arr_descr
                                            ->fill_dimension_descriptor(
                                                desc_type, new_desc, 1);
                                        llvm::Value *alloc_bytes =
                                            builder->CreateMul(ne64,
                                                llvm::ConstantInt::get(
                                                    i64, me_size));
                                        llvm::Value *new_dp =
                                            builder->CreateCall(
                                                mfn2, {alloc_bytes});
                                        llvm::Value *new_dpp =
                                            arr_descr
                                                ->get_pointer_to_data(
                                                    desc_type,
                                                    new_desc);
                                        builder->CreateStore(
                                            builder->CreatePointerCast(
                                                new_dp,
                                                mem_el_llvm
                                                    ->getPointerTo()),
                                            new_dpp);
                                        llvm::Value *dim_des_arr =
                                            arr_descr
                                                ->get_pointer_to_dimension_descriptor_array(
                                                    desc_type,
                                                    new_desc);
                                        llvm::Value *dim0 =
                                            arr_descr
                                                ->get_pointer_to_dimension_descriptor(
                                                    dim_des_arr,
                                                    llvm::ConstantInt::get(
                                                        i32, 0));
                                        llvm::Value *extent_ptr =
                                            arr_descr
                                                ->get_dimension_size(
                                                    dim0, false);
                                        builder->CreateStore(
                                            builder->CreateSExtOrTrunc(
                                                ne64,
                                                llvm::Type::getInt64Ty(
                                                    context)),
                                            extent_ptr);
                                        dps.push_back(new_dp);
                                    } else {
                                        llvm::Value *ne =
                                            arr_descr->get_array_size(
                                                desc_type, dp,
                                                nullptr, 4);
                                        llvm::Value *ne64 =
                                            builder->CreateSExtOrTrunc(
                                                ne, i64);
                                        szs.push_back(ne64);
                                        dps.push_back(raw);
                                    }
                                    tot = builder->CreateAdd(
                                        tot, szs.back());
                                }
                                // Allocate flat data buffer
                                llvm::Value *tot_bytes =
                                    builder->CreateMul(tot,
                                        llvm::ConstantInt::get(
                                            i64, me_size));
                                llvm::FunctionType *mft =
                                    llvm::FunctionType::get(
                                        i8_ptr, {i64}, false);
                                llvm::Function *mfn =
                                    get_gpu_runtime_func(
                                        "malloc", mft);
                                llvm::Value *flat =
                                    builder->CreateCall(
                                        mfn, {tot_bytes});
                                // Allocate offsets buffer
                                llvm::Value *off_sz =
                                    llvm::ConstantInt::get(
                                        i64, total_elements * 4);
                                llvm::Value *off_buf =
                                    builder->CreateCall(
                                        mfn, {off_sz});
                                llvm::Value *off_ptr =
                                    builder->CreatePointerCast(
                                        off_buf,
                                        llvm::Type::getInt32Ty(
                                            context)->getPointerTo());
                                // Copy data and store offsets
                                llvm::Value *run =
                                    llvm::ConstantInt::get(i64, 0);
                                for (int64_t k = 0;
                                        k < total_elements; k++) {
                                    llvm::Value *off32 =
                                        builder->CreateTrunc(
                                            run,
                                            llvm::Type::getInt32Ty(
                                                context));
                                    builder->CreateStore(off32,
                                        builder->CreateGEP(
                                            llvm::Type::getInt32Ty(
                                                context),
                                            off_ptr,
                                            llvm::ConstantInt::get(
                                                i32, k)));
                                    llvm::Value *boff =
                                        builder->CreateMul(run,
                                            llvm::ConstantInt::get(
                                                i64, me_size));
                                    llvm::Value *dst =
                                        builder->CreateGEP(
                                            llvm::Type::getInt8Ty(
                                                context),
                                            flat, boff);
                                    // Only copy existing data when
                                    // members were not VLA-sized
                                    // (VLA-sized members were freshly
                                    // allocated for write-back).
                                    if (vla_member_sz == 0
                                            && !runtime_sourced) {
                                        llvm::Value *sb =
                                            builder->CreateMul(
                                                szs[k],
                                                llvm::ConstantInt::get(
                                                    i64, me_size));
                                        builder->CreateMemCpy(
                                            dst,
                                            llvm::MaybeAlign(1),
                                            dps[k],
                                            llvm::MaybeAlign(1),
                                            sb);
                                    } else {
                                        // Copy pre-existing data if
                                        // the member was allocated.
                                        // The new dps[k] always points
                                        // to a valid buffer.
                                        llvm::Value *sb =
                                            builder->CreateMul(
                                                szs[k],
                                                llvm::ConstantInt::get(
                                                    i64, me_size));
                                        builder->CreateMemCpy(
                                            dst,
                                            llvm::MaybeAlign(1),
                                            dps[k],
                                            llvm::MaybeAlign(1),
                                            sb);
                                    }
                                    run = builder->CreateAdd(
                                        run, szs[k]);
                                }
                                // Set data buffer
                                emit_gpu_buffer(flat, tot_bytes);
                                // Set offsets buffer
                                emit_gpu_buffer(off_buf, off_sz);
                                // Set sizes buffer (per-element sizes)
                                llvm::Value *sz_buf =
                                    builder->CreateCall(
                                        mfn, {off_sz});
                                llvm::Value *sz_ptr =
                                    builder->CreatePointerCast(
                                        sz_buf,
                                        llvm::Type::getInt32Ty(
                                            context)->getPointerTo());
                                for (int64_t k = 0;
                                        k < total_elements; k++) {
                                    llvm::Value *sz32 =
                                        builder->CreateTrunc(
                                            szs[k],
                                            llvm::Type::getInt32Ty(
                                                context));
                                    builder->CreateStore(sz32,
                                        builder->CreateGEP(
                                            llvm::Type::getInt32Ty(
                                                context),
                                            sz_ptr,
                                            llvm::ConstantInt::get(
                                                i32, k)));
                                }
                                emit_gpu_buffer(sz_buf, off_sz);
                                // Record write-back info for copying
                                // data from the flat buffer back to
                                // struct member descriptors after
                                // kernel launch.
                                struct_writebacks.push_back(
                                    {flat, dps, szs, me_size,
                                     total_elements,
                                     vla_member_sz > 0
                                         || runtime_sourced,
                                     desc_type, arr_llvm,
                                     struct_llvm, arg_val,
                                     field_idx});
                                // Save first element's size for VLA
                                // workspace allocation of function-call
                                // result temps that depend on this member.
                                if (!szs.empty()) {
                                    struct_member_first_sizes[sm_key] =
                                        szs[0];
                                }
                                field_idx++;
                            }
                        }
                    }
                    // For allocatable arrays of structs with allocatable
                    // members, build auxiliary data/offsets/sizes buffers
                    // at runtime using LLVM IR loops (element count is
                    // not known at compile time).
                    // For struct arrays with allocatable members and
                    // non-constant dimensions, build auxiliary
                    // data/offsets/sizes buffers at runtime using LLVM
                    // IR loops.
                    if (!all_constant &&
                            ASR::is_a<ASR::StructType_t>(
                                *ASRUtils::extract_type(arr->m_type)) &&
                            ASR::is_a<ASR::Var_t>(*arg_expr)) {
                        ASR::Variable_t *arr_var =
                            ASR::down_cast<ASR::Variable_t>(
                                ASRUtils::symbol_get_past_external(
                                    ASR::down_cast<ASR::Var_t>(
                                        arg_expr)->m_v));
                        ASR::Struct_t *st = nullptr;
                        if (arr_var->m_type_declaration) {
                            ASR::symbol_t *s =
                                ASRUtils::symbol_get_past_external(
                                    arr_var->m_type_declaration);
                            if (ASR::is_a<ASR::Struct_t>(*s)) {
                                st = ASR::down_cast<ASR::Struct_t>(s);
                            }
                        }
                        if (st) {
                            llvm::Type *struct_llvm =
                                llvm_utils->getStructType(
                                    st, module.get());
                            // Get total element count at runtime.
                            // For allocatable/descriptor arrays, use
                            // the descriptor; otherwise, derive from
                            // byte_size / elem_size.
                            llvm::Value *n_elems_64;
                            if (is_allocatable_array ||
                                    is_descriptor_array) {
                                ASR::ttype_t *alloc_desc_asr =
                                    ASRUtils::type_get_past_allocatable(
                                        arg_type);
                                llvm::Type *alloc_desc_llvm =
                                    llvm_utils->get_type_from_ttype_t_util(
                                        arg_expr, alloc_desc_asr,
                                        module.get());
                                llvm::Value *n_elems_rt =
                                    arr_descr->get_array_size(
                                        alloc_desc_llvm, arg_val,
                                        nullptr, 4);
                                n_elems_64 =
                                    builder->CreateSExtOrTrunc(
                                        n_elems_rt, i64);
                            } else {
                                uint64_t struct_sz =
                                    module->getDataLayout()
                                        .getTypeAllocSize(struct_llvm);
                                n_elems_64 = builder->CreateUDiv(
                                    byte_size,
                                    llvm::ConstantInt::get(
                                        i64, struct_sz));
                            }
                            // Typed pointer to the struct array data
                            llvm::Value *typed_data =
                                builder->CreatePointerCast(
                                    data_ptr,
                                    struct_llvm->getPointerTo());
                            llvm::FunctionType *mft =
                                llvm::FunctionType::get(
                                    i8_ptr, {i64}, false);
                            llvm::Function *mfn =
                                get_gpu_runtime_func("malloc", mft);
                            int field_idx = 0;
                            for (size_t m = 0; m < st->n_members;
                                    m++) {
                                ASR::symbol_t *mem =
                                    st->m_symtab->get_symbol(
                                        st->m_members[m]);
                                if (!mem ||
                                        !ASR::is_a<ASR::Variable_t>(
                                            *mem)) {
                                    field_idx++;
                                    continue;
                                }
                                ASR::Variable_t *mv =
                                    ASR::down_cast<ASR::Variable_t>(
                                        mem);
                                if (!ASRUtils::is_allocatable(
                                        mv->m_type)) {
                                    field_idx++;
                                    continue;
                                }
                                ASR::ttype_t *inner =
                                    ASRUtils::type_get_past_allocatable(
                                        mv->m_type);
                                if (!ASR::is_a<ASR::Array_t>(*inner)) {
                                    field_idx++;
                                    continue;
                                }
                                ASR::Array_t *mem_arr =
                                    ASR::down_cast<ASR::Array_t>(
                                        inner);
                                llvm::Type *mem_el_llvm = nullptr;
                                if (ASR::is_a<ASR::StructType_t>(
                                        *ASRUtils::extract_type(
                                            mem_arr->m_type)) &&
                                        mv->m_type_declaration) {
                                    ASR::symbol_t *es =
                                        ASRUtils::symbol_get_past_external(
                                            mv->m_type_declaration);
                                    if (ASR::is_a<ASR::Struct_t>(
                                            *es)) {
                                        mem_el_llvm =
                                            llvm_utils->getStructType(
                                                ASR::down_cast<
                                                    ASR::Struct_t>(es),
                                                module.get());
                                    }
                                }
                                if (!mem_el_llvm) {
                                    mem_el_llvm =
                                        llvm_utils->get_el_type(
                                            nullptr, mem_arr->m_type,
                                            module.get());
                                }
                                llvm::Type *mem_desc_type =
                                    arr_descr->get_array_type(
                                        nullptr, inner,
                                        mem_el_llvm, false);
                                int me_size = 4;
                                if (mem_arr->m_type->type ==
                                        ASR::ttypeType::Real) {
                                    me_size =
                                        ASR::down_cast<ASR::Real_t>(
                                            mem_arr->m_type)->m_kind;
                                } else if (mem_arr->m_type->type ==
                                        ASR::ttypeType::Integer) {
                                    me_size =
                                        ASR::down_cast<ASR::Integer_t>(
                                            mem_arr->m_type)->m_kind;
                                }
                                llvm::Value *me_size_val =
                                    llvm::ConstantInt::get(i64,
                                        me_size);
                                // Allocate sizes and offsets buffers
                                llvm::Value *buf_bytes =
                                    builder->CreateMul(n_elems_64,
                                        llvm::ConstantInt::get(i64, 4));
                                llvm::Value *sz_buf =
                                    builder->CreateCall(mfn,
                                        {buf_bytes});
                                llvm::Value *sz_ptr =
                                    builder->CreatePointerCast(sz_buf,
                                        llvm::Type::getInt32Ty(
                                            context)->getPointerTo());
                                llvm::Value *off_buf =
                                    builder->CreateCall(mfn,
                                        {buf_bytes});
                                llvm::Value *off_ptr =
                                    builder->CreatePointerCast(off_buf,
                                        llvm::Type::getInt32Ty(
                                            context)->getPointerTo());
                                // Alloca for loop counter and total
                                llvm::AllocaInst *k_alloca =
                                    llvm_utils->CreateAlloca(i64);
                                llvm::AllocaInst *tot_alloca =
                                    llvm_utils->CreateAlloca(i64);
                                builder->CreateStore(
                                    llvm::ConstantInt::get(i64, 0),
                                    k_alloca);
                                builder->CreateStore(
                                    llvm::ConstantInt::get(i64, 0),
                                    tot_alloca);
                                // Loop 1: compute per-element sizes
                                // and running total
                                llvm::Function *fn =
                                    builder->GetInsertBlock()
                                        ->getParent();
                                llvm::BasicBlock *l1h =
                                    llvm::BasicBlock::Create(context,
                                        "sa_sz.head", fn);
                                llvm::BasicBlock *l1b =
                                    llvm::BasicBlock::Create(context,
                                        "sa_sz.body", fn);
                                llvm::BasicBlock *l1e =
                                    llvm::BasicBlock::Create(context,
                                        "sa_sz.end", fn);
                                builder->CreateBr(l1h);
                                builder->SetInsertPoint(l1h);
                                {
                                    llvm::Value *kv =
                                        llvm_utils->CreateLoad2(
                                            i64, k_alloca);
                                    llvm::Value *c1 =
                                        builder->CreateICmpSLT(
                                            kv, n_elems_64);
                                    builder->CreateCondBr(c1, l1b, l1e);
                                }
                                builder->SetInsertPoint(l1b);
                                {
                                    llvm::Value *kv =
                                        llvm_utils->CreateLoad2(
                                            i64, k_alloca);
                                    llvm::Value *kv32 =
                                        builder->CreateTrunc(kv,
                                            llvm::Type::getInt32Ty(
                                                context));
                                    // GEP to field within element kv
                                    llvm::Value *fp =
                                        builder->CreateGEP(
                                            struct_llvm, typed_data,
                                            {kv, llvm::ConstantInt::get(
                                                i32, field_idx)});
                                    llvm::Value *dp =
                                        llvm_utils->CreateLoad2(
                                            mem_desc_type
                                                ->getPointerTo(),
                                            fp);
                                    llvm::Value *ne =
                                        arr_descr->get_array_size(
                                            mem_desc_type, dp,
                                            nullptr, 4);
                                    llvm::Value *ne64 =
                                        builder->CreateSExtOrTrunc(
                                            ne, i64);
                                    // Store size
                                    llvm::Value *ne32 =
                                        builder->CreateTrunc(ne64,
                                            llvm::Type::getInt32Ty(
                                                context));
                                    builder->CreateStore(ne32,
                                        builder->CreateGEP(
                                            llvm::Type::getInt32Ty(
                                                context),
                                            sz_ptr, kv32));
                                    // Accumulate total
                                    llvm::Value *cur =
                                        llvm_utils->CreateLoad2(
                                            i64, tot_alloca);
                                    builder->CreateStore(
                                        builder->CreateAdd(cur, ne64),
                                        tot_alloca);
                                    // Increment k
                                    builder->CreateStore(
                                        builder->CreateAdd(kv,
                                            llvm::ConstantInt::get(
                                                i64, 1)),
                                        k_alloca);
                                    builder->CreateBr(l1h);
                                }
                                builder->SetInsertPoint(l1e);
                                // Allocate flat data buffer
                                llvm::Value *tot =
                                    llvm_utils->CreateLoad2(
                                        i64, tot_alloca);
                                llvm::Value *tot_bytes =
                                    builder->CreateMul(tot,
                                        me_size_val);
                                llvm::Value *flat =
                                    builder->CreateCall(mfn,
                                        {tot_bytes});
                                // Loop 2: copy data and fill offsets
                                builder->CreateStore(
                                    llvm::ConstantInt::get(i64, 0),
                                    k_alloca);
                                builder->CreateStore(
                                    llvm::ConstantInt::get(i64, 0),
                                    tot_alloca);
                                llvm::BasicBlock *l2h =
                                    llvm::BasicBlock::Create(context,
                                        "sa_cp.head", fn);
                                llvm::BasicBlock *l2b =
                                    llvm::BasicBlock::Create(context,
                                        "sa_cp.body", fn);
                                llvm::BasicBlock *l2e =
                                    llvm::BasicBlock::Create(context,
                                        "sa_cp.end", fn);
                                builder->CreateBr(l2h);
                                builder->SetInsertPoint(l2h);
                                {
                                    llvm::Value *kv =
                                        llvm_utils->CreateLoad2(
                                            i64, k_alloca);
                                    llvm::Value *c2 =
                                        builder->CreateICmpSLT(
                                            kv, n_elems_64);
                                    builder->CreateCondBr(c2, l2b, l2e);
                                }
                                builder->SetInsertPoint(l2b);
                                {
                                    llvm::Value *kv =
                                        llvm_utils->CreateLoad2(
                                            i64, k_alloca);
                                    llvm::Value *kv32 =
                                        builder->CreateTrunc(kv,
                                            llvm::Type::getInt32Ty(
                                                context));
                                    // Store offset
                                    llvm::Value *run =
                                        llvm_utils->CreateLoad2(
                                            i64, tot_alloca);
                                    llvm::Value *off32 =
                                        builder->CreateTrunc(run,
                                            llvm::Type::getInt32Ty(
                                                context));
                                    builder->CreateStore(off32,
                                        builder->CreateGEP(
                                            llvm::Type::getInt32Ty(
                                                context),
                                            off_ptr, kv32));
                                    // Get descriptor and data pointer
                                    llvm::Value *fp =
                                        builder->CreateGEP(
                                            struct_llvm, typed_data,
                                            {kv, llvm::ConstantInt::get(
                                                i32, field_idx)});
                                    llvm::Value *dp =
                                        llvm_utils->CreateLoad2(
                                            mem_desc_type
                                                ->getPointerTo(),
                                            fp);
                                    llvm::Value *dpp =
                                        arr_descr->get_pointer_to_data(
                                            mem_desc_type, dp);
                                    llvm::Value *raw =
                                        llvm_utils->CreateLoad2(
                                            llvm::PointerType::getUnqual(
                                                llvm::Type::getInt8Ty(
                                                    context)),
                                            dpp);
                                    raw = builder->CreatePointerCast(
                                        raw, i8_ptr);
                                    // Get per-element size from
                                    // sizes buffer
                                    llvm::Value *esz =
                                        llvm_utils->CreateLoad2(
                                            llvm::Type::getInt32Ty(
                                                context),
                                            builder->CreateGEP(
                                                llvm::Type::getInt32Ty(
                                                    context),
                                                sz_ptr, kv32));
                                    llvm::Value *esz64 =
                                        builder->CreateSExtOrTrunc(
                                            esz, i64);
                                    // Copy data
                                    llvm::Value *boff =
                                        builder->CreateMul(run,
                                            me_size_val);
                                    llvm::Value *dst =
                                        builder->CreateGEP(
                                            llvm::Type::getInt8Ty(
                                                context),
                                            flat, boff);
                                    llvm::Value *sb =
                                        builder->CreateMul(esz64,
                                            me_size_val);
                                    builder->CreateMemCpy(
                                        dst,
                                        llvm::MaybeAlign(1),
                                        raw,
                                        llvm::MaybeAlign(1),
                                        sb);
                                    // Update running offset
                                    builder->CreateStore(
                                        builder->CreateAdd(run, esz64),
                                        tot_alloca);
                                    // Increment k
                                    builder->CreateStore(
                                        builder->CreateAdd(kv,
                                            llvm::ConstantInt::get(
                                                i64, 1)),
                                        k_alloca);
                                    builder->CreateBr(l2h);
                                }
                                builder->SetInsertPoint(l2e);
                                // Set data buffer
                                emit_gpu_buffer(flat, tot_bytes);
                                // Set offsets buffer
                                emit_gpu_buffer(off_buf, buf_bytes);
                                // Set sizes buffer
                                emit_gpu_buffer(sz_buf, buf_bytes);
                                field_idx++;
                            }
                        }
                    }
            } else if (ASR::is_a<ASR::StructType_t>(
                    *ASRUtils::extract_type(arg_type))) {
                // Struct: pass as a buffer so the kernel can write to it
                // and results are copied back to the host
                llvm::Value *struct_ptr;
                llvm::Type *struct_type;
                if (arg_val->getType()->isPointerTy()) {
                    struct_ptr = builder->CreatePointerCast(arg_val, i8_ptr);
                    struct_type = llvm_utils->get_type_from_ttype_t_util(
                        arg_expr, arg_type, module.get());
                } else {
                    // Host-associated struct loaded as a value (e.g. via
                    // nested context); store to an alloca to get a pointer.
                    struct_type = arg_val->getType();
                    llvm::AllocaInst *struct_alloca =
                        llvm_utils->CreateAlloca(struct_type);
                    builder->CreateStore(arg_val, struct_alloca);
                    struct_ptr = builder->CreatePointerCast(struct_alloca, i8_ptr);
                }
                uint64_t sz = module->getDataLayout().getTypeAllocSize(struct_type);
                llvm::Value *struct_size = llvm::ConstantInt::get(i64, sz);
                if (packed_mode) {
                    buffer_arg_infos.push_back({struct_ptr, struct_size});
                } else {
                    llvm::Value *idx = llvm::ConstantInt::get(i32, buffer_idx++);
                    builder->CreateCall(set_buffer_fn, {gpu_kernel, idx, struct_ptr, struct_size});
                }
            } else {
                // Scalar: collect for packing into a single struct buffer
                scalar_arg_infos.push_back({arg_val, arg_val->getType()});
            }

            // Save scalar arg values for VLA workspace size computation
            if (!ASRUtils::is_array(arg_type) &&
                    !ASR::is_a<ASR::StructType_t>(
                        *ASRUtils::extract_type(arg_type))) {
                call_arg_values[i] = arg_val;
            }
        }

        // Add per-dimension size scalars for DescriptorArray
        // (assumed-shape) kernel arguments, matching the Metal codegen's
        // scalar struct layout. We check the kernel function's parameter
        // types (not call arg types) since the Metal codegen uses those.
        for (size_t i = 0; i < x.n_args; i++) {
            ASR::expr_t *arg_expr = x.m_args[i].m_value;
            if (!arg_expr) continue;
            ASR::ttype_t *arg_type = ASRUtils::expr_type(arg_expr);
            if (!ASRUtils::is_array(arg_type)) continue;
            // Check the kernel function's parameter type for null dims
            ASR::Variable_t *kparam = ASR::down_cast<ASR::Variable_t>(
                ASR::down_cast<ASR::Var_t>(
                    kernel_func->m_args[i])->m_v);
            // Skip synthetic kernel args (__data_*, etc.)
            std::string kparam_name(kparam->m_name);
            if (kparam_name.substr(0, 2) == "__") continue;
            ASR::ttype_t *kparam_type =
                ASRUtils::type_get_past_allocatable(kparam->m_type);
            if (!ASR::is_a<ASR::Array_t>(*kparam_type)) continue;
            ASR::Array_t *kparam_arr =
                ASR::down_cast<ASR::Array_t>(kparam_type);
            bool has_null_dim = false;
            for (size_t d = 0; d < kparam_arr->n_dims; d++) {
                if (!kparam_arr->m_dims[d].m_length) {
                    has_null_dim = true;
                    break;
                }
            }
            if (!has_null_dim) continue;
            // Get the descriptor for this call arg to extract dim sizes
            bool is_allocatable_array2 =
                ASR::is_a<ASR::Allocatable_t>(*arg_type)
                && ASRUtils::is_array(arg_type);
            bool is_descriptor_array2 = false;
            ASR::ttype_t *past_alloc2 =
                ASRUtils::type_get_past_allocatable(arg_type);
            if (ASR::is_a<ASR::Array_t>(*past_alloc2)) {
                ASR::Array_t *call_arr =
                    ASR::down_cast<ASR::Array_t>(past_alloc2);
                for (size_t d = 0; d < call_arr->n_dims; d++) {
                    if (!call_arr->m_dims[d].m_length) {
                        is_descriptor_array2 = true;
                        break;
                    }
                }
            }
            if (is_allocatable_array2 || is_descriptor_array2) {
                // Call arg is also a descriptor: extract sizes from it
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = 1;
                this->visit_expr(*arg_expr);
                ptr_loads = ptr_loads_copy;
                llvm::Value *desc_val = tmp;
                llvm::Type *desc_type =
                    llvm_utils->get_type_from_ttype_t_util(
                        arg_expr, past_alloc2, module.get());
                for (size_t d = 0; d < kparam_arr->n_dims; d++) {
                    llvm::Value *dim_1based = llvm::ConstantInt::get(
                        i32, d + 1);
                    llvm::Value *dim_size =
                        arr_descr->get_array_size(
                            desc_type, desc_val, dim_1based, 4, 4);
                    scalar_arg_infos.push_back(
                        {dim_size, llvm::Type::getInt32Ty(context)});
                }
            } else {
                // Call arg has known dims: compute sizes from ASR
                ASR::Array_t *call_arr =
                    ASR::down_cast<ASR::Array_t>(past_alloc2);
                int64_t ptr_loads_copy = ptr_loads;
                ptr_loads = 2;
                for (size_t d = 0; d < kparam_arr->n_dims; d++) {
                    llvm::Value *dim_size;
                    if (d < call_arr->n_dims &&
                            call_arr->m_dims[d].m_length) {
                        this->visit_expr(*call_arr->m_dims[d].m_length);
                        dim_size = tmp;
                        if (dim_size->getType()->isPointerTy()) {
                            dim_size = llvm_utils->CreateLoad2(
                                llvm::Type::getInt32Ty(context),
                                dim_size);
                        }
                        dim_size = builder->CreateSExtOrTrunc(
                            dim_size,
                            llvm::Type::getInt32Ty(context));
                    } else {
                        dim_size = llvm::ConstantInt::get(
                            llvm::Type::getInt32Ty(context), 1);
                    }
                    scalar_arg_infos.push_back(
                        {dim_size, llvm::Type::getInt32Ty(context)});
                }
                ptr_loads = ptr_loads_copy;
            }
        }

        // In packed mode: allocate combined buffer, copy data in, and set
        // as a single Metal buffer. Offsets go into the scalar struct.
        llvm::Value *packed_buf_ptr = nullptr;
        std::vector<llvm::Value*> packed_offsets;
        std::vector<llvm::Value*> packed_sizes;
        if (packed_mode && !buffer_arg_infos.empty()) {
            llvm::Value *align_val = llvm::ConstantInt::get(i64,
                PACKED_BUFFER_ALIGN - 1);
            llvm::Value *align_mask = llvm::ConstantInt::get(i64,
                ~(int64_t)(PACKED_BUFFER_ALIGN - 1));

            // Compute aligned offsets for each buffer arg
            llvm::Value *running = llvm::ConstantInt::get(i64, 0);
            for (size_t bi = 0; bi < buffer_arg_infos.size(); bi++) {
                // Align running offset up to PACKED_BUFFER_ALIGN
                llvm::Value *aligned = builder->CreateAnd(
                    builder->CreateAdd(running, align_val), align_mask);
                packed_offsets.push_back(aligned);
                packed_sizes.push_back(buffer_arg_infos[bi].byte_size);
                running = builder->CreateAdd(aligned,
                    buffer_arg_infos[bi].byte_size);
            }
            llvm::Value *total_size = running;

            // Allocate combined buffer
            llvm::FunctionType *malloc_ft =
                llvm::FunctionType::get(i8_ptr, {i64}, false);
            llvm::Function *malloc_fn =
                get_gpu_runtime_func("malloc", malloc_ft);
            packed_buf_ptr = builder->CreateCall(malloc_fn, {total_size});

            // memcpy each array's data into the combined buffer
            for (size_t bi = 0; bi < buffer_arg_infos.size(); bi++) {
                llvm::Value *dest = builder->CreateGEP(
                    llvm::Type::getInt8Ty(context), packed_buf_ptr,
                    packed_offsets[bi]);
                builder->CreateMemCpy(dest, llvm::MaybeAlign(1),
                    buffer_arg_infos[bi].data_ptr, llvm::MaybeAlign(1),
                    buffer_arg_infos[bi].byte_size);
            }

            // Set combined buffer as Metal buffer 0
            buffer_idx = 0;
            llvm::Value *buf0_idx = llvm::ConstantInt::get(i32, buffer_idx++);
            builder->CreateCall(set_buffer_fn,
                {gpu_kernel, buf0_idx, packed_buf_ptr, total_size});
        }

        // Pack all scalar args (and offsets in packed mode) into a struct
        bool has_scalar_struct = !scalar_arg_infos.empty() ||
            (packed_mode && !buffer_arg_infos.empty());
        if (has_scalar_struct) {
            std::vector<llvm::Type*> field_types;
            for (auto &si : scalar_arg_infos) {
                field_types.push_back(si.type);
            }
            if (packed_mode) {
                for (size_t bi = 0; bi < buffer_arg_infos.size(); bi++) {
                    field_types.push_back(i32); // uint offset
                }
            }
            llvm::StructType *scalar_struct =
                llvm::StructType::get(context, field_types, false);
            llvm::AllocaInst *struct_alloca =
                llvm_utils->CreateAlloca(scalar_struct);
            unsigned field_idx = 0;
            for (size_t s = 0; s < scalar_arg_infos.size(); s++) {
                std::vector<llvm::Value*> gep_idx = {
                    llvm::ConstantInt::get(i32, 0),
                    llvm::ConstantInt::get(i32, field_idx++)};
                llvm::Value *field_ptr = llvm_utils->CreateGEP2(
                    scalar_struct, struct_alloca, gep_idx);
                builder->CreateStore(scalar_arg_infos[s].value, field_ptr);
            }
            if (packed_mode) {
                for (size_t bi = 0; bi < packed_offsets.size(); bi++) {
                    std::vector<llvm::Value*> gep_idx = {
                        llvm::ConstantInt::get(i32, 0),
                        llvm::ConstantInt::get(i32, field_idx++)};
                    llvm::Value *field_ptr = llvm_utils->CreateGEP2(
                        scalar_struct, struct_alloca, gep_idx);
                    llvm::Value *offset_i32 =
                        builder->CreateTrunc(packed_offsets[bi], i32);
                    builder->CreateStore(offset_i32, field_ptr);
                }
            }
            llvm::Value *scalar_buf_ptr =
                builder->CreatePointerCast(struct_alloca, i8_ptr);
            uint64_t struct_sz =
                module->getDataLayout().getTypeAllocSize(scalar_struct);
            llvm::Value *scalar_buf_size =
                llvm::ConstantInt::get(i64, struct_sz);
            llvm::Value *scalar_buf_idx =
                llvm::ConstantInt::get(i32, buffer_idx++);
            builder->CreateCall(set_scalar_fn,
                {gpu_kernel, scalar_buf_idx, scalar_buf_ptr, scalar_buf_size});
        }

        // 4b. Allocate and pass VLA workspace buffers
        // Metal does not support variable-length arrays in shaders, so the
        // Metal codegen converts each block-local VLA into an extra device
        // buffer parameter.  Here we allocate host-side memory for those
        // buffers, pass them to the kernel, and free them after launch.
        std::vector<llvm::Value*> vla_workspace_ptrs;
        // gpu_vla_workspaces was computed earlier for struct member sizing
        if (!gpu_vla_workspaces.empty()) {
            // Evaluate grid/block sizes to know the total thread count
            this->visit_expr(*x.m_grid_size);
            llvm::Value *gs = tmp;
            this->visit_expr(*x.m_block_size);
            llvm::Value *bs = tmp;
            llvm::Value *total_threads = builder->CreateMul(
                builder->CreateIntCast(gs, i64, true),
                builder->CreateIntCast(bs, i64, true));

            llvm::FunctionType *malloc_ft =
                llvm::FunctionType::get(i8_ptr, {i64}, false);
            llvm::Function *malloc_fn =
                get_gpu_runtime_func("malloc", malloc_ft);

            for (auto &ws : gpu_vla_workspaces) {
                llvm::Value *per_thread_elems =
                    llvm::ConstantInt::get(i64, 1);
                for (auto &dim : ws.dims) {
                    if (dim.is_constant) {
                        per_thread_elems = builder->CreateMul(
                            per_thread_elems,
                            llvm::ConstantInt::get(i64, dim.constant_value));
                    } else if (dim.is_struct_member_size) {
                        // Read size from the struct member's first
                        // element size, saved during decomposition.
                        auto sit = struct_member_first_sizes.find(
                            dim.struct_member_key);
                        if (sit != struct_member_first_sizes.end()) {
                            per_thread_elems = builder->CreateMul(
                                per_thread_elems,
                                builder->CreateIntCast(
                                    sit->second, i64, true));
                        }
                    } else {
                        llvm::Value *dim_val =
                            call_arg_values[dim.call_arg_index];
                        LCOMPILERS_ASSERT(dim_val != nullptr);
                        per_thread_elems = builder->CreateMul(
                            per_thread_elems,
                            builder->CreateIntCast(dim_val, i64, true));
                    }
                }
                llvm::Value *workspace_size = builder->CreateMul(
                    builder->CreateMul(total_threads, per_thread_elems),
                    llvm::ConstantInt::get(i64, ws.elem_size));
                llvm::Value *ptr =
                    builder->CreateCall(malloc_fn, {workspace_size});
                vla_workspace_ptrs.push_back(ptr);
                llvm::Value *buf_idx =
                    llvm::ConstantInt::get(i32, ws.buffer_index);
                builder->CreateCall(set_buffer_fn,
                    {gpu_kernel, buf_idx, ptr, workspace_size});
            }
        }

        // 5. Launch: lfortran_gpu_launch(ctx, kernel, grid, block)
        llvm::FunctionType *launch_ft = llvm::FunctionType::get(
            void_type, {i8_ptr, i8_ptr, i8_ptr, i8_ptr}, false);
        llvm::Function *launch_fn = get_gpu_runtime_func("lfortran_gpu_launch", launch_ft);

        // Build grid[3] and block[3] arrays on stack
        llvm::ArrayType *arr3 = llvm::ArrayType::get(i32, 3);
        llvm::AllocaInst *grid_arr = llvm_utils->CreateAlloca(arr3);
        llvm::AllocaInst *block_arr = llvm_utils->CreateAlloca(arr3);

        // Evaluate grid_size and block_size
        this->visit_expr(*x.m_grid_size);
        llvm::Value *grid_size_val = tmp;
        this->visit_expr(*x.m_block_size);
        llvm::Value *block_size_val = tmp;

        // Store grid = {grid_size, 1, 1}
        auto *zero = llvm::ConstantInt::get(i32, 0);
        auto *one_i32 = llvm::ConstantInt::get(i32, 1);
        builder->CreateStore(grid_size_val,
            builder->CreateGEP(arr3, grid_arr, {zero, zero}));
        builder->CreateStore(one_i32,
            builder->CreateGEP(arr3, grid_arr, {zero, one_i32}));
        builder->CreateStore(one_i32,
            builder->CreateGEP(arr3, grid_arr, {zero, llvm::ConstantInt::get(i32, 2)}));

        // Store block = {block_size, 1, 1}
        builder->CreateStore(block_size_val,
            builder->CreateGEP(arr3, block_arr, {zero, zero}));
        builder->CreateStore(one_i32,
            builder->CreateGEP(arr3, block_arr, {zero, one_i32}));
        builder->CreateStore(one_i32,
            builder->CreateGEP(arr3, block_arr, {zero, llvm::ConstantInt::get(i32, 2)}));

        llvm::Value *grid_ptr = builder->CreatePointerCast(grid_arr, i8_ptr);
        llvm::Value *block_ptr = builder->CreatePointerCast(block_arr, i8_ptr);

        builder->CreateCall(launch_fn, {gpu_ctx, gpu_kernel, grid_ptr, block_ptr});

        // 5b. In packed mode, copy data back from the combined buffer
        // to the original host arrays, then free the combined buffer.
        if (packed_mode && packed_buf_ptr) {
            for (size_t bi = 0; bi < buffer_arg_infos.size(); bi++) {
                llvm::Value *src_ptr = builder->CreateGEP(
                    llvm::Type::getInt8Ty(context), packed_buf_ptr,
                    packed_offsets[bi]);
                builder->CreateMemCpy(
                    buffer_arg_infos[bi].data_ptr, llvm::MaybeAlign(1),
                    src_ptr, llvm::MaybeAlign(1),
                    buffer_arg_infos[bi].byte_size);
            }
            llvm::FunctionType *free_packed_ft =
                llvm::FunctionType::get(void_type, {i8_ptr}, false);
            llvm::Function *free_packed_fn =
                get_gpu_runtime_func("free", free_packed_ft);
            builder->CreateCall(free_packed_fn, {packed_buf_ptr});
        }

        // 5c. Write back decomposed struct member buffers.
        // After kernel execution, copy data from the flat buffer
        // back to the original struct member array descriptors.
        for (auto &wb : struct_writebacks) {
            llvm::Value *run_wb =
                llvm::ConstantInt::get(i64, 0);
            for (int64_t k = 0;
                    k < wb.total_elements; k++) {
                llvm::Value *boff_wb =
                    builder->CreateMul(run_wb,
                        llvm::ConstantInt::get(
                            i64, wb.elem_byte_size));
                llvm::Value *src_wb =
                    builder->CreateGEP(
                        llvm::Type::getInt8Ty(context),
                        wb.flat_buf, boff_wb);
                llvm::Value *sb_wb =
                    builder->CreateMul(wb.sizes[k],
                        llvm::ConstantInt::get(
                            i64, wb.elem_byte_size));
                builder->CreateMemCpy(
                    wb.data_ptrs[k],
                    llvm::MaybeAlign(1),
                    src_wb, llvm::MaybeAlign(1),
                    sb_wb);
                run_wb = builder->CreateAdd(
                    run_wb, wb.sizes[k]);
            }
        }

        // 6. Free VLA workspace buffers allocated in step 4b
        if (!vla_workspace_ptrs.empty()) {
            llvm::FunctionType *free_ft =
                llvm::FunctionType::get(void_type, {i8_ptr}, false);
            llvm::Function *free_fn =
                get_gpu_runtime_func("free", free_ft);
            for (llvm::Value *ptr : vla_workspace_ptrs) {
                builder->CreateCall(free_fn, {ptr});
            }
        }
    }

    void visit_GpuSync(const ASR::GpuSync_t & /* x */) {
        // For the prototype, gpu_sync needs the context.
        // We call lfortran_gpu_sync with a null pointer and let the runtime
        // use the last command buffer. In a full implementation, the context
        // would be tracked as module-level state.
        llvm::Type *i8_ptr = llvm::PointerType::getUnqual(
            llvm::Type::getInt8Ty(context));
        llvm::Type *void_type = llvm::Type::getVoidTy(context);

        // For simplicity, call init again (it should cache/return same ctx)
        llvm::FunctionType *init_ft = llvm::FunctionType::get(i8_ptr, {}, false);
        llvm::Function *init_fn = get_gpu_runtime_func("lfortran_gpu_init", init_ft);
        llvm::Value *gpu_ctx = builder->CreateCall(init_fn, {});

        llvm::FunctionType *sync_ft = llvm::FunctionType::get(
            void_type, {i8_ptr}, false);
        llvm::Function *sync_fn = get_gpu_runtime_func("lfortran_gpu_sync", sync_ft);
        builder->CreateCall(sync_fn, {gpu_ctx});
    }

    void visit_GpuThreadIndex(const ASR::GpuThreadIndex_t & /* x */) {
        throw CodeGenError("GpuThreadIndex should not appear in LLVM backend");
    }

    void visit_GpuBlockIndex(const ASR::GpuBlockIndex_t & /* x */) {
        throw CodeGenError("GpuBlockIndex should not appear in LLVM backend");
    }

    void visit_GpuBlockSize(const ASR::GpuBlockSize_t & /* x */) {
        throw CodeGenError("GpuBlockSize should not appear in LLVM backend");
    }

    template <typename T>
    inline void set_func_subrout_params(T* func_subrout, ASR::abiType& x_abi,
                                        std::uint32_t& m_h, ASR::Variable_t*& orig_arg,
                                        std::string& orig_arg_name, ASR::intentType& arg_intent,
                                        size_t arg_idx) {
        m_h = get_hash((ASR::asr_t*)func_subrout);
        LCOMPILERS_ASSERT(arg_idx < func_subrout->n_args);
        if( ASR::is_a<ASR::Var_t>(*func_subrout->m_args[arg_idx]) ) {
            ASR::Var_t* arg_var = ASR::down_cast<ASR::Var_t>(func_subrout->m_args[arg_idx]);
            ASR::symbol_t* arg_sym = symbol_get_past_external(arg_var->m_v);
            if( ASR::is_a<ASR::Variable_t>(*arg_sym) ) {
                orig_arg = ASR::down_cast<ASR::Variable_t>(arg_sym);
                orig_arg_name = orig_arg->m_name;
                arg_intent = orig_arg->m_intent;
            }
        }
        x_abi = ASRUtils::get_FunctionType(func_subrout)->m_abi;
    }

    /// Checks if types are of matching stringPhysicalType.
    /// If expressions aren't of string type, Do NOTHING.
    void check_strings_phsyicalType_match(ASR::ttype_t* const a, ASR::ttype_t* const b){
        if(ASRUtils::type_get_past_pointer(ASRUtils::type_get_past_allocatable(a))->type == ASR::Array
        || ASRUtils::type_get_past_pointer(ASRUtils::type_get_past_allocatable(b))->type == ASR::Array) return; //Don't Check - Workaround for now

        if(ASRUtils::extract_type(a)->type == ASR::String
            && ASRUtils::extract_type(b)->type == ASR::String){
            LCOMPILERS_ASSERT_MSG(
                ASRUtils::is_character_phsyical_types_matched(
                    ASRUtils::extract_type(a),
                    ASRUtils::extract_type(b)),
                "Unmatching String Physical Types");
        }        
    }

    template <typename T>
    std::vector<llvm::Value*> convert_call_args(const T &x, bool skip_self = false, size_t skip_self_idx = 0) {
        std::vector<llvm::Value *> args;
        convert_call_args_depth++;
        // Only reset alloca pool indices at outermost call to avoid
        // nested calls (in argument expressions) clobbering allocas
        if (convert_call_args_depth == 1) {
            pending_flat_copybacks.clear();
            reset_call_arg_alloca_pool();
        }
        for (size_t i=0; i<x.n_args; i++) {
            if (skip_self && i == skip_self_idx) continue;
            ASR::symbol_t* func_subrout = symbol_get_past_external(x.m_name);
            ASR::abiType x_abi = (ASR::abiType) 0;
            ASR::intentType orig_arg_intent = ASR::intentType::Unspecified;
            std::uint32_t m_h;
            ASR::Variable_t *orig_arg = nullptr;
            std::string orig_arg_name = "";
            if( func_subrout->type == ASR::symbolType::Function ) {
                ASR::Function_t* func = down_cast<ASR::Function_t>(func_subrout);
                set_func_subrout_params(func, x_abi, m_h, orig_arg, orig_arg_name, orig_arg_intent, i);
            } else if( func_subrout->type == ASR::symbolType::StructMethodDeclaration ) {
                ASR::StructMethodDeclaration_t* clss_proc = ASR::down_cast<ASR::StructMethodDeclaration_t>(func_subrout);
                if( clss_proc->m_proc->type == ASR::symbolType::Function ) {
                    ASR::Function_t* func = down_cast<ASR::Function_t>(clss_proc->m_proc);
                    set_func_subrout_params(func, x_abi, m_h, orig_arg, orig_arg_name, orig_arg_intent, i);
                    func_subrout = clss_proc->m_proc;
                }
            } else if( func_subrout->type == ASR::symbolType::Variable ) {
                ASR::Variable_t* v = down_cast<ASR::Variable_t>(func_subrout);
                if (v->m_type_declaration != nullptr) {
                    ASR::Function_t* func = down_cast<ASR::Function_t>(ASRUtils::symbol_get_past_external(v->m_type_declaration));
                    func_subrout = ASRUtils::symbol_get_past_external(v->m_type_declaration);
                    if (i < func->n_args) {
                        set_func_subrout_params(func, x_abi, m_h, orig_arg, orig_arg_name, orig_arg_intent, i);
                    }
                }
                // If m_type_declaration is nullptr (implicit interface without call),
                // continue with default values for x_abi, orig_arg, etc.
            } else {
                LCOMPILERS_ASSERT(false)
            }

            if(orig_arg && x.m_args[i].m_value){
                check_strings_phsyicalType_match(orig_arg->m_type, expr_type(x.m_args[i].m_value));
            }

            // For implicit-interface procedure pointers (procedure() with
            // no declared parameters), orig_arg is nullptr because the
            // declared Function_t has fewer args than the call provides.
            // Short-circuit: evaluate the argument and pass by reference.
            if (orig_arg == nullptr && x.m_args[i].m_value != nullptr) {
                this->visit_expr_wrapper(x.m_args[i].m_value, true);
                if (!tmp->getType()->isPointerTy()) {
                    llvm::AllocaInst *target = get_call_arg_alloca(tmp->getType());
                    builder->CreateStore(tmp, target);
                    tmp = target;
                }
                args.push_back(tmp);
                continue;
            }

            if( x.m_args[i].m_value == nullptr ) {
                LCOMPILERS_ASSERT(orig_arg != nullptr);
                llvm::Type* llvm_orig_arg_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                    al, orig_arg->base.base.loc, &orig_arg->base)),
                    orig_arg->m_type, module.get());
                if (x_abi == ASR::abiType::BindC) {
                    // Per Fortran standard, absent optional bind(C) args
                    // are passed as NULL pointers.
                    // For array parameters, the LLVM type from
                    // get_type_from_ttype_t_util is already a descriptor
                    // pointer matching the function signature, so use it
                    // as-is. For scalar parameters (including
                    // allocatable/pointer scalars), add a pointer level to
                    // match the pass-by-reference calling convention.
                    llvm::PointerType* null_ptr_type;
                    ASR::ttype_t* arg_type_no_ap =
                        ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(
                            orig_arg->m_type));
                    if (ASRUtils::is_array(arg_type_no_ap)
                            && llvm_orig_arg_type->isPointerTy()) {
                        null_ptr_type = llvm::cast<llvm::PointerType>(
                            llvm_orig_arg_type);
                    } else {
                        null_ptr_type = llvm_orig_arg_type->getPointerTo();
                    }
                    args.push_back(llvm::ConstantPointerNull::get(
                        null_ptr_type));
                } else {
                    llvm::Value* llvm_arg = llvm_utils->CreateAlloca(*builder, llvm_orig_arg_type);
                    args.push_back(llvm_arg);
                }
                continue ;
            }

            if (ASR::is_a<ASR::Var_t>(*x.m_args[i].m_value)) {
                ASR::symbol_t* var_sym = ASRUtils::symbol_get_past_external(
                    ASR::down_cast<ASR::Var_t>(x.m_args[i].m_value)->m_v);
                if (ASR::is_a<ASR::Variable_t>(*var_sym)) {
                    ASR::Variable_t *arg = EXPR2VAR(x.m_args[i].m_value);
                    uint32_t h = get_hash((ASR::asr_t*)arg);
                    bool using_deep_copy = false;
                    if (llvm_symtab.find(h) != llvm_symtab.end()) {
                        if (llvm_symtab_deep_copy.find({h, current_scope}) != llvm_symtab_deep_copy.end()) {
                            tmp = llvm_symtab_deep_copy[{h, current_scope}];
                            using_deep_copy = true;
                        } else {
                            tmp = llvm_symtab[h];
                        }
                        if( !ASRUtils::is_array(arg->m_type) ) {

                            if ((x_abi == ASR::abiType::Source || x_abi == ASR::abiType::ExternalUndefined)
                                     && ASR::is_a<ASR::CPtr_t>(*arg->m_type)) {
                                if ( orig_arg_intent != ASRUtils::intent_out &&
                                        arg->m_intent == intent_local ) {
                                    // Local variable of type
                                    // CPtr is a void**, so we
                                    // have to load it
                                    llvm::Type* cptr_type = llvm::Type::getVoidTy(context)->getPointerTo();
                                    tmp = llvm_utils->CreateLoad2(cptr_type, tmp);
                                }
                            } else if ( x_abi == ASR::abiType::BindC && orig_arg != nullptr ) {
                                if (orig_arg->m_abi == ASR::abiType::BindC && orig_arg->m_value_attr) {
                                    ASR::ttype_t* arg_type = arg->m_type;
                                    llvm::Type* arg_llvm_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                                        al, arg->base.base.loc, &arg->base)), arg_type, module.get());
                                    if (ASR::is_a<ASR::Complex_t>(*arg_type)) {
                                        if (!startswith(compiler_options.target, "wasm")) {
                                            int c_kind = ASRUtils::extract_kind_from_ttype_t(arg_type);
                                            if (c_kind == 4) {
                                                if (compiler_options.platform == Platform::Windows) {
                                                    // tmp is {float, float}*
                                                    // type_fx2 is i64
                                                    llvm::Type* type_fx2 = llvm::Type::getInt64Ty(context);
                                                    llvm::Value* c32_as_i64_ptr = builder->CreateBitCast(tmp, type_fx2->getPointerTo());
                                                    tmp = llvm_utils->CreateLoad2(type_fx2, c32_as_i64_ptr);
                                                } else if (compiler_options.platform == Platform::macOS_ARM) {
                                                    // tmp is {float, float}*
                                                    // type_fx2 is [2 x float]
                                                    llvm::Type* type_fx2 = llvm::ArrayType::get(llvm::Type::getFloatTy(context), 2);
                                                    // we bitcast complex_type_4 to [2 x float]*
                                                    llvm::Value *complex_as_array_ptr = builder->CreateBitCast(tmp, type_fx2->getPointerTo());
                                                    tmp = llvm_utils->CreateLoad2(type_fx2, complex_as_array_ptr);
                                                } else {
                                                    // tmp is {float, float}*
                                                    // type_fx2 is <2 x float>
                                                    llvm::Type* type_fx2 = FIXED_VECTOR_TYPE::get(llvm::Type::getFloatTy(context), 2);
                                                    llvm::Value* c32_as_vec2_ptr = builder->CreateBitCast(tmp, type_fx2->getPointerTo());
                                                    tmp = llvm_utils->CreateLoad2(type_fx2, c32_as_vec2_ptr);
                                                }
                                            } else {
                                                LCOMPILERS_ASSERT(c_kind == 8)
                                                if (compiler_options.platform == Platform::Windows) {
                                                    // 128 bit aggregate type is passed by reference
                                                } else {
                                                    // Pass by value
                                                    tmp = llvm_utils->CreateLoad2(arg_llvm_type, tmp);
                                                }
                                            }
                                        }
                                    } else if (is_a<ASR::CPtr_t>(*arg_type)) {
                                        if ( arg->m_intent == intent_local ||
                                                arg->m_intent == ASRUtils::intent_out) {
                                            // Local variable or Dummy out argument
                                            // of type CPtr is a void**, so we
                                            // have to load it
                                            llvm::Type* cptr_type = llvm::Type::getVoidTy(context)->getPointerTo();
                                            tmp = llvm_utils->CreateLoad2(cptr_type, tmp);
                                        }
                                    } else {
                                        if (!arg->m_value_attr && !ASR::is_a<ASR::String_t>(*arg_type)
                                                && !ASR::is_a<ASR::StructType_t>(*arg_type)) {
                                            // Dereference the pointer argument (unless it is a CPtr)
                                            // to pass by value
                                            // E.g.:
                                            // i32* -> i32
                                            // {double,double}* -> {double,double}
                                            // StructType is excluded because the LLVM function
                                            // signature always uses a pointer for struct args,
                                            // even with bind(C) + value.
                                            tmp = llvm_utils->CreateLoad2(arg_llvm_type, tmp);
                                        }
                                    }
                                }
                                if (!orig_arg->m_value_attr && arg->m_value_attr
                                        && arg->m_abi == ASR::abiType::BindC) {
                                    pass_bindc_value_arg(orig_arg, arg->m_type);
                                }
                            } else {
                                bool is_func_type_arg = ASR::is_a<ASR::FunctionType_t>(*arg->m_type) ||
                                    (ASRUtils::is_pointer(arg->m_type) &&
                                     ASR::is_a<ASR::FunctionType_t>(*ASRUtils::type_get_past_pointer(arg->m_type)));
                                bool needs_func_ptr_load = is_func_type_arg &&
                                    (arg->m_intent == intent_local ||
                                     (ASRUtils::is_pointer(arg->m_type) && ASRUtils::is_arg_dummy(arg->m_intent)));
                                if( needs_func_ptr_load ) {
                                    // (FunctionType**) --> (FunctionType*)
                                    bool pass_by_value = true;
                                    if ( ASR::is_a<ASR::Function_t>(*func_subrout) ) {
                                        ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(func_subrout);
                                        size_t arg_idx = i;
                                        if ( arg_idx < func->n_args && ASR::is_a<ASR::Var_t>(*func->m_args[arg_idx]) ){
                                            ASR::symbol_t* func_var_sym = ASRUtils::symbol_get_past_external(
                                                ASR::down_cast<ASR::Var_t>(func->m_args[arg_idx])->m_v);
                                            if ( ASR::is_a<ASR::Variable_t>(*func_var_sym) ) {
                                                ASR::Variable_t* func_variable = ASR::down_cast<ASR::Variable_t>(func_var_sym);
                                                if ( func_variable->m_intent == ASRUtils::intent_inout || func_variable->m_intent == ASRUtils::intent_out ||
                                                     ASRUtils::is_pointer(func_variable->m_type) ) {
                                                    pass_by_value = false;
                                                }
                                            }
                                        }
                                    }

                                    if ( pass_by_value ) {
                                        // Pass-by-Value
                                        ASR::ttype_t* load_type = ASRUtils::type_get_past_pointer(arg->m_type);
                                        tmp = llvm_utils->CreateLoad2(llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                                            al, arg->base.base.loc, &arg->base)), load_type, module.get()), tmp);
                                    }
                                }
                                if( orig_arg &&
                                    !using_deep_copy &&
                                    !LLVM::is_llvm_pointer(*orig_arg->m_type) &&
                                    LLVM::is_llvm_pointer(*arg->m_type) &&
                                    !is_func_type_arg &&
                                    !ASRUtils::is_character(*arg->m_type) &&
                                    !ASRUtils::is_class_type(ASRUtils::type_get_past_allocatable_pointer(arg->m_type)) && 
                                    !ASRUtils::is_class_type(ASRUtils::type_get_past_allocatable_pointer(orig_arg->m_type))) {
                                    // TODO: Remove call to ASRUtils::check_equal_type
                                    // pass(rhs) is not respected in integration_tests/class_08.f90

                                    llvm::Type* load_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, arg->m_type, module.get());
                                    tmp = llvm_utils->CreateLoad2(load_type, tmp);
                                }
                                if (orig_arg && ASRUtils::is_class_type(
                                        ASRUtils::extract_type(arg->m_type))
                                    && !ASRUtils::is_class_type(
                                        ASRUtils::extract_type(orig_arg->m_type)) ) {
                                    tmp = convert_class_to_type(x.m_args[i].m_value, ASRUtils::EXPR(ASR::make_Var_t(
                                        al, orig_arg->base.base.loc, &orig_arg->base)), orig_arg->m_type, tmp);
                                }
                                if (orig_arg && !orig_arg->m_value_attr && arg->m_value_attr
                                        && arg->m_abi == ASR::abiType::BindC) {
                                    pass_bindc_value_arg(orig_arg, arg->m_type);
                                }
                            }
                        } else {
                            if( orig_arg &&
                                !LLVM::is_llvm_pointer(*orig_arg->m_type) &&
                                LLVM::is_llvm_pointer(*arg->m_type) &&
                                !(ASRUtils::is_unlimited_polymorphic_type(arg->m_type) &&
                                  ASRUtils::is_unlimited_polymorphic_type(orig_arg->m_type)) ) {
                                llvm::Type* ptr_load_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, arg->m_type, module.get());
                                tmp = llvm_utils->CreateLoad2(ptr_load_type, tmp);
                            }
                            // If both are struct arrays and types don't match, bitcast to orig_arg_type
                            if (ASRUtils::is_array(orig_arg->m_type) && ASRUtils::is_array(arg->m_type) &&
                                !ASRUtils::is_class_type(ASRUtils::extract_type(orig_arg->m_type)) &&
                                ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(orig_arg->m_type)) &&
                                ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(arg->m_type))) {
                                llvm::Type* llvm_orig_arg_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                                    al, orig_arg->base.base.loc, &orig_arg->base)),
                                    orig_arg->m_type, module.get());
                                llvm::Type* llvm_arg_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, arg->m_type, module.get());
                                if (llvm_orig_arg_type != llvm_arg_type) {
                                    tmp = builder->CreateBitCast(tmp, llvm_orig_arg_type->getPointerTo());
                                }
                            }
                            if (orig_arg &&
                                ASRUtils::is_array(orig_arg->m_type) &&
                                ASRUtils::is_array(arg->m_type) &&
                                ASRUtils::is_class_type(ASRUtils::extract_type(arg->m_type)) &&
                                !ASRUtils::is_class_type(ASRUtils::extract_type(orig_arg->m_type))) {
                                tmp = convert_class_to_type(x.m_args[i].m_value, ASRUtils::EXPR(ASR::make_Var_t(
                                    al, orig_arg->base.base.loc, &orig_arg->base)), orig_arg->m_type, tmp);
                            }
                        }
                    } else {
                        if ( arg->m_type_declaration && ASR::is_a<ASR::Function_t>(
                                *ASRUtils::symbol_get_past_external(arg->m_type_declaration)) ) {
                            ASR::Function_t* fn = ASR::down_cast<ASR::Function_t>(
                                symbol_get_past_external(arg->m_type_declaration));
                            uint32_t h = get_hash((ASR::asr_t*)fn);
                            if (ASRUtils::get_FunctionType(fn)->m_deftype == ASR::deftypeType::Implementation) {
                                LCOMPILERS_ASSERT(llvm_symtab_fn.find(h) != llvm_symtab_fn.end());
                                tmp = llvm_symtab_fn[h];
                            } else {
                                // Must be an argument/chained procedure pass
                                tmp = llvm_symtab_fn_arg[h];
                            }
                        } else {
                            if (arg->m_value == nullptr) {
                                throw CodeGenError(std::string(arg->m_name) + " isn't defined in any scope.");
                            }
                            this->visit_expr_wrapper(arg->m_value, true);
                            if( x_abi != ASR::abiType::BindC &&
                                !ASR::is_a<ASR::ArrayConstant_t>(*arg->m_value) ) {
                                llvm::Type* alloca_type = llvm_utils->get_type_from_ttype_t_util(
                                    ASRUtils::EXPR(ASR::make_Var_t(
                                    al, arg->base.base.loc, &arg->base)), arg->m_type, module.get());
                                llvm::AllocaInst *target = get_call_arg_alloca(alloca_type);
                                builder->CreateStore(tmp, target);
                                tmp = target;
                            }
                        }
                    }
#if LLVM_VERSION_MAJOR < 15
                    // Bitcast procedure pointer if LLVM types don't match.
                    // Handles implicit/explicit interfaces where LLVM generates
                    // structurally equivalent but nominally different types
                    // (e.g., named vs anonymous structs). When both actual and
                    // formal are Pointer(FunctionType_t), tmp is at alloca
                    // level (extra pointer), so compare at that level.
                    if (orig_arg &&
                            ASR::is_a<ASR::FunctionType_t>(
                                *ASRUtils::type_get_past_pointer(arg->m_type)) &&
                            ASR::is_a<ASR::FunctionType_t>(
                                *ASRUtils::type_get_past_pointer(orig_arg->m_type))) {
                        llvm::Type* expected_type = llvm_utils->get_type_from_ttype_t_util(
                            ASRUtils::EXPR(ASR::make_Var_t(al, orig_arg->base.base.loc, &orig_arg->base)),
                            orig_arg->m_type, module.get());
                        // When the variable is a procedure pointer stored in an
                        // alloca, tmp has an extra pointer level. Match at that
                        // level so we bitcast void({void*}*)** → void(%named*)**
                        // rather than incorrectly casting void()** → void()*.
                        if (ASRUtils::is_pointer(arg->m_type) &&
                                ASRUtils::is_pointer(orig_arg->m_type)) {
                            expected_type = expected_type->getPointerTo();
                        }
                        if (tmp->getType() != expected_type) {
                            tmp = builder->CreateBitCast(tmp, expected_type);
                        }
                    }
#endif
                    // When the formal is a pointer type but the actual is not,
                    // wrap the value with an extra pointer level. Skip this when
                    // the formal is a class type — convert_to_polymorphic_arg
                    // handles the pointer level during class wrapping.
                    if (orig_arg &&
                        LLVM::is_llvm_pointer(*orig_arg->m_type) &&
                        !LLVM::is_llvm_pointer(*arg->m_type) &&
                        !ASRUtils::is_class_type(ASRUtils::type_get_past_allocatable_pointer(orig_arg->m_type))) {
                        if (tmp->getType()->isIntegerTy(1) &&
                            ASRUtils::is_logical(*arg->m_type)) {
                            int kind = ASRUtils::extract_kind_from_ttype_t(arg->m_type);
                            tmp = builder->CreateZExt(tmp, llvm_utils->getIntType(kind));
                        }
                        llvm::Value* ptr_to_tmp = llvm_utils->CreateAlloca(*builder, tmp->getType());
                        builder->CreateStore(tmp, ptr_to_tmp);
                        tmp = ptr_to_tmp;
                    }
                } else if (ASR::is_a<ASR::Function_t>(*var_sym)) {
                    ASR::Function_t* fn = ASR::down_cast<ASR::Function_t>(var_sym);
                    uint32_t h = get_hash((ASR::asr_t*)fn);
                    if (ASRUtils::get_FunctionType(fn)->m_deftype == ASR::deftypeType::Implementation) {
                        LCOMPILERS_ASSERT(llvm_symtab_fn.find(h) != llvm_symtab_fn.end());
                        tmp = llvm_symtab_fn[h];
                    } else if (llvm_symtab_fn_arg.find(h) == llvm_symtab_fn_arg.end() &&
                                ASR::is_a<ASR::Function_t>(*var_sym) &&
                                ASRUtils::get_FunctionType(fn)->m_deftype == ASR::deftypeType::Interface ) {
                        LCOMPILERS_ASSERT(llvm_symtab_fn.find(h) != llvm_symtab_fn.end());
                        tmp = llvm_symtab_fn[h];
                        LCOMPILERS_ASSERT(tmp != nullptr)
                    } else {
                        // Must be an argument/chained procedure pass
                        LCOMPILERS_ASSERT(llvm_symtab_fn_arg.find(h) != llvm_symtab_fn_arg.end());
                        tmp = llvm_symtab_fn_arg[h];
                        LCOMPILERS_ASSERT(tmp != nullptr)
                    }
#if LLVM_VERSION_MAJOR < 15
                    // Bitcast function pointer if LLVM types don't match.
                    // Handles implicit interfaces and typed-pointer LLVM
                    // where named vs anonymous struct types may differ.
                    // Must happen BEFORE alloca wrapping to preserve the
                    // correct pointer indirection level.
                    if (orig_arg && orig_arg->m_type_declaration &&
                            ASR::is_a<ASR::FunctionType_t>(
                            *ASRUtils::type_get_past_pointer(orig_arg->m_type))) {
                        llvm::Type* expected_type = llvm_utils->get_type_from_ttype_t_util(
                            ASRUtils::EXPR(ASR::make_Var_t(al, orig_arg->base.base.loc, &orig_arg->base)),
                            ASRUtils::type_get_past_pointer(orig_arg->m_type), module.get());
                        if (tmp->getType() != expected_type) {
                            tmp = builder->CreateBitCast(tmp, expected_type);
                        }
                    }
#endif
                    // If the target parameter is a procedure pointer,
                    // wrap the function pointer in an alloca
                    if (orig_arg &&
                        LLVM::is_llvm_pointer(*orig_arg->m_type)) {
                        llvm::Value* ptr_to_tmp = llvm_utils->CreateAlloca(
                            *builder, tmp->getType());
                        builder->CreateStore(tmp, ptr_to_tmp);
                        tmp = ptr_to_tmp;
                    }
                }
            } else if (ASR::is_a<ASR::ArrayPhysicalCast_t>(*x.m_args[i].m_value)) {
                ASR::ArrayPhysicalCast_t* apc = ASR::down_cast<ASR::ArrayPhysicalCast_t>(
                    x.m_args[i].m_value);
                int64_t ptr_loads_copy = ptr_loads;
                if (apc->m_old == ASR::array_physical_typeType::AssumedRankArray &&
                        apc->m_new == ASR::array_physical_typeType::DescriptorArray &&
                        !ASRUtils::is_array(apc->m_type)) {
                    ptr_loads = 0;
                }
                this->visit_expr_wrapper(x.m_args[i].m_value);
                ptr_loads = ptr_loads_copy;
                if (ASRUtils::is_class_type(
                        ASRUtils::extract_type(ASRUtils::expr_type(x.m_args[i].m_value)))
                    && !ASRUtils::is_class_type(
                        ASRUtils::extract_type(orig_arg->m_type)) ) {
                    tmp = convert_class_to_type(x.m_args[i].m_value, ASRUtils::EXPR(ASR::make_Var_t(
                        al, orig_arg->base.base.loc, &orig_arg->base)), orig_arg->m_type, tmp);
                }

            } else if (ASR::is_a<ASR::Cast_t>(*x.m_args[i].m_value) &&
                        ASR::down_cast<ASR::Cast_t>(x.m_args[i].m_value)->m_kind ==
                            ASR::cast_kindType::ClassToIntrinsic) {
                // ClassToIntrinsic cast: visit with ptr_loads=0 to get pointer
                // to the intrinsic data inside the polymorphic wrapper.
                // This ensures write-through for intent(out)/(inout) arguments.
                this->visit_expr_load_wrapper(x.m_args[i].m_value, 0);
            } else if((ASR::is_a<ASR::Cast_t>(*x.m_args[i].m_value) && 
                        ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(ASRUtils::expr_type(x.m_args[i].m_value))) && 
                        !ASRUtils::is_class_type(ASRUtils::extract_type(orig_arg->m_type)))) {
                this->visit_expr_wrapper(x.m_args[i].m_value);
                if (ASRUtils::is_class_type(
                        ASRUtils::extract_type(ASRUtils::expr_type(x.m_args[i].m_value)))) {
                    tmp = convert_class_to_type(x.m_args[i].m_value, ASRUtils::EXPR(ASR::make_Var_t(
                        al, orig_arg->base.base.loc, &orig_arg->base)), orig_arg->m_type, tmp);
                } else if( orig_arg &&
                    !LLVM::is_llvm_pointer(*orig_arg->m_type) &&
                    LLVM::is_llvm_pointer(*ASRUtils::expr_type(x.m_args[i].m_value)) ) {
                    llvm::Type* ptr_load_type = llvm_utils->get_type_from_ttype_t_util(
                        x.m_args[i].m_value, ASRUtils::expr_type(x.m_args[i].m_value), module.get());
                    tmp = llvm_utils->CreateLoad2(ptr_load_type, tmp);
                }
            } else if( ASR::is_a<ASR::FunctionType_t>(
                *ASRUtils::type_get_past_pointer(
                    ASRUtils::type_get_past_allocatable(
                        ASRUtils::expr_type(x.m_args[i].m_value)))) ) {
                this->visit_expr_wrapper(x.m_args[i].m_value, true);
                if (orig_arg && orig_arg->m_type_declaration &&
                        ASR::is_a<ASR::FunctionType_t>(
                        *ASRUtils::type_get_past_pointer(orig_arg->m_type))) {
#if LLVM_VERSION_MAJOR < 15
                    // Bitcast function pointer if LLVM types don't match.
                    // Handles both implicit and explicit interfaces where
                    // typed-pointer LLVM generates mismatching types.
                    llvm::Type* expected_type = llvm_utils->get_type_from_ttype_t_util(
                        ASRUtils::EXPR(ASR::make_Var_t(al, orig_arg->base.base.loc, &orig_arg->base)),
                        ASRUtils::type_get_past_pointer(orig_arg->m_type), module.get());
                    if (tmp->getType() != expected_type) {
                        tmp = builder->CreateBitCast(tmp, expected_type);
                    }
#endif
                    if (ASRUtils::is_pointer(orig_arg->m_type)) {
                        llvm::AllocaInst *target = get_call_arg_alloca(tmp->getType());
                        builder->CreateStore(tmp, target);
                        tmp = target;
                    }
                }
            } else {
                ASR::ttype_t* arg_type = expr_type(x.m_args[i].m_value);
                this->visit_expr_wrapper(x.m_args[i].m_value);
                if( x_abi == ASR::abiType::BindC ) {
                    if( (ASR::is_a<ASR::ArrayItem_t>(*x.m_args[i].m_value) &&
                         orig_arg_intent ==  ASR::intentType::In) ||
                        ASR::is_a<ASR::StructInstanceMember_t>(*x.m_args[i].m_value) ||
                        (ASR::is_a<ASR::CPtr_t>(*arg_type) &&
                         ASR::is_a<ASR::StructInstanceMember_t>(*x.m_args[i].m_value)) ) {
                        if( ASR::is_a<ASR::StructInstanceMember_t>(*x.m_args[i].m_value) &&
                            ASRUtils::is_array(arg_type) ) {
                            ASR::dimension_t* arg_m_dims = nullptr;
                            size_t n_dims = ASRUtils::extract_dimensions_from_ttype(arg_type, arg_m_dims);
                            if( !(ASRUtils::is_fixed_size_array(arg_m_dims, n_dims) &&
                                  ASRUtils::expr_abi(x.m_args[i].m_value) == ASR::abiType::BindC) ) {
                                llvm::Type* elem_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, ASRUtils::get_contained_type(arg_type), module.get());
                                llvm::Type* _type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, arg_type, module.get());
                                tmp = llvm_utils->CreateLoad2(elem_type->getPointerTo(), arr_descr->get_pointer_to_data(_type, tmp));
                            } else {
                                llvm::Type* arg_llvm_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, arg_type, module.get());
                                tmp = llvm_utils->create_gep2(arg_llvm_type, tmp, llvm::ConstantInt::get(
                                        llvm::Type::getInt32Ty(context), llvm::APInt(32, 0)));
                            }
                        } else if (orig_arg && orig_arg->m_abi == ASR::abiType::BindC &&
                                   orig_arg->m_value_attr) {
                            // Only load scalar StructInstanceMember for true BindC
                            // pass-by-value (m_value_attr = true). For implicit
                            // interfaces, m_value_attr is false and the callee
                            // expects a pointer.
                            llvm::Type* load_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, arg_type, module.get());
                            tmp = llvm_utils->CreateLoad2(load_type, tmp);
                        }
                    }
                }
                // Passing a scalar allocatable/pointer struct member
                // (`i32**`-typed lvalue in LLVM) to a plain scalar dummy
                // argument requires one dereference so the callee
                // receives the underlying data pointer (`i32*`). For
                // struct-typed allocatable members the existing
                // scalar/struct handling already forwards the correct
                // pointer, so keep the pre-existing behavior there.
                bool alloc_scalar_member_needs_deref =
                    orig_arg && ASRUtils::is_allocatable(arg_type) &&
                    !ASRUtils::is_array(arg_type) &&
                    ASR::is_a<ASR::StructInstanceMember_t>(*x.m_args[i].m_value) &&
                    !ASRUtils::is_character(*ASRUtils::extract_type(arg_type)) &&
                    !ASR::is_a<ASR::StructType_t>(
                        *ASRUtils::extract_type(arg_type)) &&
                    !ASRUtils::is_class_type(ASRUtils::extract_type(orig_arg->m_type)) &&
                    !LLVM::is_llvm_pointer(*orig_arg->m_type);
                if ((orig_arg && ASRUtils::is_pointer(arg_type) && !ASRUtils::is_array(arg_type) &&
                        ASR::is_a<ASR::StructInstanceMember_t>(*x.m_args[i].m_value) &&
                        !ASRUtils::is_character(*ASRUtils::extract_type(arg_type)) &&
                        !ASRUtils::is_class_type(ASRUtils::extract_type(orig_arg->m_type)) &&
                        !LLVM::is_llvm_pointer(*orig_arg->m_type))
                        || alloc_scalar_member_needs_deref) {
                    llvm::Type *el_type = llvm_utils->get_type_from_ttype_t_util(ASRUtils::EXPR(ASR::make_Var_t(
                    al, orig_arg->base.base.loc, &orig_arg->base)), orig_arg->m_type, module.get());
                    tmp = llvm_utils->CreateLoad2(el_type->getPointerTo(), tmp);
                }
                llvm::Value *value = tmp;
                if (orig_arg_intent == ASR::intentType::In ||
                    orig_arg_intent == ASR::intentType::InOut ||
                    orig_arg_intent == ASR::intentType::Out) {
                    /*
                        For the cases where argument intent is In or InOut or Out, we
                        cannot pass the evaluated value of expression directly to it. Currently
                        the value of `value` is the evaluated value of expression and this is because
                        for every expression we visit, we have a check to prefer the evaluated value.

                        To avoid this problem, we manually set the expr value to nullptr.
                        For example, refer integration_tests/intent_03.f90
                    */
                    // TODO: this is possible in other cases as well, support those.
                    if ( ASR::is_a<ASR::ArrayItem_t>(*x.m_args[i].m_value) ) {
                        ASR::ArrayItem_t* arr_item = ASR::down_cast<ASR::ArrayItem_t>(x.m_args[i].m_value);
                        arr_item->m_value = nullptr;
                        this->visit_expr_wrapper((ASR::expr_t*)arr_item);
                        value = tmp;
                    } else if ( ASR::is_a<ASR::ComplexRe_t>(*x.m_args[i].m_value) ||
                                ASR::is_a<ASR::ComplexIm_t>(*x.m_args[i].m_value) ) {
                        bool is_re = ASR::is_a<ASR::ComplexRe_t>(*x.m_args[i].m_value);
                        ASR::expr_t* complex_elem_expr = is_re
                            ? ASR::down_cast<ASR::ComplexRe_t>(x.m_args[i].m_value)->m_arg
                            : ASR::down_cast<ASR::ComplexIm_t>(x.m_args[i].m_value)->m_arg;
                        int ptr_loads_copy = ptr_loads;
                        ptr_loads = 0;
                        this->visit_expr(*complex_elem_expr);
                        ptr_loads = ptr_loads_copy;
                        llvm::Value* complex_ptr = tmp;
                        if (!complex_ptr->getType()->isPointerTy()) {
                            llvm::AllocaInst* alloc = llvm_utils->CreateAlloca(*builder, complex_ptr->getType());
                            builder->CreateStore(complex_ptr, alloc);
                            complex_ptr = alloc;
                        }
                        int cplx_kind = ASRUtils::extract_kind_from_ttype_t(
                            ASRUtils::extract_type(ASRUtils::expr_type(complex_elem_expr)));
                        llvm::Type* cplx_llvm_type = (cplx_kind == 4) ? complex_type_4 : complex_type_8;
                        value = llvm_utils->create_gep2(cplx_llvm_type, complex_ptr, is_re ? 0 : 1);
                    }
                }
                // TODO: we are getting a warning of uninitialized variable,
                // there might be a bug below.
                llvm::Type *target_type = nullptr;
                ASR::ttype_t* arg_type_ = ASRUtils::type_get_past_array(arg_type);
                switch (arg_type_->type) {
                    case (ASR::ttypeType::Integer) : {
                        int a_kind = down_cast<ASR::Integer_t>(arg_type_)->m_kind;
                        target_type = llvm_utils->getIntType(a_kind);
                        break;
                    }
                    case (ASR::ttypeType::UnsignedInteger) : {
                        int a_kind = down_cast<ASR::UnsignedInteger_t>(arg_type_)->m_kind;
                        target_type = llvm_utils->getIntType(a_kind);
                        break;
                    }
                    case (ASR::ttypeType::Real) : {
                        int a_kind = down_cast<ASR::Real_t>(arg_type_)->m_kind;
                        target_type = llvm_utils->getFPType(a_kind);
                        break;
                    }
                    case (ASR::ttypeType::Complex) : {
                        int a_kind = down_cast<ASR::Complex_t>(arg_type_)->m_kind;
                        target_type = llvm_utils->getComplexType(a_kind);
                        break;
                    }
                    case (ASR::ttypeType::String) : {
                        target_type = llvm_utils->get_StringType(arg_type_);
                        break;
                    }
                    case (ASR::ttypeType::Logical) : {
                        int a_kind = down_cast<ASR::Logical_t>(arg_type_)->m_kind;
                        target_type = llvm_utils->getIntType(a_kind);
                        break;
                    }
                    case (ASR::ttypeType::EnumType) :
                        target_type = llvm::Type::getInt32Ty(context);
                        break;
                    case (ASR::ttypeType::StructType) :
                        break;
                    case (ASR::ttypeType::CPtr) :
                        target_type = llvm::Type::getVoidTy(context)->getPointerTo();
                        break;
                    case ASR::ttypeType::Allocatable:
                    case (ASR::ttypeType::Pointer) : {
                        ASR::ttype_t* type_ = ASRUtils::get_contained_type(arg_type);
                        target_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, type_, module.get());
                        target_type = target_type->getPointerTo();
                        break;
                    }
                    case (ASR::ttypeType::List) : {
                        target_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, arg_type_, module.get());
                        break ;
                    }
                    case (ASR::ttypeType::Tuple) : {
                        target_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, arg_type_, module.get());
                        break ;
                    }
                    case (ASR::ttypeType::FunctionType): {
                        target_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, arg_type_, module.get());
                        break;
                    }
                    default :
                        throw CodeGenError("Type " + ASRUtils::type_to_str_fortran_expr(arg_type, x.m_args[i].m_value) + " not implemented yet.");
                }
                if( ASR::is_a<ASR::EnumValue_t>(*x.m_args[i].m_value) ) {
                    target_type = llvm::Type::getInt32Ty(context);
                }
                if (ASR::is_a<ASR::StructType_t>(*arg_type) && !ASRUtils::is_class_type(arg_type)) {
                    bool pass_by_value = false;
                    ASR::Variable_t *struct_orig_arg = nullptr;
                    if (func_subrout->type == ASR::symbolType::Function) {
                        ASR::Function_t* func = down_cast<ASR::Function_t>(func_subrout);
                        size_t arg_idx = i;
                        if (arg_idx < func->n_args && func->m_args[arg_idx] != nullptr) {
                            struct_orig_arg = EXPR2VAR(func->m_args[arg_idx]);
                            if (struct_orig_arg && struct_orig_arg->m_abi == ASR::abiType::BindC
                                && struct_orig_arg->m_value_attr) {
                                pass_by_value = true;
                            }
                        }
                    }
                    if (!pass_by_value && !value->getType()->isPointerTy()) {
                        // Value is not a pointer but callee expects pointer - allocate temp storage
                        llvm::Type* struct_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                            x.m_args[i].m_value, arg_type, module.get());
                        llvm::AllocaInst *target = get_call_arg_alloca(struct_llvm_type);
                        builder->CreateStore(value, target);
                        tmp = target;
                    } else {
                        tmp = value;
                    }
                } else if(ASR::is_a<ASR::String_t>(*arg_type)){
                    if (orig_arg && orig_arg->m_abi == ASR::abiType::BindC
                            && orig_arg->m_value_attr) {
                        // For bind(C) character with value attribute,
                        // load the i8 value from the pointer
                        if (value->getType()->isPointerTy()) {
                            tmp = llvm_utils->CreateLoad2(target_type, value);
                        } else {
                            tmp = value;
                        }
                    } else if (!value->getType()->isPointerTy()) {
                        // String value was loaded (e.g., from ClassToIntrinsic cast);
                        // store to temp to pass by reference
                        llvm::AllocaInst *target = get_call_arg_alloca(target_type);
                        builder->CreateStore(value, target);
                        tmp = target;
                    } else {
                        tmp = value;
                    }
                }else {
                    bool use_value = false;
                    ASR::Variable_t *orig_arg = nullptr;
                    if( func_subrout->type == ASR::symbolType::Function ) {
                        ASR::Function_t* func = down_cast<ASR::Function_t>(func_subrout);
                        size_t arg_idx = i;
                        if (arg_idx < func->n_args) {
                            LCOMPILERS_ASSERT(func->m_args[arg_idx] != nullptr);
                            orig_arg = EXPR2VAR(func->m_args[arg_idx]);
                        } else {
                            LCOMPILERS_ASSERT_MSG(compiler_options.implicit_interface == true &&
                                            symbol_get_past_external(x.m_name)->type == ASR::symbolType::Variable,
                                            "argument/funcParams mismatch only allowed with implicit interfaces "
                                            "and call to variable function")
                        }
                    } else {
                        LCOMPILERS_ASSERT(false)
                    }
                    if (orig_arg != nullptr && orig_arg->m_abi == ASR::abiType::BindC
                        && orig_arg->m_value_attr) {
                        use_value = true;
                    }
                    if (ASR::is_a<ASR::ArrayItem_t>(*x.m_args[i].m_value)) {
                        if (ASRUtils::is_class_type(
                                ASRUtils::extract_type(arg_type))
                            && !ASRUtils::is_class_type(
                                ASRUtils::extract_type(orig_arg->m_type))) {
                            value = convert_class_to_type(x.m_args[i].m_value, ASRUtils::EXPR(ASR::make_Var_t(
                                al, orig_arg->base.base.loc, &orig_arg->base)),
                                orig_arg->m_type, value);
                        }
                        use_value = true;
                    } else if ( (ASR::is_a<ASR::ComplexRe_t>(*x.m_args[i].m_value) ||
                                 ASR::is_a<ASR::ComplexIm_t>(*x.m_args[i].m_value)) &&
                                value->getType()->isPointerTy() ) {
                        use_value = true;
                    } else if (ASR::is_a<ASR::GetPointer_t>(*x.m_args[i].m_value) &&
                               value->getType()->isPointerTy()) {
                        use_value = true;
                    }
                    if (use_value) {
                        tmp = value;
                    }
                    if (!use_value && orig_arg != nullptr) {
                        // Create alloca to get a pointer, but do it
                        // at the beginning of the function to avoid
                        // using alloca inside a loop, which would
                        // run out of stack
                        if (((ASR::is_a<ASR::StructInstanceMember_t>(*x.m_args[i].m_value)
                                    && ((!ASRUtils::is_allocatable(orig_arg->m_type)
                                        && ASRUtils::is_allocatable(arg_type))
                                        || !ASRUtils::is_allocatable(arg_type))
                                    && (ASRUtils::is_array(arg_type)
                                        || (ASR::is_a<ASR::CPtr_t>(
                                                *ASRUtils::expr_type(x.m_args[i].m_value))
                                            && is_cptr_dummy_passed_by_value(orig_arg))))
                                || (ASR::is_a<ASR::StructInstanceMember_t>(*x.m_args[i].m_value)
                                    && ASRUtils::is_allocatable(arg_type)
                                    && !ASRUtils::is_allocatable(orig_arg->m_type)
                                    && (orig_arg->m_intent == ASR::intentType::Out
                                        || orig_arg->m_intent == ASR::intentType::InOut)))
                            && value->getType()->isPointerTy()
                            && !ASRUtils::is_character(*arg_type)) {
                                if (ASRUtils::is_class_type(
                                        ASRUtils::extract_type(arg_type))
                                    && !ASRUtils::is_class_type(
                                        ASRUtils::extract_type(orig_arg->m_type))) {
                                    value = convert_class_to_type(x.m_args[i].m_value, ASRUtils::EXPR(ASR::make_Var_t(
                                        al, orig_arg->base.base.loc, &orig_arg->base)),
                                        orig_arg->m_type, value);
                                } else if (!ASRUtils::is_class_type(ASRUtils::type_get_past_allocatable(orig_arg->m_type))) {
                                    if (!ASRUtils::is_array(ASRUtils::expr_type(x.m_args[i].m_value))
                                            && (ASRUtils::is_allocatable(x.m_args[i].m_value)
                                                || ASRUtils::is_pointer(ASRUtils::expr_type(x.m_args[i].m_value)))) {
                                        check_and_allocate_scalar(x.m_args[i].m_value);
                                    }
                                    bool skip_load_for_fixed_size_array =
                                        ASRUtils::is_array(arg_type) &&
                                        !ASRUtils::is_allocatable(arg_type) &&
                                        !ASRUtils::is_pointer(arg_type) &&
                                        ASRUtils::extract_physical_type(arg_type) ==
                                            ASR::array_physical_typeType::FixedSizeArray;
                                    bool skip_load_for_pointer_dummy_arg =
                                        LLVM::is_llvm_pointer(*orig_arg->m_type) &&
                                        LLVM::is_llvm_pointer(*arg_type);
                                    if (!skip_load_for_fixed_size_array &&
                                            !skip_load_for_pointer_dummy_arg) {
                                        llvm::Type* value_type = llvm_utils->get_type_from_ttype_t_util(x.m_args[i].m_value, arg_type, module.get());
                                        value = llvm_utils->CreateLoad2(value_type, value);
                                    }
                                }
                        }
                        if( !ASR::is_a<ASR::CPtr_t>(*arg_type) &&
                            !(orig_arg && !LLVM::is_llvm_pointer(*orig_arg->m_type) &&
                            LLVM::is_llvm_pointer(*arg_type)) &&
                            !ASRUtils::is_character(*arg_type) &&
                            !ASRUtils::is_class_type(arg_type) &&
                            (!ASR::is_a<ASR::StructInstanceMember_t>(*x.m_args[i].m_value) || ASRUtils::is_value_constant(x.m_args[i].m_value)) ) {
                            llvm::AllocaInst *target = get_call_arg_alloca(target_type);
                            if( ASR::is_a<ASR::Tuple_t>(*arg_type) ||
                                ASR::is_a<ASR::List_t>(*arg_type) ) {
                                    if (ASR::is_a<ASR::List_t>(*arg_type)) {
                                        ASR::List_t* list_t = ASR::down_cast<ASR::List_t>(
                                            arg_type);
                                        std::string tc = ASRUtils::get_type_code(
                                            list_t->m_type, false, false);
                                        list_api->free_data_using_type(tc, target, module.get());
                                        llvm_utils->deepcopy(x.m_args[i].m_value, value, target, arg_type, arg_type, module.get());
                                        list_call_arg_to_finalize[target] = tc;
                                    } else {
                                        llvm_utils->deepcopy(x.m_args[i].m_value, value, target, arg_type, arg_type, module.get());
                                    }
                                    // Free source temp data for ListConstant only.
                                    // Other non-Var sources (e.g., ListItem) may hold
                                    // shallow-copy data pointers shared with a parent
                                    // list, so they must NOT be freed here.
                                    if (ASR::is_a<ASR::List_t>(*arg_type) &&
                                        ASR::is_a<ASR::ListConstant_t>(*x.m_args[i].m_value)) {
                                        ASR::List_t* list_t = ASR::down_cast<ASR::List_t>(
                                            arg_type);
                                        std::string tc = ASRUtils::get_type_code(
                                            list_t->m_type, false, false);
                                        list_api->free_data_using_type(
                                            tc, value, module.get());
                                    }
                            } else {
                                if (value->getType()->isIntegerTy() &&
                                        target_type->isIntegerTy() &&
                                        value->getType() != target_type) {
                                    value = builder->CreateZExtOrTrunc(value, target_type);
                                }
                                builder->CreateStore(value, target);
                            }
                            tmp = target;
                        } else {
                            tmp = value;
                        }
                    } else if (!use_value) {
                        // orig_arg is null (implicit interface without arg info)
                        // Just pass the value as-is
                        tmp = value;
                    }
                }
            }

            if( orig_arg && ASRUtils::is_assumed_rank_array(orig_arg->m_type) &&
                !ASRUtils::is_array(ASRUtils::expr_type(x.m_args[i].m_value)) ) {
                llvm::Type* descriptor_type = llvm_utils->get_type_from_ttype_t_util(
                    ASRUtils::EXPR(ASR::make_Var_t(al, orig_arg->base.base.loc, &orig_arg->base)),
                    ASRUtils::type_get_past_allocatable_pointer(orig_arg->m_type), module.get());
                llvm::Value* descriptor = arr_descr->create_descriptor_alloca(descriptor_type);
                llvm::Value* data_ptr = arr_descr->get_pointer_to_data(descriptor_type, descriptor);
                // If target is unlimited polymorphic, wrap the scalar in a polymorphic type
                // For bind(C), just store raw data pointer (C expects raw data, not wrappers)
                ASR::ttype_t* orig_arg_type_past_pointer = ASRUtils::type_get_past_allocatable(
                    ASRUtils::type_get_past_pointer(orig_arg->m_type));
                if (x_abi == ASR::abiType::BindC) {
                    ASR::ttype_t* scalar_type = ASRUtils::expr_type(x.m_args[i].m_value);
                    ASR::ttype_t* elem_asr_type = ASRUtils::extract_type(scalar_type);
                    llvm::Type* scalar_llvm_type = llvm_utils->get_el_type(
                        x.m_args[i].m_value, elem_asr_type, module.get());
                    llvm::Value* scalar_ptr = tmp;
                    if (LLVM::is_llvm_pointer(*scalar_type)) {
                        scalar_ptr = llvm_utils->CreateLoad2(
                            scalar_llvm_type->getPointerTo(), tmp);
                    }
                    std::vector<FlatCopyback::Member> flat_layout;
                    uint64_t flat_sz = 0;
                    if (get_struct_flat_layout(elem_asr_type, scalar_llvm_type,
                            x.m_args[i].m_value, flat_layout, flat_sz)) {
                        llvm::Value* flat_buf = create_flat_copy_from_struct(
                            scalar_ptr, scalar_llvm_type, flat_layout, flat_sz);
                        llvm::Type* expected_ptr_type = descriptor_type->getStructElementType(0);
                        llvm::Value* flat_ptr = builder->CreateBitCast(flat_buf, expected_ptr_type);
                        builder->CreateStore(flat_ptr, data_ptr);
                        pending_flat_copybacks.push_back({
                            flat_buf, scalar_ptr, scalar_llvm_type, flat_layout});
                    } else {
                        llvm::Type* expected_ptr_type = descriptor_type->getStructElementType(0);
                        llvm::Value* raw_ptr = builder->CreateBitCast(scalar_ptr, expected_ptr_type);
                        builder->CreateStore(raw_ptr, data_ptr);
                    }
                } else if (ASRUtils::is_unlimited_polymorphic_type(ASRUtils::EXPR(
                        ASR::make_Var_t(al, orig_arg->base.base.loc, &orig_arg->base)))) {
                    // Get the element type of the polymorphic array (unlimited_polymorphic_type)
                    llvm::Type* poly_elem_type = llvm_utils->get_el_type(
                        ASRUtils::EXPR(ASR::make_Var_t(al, orig_arg->base.base.loc, &orig_arg->base)),
                        ASRUtils::type_get_past_array(orig_arg_type_past_pointer), module.get());
                    llvm::Value* poly_wrapper = llvm_utils->CreateAlloca(*builder, poly_elem_type);
                    
                    ASR::ttype_t* arg_type = ASRUtils::expr_type(x.m_args[i].m_value);
                    ASR::ttype_t* arg_type_unwrapped = ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(arg_type));
                    bool dummy_is_assumed_type = false;
                    if (orig_arg->m_type_declaration) {
                        ASR::symbol_t* dummy_decl_sym = ASRUtils::symbol_get_past_external(
                            orig_arg->m_type_declaration);
                        if (dummy_decl_sym && std::string(ASRUtils::symbol_name(
                                dummy_decl_sym)) == "~assumed_type") {
                            dummy_is_assumed_type = true;
                        }
                    }
                    if (!ASR::is_a<ASR::StructType_t>(*arg_type_unwrapped)) {
                        struct_api->store_intrinsic_type_vptr(arg_type,
                            ASRUtils::extract_kind_from_ttype_t(arg_type), poly_wrapper, module.get());
                    } else if (!dummy_is_assumed_type) {
                        ASR::symbol_t* value_struct_sym =
                            ASRUtils::get_struct_sym_from_struct_expr(
                                x.m_args[i].m_value);
                        if (value_struct_sym) {
                            struct_api->create_new_vtable_for_struct_type(
                                value_struct_sym, module.get());
                            struct_api->store_class_vptr(
                                value_struct_sym, poly_wrapper, module.get());
                        }
                    }
                    
                    llvm::Value* poly_data_ptr = llvm_utils->create_gep2(poly_elem_type, poly_wrapper, 1);
                    ASR::ttype_t* elem_asr_chk = ASRUtils::extract_type(arg_type);
                    llvm::Type* elem_llvm_chk = llvm_utils->get_el_type(
                        x.m_args[i].m_value, elem_asr_chk, module.get());
                    llvm::Value* poly_scalar_ptr = tmp;
                    if (LLVM::is_llvm_pointer(*arg_type)) {
                        poly_scalar_ptr = llvm_utils->CreateLoad2(
                            elem_llvm_chk->getPointerTo(), tmp);
                    }
                    std::vector<FlatCopyback::Member> flat_layout;
                    uint64_t flat_sz = 0;
                    if (get_struct_flat_layout(elem_asr_chk, elem_llvm_chk,
                            x.m_args[i].m_value, flat_layout, flat_sz)) {
                        llvm::Value* flat_buf = create_flat_copy_from_struct(
                            poly_scalar_ptr, elem_llvm_chk, flat_layout, flat_sz);
                        builder->CreateStore(
                            builder->CreateBitCast(flat_buf, llvm_utils->i8_ptr),
                            poly_data_ptr);
                        pending_flat_copybacks.push_back({
                            flat_buf, poly_scalar_ptr, elem_llvm_chk, flat_layout});
                    } else {
                        builder->CreateStore(
                            builder->CreateBitCast(poly_scalar_ptr, llvm_utils->i8_ptr),
                            poly_data_ptr);
                    }
                    
                    builder->CreateStore(poly_wrapper, data_ptr);
                } else {
                    llvm::Value* raw_ptr = tmp;
                    ASR::ttype_t* arg_type = ASRUtils::expr_type(x.m_args[i].m_value);
                    if (LLVM::is_llvm_pointer(*arg_type)) {
                        ASR::ttype_t* elem_asr_type = ASRUtils::extract_type(arg_type);
                        llvm::Type* elem_llvm_type = llvm_utils->get_el_type(
                            x.m_args[i].m_value, elem_asr_type, module.get());
                        raw_ptr = llvm_utils->CreateLoad2(elem_llvm_type->getPointerTo(), tmp);
                    }
                    llvm::Type* expected_ptr_type = descriptor_type->getStructElementType(0);
                    raw_ptr = builder->CreateBitCast(raw_ptr, expected_ptr_type);
                    builder->CreateStore(raw_ptr, data_ptr);
                }
                
                builder->CreateStore(llvm::ConstantInt::get(context, llvm::APInt(64, 0)),
                    arr_descr->get_offset(descriptor_type, descriptor, false));
                arr_descr->fill_dimension_descriptor(descriptor_type, descriptor, 0);
                // Set CFI descriptor fields for scalar-to-assumed-rank
                {
                    ASR::ttype_t* scalar_type = ASRUtils::expr_type(x.m_args[i].m_value);
                    ASR::ttype_t* elem_asr_type = ASRUtils::extract_type(scalar_type);
                    llvm::Type* scalar_llvm_type = llvm_utils->get_el_type(
                        x.m_args[i].m_value, elem_asr_type, module.get());
                    llvm::DataLayout data_layout(module->getDataLayout());
                    uint64_t elem_size = data_layout.getTypeAllocSize(scalar_llvm_type);
                    std::vector<FlatCopyback::Member> tmp_layout;
                    uint64_t flat_sz = 0;
                    if (get_struct_flat_layout(elem_asr_type, scalar_llvm_type,
                            x.m_args[i].m_value, tmp_layout, flat_sz)) {
                        elem_size = flat_sz;
                    }
                    set_cfi_descriptor_fields(descriptor_type, descriptor,
                        elem_asr_type, elem_size, scalar_type);
                }
                tmp = descriptor;
            }

            // To avoid segmentation faults when original argument
            // is not a ASR::Variable_t like callbacks.
            // Skip polymorphic conversion if we've already wrapped the scalar in an assumed-rank
            // polymorphic descriptor above
            bool skip_polymorphic_conversion = orig_arg && 
                ASRUtils::is_assumed_rank_array(orig_arg->m_type) &&
                !ASRUtils::is_array(ASRUtils::expr_type(x.m_args[i].m_value)) &&
                ASRUtils::is_unlimited_polymorphic_type(ASRUtils::EXPR(
                    ASR::make_Var_t(al, orig_arg->base.base.loc, &orig_arg->base)));
            // For bind(C) with type(*) dimension(..) args, skip polymorphic conversion
            // because C expects raw data pointers in descriptors, not Fortran polymorphic wrappers
            if (x_abi == ASR::abiType::BindC && orig_arg &&
                ASRUtils::is_assumed_rank_array(orig_arg->m_type)) {
                skip_polymorphic_conversion = true;
            }
            if( orig_arg && !skip_polymorphic_conversion ) {
                tmp = convert_to_polymorphic_arg(x.m_args[i].m_value, tmp,
                    ASRUtils::EXPR(ASR::make_Var_t(al, orig_arg->base.base.loc, &orig_arg->base)),
                    ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(orig_arg->m_type)),
                    ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(ASRUtils::expr_type(x.m_args[i].m_value))));
            }

            // For assumed-rank parameters, bitcast the descriptor pointer
            // to match the function signature (all descriptor types share
            // the same layout; the callee reads the rank field to determine
            // how many dim entries are valid).
            if (orig_arg &&
                    ASRUtils::is_assumed_rank_array(orig_arg->m_type)) {
                llvm::Type* expected_desc_type = llvm_utils->get_type_from_ttype_t_util(
                    ASRUtils::EXPR(ASR::make_Var_t(al, orig_arg->base.base.loc, &orig_arg->base)),
                    orig_arg->m_type, module.get());
                if (tmp->getType() != expected_desc_type->getPointerTo()) {
                    tmp = builder->CreateBitCast(tmp, expected_desc_type->getPointerTo());
                }
            }

            // For bind(c) character array arguments passed as Var, extract the raw data pointer
            // from the string descriptor. For other expression types, the conversion may have
            // already occurred via ArrayPhysicalCast or other mechanisms.
            if (orig_arg && x_abi == ASR::abiType::BindC &&
                ASRUtils::is_character(*orig_arg->m_type) &&
                ASRUtils::is_array(orig_arg->m_type) &&
                ASR::is_a<ASR::Var_t>(*x.m_args[i].m_value)) {
                ASR::array_physical_typeType phys_type = ASRUtils::extract_physical_type(orig_arg->m_type);
                if (phys_type == ASR::array_physical_typeType::PointerArray ||
                    phys_type == ASR::array_physical_typeType::StringArraySinglePointer) {
                    // tmp is a string_descriptor* - extract the char* data pointer (field 0)
                    llvm::Value* data_ptr = llvm_utils->CreateGEP2(llvm_utils->string_descriptor, tmp, 0);
                    tmp = llvm_utils->CreateLoad2(llvm::Type::getInt8Ty(context)->getPointerTo(), data_ptr);
                }
            }

            // For bind(C) with scalar allocatable/pointer deferred-length
            // character arguments:
            //  - External C function: build a CFI_cdesc_t* descriptor.
            //  - Fortran bind(C) subroutine: the callee expects string_descriptor**
            //    (generated by LFortran), so wrap the string_descriptor* in an
            //    extra alloca pointer instead of constructing a CFI descriptor.
            if (orig_arg && x_abi == ASR::abiType::BindC &&
                ASRUtils::is_character(*orig_arg->m_type) &&
                !ASRUtils::is_array(orig_arg->m_type) &&
                (ASRUtils::is_allocatable(orig_arg->m_type) ||
                 ASRUtils::is_pointer(orig_arg->m_type))) {
                // Determine callee kind before building the argument.
                ASR::FunctionType_t* arg_cft =
                    (func_subrout->type == ASR::symbolType::Function)
                    ? ASRUtils::get_FunctionType(
                        ASR::down_cast<ASR::Function_t>(func_subrout))
                    : nullptr;
                // Procedure pointer calls (where x.m_name resolves to a
                // Variable, not a Function) use the LLVM function type
                // generated from the abstract interface, which uses
                // LFortran's internal string_descriptor** convention.
                // Do not build a CFI descriptor for these calls.
                ASR::symbol_t* call_sym = symbol_get_past_external(x.m_name);
                bool is_proc_pointer_call = ASR::is_a<ASR::Variable_t>(*call_sym);
                bool callee_is_extern_c = !is_proc_pointer_call &&
                                           (!arg_cft ||
                                            is_external_interface_function(arg_cft));

                // If the interface looks external but there is a Fortran
                // bind(C) implementation in the same translation unit,
                // the callee uses LFortran's internal string_descriptor**
                // layout, not a CFI descriptor.
                if (callee_is_extern_c && arg_cft &&
                    arg_cft->m_abi == ASR::abiType::BindC) {
                    ASR::Function_t* called_fn =
                        ASR::down_cast<ASR::Function_t>(func_subrout);
                    SymbolTable* scope = called_fn->m_symtab->parent;
                    while (scope && scope->parent) scope = scope->parent;
                    if (scope) {
                        ASR::symbol_t* impl = scope->get_symbol(
                            called_fn->m_name);
                        if (impl && impl != (ASR::symbol_t*)called_fn &&
                            ASR::is_a<ASR::Function_t>(*impl)) {
                            ASR::FunctionType_t* impl_ft =
                                ASRUtils::get_FunctionType(
                                    ASR::down_cast<ASR::Function_t>(impl));
                            if (impl_ft->m_abi == ASR::abiType::BindC &&
                                impl_ft->m_deftype == ASR::deftypeType::Implementation) {
                                callee_is_extern_c = false;
                            }
                        }
                    }
                }

                llvm::Value* str_desc = builder->CreateBitCast(
                    tmp, llvm_utils->string_descriptor->getPointerTo());
                if (callee_is_extern_c) {
                    llvm::Value* data_gep = llvm_utils->create_gep2(
                        llvm_utils->string_descriptor, str_desc, 0);
                    llvm::Value* data_ptr = llvm_utils->CreateLoad2(
                        llvm::Type::getInt8Ty(context)->getPointerTo(), data_gep);
                    llvm::Value* len_gep = llvm_utils->create_gep2(
                        llvm_utils->string_descriptor, str_desc, 1);
                    llvm::Value* str_len = llvm_utils->CreateLoad2(
                        llvm::Type::getInt64Ty(context), len_gep);
                    ASR::ttype_t* char_type = ASRUtils::extract_type(orig_arg->m_type);
                    llvm::Type* char_llvm_type = llvm::Type::getInt8Ty(context);
                    llvm::Type* desc_type = arr_descr->get_array_type(
                        ASRUtils::EXPR(ASR::make_Var_t(al, orig_arg->base.base.loc, &orig_arg->base)),
                        ASRUtils::type_get_past_allocatable(
                            ASRUtils::type_get_past_pointer(orig_arg->m_type)),
                        char_llvm_type, false);
                    llvm::Value* descriptor = llvm_utils->CreateAlloca(
                        *builder, desc_type, nullptr, "cfi_desc_char");
                    llvm::DataLayout dl(module->getDataLayout());
                    uint64_t desc_size = dl.getTypeAllocSize(desc_type);
                    builder->CreateMemSet(
                        builder->CreateBitCast(descriptor, llvm::Type::getInt8Ty(context)->getPointerTo()),
                        llvm::ConstantInt::get(llvm::Type::getInt8Ty(context), 0),
                        desc_size, llvm::MaybeAlign());
                    builder->CreateStore(
                        builder->CreateBitCast(data_ptr, char_llvm_type->getPointerTo()),
                        llvm_utils->create_gep2(desc_type, descriptor, 0));
                    builder->CreateStore(
                        llvm::ConstantInt::get(llvm::Type::getInt8Ty(context), 0),
                        llvm_utils->create_gep2(desc_type, descriptor, 3));
                    set_cfi_descriptor_fields(desc_type, descriptor,
                        char_type, 1, orig_arg->m_type);
                    builder->CreateStore(str_len,
                        llvm_utils->create_gep2(desc_type, descriptor, 1));
                    tmp = descriptor;
                } else {
                    // Fortran bind(C) subroutine:
                    //  - CChar: callee expects i8**; pass tmp (already i8**).
                    //  - DescriptorString: callee expects string_descriptor**;
                    //    wrap the descriptor pointer so callee modifications
                    //    (deallocate, etc.) are reflected in the caller's.
                    ASR::String_t* formal_str =
                        ASR::down_cast<ASR::String_t>(ASRUtils::extract_type(
                            ASRUtils::type_get_past_allocatable_pointer(orig_arg->m_type)));
                    if (formal_str->m_physical_type != ASR::string_physical_typeType::CChar) {
                        llvm::Value* arg_ptr = llvm_utils->CreateAlloca(
                            *builder,
                            llvm_utils->string_descriptor->getPointerTo(),
                            nullptr, "str_arg_ptr");
                        builder->CreateStore(str_desc, arg_ptr);
                        tmp = arg_ptr;
                    }
                }
            }

            // For bind(C) calls with DescriptorArray/AssumedRankArray args,
            // convert the descriptor from LFortran's internal format (element
            // strides) to CFI format (byte strides) and set elem_len.
            // Only apply for explicit bind(C) functions (with bindc_name),
            // not implicit interface functions which use BindC ABI internally
            // but expect LFortran's descriptor format.
            ASR::FunctionType_t* callee_fn_type = nullptr;
            if (func_subrout->type == ASR::symbolType::Function) {
                callee_fn_type = ASRUtils::get_FunctionType(
                    ASR::down_cast<ASR::Function_t>(func_subrout));
            }
            // Check if callee is an implicit interface function.
            // Implicit interfaces use BindC ABI internally but the
            // actual Fortran implementation uses LFortran's internal
            // descriptor format.  Detect by looking for a Source/
            // Implementation function with the same name in the
            // global scope.
            bool is_implicit_interface = false;
            if (callee_fn_type &&
                callee_fn_type->m_abi == ASR::abiType::BindC &&
                callee_fn_type->m_deftype == ASR::deftypeType::Interface &&
                !callee_fn_type->m_bindc_name) {
                ASR::Function_t* called_fn =
                    ASR::down_cast<ASR::Function_t>(func_subrout);
                SymbolTable* scope = called_fn->m_symtab->parent;
                while (scope && scope->parent) scope = scope->parent;
                if (scope) {
                    ASR::symbol_t* impl = scope->get_symbol(
                        called_fn->m_name);
                    if (impl && impl != (ASR::symbol_t*)called_fn &&
                        ASR::is_a<ASR::Function_t>(*impl)) {
                        ASR::FunctionType_t* impl_ft =
                            ASRUtils::get_FunctionType(
                                ASR::down_cast<ASR::Function_t>(impl));
                        if (impl_ft->m_abi != ASR::abiType::BindC) {
                            is_implicit_interface = true;
                        }
                    }
                }
            }
            if (orig_arg && x_abi == ASR::abiType::BindC &&
                callee_fn_type &&
                !is_implicit_interface &&
                ASRUtils::is_array(orig_arg->m_type)) {
                ASR::array_physical_typeType phys_type =
                    ASRUtils::extract_physical_type(orig_arg->m_type);
                if (phys_type == ASR::array_physical_typeType::DescriptorArray ||
                    phys_type == ASR::array_physical_typeType::AssumedRankArray) {
                    // Use the actual argument's type to get element info and
                    // dimensions, since the formal parameter may be type(*)
                    // or assumed-rank (..) without concrete type/dim info.
                    ASR::ttype_t* actual_type = ASRUtils::expr_type(x.m_args[i].m_value);
                    ASR::ttype_t* elem_asr_type = ASRUtils::extract_type(actual_type);
                    llvm::Type* elem_llvm_type = llvm_utils->get_el_type(
                        x.m_args[i].m_value, elem_asr_type, module.get());
                    llvm::DataLayout data_layout(module->getDataLayout());
                    // For bind(C) character arrays, elem_size is the
                    // character kind size (e.g. 1 for c_char), not the
                    // size of the internal string_descriptor struct.
                    uint64_t elem_size = ASRUtils::is_character(*elem_asr_type)
                        ? ASRUtils::extract_kind_from_ttype_t(elem_asr_type)
                        : data_layout.getTypeAllocSize(elem_llvm_type);
                    // Derive the descriptor type. For scalar arguments
                    // passed to assumed-rank parameters, use the formal
                    // parameter's type (which carries the descriptor struct
                    // layout), not the scalar's type.
                    ASR::ttype_t* desc_asr_type;
                    ASR::expr_t* desc_expr;
                    if (!ASRUtils::is_array(actual_type)) {
                        desc_asr_type = ASRUtils::type_get_past_allocatable(
                            ASRUtils::type_get_past_pointer(orig_arg->m_type));
                        desc_expr = ASRUtils::EXPR(ASR::make_Var_t(al,
                            orig_arg->base.base.loc, &orig_arg->base));
                    } else {
                        desc_asr_type = ASRUtils::type_get_past_allocatable(
                            ASRUtils::type_get_past_pointer(actual_type));
                        desc_expr = x.m_args[i].m_value;
                    }
                    llvm::Type* desc_type = llvm_utils->get_type_from_ttype_t_util(
                        desc_expr, desc_asr_type, module.get());

                    // Ensure tmp is a pointer to the descriptor.
                    // For allocatable/pointer arrays the upstream Var handler
                    // may have loaded the struct VALUE (which is wrong on
                    // opaque-pointer builds where the load type loses pointer
                    // indirection). Re-derive the descriptor pointer from
                    // the variable when the actual arg is allocatable/pointer.
                    // Skip when we already created a fresh descriptor for a
                    // scalar passed to an assumed-rank parameter — that
                    // descriptor is correct and must not be overwritten.
                    bool already_has_assumed_rank_desc =
                        orig_arg &&
                        ASRUtils::is_assumed_rank_array(orig_arg->m_type) &&
                        !ASRUtils::is_array(ASRUtils::expr_type(
                            x.m_args[i].m_value));
                    if (LLVM::is_llvm_pointer(*actual_type)
                            && ASR::is_a<ASR::Var_t>(*x.m_args[i].m_value)
                            && !already_has_assumed_rank_desc) {
                        ASR::Variable_t *arg = EXPR2VAR(x.m_args[i].m_value);
                        uint32_t h = get_hash((ASR::asr_t*)arg);
                        tmp = llvm_symtab[h];
                        // tmp is now the variable's alloca (ptr-to-ptr for
                        // allocatable). Load once to get the descriptor ptr.
                        tmp = llvm_utils->CreateLoad2(
                            desc_type->getPointerTo(), tmp);
                    }

                    // convert_to_polymorphic_arg may have converted tmp to
                    // a different descriptor variant (e.g., generic %array
                    // for type(*) formals). Bitcast back to match desc_type
                    // so GEPs resolve correctly on typed-pointer LLVM builds.
                    // On opaque-pointer builds this is a no-op.
                    if (tmp->getType()->isPointerTy()) {
                        tmp = builder->CreateBitCast(tmp,
                            desc_type->getPointerTo());
                    }

                    // Convert internal descriptor to CFI layout (no offset
                    // field, byte strides) for bind(C) interop.

                    // For character arrays, the descriptor's base_addr
                    // points to a string_descriptor struct ({i8*, i64}).
                    // For CFI interop, replace it with the raw i8* data
                    // pointer (field 0 of the string_descriptor).
                    if (ASRUtils::is_character(*elem_asr_type)) {
                        // Make a temporary copy so we don't corrupt the
                        // original descriptor's base_addr.
                        llvm::Value* desc_copy = arr_descr->create_descriptor_alloca(
                            desc_type, "char_desc_copy");
                        // Copy the full descriptor via memcpy
                        llvm::DataLayout data_layout(module->getDataLayout());
                        uint64_t total_size = data_layout.getTypeAllocSize(desc_type);
                        llvm::Value* total_size_val = llvm::ConstantInt::get(
                            llvm::Type::getInt64Ty(context), total_size);
                        builder->CreateMemCpy(
                            builder->CreateBitCast(desc_copy, llvm::Type::getInt8Ty(context)->getPointerTo()),
                            llvm::MaybeAlign(8),
                            builder->CreateBitCast(tmp, llvm::Type::getInt8Ty(context)->getPointerTo()),
                            llvm::MaybeAlign(8),
                            total_size_val);
                        tmp = desc_copy;
                        llvm::Value* base_addr_gep = llvm_utils->create_gep2(
                            desc_type, tmp, 0);
                        llvm::Value* str_desc = llvm_utils->CreateLoad2(
                            elem_llvm_type->getPointerTo(), base_addr_gep);
                        llvm::Value* data_gep = llvm_utils->create_gep2(
                            elem_llvm_type, str_desc, 0);
                        llvm::Value* raw_data = llvm_utils->CreateLoad2(
                            llvm::Type::getInt8Ty(context)->getPointerTo(),
                            data_gep);
                        builder->CreateStore(
                            builder->CreateBitCast(raw_data,
                                elem_llvm_type->getPointerTo()),
                            base_addr_gep);
                    }
                    int n_dims = ASRUtils::extract_n_dims_from_ttype(actual_type);
                    bool is_type_star = ASR::is_a<ASR::StructType_t>(*elem_asr_type)
                        && ASR::down_cast<ASR::StructType_t>(elem_asr_type)->m_is_unlimited_polymorphic;
                    int8_t cfi_type_code = get_cfi_type_code(elem_asr_type);
                    int cfi_attr = 0;
                    if (actual_type && ASRUtils::is_allocatable(actual_type)) {
                        cfi_attr = 2;
                    } else if (actual_type && ASRUtils::is_pointer(actual_type)) {
                        cfi_attr = 1;
                    }
                    llvm::Value* internal_desc_ptr = tmp;
                    tmp = arr_descr->internal_to_cfi(
                        desc_type, tmp, elem_llvm_type, n_dims, elem_size,
                        cfi_type_code, cfi_attr);
                    if (is_type_star) {
                        llvm::Type* cfi_struct = llvm::cast<llvm::AllocaInst>(
                            tmp)->getAllocatedType();
                        llvm::Type* i8_ptr_type =
                            llvm::Type::getInt8Ty(context)->getPointerTo();
                        llvm::Value* cfi_base_gep = llvm_utils->create_gep2(
                            cfi_struct, tmp, 0);
                        llvm::Value* box_i8 = llvm_utils->CreateLoad2(
                            i8_ptr_type, cfi_base_gep);
                        llvm::Value* box_ptr = builder->CreateBitCast(
                            box_i8, elem_llvm_type->getPointerTo());
                        llvm::Value* data_ptr = unwrap_polymorphic_box_data(
                            elem_llvm_type, box_ptr);

                        llvm::Value* src_type = llvm_utils->CreateLoad2(
                            llvm::Type::getInt8Ty(context),
                            llvm_utils->create_gep2(desc_type, internal_desc_ptr, 4));
                        llvm::Value* src_elem_len = llvm_utils->CreateLoad2(
                            llvm::Type::getInt64Ty(context),
                            llvm_utils->create_gep2(desc_type, internal_desc_ptr, 1));

                        llvm::Value* is_char = builder->CreateICmpEQ(
                            src_type,
                            llvm::ConstantInt::get(llvm::Type::getInt8Ty(context), 40));
                        llvm::Function* fn = builder->GetInsertBlock()->getParent();
                        llvm::BasicBlock* char_bb = llvm::BasicBlock::Create(context, "type_star_char", fn);
                        llvm::BasicBlock* merge_bb = llvm::BasicBlock::Create(context, "type_star_merge", fn);
                        llvm::BasicBlock* other_bb = llvm::BasicBlock::Create(context, "type_star_other", fn);
                        builder->CreateCondBr(is_char, char_bb, other_bb);

                        builder->SetInsertPoint(char_bb);
                        llvm::Value* str_desc_ptr = builder->CreateBitCast(
                            data_ptr, llvm_utils->string_descriptor->getPointerTo());
                        llvm::Value* raw_char_data = llvm_utils->CreateLoad2(
                            i8_ptr_type,
                            llvm_utils->create_gep2(llvm_utils->string_descriptor, str_desc_ptr, 0));
                        builder->CreateBr(merge_bb);

                        builder->SetInsertPoint(other_bb);
                        builder->CreateBr(merge_bb);

                        builder->SetInsertPoint(merge_bb);
                        llvm::PHINode* final_data = builder->CreatePHI(i8_ptr_type, 2);
                        final_data->addIncoming(raw_char_data, char_bb);
                        final_data->addIncoming(data_ptr, other_bb);

                        builder->CreateStore(final_data, cfi_base_gep);

                        builder->CreateStore(src_type,
                            llvm_utils->create_gep2(cfi_struct, tmp, 4));
                        builder->CreateStore(src_elem_len,
                            llvm_utils->create_gep2(cfi_struct, tmp, 1));
                    }
                    // Bitcast descriptor pointer to match the formal parameter's
                    // descriptor type when element types differ (e.g., actual
                    // integer(c_int) passed to type(*) formal parameter).
                    const char* fn_name = callee_fn_type->m_bindc_name;
                    if (!fn_name) {
                        fn_name = ASRUtils::symbol_name(func_subrout);
                    }
                    llvm::Function* fn = module->getFunction(fn_name);
                    if (fn) {
                        llvm::Type* expected_type = fn->getFunctionType()->getParamType(i);
                        if (tmp->getType() != expected_type) {
                            tmp = builder->CreateBitCast(tmp, expected_type);
                        }
                    }
                }
            }

            // Handle scalar allocatable/pointer arguments for bind(C):
            // wrap in a rank-0 CFI descriptor so the C callee receives
            // a valid CFI_cdesc_t*.
            if (orig_arg && x_abi == ASR::abiType::BindC &&
                callee_fn_type && !is_implicit_interface &&
                !ASRUtils::is_array(orig_arg->m_type) &&
                !ASRUtils::is_character(*ASRUtils::type_get_past_allocatable(
                    ASRUtils::type_get_past_pointer(orig_arg->m_type))) &&
                LLVM::is_llvm_pointer(*orig_arg->m_type)) {
                ASR::ttype_t* elem_asr_type = ASRUtils::type_get_past_allocatable(
                    ASRUtils::type_get_past_pointer(orig_arg->m_type));
                llvm::Type* elem_llvm_type = llvm_utils->get_el_type(
                    x.m_args[i].m_value, elem_asr_type, module.get());
                // Build a CFI descriptor directly (no offset field)
                llvm::StructType* cfi_type = arr_descr->get_cfi_type(elem_llvm_type, 0);
                llvm::Value* descriptor = llvm_utils->CreateAlloca(
                    *builder, cfi_type);
                // base_addr: load the scalar's pointer (NULL if unallocated)
                llvm::Value* data_gep = llvm_utils->create_gep2(
                    cfi_type, descriptor, LLVMArrUtils::SimpleCMODescriptor::CFI_FIELD_BASE_ADDR);
                llvm::Type* expected_ptr_type = cfi_type->getElementType(0);
                llvm::Value* base_val = llvm_utils->CreateLoad2(
                    elem_llvm_type->getPointerTo(), tmp);
                if (base_val->getType() != expected_ptr_type) {
                    base_val = builder->CreateBitCast(base_val, expected_ptr_type);
                }
                builder->CreateStore(base_val, data_gep);
                // rank = 0
                builder->CreateStore(
                    llvm::ConstantInt::get(llvm::Type::getInt8Ty(context), 0),
                    llvm_utils->create_gep2(cfi_type, descriptor,
                        LLVMArrUtils::SimpleCMODescriptor::CFI_FIELD_RANK));
                // elem_len, type code, attribute
                llvm::DataLayout data_layout(module->getDataLayout());
                uint64_t elem_size = data_layout.getTypeAllocSize(elem_llvm_type);
                builder->CreateStore(
                    llvm::ConstantInt::get(context, llvm::APInt(64, elem_size)),
                    llvm_utils->create_gep2(cfi_type, descriptor,
                        LLVMArrUtils::SimpleCMODescriptor::CFI_FIELD_ELEM_LEN));
                builder->CreateStore(
                    llvm::ConstantInt::get(llvm::Type::getInt8Ty(context),
                        get_cfi_type_code(elem_asr_type)),
                    llvm_utils->create_gep2(cfi_type, descriptor,
                        LLVMArrUtils::SimpleCMODescriptor::CFI_FIELD_TYPE));
                {
                    int cfi_attr = 0;
                    if (orig_arg->m_type && ASRUtils::is_allocatable(orig_arg->m_type)) {
                        cfi_attr = 2;
                    } else if (orig_arg->m_type && ASRUtils::is_pointer(orig_arg->m_type)) {
                        cfi_attr = 1;
                    }
                    builder->CreateStore(
                        llvm::ConstantInt::get(llvm::Type::getInt8Ty(context), cfi_attr),
                        llvm_utils->create_gep2(cfi_type, descriptor,
                            LLVMArrUtils::SimpleCMODescriptor::CFI_FIELD_ATTRIBUTE));
                }
                // extra = 0 (reserved)
                builder->CreateStore(
                    llvm::ConstantInt::get(llvm::Type::getInt8Ty(context), 0),
                    llvm_utils->create_gep2(cfi_type, descriptor,
                        LLVMArrUtils::SimpleCMODescriptor::CFI_FIELD_EXTRA));
                // version
                builder->CreateStore(
                    llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 20180515),
                    llvm_utils->create_gep2(cfi_type, descriptor,
                        LLVMArrUtils::SimpleCMODescriptor::CFI_FIELD_VERSION));
                tmp = descriptor;
            }

            {
                ASR::symbol_t* called_sym =
                    symbol_get_past_external(x.m_name);
                bool is_proc_ptr_call =
                    called_sym && ASR::is_a<ASR::Variable_t>(*called_sym);
                if (orig_arg && callee_fn_type &&
                        !is_proc_ptr_call &&
                        x.m_dt == nullptr &&
                        !callee_fn_type->m_module &&
                        func_subrout->type == ASR::symbolType::Function &&
                        callee_fn_type->m_deftype == ASR::deftypeType::Interface &&
                        callee_fn_type->m_abi == ASR::abiType::Source &&
                        !ASRUtils::is_array(orig_arg->m_type) &&
                        ASR::is_a<ASR::String_t>(*ASRUtils::extract_type(orig_arg->m_type)) &&
                        tmp->getType() ==
                            llvm_utils->string_descriptor->getPointerTo()) {
                    ASR::Function_t* called_fn =
                        ASR::down_cast<ASR::Function_t>(func_subrout);
                    if (called_fn->n_body == 0) {
                        llvm::Value* data_gep = llvm_utils->create_gep2(
                            llvm_utils->string_descriptor, tmp, 0);
                        llvm::Value* data_ptr = llvm_utils->CreateLoad2(
                            llvm::Type::getInt8Ty(context)->getPointerTo(),
                            data_gep);
                        tmp = builder->CreateBitCast(
                            data_ptr,
                            llvm_utils->string_descriptor->getPointerTo());
                    }
                }
            }

            // For bind(C) calls, ensure the argument type matches the
            // function parameter type. This handles cases like passing a
            // CFI descriptor (%array*) to a function declared with i8**
            // parameter type.
            if (x_abi == ASR::abiType::BindC && callee_fn_type) {
                const char* fn_name = callee_fn_type->m_bindc_name;
                if (!fn_name) {
                    fn_name = ASRUtils::symbol_name(func_subrout);
                }
                llvm::Function* fn = module->getFunction(fn_name);
                if (fn && i < fn->getFunctionType()->getNumParams()) {
                    llvm::Type* expected_type = fn->getFunctionType()->getParamType(i);
                    if (tmp->getType() != expected_type) {
                        if (!tmp->getType()->isPointerTy() && expected_type->isPointerTy()) {
                            // Non-pointer value (e.g. loaded struct) needs to become
                            // a pointer for the callee. Store to a temp alloca.
                            llvm::AllocaInst *target = get_call_arg_alloca(tmp->getType());
                            builder->CreateStore(tmp, target);
                            tmp = target;
                        } else {
                            tmp = builder->CreateBitCast(tmp, expected_type);
                        }
                    }
                }
            }

            args.push_back(tmp);
        }
        convert_call_args_depth--;
        return args;
    }

    llvm::Value *convert_class_to_type(ASR::expr_t *arg, ASR::expr_t* dest_arg, ASR::ttype_t *dest_type, llvm::Value *class_value) {
        // if the required argument is of struct type, we need to pass
        // in the struct pointer to it
        ASR::ttype_t *arg_type = ASRUtils::expr_type(arg);
            
        bool is_unlimited_polymorphic = ASRUtils::is_unlimited_polymorphic_type(arg);
        // Handle arrays
        if (ASRUtils::is_array(arg_type) &&
                ASRUtils::extract_physical_type(arg_type) == ASR::DescriptorArray) {
            // For descriptor arrays, we need to create a new array descriptor
            // with the correct element type and copy the data pointer from the polymorphic wrapper
            ASR::ttype_t *dest_arr_type = ASRUtils::type_get_past_allocatable_pointer(dest_type);
            ASR::ttype_t *src_arr_asr_type = ASRUtils::type_get_past_allocatable_pointer(arg_type);
            llvm::Type *dest_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                dest_arg, dest_arr_type, module.get());
            llvm::Type *src_arr_type = llvm_utils->get_type_from_ttype_t_util(
                arg, src_arr_asr_type, module.get());
            llvm::Type *src_elem_type = llvm_utils->get_type_from_ttype_t_util(
                arg, ASRUtils::extract_type(arg_type), module.get());

            if (LLVM::is_llvm_pointer(*arg_type)) {
                class_value = llvm_utils->CreateLoad2(src_arr_type->getPointerTo(), class_value);
            }

                // Create new array descriptor
                llvm::Value* new_descriptor = arr_descr->create_descriptor_alloca(
                    dest_llvm_type, "array_descriptor");
                
                // Get source array's data pointer (from polymorphic wrapper's data field)
                llvm::Value* src_data_ptr = arr_descr->get_pointer_to_data(src_arr_type, class_value);
                src_data_ptr = llvm_utils->CreateLoad2(src_elem_type->getPointerTo(), src_data_ptr);

                // Get offset from source
                llvm::Value* src_offset = arr_descr->get_offset(src_arr_type, class_value);
                                
                // Extract data pointer from the polymorphic wrapper (field at index 1)
                if (is_unlimited_polymorphic) {
                    src_data_ptr = llvm_utils->CreateLoad2(
                        llvm_utils->i8_ptr, llvm_utils->create_gep2(src_elem_type, src_data_ptr, 1));
                } else {
                    llvm::Type* src_elem_struct_type = llvm_utils->getStructType(
                        ASR::down_cast<ASR::Struct_t>(ASRUtils::symbol_get_past_external(
                        ASRUtils::get_struct_sym_from_struct_expr(arg))), module.get());
                    src_data_ptr = llvm_utils->CreateLoad2(
                        src_elem_struct_type->getPointerTo(),
                        llvm_utils->create_gep2(src_elem_type, src_data_ptr, 1));
                }

                // Cast to destination element *storage* pointer type and store in new descriptor.
                llvm::Type* dest_data_type = llvm_utils->get_el_type(
                    dest_arg, ASRUtils::extract_type(dest_type), module.get());
                ASR::ttype_t* dest_base_type = ASRUtils::extract_type(dest_type);
                llvm::Value* dest_data_ptr = src_data_ptr;

                if (ASR::is_a<ASR::String_t>(*dest_base_type)) {
                    // Character arrays: consolidate per-element string_descriptors
                    // into a single descriptor with a flat contiguous buffer.
                    llvm::Value* n_elems = arr_descr->get_array_size(
                        src_arr_type, class_value, nullptr, 4);
                    llvm::Value* n_elems_i64 = builder->CreateSExt(
                        n_elems, llvm::Type::getInt64Ty(context));
                    dest_data_ptr = llvm_utils->consolidate_char_descriptors(
                        src_data_ptr, n_elems_i64);
                    llvm::Value* consolidated_flat_buf = llvm_utils->CreateLoad2(
                        character_type,
                        llvm_utils->create_gep2(string_descriptor, dest_data_ptr, 0));
                    heap_fixed_size_arrays.push_back(al, consolidated_flat_buf);
                }

                llvm::Type* expected_data_ptr_type = dest_data_type->getPointerTo();
                if (dest_data_ptr->getType() != expected_data_ptr_type) {
                    dest_data_ptr = builder->CreateBitCast(dest_data_ptr, expected_data_ptr_type);
                }
                llvm::Value* new_data_ptr_ptr = arr_descr->get_pointer_to_data(dest_llvm_type, new_descriptor);
                builder->CreateStore(dest_data_ptr, new_data_ptr_ptr);

                // Copy offset
                builder->CreateStore(src_offset, arr_descr->get_offset(dest_llvm_type, new_descriptor, false));

                // Copy rank
                llvm::Value* src_rank = arr_descr->get_rank(src_arr_type, class_value);
                // Deep-copy dimension descriptors into destination inlined storage
                arr_descr->fill_dimension_descriptor(dest_llvm_type, new_descriptor, 0);
                arr_descr->set_rank(dest_llvm_type, new_descriptor, src_rank);
                llvm::Value* src_dim_ptr = arr_descr->get_pointer_to_dimension_descriptor_array(src_arr_type, class_value);
                llvm::Value* new_dim_ptr = arr_descr->get_pointer_to_dimension_descriptor_array(dest_llvm_type, new_descriptor);
                llvm::DataLayout data_layout(module->getDataLayout());
                int dim_desc_size = (int)data_layout.getTypeAllocSize(arr_descr->get_dimension_descriptor_type(false));
                llvm::Value* dim_copy_bytes = builder->CreateMul(
                    builder->CreateSExtOrTrunc(src_rank, llvm::Type::getInt64Ty(context)),
                    llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), dim_desc_size));
                builder->CreateMemCpy(new_dim_ptr, llvm::MaybeAlign(8), src_dim_ptr, llvm::MaybeAlign(8), dim_copy_bytes);
                
                // If dest_type is Pointer(Array(...)), we need an extra level of indirection
                // so that when the caller loads from the returned value, they get the descriptor pointer
                if (LLVM::is_llvm_pointer(*dest_type)) {
                    llvm::Value* desc_ptr_ptr = llvm_utils->CreateAlloca(*builder, dest_llvm_type->getPointerTo(), nullptr, "array_descriptor_ptr");
                    builder->CreateStore(new_descriptor, desc_ptr_ptr);
                    return desc_ptr_ptr;
            }
            
            return new_descriptor;
        }
        
        if (LLVM::is_llvm_pointer(*arg_type)) {
            check_and_allocate_scalar(arg, dest_arg, dest_type);
        }
        llvm::Type* arg_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                                    arg, ASRUtils::extract_type(arg_type), module.get());
        if (LLVM::is_llvm_pointer(*arg_type) &&
                (!LLVM::is_llvm_pointer(*ASRUtils::expr_type(dest_arg)) || ASRUtils::is_class_type(
                    ASRUtils::type_get_past_allocatable_pointer(arg_type)))) {
            class_value = llvm_utils->CreateLoad2(arg_llvm_type->getPointerTo(), class_value);
        }
        if (is_unlimited_polymorphic) {
            class_value = llvm_utils->create_gep2(arg_llvm_type, class_value, 1);
            if (!LLVM::is_llvm_pointer(*dest_type)) {
                class_value = llvm_utils->CreateLoad2(
                    llvm_utils->i8_ptr, class_value);
            }
        } else {
            ASR::ttype_t* struct_type = ASRUtils::make_StructType_t_util(
                al, arg_type->base.loc, ASRUtils::symbol_get_past_external(
                ASRUtils::get_struct_sym_from_struct_expr(arg)), true);
            llvm::Type* llvm_struct_type = llvm_utils->get_type_from_ttype_t_util(
                arg, struct_type, module.get());
            class_value = llvm_utils->create_gep2(arg_llvm_type, class_value, 1);
            if (!LLVM::is_llvm_pointer(*dest_type)) {
                class_value = llvm_utils->CreateLoad2(
                    llvm_struct_type->getPointerTo(), class_value);
            }         
        }
        llvm::Type* dest_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                dest_arg, dest_type, module.get());
        if (ASRUtils::is_array(dest_type)) {
            return builder->CreateBitCast(class_value, dest_llvm_type);
        }
        return builder->CreateBitCast(class_value, dest_llvm_type->getPointerTo());
    }

    // Convert from one polymorphic class type to another (class is() in select type)
    llvm::Value* convert_class_to_class(ASR::expr_t* arg_expr, llvm::Value* dt,
            ASR::ttype_t* dest_type, ASR::symbol_t* dest_sym) {
        ASR::ttype_t* arg_type = ASRUtils::expr_type(arg_expr);
        
        if (!ASRUtils::is_class_type(ASRUtils::extract_type(dest_type)) || 
            !ASRUtils::is_class_type(ASRUtils::extract_type(arg_type))) {
            return dt;
        }
        
        // TODO: Handle arrays properly here
        if (ASRUtils::is_array(arg_type) && 
            ASRUtils::extract_physical_type(arg_type) == ASR::array_physical_typeType::DescriptorArray) {
            // TODO: Handle this properly 
            llvm::Type* dest_array_type = llvm_utils->get_type_from_ttype_t_util(
                nullptr, dest_type, module.get());
            return builder->CreateBitCast(dt, dest_array_type);
        }
    
        llvm::Type* call_arg_struct_type = llvm_utils->get_type_from_ttype_t_util(
            arg_expr, ASRUtils::extract_type(arg_type), module.get());
        
        if (LLVM::is_llvm_pointer(*arg_type) &&
                !LLVM::is_llvm_pointer(*dest_type)) {
            dt = llvm_utils->CreateLoad2(call_arg_struct_type->getPointerTo(), dt);
        }
        llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(
            dest_type, dest_sym, module.get());
        if (!ASRUtils::is_array(dest_type)) {
            return builder->CreateBitCast(dt, type->getPointerTo());
        } else {
            return builder->CreateBitCast(dt, type);
        }
    }

    void generate_flip_sign(ASR::call_arg_t* m_args) {
        this->visit_expr_wrapper(m_args[0].m_value, true);
        llvm::Value* signal = tmp;
        LCOMPILERS_ASSERT(m_args[1].m_value->type == ASR::exprType::Var);
        ASR::Var_t* asr_var = ASR::down_cast<ASR::Var_t>(m_args[1].m_value);
        ASR::Variable_t* asr_variable = ASR::down_cast<ASR::Variable_t>(asr_var->m_v);
        uint32_t x_h = get_hash((ASR::asr_t*)asr_variable);
        llvm::Value* variable = llvm_symtab[x_h];
        // variable = xor(shiftl(int(Nd), 63), variable)
        ASR::ttype_t* signal_type = ASRUtils::expr_type(m_args[0].m_value);
        int signal_kind = ASRUtils::extract_kind_from_ttype_t(signal_type);
        llvm::Value* num_shifts = llvm::ConstantInt::get(context, llvm::APInt(32, signal_kind * 8 - 1));
        llvm::Value* shifted_signal = builder->CreateShl(signal, num_shifts);
        llvm::Type* variable_load_type = llvm_utils->get_type_from_ttype_t_util(&asr_var->base, asr_variable->m_type, module.get());
        llvm::Value* int_var = builder->CreateBitCast(llvm_utils->CreateLoad2(variable_load_type, variable), shifted_signal->getType());        tmp = builder->CreateXor(shifted_signal, int_var);
        llvm::Type* variable_type = llvm_utils->get_type_from_ttype_t_util(&asr_var->base, asr_variable->m_type, module.get());
        tmp = builder->CreateBitCast(tmp, variable_type);
    }

    void generate_fma(ASR::call_arg_t* m_args) {
        this->visit_expr_load_wrapper(m_args[0].m_value,
            LLVM::is_llvm_pointer(*expr_type(m_args[0].m_value)) ? 2 : 1,
            true);
        llvm::Value* a = tmp;
        this->visit_expr_load_wrapper(m_args[1].m_value,
            LLVM::is_llvm_pointer(*expr_type(m_args[1].m_value)) ? 2 : 1,
            true);
        llvm::Value* b = tmp;
        this->visit_expr_load_wrapper(m_args[2].m_value,
            LLVM::is_llvm_pointer(*expr_type(m_args[2].m_value)) ? 2 : 1,
            true);
        llvm::Value* c = tmp;
#if LLVM_VERSION_MAJOR >= 8
        tmp = builder->CreateIntrinsic(llvm::Intrinsic::fma,
                {a->getType()},
                {b, c, a});
#else
        // LLVM 7: Use getDeclaration instead
        llvm::Function *fn = llvm::Intrinsic::getDeclaration(module.get(), llvm::Intrinsic::fma, {a->getType()});
        tmp = builder->CreateCall(fn, {b, c, a});
#endif
    }

    void generate_sign_from_value(ASR::call_arg_t* m_args) {
        this->visit_expr_wrapper(m_args[0].m_value, true);
        llvm::Value* arg0 = tmp;
        this->visit_expr_wrapper(m_args[1].m_value, true);
        llvm::Value* arg1 = tmp;
        llvm::Type* common_llvm_type = arg0->getType();
        ASR::ttype_t *arg1_type = ASRUtils::expr_type(m_args[1].m_value);
        uint64_t kind = ASRUtils::extract_kind_from_ttype_t(arg1_type);
        llvm::Value* num_shifts = llvm::ConstantInt::get(context, llvm::APInt(kind * 8, kind * 8 - 1));
        llvm::Value* shifted_one = builder->CreateShl(llvm::ConstantInt::get(context, llvm::APInt(kind * 8, 1)), num_shifts);
        arg1 = builder->CreateBitCast(arg1, shifted_one->getType());
        arg0 = builder->CreateBitCast(arg0, shifted_one->getType());
        tmp = builder->CreateXor(arg0, builder->CreateAnd(shifted_one, arg1));
        tmp = builder->CreateBitCast(tmp, common_llvm_type);
    }

    template <typename T>
    bool generate_optimization_instructions(const T* routine, ASR::call_arg_t* m_args) {
        std::string routine_name = std::string(routine->m_name);
        if( routine_name.find("flipsign") != std::string::npos ) {
            generate_flip_sign(m_args);
            return true;
        } else if( routine_name.find("fma") != std::string::npos ) {
            generate_fma(m_args);
            return true;
        } else if( routine_name.find("signfromvalue") != std::string::npos ) {
            generate_sign_from_value(m_args);
            return true;
        }
        return false;
    }

    int get_class_hash(ASR::symbol_t* class_sym) {
        if( type2vtabid.find(class_sym) == type2vtabid.end() ) {
            type2vtabid[class_sym] = type2vtabid.size();
        }
        return type2vtabid[class_sym];
    }

    llvm::Value* convert_to_polymorphic_arg(ASR::expr_t* arg_expr, llvm::Value* dt, ASR::expr_t* s_m_args0,
        ASR::ttype_t* s_m_args0_type, ASR::ttype_t* arg_type) {
        if ( compiler_options.new_classes ) {
            if( !ASRUtils::is_class_type(ASRUtils::extract_type(
                    ASRUtils::expr_type(s_m_args0))) ) {
                return dt;
            }
            if (ASRUtils::is_unlimited_polymorphic_type(s_m_args0)) {
                if (ASRUtils::is_unlimited_polymorphic_type(arg_expr)) {
                    if (LLVM::is_llvm_pointer(*ASRUtils::expr_type(arg_expr)) &&
                            !LLVM::is_llvm_pointer(*ASRUtils::expr_type(s_m_args0))) {
                        llvm::Type* _type = llvm_utils->get_type_from_ttype_t_util(
                            arg_expr, ASRUtils::expr_type(arg_expr), module.get());
                        dt = llvm_utils->CreateLoad2(_type, dt);
                    }
                    return dt;
                }
                if (ASRUtils::is_array(arg_type)) {
                    llvm::Type* actual_array_type = llvm_utils->get_type_from_ttype_t_util(
                        arg_expr, arg_type, module.get());
                    // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                    llvm::Type* actual_array_data_type = llvm_utils->get_el_type(
                        arg_expr, ASRUtils::extract_type(arg_type), module.get());
                    llvm::Type* array_type = llvm_utils->get_type_from_ttype_t_util(
                        s_m_args0, s_m_args0_type, module.get());
                    llvm::Type* array_data_type = llvm_utils->get_el_type(
                        s_m_args0, ASRUtils::extract_type(s_m_args0_type), module.get());
                    llvm::Value* unlimited_polymorphic_type_array = arr_descr->create_descriptor_alloca(
                        array_type);
                    llvm::Value* array_data = llvm_utils->CreateAlloca(*builder, array_data_type);
                    builder->CreateStore(
                        array_data, arr_descr->get_pointer_to_data(array_type, unlimited_polymorphic_type_array));
                    arr_descr->fill_array_details(arg_expr,
                                                s_m_args0,
                                                dt,
                                                unlimited_polymorphic_type_array,
                                                arg_type,
                                                s_m_args0_type,
                                                module.get(),
                                                true);
                    // Set CFI metadata fields (elem_len, type, attribute)
                    // in the polymorphic descriptor. Compute elem_len
                    // and type from the concrete element type, because
                    // the source descriptor of a concrete type does not
                    // populate those fields.
                    {
                        llvm::DataLayout dl(module->getDataLayout());
                        ASR::ttype_t* elem_type = ASRUtils::extract_type(arg_type);
                        if (ASR::is_a<ASR::String_t>(*elem_type)) {
                            llvm::Value* src_base = llvm_utils->CreateLoad2(
                                actual_array_data_type->getPointerTo(),
                                llvm_utils->create_gep2(actual_array_type, dt, 0));
                            llvm::Value* char_len = llvm_utils->CreateLoad2(
                                llvm::Type::getInt64Ty(context),
                                llvm_utils->create_gep2(llvm_utils->string_descriptor, src_base, 1));
                            builder->CreateStore(char_len,
                                llvm_utils->create_gep2(array_type, unlimited_polymorphic_type_array, 1));
                        } else {
                            uint64_t elem_size = dl.getTypeAllocSize(actual_array_data_type);
                            builder->CreateStore(
                                llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), elem_size),
                                llvm_utils->create_gep2(array_type, unlimited_polymorphic_type_array, 1));
                        }
                        int8_t cfi_type_code = get_cfi_type_code(elem_type);
                        builder->CreateStore(
                            llvm::ConstantInt::get(llvm::Type::getInt8Ty(context), cfi_type_code),
                            llvm_utils->create_gep2(array_type, unlimited_polymorphic_type_array, 4));
                        llvm::Value* src_attr = llvm_utils->CreateLoad2(
                            llvm::Type::getInt8Ty(context),
                            llvm_utils->create_gep2(actual_array_type, dt, 5));
                        builder->CreateStore(src_attr,
                            llvm_utils->create_gep2(array_type, unlimited_polymorphic_type_array, 5));
                    }
                    llvm::Value* unlimited_polymorphic_struct = llvm_utils->CreateLoad2(
                        array_data_type->getPointerTo(),
                        arr_descr->get_pointer_to_data(array_type, unlimited_polymorphic_type_array));

                    llvm::Value* data_ptr = llvm_utils->create_gep2(array_data_type, unlimited_polymorphic_struct, 1);
                    llvm::Value* actual_data = llvm_utils->CreateLoad2(
                        actual_array_data_type->getPointerTo(), llvm_utils->create_gep2(actual_array_type, dt, 0));
                    arg_type = ASRUtils::extract_type(arg_type);

                    // Store vptr in wrapper field 0
                    if (ASR::is_a<ASR::StructType_t>(*arg_type)) {
                        if (ASRUtils::is_class_type(arg_type)) {
                            llvm::Value* v = builder->CreateBitCast(dt, llvm_utils->vptr_type->getPointerTo());
                            llvm::Value* loaded_vptr = llvm_utils->CreateLoad2(llvm_utils->vptr_type, v);
                            llvm::Value* w = builder->CreateBitCast(
                                unlimited_polymorphic_struct, llvm_utils->vptr_type->getPointerTo());
                            builder->CreateStore(loaded_vptr, w);
                        } else {
                            struct_api->store_class_vptr(ASRUtils::symbol_get_past_external(
                                ASRUtils::get_struct_sym_from_struct_expr(arg_expr)),
                                unlimited_polymorphic_struct, module.get());
                        }
                    } else {
                        struct_api->store_intrinsic_type_vptr(arg_type,
                            ASRUtils::extract_kind_from_ttype_t(arg_type), unlimited_polymorphic_struct, module.get());
                    }
                    // Store intrinsic type data ptr
                    if (ASR::is_a<ASR::String_t>(*arg_type)) {
                        llvm::Type* str_desc_ty = llvm_utils->string_descriptor;
                        llvm::Value* src_char_data = llvm_utils->CreateLoad2(
                            llvm_utils->i8_ptr,
                            llvm_utils->create_gep2(str_desc_ty, actual_data, 0));
                        llvm::Value* char_len = llvm_utils->CreateLoad2(
                            llvm::Type::getInt64Ty(context),
                            llvm_utils->create_gep2(str_desc_ty, actual_data, 1));
                        llvm::Value* n_elems = arr_descr->get_array_size(
                            actual_array_type, dt, nullptr, 4);
                        llvm::Value* n_elems_i64 = builder->CreateSExtOrTrunc(
                            n_elems, llvm::Type::getInt64Ty(context));
                        llvm::Value* descs_i8 = llvm_utils->expand_flat_to_char_descriptors(
                            src_char_data, char_len, n_elems_i64);
                        builder->CreateStore(descs_i8, data_ptr);
                    } else {
                        builder->CreateStore(builder->CreateBitCast(actual_data, llvm_utils->i8_ptr),
                                            data_ptr);
                    }
                    return unlimited_polymorphic_type_array;
                } else {
                    llvm::Type* unlimited_polymorphic_type = llvm_utils->get_type_from_ttype_t_util(
                            s_m_args0, s_m_args0_type, module.get());
                    llvm::Value* unlimited_polymorphic_struct = llvm_utils->CreateAlloca(unlimited_polymorphic_type);
                    llvm::Value* data_ptr = llvm_utils->create_gep2(unlimited_polymorphic_type, unlimited_polymorphic_struct, 1);

                    if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(arg_type))) {
                        // Store Vptr from original struct
                        llvm::Value* vptr;
                        if (ASRUtils::is_class_type(arg_type)) {
                            llvm::Type* wrapper_struct_type = llvm::StructType::get(
                                context, {llvm_utils->vptr_type, llvm_utils->i8_ptr});
                            ASR::ttype_t* arg_full_type = ASRUtils::expr_type(arg_expr);
                            llvm::Value* wrapper_ptr = dt;
                            if (LLVM::is_llvm_pointer(*arg_full_type)) {

                                wrapper_ptr = llvm_utils->CreateLoad2(
                                    wrapper_struct_type->getPointerTo(),
                                    builder->CreateBitCast(dt,
                                        wrapper_struct_type->getPointerTo()->getPointerTo()));
                            } else {
                                wrapper_ptr = builder->CreateBitCast(
                                    dt, wrapper_struct_type->getPointerTo());
                            }
                            llvm::Value* vptr_slot = llvm_utils->create_gep2(
                                wrapper_struct_type, wrapper_ptr, 0);
                            vptr = llvm_utils->CreateLoad2(llvm_utils->vptr_type, vptr_slot);
                            builder->CreateStore(vptr, llvm_utils->create_gep2(
                                unlimited_polymorphic_type, unlimited_polymorphic_struct, 0));

                            llvm::Value* wrapper_data_slot = llvm_utils->create_gep2(
                                wrapper_struct_type, wrapper_ptr, 1);
                            llvm::Value* wrapper_data = llvm_utils->CreateLoad2(
                                llvm_utils->i8_ptr, wrapper_data_slot);
                            builder->CreateStore(wrapper_data, data_ptr);
                            return unlimited_polymorphic_struct;
                        } else {
                            vptr = struct_api->get_pointer_to_method(ASRUtils::symbol_get_past_external(
                                ASRUtils::get_struct_sym_from_struct_expr(arg_expr)), module.get());
                        }
                        builder->CreateStore(vptr, llvm_utils->create_gep2(
                            unlimited_polymorphic_type, unlimited_polymorphic_struct, 0));
                    } else {
                        // An intrinsic type scalar is passes as argument to a `class(*)` function parameter
                        struct_api->store_intrinsic_type_vptr(arg_type,
                                                    ASRUtils::extract_kind_from_ttype_t(arg_type),
                                                    unlimited_polymorphic_struct, module.get());
                    }
                                               
                    builder->CreateStore(builder->CreateBitCast(dt, llvm_utils->i8_ptr), data_ptr);
                    return unlimited_polymorphic_struct;
                }
            } else if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(arg_type))) {
                if (ASRUtils::is_array(arg_type) && 
                    ASRUtils::extract_physical_type(arg_type) == ASR::array_physical_typeType::DescriptorArray) {
                    if (ASRUtils::is_class_type(ASRUtils::extract_type(arg_type))) {
                        return dt;
                    }
                    // For struct array arguments: wrap each struct* in a class wrapper (vptr + struct*)
                    // The class wrapper array has class_elem_type = {vptr, struct*}
                    llvm::Type* target_array_type = llvm_utils->get_type_from_ttype_t_util(
                        s_m_args0, s_m_args0_type, module.get());
                    llvm::Type* target_array_data_type = llvm_utils->get_el_type(
                        s_m_args0, ASRUtils::extract_type(s_m_args0_type), module.get());
                    llvm::Type* actual_array_type = llvm_utils->get_type_from_ttype_t_util(
                        arg_expr, arg_type, module.get());
                    llvm::Type* actual_array_data_type = llvm_utils->get_el_type(
                        arg_expr, ASRUtils::extract_type(arg_type), module.get());
                    ASR::symbol_t* target_struct_sym = ASRUtils::symbol_get_past_external(
                        ASRUtils::get_struct_sym_from_struct_expr(s_m_args0));
                    llvm::Type* target_struct_type = llvm_utils->getStructType(
                        ASR::down_cast<ASR::Struct_t>(target_struct_sym), module.get());

                    // Allocate the target class array descriptor
                    llvm::Value* class_array = arr_descr->create_descriptor_alloca(
                        target_array_type);
                    
                    // Allocate buffer for class wrapper array (vptr + struct_ptr per element)
                    llvm::Value* class_array_data = llvm_utils->CreateAlloca(
                        target_array_data_type);
                    builder->CreateStore(class_array_data,
                        arr_descr->get_pointer_to_data(target_array_type, class_array));

                    // Copy dimension descriptors from source to target (inline memcpy)
                    llvm::Value* actual_dim_des =
                        arr_descr->get_pointer_to_dimension_descriptor_array(actual_array_type, dt);
                    llvm::Value* target_dim_des_ptr = arr_descr->get_pointer_to_dimension_descriptor_array(target_array_type, class_array);
                    int n_dims_copy = ASRUtils::extract_n_dims_from_ttype(arg_type);
                    llvm::DataLayout data_layout(module->getDataLayout());
                    int dim_des_size = (int)data_layout.getTypeAllocSize(arr_descr->get_dimension_descriptor_type());
                    builder->CreateMemCpy(
                        builder->CreateBitCast(target_dim_des_ptr, llvm_utils->i8_ptr),
                        llvm::MaybeAlign(8),
                        builder->CreateBitCast(actual_dim_des, llvm_utils->i8_ptr),
                        llvm::MaybeAlign(8),
                        dim_des_size * n_dims_copy);
                    
                    // Copy rank and offset
                    llvm::Value* actual_offset = arr_descr->get_offset(actual_array_type, dt, true);
                    builder->CreateStore(actual_offset,
                        arr_descr->get_offset(target_array_type, class_array, false));
                    
                    llvm::Value* actual_rank = arr_descr->get_rank(actual_array_type, dt, false);
                    arr_descr->set_rank(target_array_type, class_array, actual_rank);

                    // Get source struct array data
                    llvm::Value* actual_array_data = llvm_utils->CreateLoad2(
                        actual_array_data_type->getPointerTo(),
                        arr_descr->get_pointer_to_data(actual_array_type, dt));
                    builder->CreateStore(builder->CreateBitCast(
                        actual_array_data, target_struct_type->getPointerTo()),
                        llvm_utils->create_gep2(target_array_data_type, class_array_data, 1));
                    
                    // Get vptr for the struct type
                    ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(
                        ASRUtils::get_struct_sym_from_struct_expr(arg_expr));
                    llvm::Value* vptr_val = struct_api->get_pointer_to_method(struct_sym, module.get());
                    builder->CreateStore(vptr_val,
                        llvm_utils->create_gep2(target_array_data_type, class_array_data, 0));

                    return class_array;
                }

                // Create Class wrapper
                llvm::Type* target_struct_type = llvm_utils->get_type_from_ttype_t_util(
                    s_m_args0, ASRUtils::extract_type(s_m_args0_type), module.get());
                llvm::Type* call_arg_struct_type = llvm_utils->get_type_from_ttype_t_util(
                    arg_expr, ASRUtils::extract_type(arg_type), module.get());

                if (ASRUtils::is_class_type(ASRUtils::extract_type(arg_type))) {
                    if (LLVM::is_llvm_pointer(*ASRUtils::expr_type(arg_expr)) &&
                            !LLVM::is_llvm_pointer(*ASRUtils::expr_type(s_m_args0))) {
                        dt = llvm_utils->CreateLoad2(call_arg_struct_type->getPointerTo(), dt);
                    }
                    llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(
                        s_m_args0, ASRUtils::expr_type(s_m_args0), module.get());
                    if (!ASRUtils::is_array(ASRUtils::expr_type(s_m_args0))) {
                        return builder->CreateBitCast(dt, type->getPointerTo());
                    } else {
                        return builder->CreateBitCast(dt, type);
                    }
                }

                if (LLVM::is_llvm_pointer(*ASRUtils::expr_type(arg_expr)) &&
                        !ASRUtils::is_array(ASRUtils::expr_type(s_m_args0))) {
                    dt = llvm_utils->CreateLoad2(call_arg_struct_type->getPointerTo(), dt);
                }

                llvm::Value* class_wrapper;
                llvm::Value* class_value;

                if (LLVM::is_llvm_pointer(*ASRUtils::expr_type(s_m_args0))) {
                    // For POINTER parameters, we need two levels:
                    // 1. Allocate the actual class structure
                    // 2. Allocate a pointer to it
                    class_value = llvm_utils->CreateAlloca(target_struct_type);
                    class_wrapper = llvm_utils->CreateAlloca(target_struct_type->getPointerTo());
                    builder->CreateStore(class_value, class_wrapper);
                } else {
                    // For non-POINTER parameters, allocate class structure directly
                    class_wrapper = llvm_utils->CreateAlloca(
                        llvm_utils->get_type_from_ttype_t_util(s_m_args0,
                        ASRUtils::type_get_past_array(ASRUtils::expr_type(s_m_args0)), module.get()));
                    class_value = class_wrapper;
                }

                // Store Vptr and its member from original struct
                if (ASRUtils::is_class_type(ASRUtils::extract_type(arg_type))) {
                    llvm::Value* vptr = builder->CreateBitCast(dt, llvm_utils->vptr_type->getPointerTo());
                    vptr = llvm_utils->CreateLoad2(llvm_utils->vptr_type, vptr);
                    builder->CreateStore(vptr, llvm_utils->create_gep2(
                        target_struct_type, class_value, 0));
                    llvm::Value* actual_struct = llvm_utils->create_gep2(call_arg_struct_type, dt, 1);

                    actual_struct = llvm_utils->CreateLoad2(llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(
                        ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(arg_expr))), module.get(), true), actual_struct);
                    builder->CreateStore(builder->CreateBitCast(actual_struct, llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(
                        ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(s_m_args0))), module.get(), true)),
                        llvm_utils->create_gep2(target_struct_type, class_value, 1));
                } else {
                    builder->CreateStore(struct_api->get_pointer_to_method(ASRUtils::symbol_get_past_external(
                        ASRUtils::get_struct_sym_from_struct_expr(arg_expr)), module.get()),
                        llvm_utils->create_gep2(target_struct_type, class_value, 0));
                    builder->CreateStore(builder->CreateBitCast(dt, llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(
                        ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(s_m_args0))), module.get(), true)),
                        llvm_utils->create_gep2(target_struct_type, class_value, 1));
                }
                return class_wrapper;
            } else {
                return dt;
            }
        }
        if( !ASRUtils::is_class_type(ASRUtils::type_get_past_array(s_m_args0_type)) ) {
            return dt;
        }
        if (ASRUtils::is_unlimited_polymorphic_type(s_m_args0)) {
            if (ASRUtils::is_unlimited_polymorphic_type(arg_expr)) {
                if (LLVM::is_llvm_pointer(*ASRUtils::expr_type(arg_expr)) &&
                        !LLVM::is_llvm_pointer(*ASRUtils::expr_type(s_m_args0))) {
                    llvm::Type* _type = llvm_utils->get_type_from_ttype_t_util(
                        arg_expr, ASRUtils::expr_type(arg_expr), module.get());
                    dt = llvm_utils->CreateLoad2(_type, dt);
                }
                return dt;
            }
            if (ASRUtils::is_array(arg_type)) {
                llvm::Type* actual_array_type = llvm_utils->get_type_from_ttype_t_util(
                    arg_expr, arg_type, module.get());
                // Use the array element *storage* type (e.g. logical arrays are i8-backed).
                llvm::Type* actual_array_data_type = llvm_utils->get_el_type(
                    arg_expr, ASRUtils::extract_type(arg_type), module.get());
                llvm::Type* array_type = llvm_utils->get_type_from_ttype_t_util(
                    s_m_args0, s_m_args0_type, module.get());
                llvm::Type* array_data_type = llvm_utils->get_el_type(
                    s_m_args0, ASRUtils::extract_type(s_m_args0_type), module.get());

                // ONE-wrapper layout: alloca single {vptr, i8*} wrapper on stack
                llvm::Value* array_data = llvm_utils->CreateAlloca(*builder, array_data_type);

                // Create polymorphic array descriptor
                llvm::Value* unlimited_polymorphic_type_array =
                    arr_descr->create_descriptor_alloca(
                        array_type);
                builder->CreateStore(
                    array_data, arr_descr->get_pointer_to_data(array_type, unlimited_polymorphic_type_array));
                arr_descr->fill_array_details(arg_expr,
                                            s_m_args0,
                                            dt,
                                            unlimited_polymorphic_type_array,
                                            arg_type,
                                            s_m_args0_type,
                                            module.get(),
                                            true);

                // Get the single wrapper and source data pointer
                llvm::Value* unlimited_polymorphic_struct = llvm_utils->CreateLoad2(
                    array_data_type->getPointerTo(),
                    arr_descr->get_pointer_to_data(array_type, unlimited_polymorphic_type_array));
                llvm::Value* data_ptr = llvm_utils->create_gep2(array_data_type, unlimited_polymorphic_struct, 1);
                llvm::Value* actual_data = llvm_utils->CreateLoad2(
                    actual_array_data_type->getPointerTo(), llvm_utils->create_gep2(actual_array_type, dt, 0));
                arg_type = ASRUtils::extract_type(arg_type);

                if (ASR::is_a<ASR::String_t>(*arg_type)) {
                    // Character arrays: source has ONE string_descriptor with flat
                    // char buffer, but class(*) copy function expects per-element
                    // string_descriptors. Create N string_descriptors from the flat buffer.
                    llvm::Type* str_desc_ty = llvm_utils->string_descriptor;
                    llvm::Value* src_char_data = llvm_utils->CreateLoad2(
                        llvm_utils->i8_ptr,
                        llvm_utils->create_gep2(str_desc_ty, actual_data, 0));
                    llvm::Value* char_len = llvm_utils->CreateLoad2(
                        llvm::Type::getInt64Ty(context),
                        llvm_utils->create_gep2(str_desc_ty, actual_data, 1));

                    llvm::Value* n_elems = arr_descr->get_array_size(
                        actual_array_type, dt, nullptr, 4);
                    llvm::Value* n_elems_i64 = builder->CreateSExt(
                        n_elems, llvm::Type::getInt64Ty(context));
                    llvm::Value* descs_i8 = llvm_utils->expand_flat_to_char_descriptors(
                        src_char_data, char_len, n_elems_i64);

                    builder->CreateStore(descs_i8, data_ptr);
                } else {
                    // Non-character types: source data is already contiguous
                    builder->CreateStore(
                        builder->CreateBitCast(actual_data, llvm_utils->i8_ptr), data_ptr);
                }

                // Store vptr in wrapper field 0
                if (ASR::is_a<ASR::StructType_t>(*arg_type)) {
                    if (ASRUtils::is_class_type(arg_type)) {
                        // For class types, load vptr from the source's class wrapper
                        llvm::Value* v = builder->CreateBitCast(dt, llvm_utils->vptr_type->getPointerTo());
                        llvm::Value* loaded_vptr = llvm_utils->CreateLoad2(llvm_utils->vptr_type, v);
                        llvm::Value* w = builder->CreateBitCast(
                            unlimited_polymorphic_struct, llvm_utils->vptr_type->getPointerTo());
                        builder->CreateStore(loaded_vptr, w);
                    } else {
                        struct_api->store_class_vptr(ASRUtils::symbol_get_past_external(
                            ASRUtils::get_struct_sym_from_struct_expr(arg_expr)),
                            unlimited_polymorphic_struct, module.get());
                    }
                } else {
                    struct_api->store_intrinsic_type_vptr(arg_type,
                        ASRUtils::extract_kind_from_ttype_t(arg_type),
                        unlimited_polymorphic_struct, module.get());
                }
                return unlimited_polymorphic_type_array;
            } else {
                llvm::Type* unlimited_polymorphic_type = llvm_utils->get_type_from_ttype_t_util(
                        s_m_args0, s_m_args0_type, module.get());
                llvm::Value* unlimited_polymorphic_struct = llvm_utils->CreateAlloca(unlimited_polymorphic_type);
                llvm::Value* data_ptr = llvm_utils->create_gep2(unlimited_polymorphic_type, unlimited_polymorphic_struct, 1);

                if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(arg_type))) {
                    // Store Vptr from original struct
                    llvm::Value* vptr;
                    if (ASRUtils::is_class_type(arg_type)) {
                        vptr = builder->CreateBitCast(dt, llvm_utils->vptr_type->getPointerTo());
                        vptr = llvm_utils->CreateLoad2(llvm_utils->vptr_type, vptr);
                    } else {
                        vptr = struct_api->get_pointer_to_method(ASRUtils::symbol_get_past_external(
                            ASRUtils::get_struct_sym_from_struct_expr(arg_expr)), module.get());
                    }
                    builder->CreateStore(vptr, llvm_utils->create_gep2(
                        unlimited_polymorphic_type, unlimited_polymorphic_struct, 0));
                } else {
                    // An intrinsic type scalar is passes as argument to a `class(*)` function parameter
                    struct_api->store_intrinsic_type_vptr(arg_type,
                                                ASRUtils::extract_kind_from_ttype_t(arg_type),
                                                unlimited_polymorphic_struct, module.get());
                }
                                            
                builder->CreateStore(builder->CreateBitCast(dt, llvm_utils->i8_ptr), data_ptr);
                return unlimited_polymorphic_struct;
            }
        } else if (ASR::is_a<ASR::StructType_t>(*ASRUtils::extract_type(arg_type))) {
            if (ASRUtils::is_array(arg_type) && 
                ASRUtils::extract_physical_type(arg_type) == ASR::array_physical_typeType::DescriptorArray) {
                if (ASRUtils::is_class_type(ASRUtils::extract_type(arg_type))) {
                    return dt;
                }
                // For struct array arguments: wrap each struct* in a class wrapper (vptr + struct*)
                // The class wrapper array has class_elem_type = {vptr, struct*}
                llvm::Type* target_array_type = llvm_utils->get_type_from_ttype_t_util(
                    s_m_args0, s_m_args0_type, module.get());
                llvm::Type* target_array_data_type = llvm_utils->get_el_type(
                    s_m_args0, ASRUtils::extract_type(s_m_args0_type), module.get());
                llvm::Type* actual_array_type = llvm_utils->get_type_from_ttype_t_util(
                    arg_expr, arg_type, module.get());
                llvm::Type* actual_array_data_type = llvm_utils->get_el_type(
                    arg_expr, ASRUtils::extract_type(arg_type), module.get());
                ASR::symbol_t* target_struct_sym = ASRUtils::symbol_get_past_external(
                    ASRUtils::get_struct_sym_from_struct_expr(s_m_args0));
                llvm::Type* target_struct_type = llvm_utils->getStructType(
                    ASR::down_cast<ASR::Struct_t>(target_struct_sym), module.get());

                // Allocate the target class array descriptor
                llvm::Value* class_array = arr_descr->create_descriptor_alloca(
                    target_array_type);
                
                // Allocate buffer for class wrapper array (vptr + struct_ptr per element)
                llvm::Value* class_array_data = llvm_utils->CreateAlloca(
                    target_array_data_type);
                builder->CreateStore(class_array_data,
                    arr_descr->get_pointer_to_data(target_array_type, class_array));

                // Copy dimension descriptors from source to target
                llvm::Value* actual_dim_des = 
                    arr_descr->get_pointer_to_dimension_descriptor_array(actual_array_type, dt);
                llvm::Value* target_dim_des_ptr = arr_descr->get_pointer_to_dimension_descriptor_array(target_array_type, class_array, false);
                builder->CreateStore(actual_dim_des, target_dim_des_ptr);
                
                // Copy rank and offset
                llvm::Value* actual_offset = arr_descr->get_offset(actual_array_type, dt, true);
                builder->CreateStore(actual_offset,
                    arr_descr->get_offset(target_array_type, class_array, false));
                
                llvm::Value* actual_rank = arr_descr->get_rank(actual_array_type, dt, false);
                arr_descr->set_rank(target_array_type, class_array, actual_rank);

                // Get source struct array data
                llvm::Value* actual_array_data = llvm_utils->CreateLoad2(
                    actual_array_data_type->getPointerTo(),
                    arr_descr->get_pointer_to_data(actual_array_type, dt));
                builder->CreateStore(builder->CreateBitCast(
                    actual_array_data, target_struct_type->getPointerTo()),
                    llvm_utils->create_gep2(target_array_data_type, class_array_data, 1));
                
                // Get vptr for the struct type
                ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(
                    ASRUtils::get_struct_sym_from_struct_expr(arg_expr));
                llvm::Value* vptr_val = struct_api->get_pointer_to_method(struct_sym, module.get());
                builder->CreateStore(vptr_val,
                    llvm_utils->create_gep2(target_array_data_type, class_array_data, 0));

                return class_array;
            }

            // Create Class wrapper
            llvm::Type* target_struct_type = llvm_utils->get_type_from_ttype_t_util(
                s_m_args0, ASRUtils::extract_type(s_m_args0_type), module.get());
            llvm::Type* call_arg_struct_type = llvm_utils->get_type_from_ttype_t_util(
                arg_expr, ASRUtils::extract_type(arg_type), module.get());

            if (ASRUtils::is_class_type(ASRUtils::extract_type(arg_type))) {
                if (LLVM::is_llvm_pointer(*ASRUtils::expr_type(arg_expr)) &&
                        !LLVM::is_llvm_pointer(*ASRUtils::expr_type(s_m_args0))) {
                    dt = llvm_utils->CreateLoad2(call_arg_struct_type->getPointerTo(), dt);
                }
                llvm::Type* type = llvm_utils->get_type_from_ttype_t_util(
                    s_m_args0, ASRUtils::expr_type(s_m_args0), module.get());
                if (!ASRUtils::is_array(ASRUtils::expr_type(s_m_args0))) {
                    return builder->CreateBitCast(dt, type->getPointerTo());
                } else {
                    return builder->CreateBitCast(dt, type);
                }
            }

            if (LLVM::is_llvm_pointer(*ASRUtils::expr_type(arg_expr)) &&
                    !ASRUtils::is_array(ASRUtils::expr_type(s_m_args0))) {
                dt = llvm_utils->CreateLoad2(call_arg_struct_type->getPointerTo(), dt);
            }

            llvm::Value* class_wrapper;
            llvm::Value* class_value;

            if (LLVM::is_llvm_pointer(*ASRUtils::expr_type(s_m_args0))) {
                // For POINTER parameters, we need two levels:
                // 1. Allocate the actual class structure
                // 2. Allocate a pointer to it
                class_value = llvm_utils->CreateAlloca(target_struct_type);
                class_wrapper = llvm_utils->CreateAlloca(target_struct_type->getPointerTo());
                builder->CreateStore(class_value, class_wrapper);
            } else {
                // For non-POINTER parameters, allocate class structure directly
                class_wrapper = llvm_utils->CreateAlloca(
                    llvm_utils->get_type_from_ttype_t_util(s_m_args0,
                    ASRUtils::type_get_past_array(ASRUtils::expr_type(s_m_args0)), module.get()));
                class_value = class_wrapper;
            }

            // Store Vptr and its member from original struct
            if (ASRUtils::is_class_type(ASRUtils::extract_type(arg_type))) {
                llvm::Value* vptr = builder->CreateBitCast(dt, llvm_utils->vptr_type->getPointerTo());
                vptr = llvm_utils->CreateLoad2(llvm_utils->vptr_type, vptr);
                builder->CreateStore(vptr, llvm_utils->create_gep2(
                    target_struct_type, class_value, 0));
                llvm::Value* actual_struct = llvm_utils->create_gep2(call_arg_struct_type, dt, 1);

                actual_struct = llvm_utils->CreateLoad2(llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(arg_expr))), module.get(), true), actual_struct);
                builder->CreateStore(builder->CreateBitCast(actual_struct, llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(s_m_args0))), module.get(), true)),
                    llvm_utils->create_gep2(target_struct_type, class_value, 1));
            } else {
                builder->CreateStore(struct_api->get_pointer_to_method(ASRUtils::symbol_get_past_external(
                    ASRUtils::get_struct_sym_from_struct_expr(arg_expr)), module.get()),
                    llvm_utils->create_gep2(target_struct_type, class_value, 0));
                builder->CreateStore(builder->CreateBitCast(dt, llvm_utils->getStructType(ASR::down_cast<ASR::Struct_t>(
                    ASRUtils::symbol_get_past_external(ASRUtils::get_struct_sym_from_struct_expr(s_m_args0))), module.get(), true)),
                    llvm_utils->create_gep2(target_struct_type, class_value, 1));
            }
            return class_wrapper;
        } else {
            return dt;
        }
    }

    llvm::Value* get_array_size_from_asr_type(ASR::ttype_t* type) {
        ASR::dimension_t* m_dims = nullptr;
        int n_dims = ASRUtils::extract_dimensions_from_ttype(type, m_dims);
        if( ASRUtils::extract_physical_type(type) == ASR::array_physical_typeType::FixedSizeArray ) {
            int64_t size = ASRUtils::get_fixed_size_of_array(m_dims, n_dims);
            return llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, size));
        } else {
            llvm::Value* llvm_size = llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, 1));
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 2;
            for( int i = 0; i < n_dims; i++ ) {
                if (m_dims[i].m_length) {
                    load_array_size_deep_copy(m_dims[i].m_length);
                    tmp = builder->CreateSExtOrTrunc(tmp, llvm_utils->getIntType(4));
                    llvm_size = builder->CreateMul(tmp, llvm_size);
                }
            }
            ptr_loads = ptr_loads_copy;
            return llvm_size;
        }
    }

    llvm::Value* expr_is_unallocated(ASR::expr_t* arg_expr) {
        ASR::ttype_t* arg_expr_type = ASRUtils::expr_type(arg_expr);
        LCOMPILERS_ASSERT(ASRUtils::is_allocatable(arg_expr_type))
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 1 - !LLVM::is_llvm_pointer(*arg_expr_type);
        this->visit_expr_wrapper(arg_expr, true);
        ptr_loads = ptr_loads_copy;

        llvm::Value* cond = nullptr;
        llvm::Type* arg_expr_llvm_type = llvm_utils->get_type_from_ttype_t_util(arg_expr, arg_expr_type, module.get());
        const bool is_descriptor_array = ASR::is_a<ASR::Array_t>(*ASRUtils::type_get_past_allocatable_pointer(arg_expr_type))
                                                && ASRUtils::extract_physical_type(arg_expr_type) == ASR::DescriptorArray;
        if (is_descriptor_array) {
            llvm::Value* is_allocated = arr_descr->get_is_allocated_flag(tmp, arg_expr);
            cond = builder->CreateNot(is_allocated);
        } else if (ASRUtils::is_string_only(arg_expr_type)) {
            tmp = llvm_utils->get_string_data(ASR::down_cast<ASR::String_t>(ASRUtils::type_get_past_allocatable_pointer(arg_expr_type)), tmp);
            cond = builder->CreateICmpEQ(
                builder->CreatePtrToInt(tmp,
                    llvm::Type::getInt64Ty(context)),
                builder->CreatePtrToInt(llvm::ConstantPointerNull::get(arg_expr_llvm_type->getPointerTo()),
                    llvm::Type::getInt64Ty(context)));
        } else {
            cond = builder->CreateICmpEQ(
                builder->CreatePtrToInt(tmp,
                    llvm::Type::getInt64Ty(context)),
                builder->CreatePtrToInt(llvm::ConstantPointerNull::get(arg_expr_llvm_type->getPointerTo()),
                    llvm::Type::getInt64Ty(context)));
        }

        return cond;
    }

    template<typename T>
    void bounds_check_call(T& x, bool subroutinecall_was_functioncall = false) {
        ASR::Function_t* function = nullptr;
        ASR::symbol_t* m_name = ASRUtils::symbol_get_past_external(x.m_name);
        if (ASR::is_a<ASR::Function_t>(*m_name)) {
            function = ASR::down_cast<ASR::Function_t>(m_name);
        } else if (ASR::is_a<ASR::StructMethodDeclaration_t>(*m_name)) {
            ASR::StructMethodDeclaration_t *clss_proc = ASR::down_cast<
                ASR::StructMethodDeclaration_t>(m_name);
            function = ASR::down_cast<ASR::Function_t>(clss_proc->m_proc);
        } else if (ASR::is_a<ASR::Variable_t>(*m_name)) {
            // Ignore functions passed in as arguments
            return;
        } else {
            throw CodeGenError("bounds_check_call: Symbol type not supported");
        }
        for (size_t i = 0; i < x.n_args; i++) {
            ASR::expr_t* arg_expr = x.m_args[i].m_value;
            if (arg_expr == nullptr) continue;
            ASR::ttype_t* arg_expr_type = ASRUtils::expr_type(x.m_args[i].m_value);
            if (ASR::is_a<ASR::ArrayPhysicalCast_t>(*arg_expr)) {
                ASR::ArrayPhysicalCast_t* arr_cast = ASR::down_cast<ASR::ArrayPhysicalCast_t>(arg_expr);
                // Use strict bounds checking if SubroutineCall was a FunctionCall before getting converted by subroutine_from_function
                // Last argument of converted subroutine is the return value of the FunctionCall
                // This argument should be checked strictly. It's size must be exactly equal to the expected size, it cannot be larger
                bool is_return_value = subroutinecall_was_functioncall && i == (x.n_args - 1);

                llvm::Value* is_present_flag = nullptr;
                llvm::BasicBlock *optional_check_mergeBB = nullptr;
                if (i + 1 < x.n_args && x.m_args[i + 1].m_value != nullptr) {
                    ASR::expr_t* next_arg = x.m_args[i + 1].m_value;
                    if (ASR::is_a<ASR::Var_t>(*next_arg) &&
                        ASR::is_a<ASR::Variable_t>(*ASRUtils::symbol_get_past_external(
                            ASR::down_cast<ASR::Var_t>(next_arg)->m_v))) {
                        ASR::Variable_t* next_var = ASRUtils::EXPR2VAR(next_arg);
                        std::string var_name = std::string(next_var->m_name);
                        ASR::ttype_t* var_type = ASRUtils::type_get_past_pointer(next_var->m_type);
                        std::string prefix = "__libasr_is_present_";
                        if (var_name.size() > prefix.size() && 
                            var_name.substr(0, prefix.size()) == prefix &&
                            ASR::is_a<ASR::Logical_t>(*var_type)) {
                          
                            this->visit_expr_wrapper(next_arg, true);
                            is_present_flag = tmp; 
                            
                            llvm::Function *fn = builder->GetInsertBlock()->getParent();
                            llvm::BasicBlock *present_checkBB = llvm::BasicBlock::Create(context, "optional_present", fn);
                            optional_check_mergeBB = llvm::BasicBlock::Create(context, "optional_merge");
                            builder->CreateCondBr(llvm_utils->to_i1(is_present_flag), present_checkBB, optional_check_mergeBB);
                            builder->SetInsertPoint(present_checkBB);
                        }
                    }
                }

                if (arr_cast->m_old == ASR::DescriptorArray && (arr_cast->m_new == ASR::PointerArray || arr_cast->m_new == ASR::FixedSizeArray)) {
                    int64_t ptr_loads_copy = ptr_loads;
                    ptr_loads = 1 - !LLVM::is_llvm_pointer(*ASRUtils::expr_type(arr_cast->m_arg));
                    this->visit_expr_wrapper(arr_cast->m_arg, true);
                    ptr_loads = ptr_loads_copy;

                    llvm::Type* arr_type = llvm_utils->get_type_from_ttype_t_util(arr_cast->m_arg,
                        ASRUtils::type_get_past_allocatable_pointer(ASRUtils::expr_type(arr_cast->m_arg)),
                        module.get());
                    llvm::Value* arg = tmp;

                    // Throw error if descriptor array is not allocated
                    llvm::Value* is_allocated = arr_descr->get_is_allocated_flag(arg, arr_cast->m_arg);
                    llvm_utils->generate_runtime_error(builder->CreateNot(is_allocated),
                            "Argument %d of subroutine %s is unallocated.",
                            {LLVMUtils::RuntimeLabel("This is unallocated", {arg_expr->base.loc}, {})},
                            infile,
                            location_manager,
                            llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, i + 1)),
                            LCompilers::create_global_string_ptr(context, *module, *builder, ASRUtils::symbol_name(x.m_name)));

                    // Throw error if shapes don't match
                    ASR::dimension_t* m_dims = nullptr;
                    int n_dims = ASRUtils::extract_dimensions_from_ttype(arr_cast->m_type, m_dims);
                    llvm::Value* desc_rank = arr_descr->get_rank(arr_type, arg);
                    llvm::Value* pointer_rank = llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, n_dims));
                    llvm::Function *fn = builder->GetInsertBlock()->getParent();

                    llvm::BasicBlock *thenBB = llvm::BasicBlock::Create(context, "then", fn);
                    llvm::BasicBlock *elseBB = llvm::BasicBlock::Create(context, "else");
                    llvm::BasicBlock *mergeBB = llvm::BasicBlock::Create(context, "ifcont");

                    builder->CreateCondBr(builder->CreateICmpEQ(desc_rank, pointer_rank), thenBB, elseBB);
                    builder->SetInsertPoint(thenBB);
                    for (int j = 0; j < n_dims; j++) {
                        if (m_dims[j].m_length) {
                            llvm::Value* dim = llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, j + 1));
                            llvm::Value* descriptor_length = arr_descr->get_array_size(arr_type, arg, dim, 4);
                            load_array_size_deep_copy(m_dims[j].m_length);
                            // Convert user dimension expression to i32 to match descriptor format
                            llvm::Value* pointer_length = builder->CreateSExtOrTrunc(
                                tmp, llvm::Type::getInt32Ty(context));
                            // Fortran standard: negative extent means zero-size array
                            llvm::Value* zero_i32 = llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), 0);
                            pointer_length = builder->CreateSelect(
                                builder->CreateICmpSLT(pointer_length, zero_i32), zero_i32, pointer_length);
                            llvm::Value* cond = nullptr;
                            if (compiler_options.po.strict_bounds_checking || is_return_value) {
                                cond = builder->CreateICmpNE(descriptor_length, pointer_length);
                            } else {
                                cond = builder->CreateICmpSLT(descriptor_length, pointer_length);
                            }
                            llvm_utils->generate_runtime_error(cond,
                                    "Array shape mismatch in subroutine '%s'. Tried to match size %d of dimension %d of argument number %d, but expected size is %d",
                                    {LLVMUtils::RuntimeLabel("", {arg_expr->base.loc}),
                                        function->m_start_name ? LLVMUtils::RuntimeLabel("Here", {*function->m_start_name}, {}, false): LLVMUtils::RuntimeLabel("", {}),
                                        LLVMUtils::RuntimeLabel("Parameter is size %d, argument is size %d", {m_dims[j].loc}, {pointer_length, descriptor_length}, false)},
                                    infile,
                                    location_manager,
                                    LCompilers::create_global_string_ptr(context, *module, *builder, ASRUtils::symbol_name(x.m_name)),
                                    descriptor_length,
                                    dim,
                                    llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, i + 1)),
                                    pointer_length);
                        }
                    }
                    builder->CreateBr(mergeBB);
                    start_new_block(elseBB);
                    llvm::Value* desc_size = arr_descr->get_array_size(arr_type, arg, nullptr, 4);
                    // Convert user array size to i32 to match descriptor format
                    llvm::Value* pointer_size = builder->CreateSExtOrTrunc(
                        get_array_size_from_asr_type(arr_cast->m_type),
                        llvm::Type::getInt32Ty(context));
                    llvm::Value* cond = nullptr;
                    if (compiler_options.po.strict_bounds_checking || is_return_value) {
                        cond = builder->CreateICmpNE(desc_size, pointer_size);
                    } else {
                        cond = builder->CreateICmpSLT(desc_size, pointer_size);
                    }
                    llvm_utils->generate_runtime_error(cond,
                            "Array size mismatch in subroutine '%s'. Tried to match size %d of argument number %d, but expected size is %d",
                            {LLVMUtils::RuntimeLabel("", {arg_expr->base.loc})},
                            infile,
                            location_manager,
                            LCompilers::create_global_string_ptr(context, *module, *builder, ASRUtils::symbol_name(x.m_name)),
                            desc_size,
                            llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, i + 1)),
                            pointer_size);
                    builder->CreateBr(mergeBB);
                    start_new_block(mergeBB);
                } else if (arr_cast->m_old == ASR::FixedSizeArray && arr_cast->m_new == ASR::PointerArray) {
                    ASR::ttype_t* fixed_size_type = ASRUtils::expr_type(arr_cast->m_arg);
                    ASR::dimension_t* m_dims_fixed = nullptr;
                    int n_dims_fixed = ASRUtils::extract_dimensions_from_ttype(fixed_size_type, m_dims_fixed);

                    ASR::dimension_t* m_dims_pointer = nullptr;
                    int n_dims_pointer = ASRUtils::extract_dimensions_from_ttype(arr_cast->m_type, m_dims_pointer);

                    if (n_dims_fixed == n_dims_pointer) {
                        for (int j = 0; j < n_dims_fixed; j++) {
                            if (m_dims_pointer[j].m_length) {
                                llvm::Value* dim = llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, j + 1));
                                llvm::Value* fixed_length = llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, ASRUtils::extract_dim_value_int(m_dims_fixed[j].m_length)));
                                load_array_size_deep_copy(m_dims_pointer[j].m_length);
                                // Convert user dimension expression to i32 to match fixed-size format
                                llvm::Value* pointer_length = builder->CreateSExtOrTrunc(
                                    tmp, llvm::Type::getInt32Ty(context));
                                llvm::Value* cond = nullptr;
                                if (compiler_options.po.strict_bounds_checking || is_return_value) {
                                    cond = builder->CreateICmpNE(fixed_length, pointer_length);
                                } else {
                                    cond = builder->CreateICmpSLT(fixed_length, pointer_length);
                                }
                                llvm_utils->generate_runtime_error(cond,
                                        "Runtime error: Array shape mismatch in subroutine '%s'\n\n"
                                        "Tried to match size %d of dimension %d of argument number %d, but expected size is %d\n",
                                        {LLVMUtils::RuntimeLabel("", {arg_expr->base.loc})},
                                        infile,
                                        location_manager,
                                        LCompilers::create_global_string_ptr(context, *module, *builder, ASRUtils::symbol_name(x.m_name)),
                                        fixed_length,
                                        dim,
                                        llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, i + 1)),
                                        pointer_length);
                            }
                        }
                    } else {
                        llvm::Value* fixed_size = llvm::ConstantInt::get(llvm_utils->getIntType(4), ASRUtils::get_fixed_size_of_array(fixed_size_type));
                        // Convert user array size to i32 to match fixed-size format
                        llvm::Value* pointer_size = builder->CreateSExtOrTrunc(
                            get_array_size_from_asr_type(arr_cast->m_type),
                            llvm::Type::getInt32Ty(context));
                        llvm::Value* cond = nullptr;
                        if (compiler_options.po.strict_bounds_checking || is_return_value) {
                            cond = builder->CreateICmpNE(fixed_size, pointer_size);
                        } else {
                            cond = builder->CreateICmpSLT(fixed_size, pointer_size);
                        }
                        llvm_utils->generate_runtime_error(cond,
                                "Runtime error: Array size mismatch in subroutine '%s'\n\n"
                                "Tried to match size %d of argument number %d, but expected size is %d\n",
                                {LLVMUtils::RuntimeLabel("", {arg_expr->base.loc})},
                                infile,
                                location_manager,
                                LCompilers::create_global_string_ptr(context, *module, *builder, ASRUtils::symbol_name(x.m_name)),
                                fixed_size,
                                llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, i + 1)),
                                pointer_size);
                    }
                }

                if (optional_check_mergeBB != nullptr) {
                    builder->CreateBr(optional_check_mergeBB);
                    start_new_block(optional_check_mergeBB);
                }
            } else {
                ASR::expr_t* alloc_check_expr = arg_expr;
                ASR::ttype_t* alloc_check_type = arg_expr_type;
                if (ASR::is_a<ASR::StringSection_t>(*arg_expr)) {
                    ASR::StringSection_t* ss = ASR::down_cast<ASR::StringSection_t>(arg_expr);
                    ASR::ttype_t* string_arg_type = ASRUtils::expr_type(ss->m_arg);
                    if (ASRUtils::is_allocatable(string_arg_type)) {
                        alloc_check_expr = ss->m_arg;
                        alloc_check_type = string_arg_type;
                    }
                }

                if (ASRUtils::is_allocatable(alloc_check_type)) {
                    ASR::FunctionType_t *ft = ASRUtils::get_FunctionType(function);
                    ASR::Variable_t *func_arg_variable = ASRUtils::expr_to_variable_or_null(function->m_args[i]);
                    LCOMPILERS_ASSERT(func_arg_variable != nullptr);
                    if (!ASRUtils::is_allocatable(ft->m_arg_types[i]) &&
                        ASRUtils::symbol_intent((ASR::symbol_t *)func_arg_variable) != ASRUtils::intent_out) {
                        llvm::Value* is_unallocated = expr_is_unallocated(alloc_check_expr);
                        llvm::Value* arg_number = llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, i + 1));
                        llvm::Value* subroutine_name = LCompilers::create_global_string_ptr(
                            context, *module, *builder, ASRUtils::symbol_name(x.m_name));
                        llvm_utils->generate_runtime_error(is_unallocated,
                                "Argument %d of subroutine %s is unallocated.",
                                {LLVMUtils::RuntimeLabel("This is unallocated", {arg_expr->base.loc})},
                                infile,
                                // arg_expr->base.loc,
                                location_manager,
                                arg_number,
                                subroutine_name);
                    }
                }
            }
        }
    }

    // When a procedure pointer is accessed through a polymorphic allocatable
    // member (class(T), allocatable), visit_expr on the StructInstanceMember
    // leaves tmp pointing at the class descriptor pointer field.  This helper
    // navigates: descriptor → data pointer → procedure pointer member.
    void navigate_class_descriptor_to_proc_ptr(ASR::expr_t* dt,
                                               ASR::symbol_t* proc_sym) {
        ASR::ttype_t* dt_type = ASRUtils::expr_type(dt);
        ASR::ttype_t* dt_type_inner = ASRUtils::type_get_past_allocatable(dt_type);
        if (ASRUtils::is_allocatable(dt_type) &&
            ASR::is_a<ASR::StructType_t>(*dt_type_inner) &&
            ASRUtils::is_class_type(dt_type_inner)) {
            llvm::Type* class_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                dt, dt_type_inner, module.get());
            tmp = llvm_utils->CreateLoad2(class_llvm_type->getPointerTo(), tmp);
            tmp = llvm_utils->create_gep2(class_llvm_type, tmp, 1);
            ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(
                ASRUtils::get_struct_sym_from_struct_expr(dt));
            std::string type_key = get_type_key(struct_sym);
            llvm::Type* data_type = name2dertype[type_key];
            tmp = llvm_utils->CreateLoad2(data_type->getPointerTo(), tmp);
            ASR::Variable_t* proc_var = ASR::down_cast<ASR::Variable_t>(
                ASRUtils::symbol_get_past_external(proc_sym));
            std::string member_name = std::string(proc_var->m_name);
            int member_idx = name2memidx[type_key][member_name];
            tmp = llvm_utils->create_gep2(data_type, tmp, member_idx);
        }
    }

    void visit_SubroutineCall(const ASR::SubroutineCall_t &x) {
        DeallocateStringsScope _scope(this);
        if (compiler_options.emit_debug_info) debug_emit_loc(x);
        if( ASRUtils::is_intrinsic_optimization(x.m_name) ) {
            ASR::Function_t* routine = ASR::down_cast<ASR::Function_t>(
                        ASRUtils::symbol_get_past_external(x.m_name));
            if( generate_optimization_instructions(routine, x.m_args) ) {
                return ;
            }
        }

        // Generate runtime error if array arguments' shape doesn't match
        if (compiler_options.po.bounds_checking) {
            bounds_check_call(x, x.m_strict_bounds_checking);
        }

        std::vector<llvm::Value*> args;
        if( x.m_dt && ASR::is_a<ASR::StructInstanceMember_t>(*x.m_dt) &&
            ASR::is_a<ASR::Variable_t>(*ASRUtils::symbol_get_past_external(x.m_name)) &&
            ASR::is_a<ASR::FunctionType_t>(*ASRUtils::type_get_past_pointer(ASRUtils::symbol_type(x.m_name))) ) {
            ASR::Function_t* func = nullptr;
            if (ASR::is_a<ASR::Variable_t>(*ASRUtils::symbol_get_past_external(x.m_name))) {
                ASR::Variable_t* var = ASR::down_cast<ASR::Variable_t>(
                    ASRUtils::symbol_get_past_external(x.m_name));
                func = ASR::down_cast<ASR::Function_t>(
                    ASRUtils::symbol_get_past_external(var->m_type_declaration));
            } else if (ASR::is_a<ASR::Function_t>(*ASRUtils::symbol_get_past_external(x.m_name))) {
                func = ASR::down_cast<ASR::Function_t>(
                    ASRUtils::symbol_get_past_external(x.m_name));
            }

            uint64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 1;
            this->visit_expr(*x.m_dt);
            ptr_loads = ptr_loads_copy;
            navigate_class_descriptor_to_proc_ptr(x.m_dt, x.m_name);

            llvm::FunctionType* fntype = llvm_utils->get_function_type(*func, module.get());
            llvm::Type* func_ptr_type = fntype->getPointerTo();
            llvm::Value* callee = llvm_utils->CreateLoad2(func_ptr_type, tmp);

            // Self argument has been inserted into args by the semantic
            // layer (with a value distinct from m_dt — see
            // make_SubroutineCall_t_util).  Let convert_call_args handle
            // all args including self with proper class wrapping.
            args = convert_call_args(x, false);

            // For implicit-interface procedure pointers (procedure() with
            // no declared parameters), the declared function type may have
            // fewer parameters than the actual call arguments.  Reconstruct
            // the correct LLVM function type from the actual argument types.
            if (fntype->getNumParams() != args.size()
                && compiler_options.implicit_interface) {
                std::vector<llvm::Type*> arg_types;
                for (auto* arg_val : args) {
                    arg_types.push_back(arg_val->getType());
                }
                fntype = llvm::FunctionType::get(
                    fntype->getReturnType(), arg_types, false);
                callee = builder->CreateBitCast(callee, fntype->getPointerTo());
            }
            tmp = builder->CreateCall(fntype, callee, args);
            return ;
        }

        const ASR::symbol_t *proc_sym = symbol_get_past_external(x.m_name);
        std::string proc_sym_name = "";
        bool is_runtime_polymorphism = false;
        bool is_nopass = false;
        if( ASR::is_a<ASR::StructMethodDeclaration_t>(*proc_sym) ) {
            ASR::StructMethodDeclaration_t* class_proc =
                ASR::down_cast<ASR::StructMethodDeclaration_t>(proc_sym);
            is_runtime_polymorphism = ASRUtils::is_class_type(ASRUtils::extract_type(ASRUtils::expr_type(x.m_dt)));
            proc_sym_name = class_proc->m_name;
            is_nopass = class_proc->m_is_nopass;
        }
        ASR::Function_t *s;
        if (ASR::is_a<ASR::Function_t>(*proc_sym)) {
            s = ASR::down_cast<ASR::Function_t>(proc_sym);
        } else if (ASR::is_a<ASR::StructMethodDeclaration_t>(*proc_sym)) {
            ASR::StructMethodDeclaration_t *clss_proc = ASR::down_cast<
                ASR::StructMethodDeclaration_t>(proc_sym);
            s = ASR::down_cast<ASR::Function_t>(clss_proc->m_proc);
            proc_sym = clss_proc->m_proc;
        } else if (ASR::is_a<ASR::Variable_t>(*proc_sym)) {
            ASR::symbol_t *type_decl = ASR::down_cast<ASR::Variable_t>(proc_sym)->m_type_declaration;
            LCOMPILERS_ASSERT(type_decl && ASR::is_a<ASR::Function_t>(*ASRUtils::symbol_get_past_external(type_decl)));
            s = ASR::down_cast<ASR::Function_t>(ASRUtils::symbol_get_past_external(type_decl));
            is_nopass = true;  // procedure pointer variables are always nopass
        } else {
            throw CodeGenError("SubroutineCall: Symbol type not supported");
        }
        if( s == nullptr ) {
            s = ASR::down_cast<ASR::Function_t>(symbol_get_past_external(x.m_name));
        }
        bool is_method = false;
        if (x.m_dt && (!is_nopass)) {
            is_method = true;
        }
        if (is_runtime_polymorphism) {
            visit_RuntimePolymorphicSubroutineCall(x, proc_sym_name);
            return;
        }
        std::string sub_name = s->m_name;
        uint32_t h;
        ASR::FunctionType_t* s_func_type = ASR::down_cast<ASR::FunctionType_t>(s->m_function_signature);
        if (s_func_type->m_abi == ASR::abiType::LFortranModule) {
            throw CodeGenError("Subroutine LCompilers interfaces not implemented yet");
        } else if (s_func_type->m_abi == ASR::abiType::ExternalUndefined) {
            h = get_hash((ASR::asr_t*)proc_sym);
        } else if (s_func_type->m_abi == ASR::abiType::Source) {
            h = get_hash((ASR::asr_t*)proc_sym);
        } else if (s_func_type->m_abi == ASR::abiType::BindC) {
            h = get_hash((ASR::asr_t*)proc_sym);
        } else if (s_func_type->m_abi == ASR::abiType::Intrinsic) {
            if (sub_name == "get_command_argument") {
                llvm::Function *fn = module->getFunction("_lpython_get_argv");
                if (!fn) {
                    llvm::FunctionType *function_type = llvm::FunctionType::get(
                        character_type, {
                            llvm::Type::getInt32Ty(context)
                        }, false);
                    fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, "_lpython_get_argv", module.get());
                }
                args = convert_call_args(x, is_method /* skip_self */);
                LCOMPILERS_ASSERT(args.size() > 0);
                tmp = builder->CreateCall(fn, {llvm_utils->CreateLoad2(
                    llvm::Type::getInt32Ty(context), args[0])});
                if (args.size() > 1)
                    builder->CreateStore(tmp, args[1]);
                return;
            } else if (sub_name == "get_environment_variable") {
                llvm::Function *fn = module->getFunction("_lfortran_get_env_variable");
                if (!fn) {
                    llvm::FunctionType *function_type = llvm::FunctionType::get(
                        character_type, {
                            character_type
                        }, false);
                    fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, "_lfortran_get_env_variable", module.get());
                }
                args = convert_call_args(x, is_method /* skip_self */);
                LCOMPILERS_ASSERT(args.size() > 0);
                tmp = builder->CreateCall(fn, { llvm_utils->CreateLoad2(character_type, args[0]) });
                if (args.size() > 1)
                    builder->CreateStore(tmp, args[1]);
                return;
            } else if (sub_name == "_lcompilers_execute_command_line_" ||
                       sub_name == "_lcompilers_system_") {
                llvm::Function *fn = module->getFunction("_lfortran_exec_command");
                if (!fn) {
                    llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getInt32Ty(context), {
                            character_type
                        }, false);
                    fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, "_lfortran_exec_command", module.get());
                }
                args = convert_call_args(x, is_method /* skip_self */);
                LCOMPILERS_ASSERT(args.size() > 0);
                tmp = builder->CreateCall(fn, { llvm_utils->CreateLoad2(character_type, args[0]) });
                return;
            } else if (sub_name == "_lcompilers_sleep_") {
                llvm::Function *fn = module->getFunction("_lfortran_sleep");
                if (!fn) {
                    llvm::FunctionType *function_type = llvm::FunctionType::get(
                        llvm::Type::getVoidTy(context), {
                            llvm::Type::getInt32Ty(context)
                        }, false);
                    fn = llvm::Function::Create(function_type,
                        llvm::Function::ExternalLinkage, "_lfortran_sleep", module.get());
                }
                args = convert_call_args(x, is_method /* skip_self */);
                LCOMPILERS_ASSERT(args.size() > 0);
                tmp = builder->CreateCall(fn, { llvm_utils->CreateLoad2(llvm::Type::getInt32Ty(context), args[0]) });
                return;
            }
            h = get_hash((ASR::asr_t*)proc_sym);
        } else {
            throw CodeGenError("ABI type not implemented yet in SubroutineCall.");
        }
        if (llvm_symtab_fn_arg.find(h) != llvm_symtab_fn_arg.end()) {
            // Check if this is a callback function
            llvm::Value* fn = llvm_symtab_fn_arg[h];
            llvm::FunctionType* fntype = llvm_symtab_fn[h]->getFunctionType();
            std::string m_name = ASRUtils::symbol_name(x.m_name);
            if ( x.m_original_name && ASR::is_a<ASR::Variable_t>(*x.m_original_name) ) {
                ASR::Variable_t* x_m_original_name = ASR::down_cast<ASR::Variable_t>(x.m_original_name);
                if ( x_m_original_name->m_intent == ASRUtils::intent_out || x_m_original_name->m_intent == ASRUtils::intent_inout
                        || ASRUtils::is_pointer(x_m_original_name->m_type) ) {
                    fn = llvm_utils->CreateLoad2(fntype->getPointerTo(), fn);
                }
            } else if (ASR::is_a<ASR::Variable_t>(*proc_sym) &&
                    ASRUtils::is_pointer(ASR::down_cast<ASR::Variable_t>(proc_sym)->m_type)) {
                fn = llvm_utils->CreateLoad2(fntype->getPointerTo(), fn);
            }
            args = convert_call_args(x, is_method /* skip_self */);

            tmp = builder->CreateCall(fntype, fn, args);
        } else if (ASR::is_a<ASR::Variable_t>(*proc_sym) &&
                llvm_symtab.find(h) != llvm_symtab.end()) {
            llvm::Value* fn = llvm_symtab[h];
            ASR::Variable_t* v = ASR::down_cast<ASR::Variable_t>(proc_sym);
            if (v->m_type_declaration == nullptr) {
                throw CodeGenError("Procedure variable '" + std::string(v->m_name)
                    + "' has no interface. Add explicit interface or ensure it is called somewhere.");
            }
            llvm::FunctionType* fntype = llvm_utils->get_function_type(*ASR::down_cast<ASR::Function_t>(ASRUtils::symbol_get_past_external(v->m_type_declaration)), module.get());
            fn = llvm_utils->CreateLoad2(fntype->getPointerTo(), fn);
            std::string m_name = ASRUtils::symbol_name(x.m_name);
            args = convert_call_args(x, is_method /* skip_self */);

            tmp = builder->CreateCall(fntype, fn, args);
        } else if (llvm_symtab_fn.find(h) == llvm_symtab_fn.end()) {
            throw CodeGenError("Subroutine code not generated for '"
                + std::string(s->m_name) + "'");
            return;
        } else {
            llvm::Function *fn = llvm_symtab_fn[h];
            std::string m_name = ASRUtils::symbol_name(x.m_name);
            args = convert_call_args(x, false);
            // check if type of each arg is same as type of each arg in subrout_called
            if (ASR::is_a<ASR::Function_t>(*symbol_get_past_external(x.m_name))) {
                ASR::Function_t* subrout_called = ASR::down_cast<ASR::Function_t>(symbol_get_past_external(x.m_name));
                for (size_t i = 0; i < subrout_called->n_args; i++) {
                    ASR::expr_t* expected_arg = subrout_called->m_args[i];
                    ASR::expr_t* passed_arg = x.m_args[i].m_value;
                    ASR::ttype_t* expected_arg_type = ASRUtils::expr_type(expected_arg);
                    ASR::ttype_t* passed_arg_type = ASRUtils::expr_type(passed_arg);
                    if (ASR::is_a<ASR::ArrayItem_t>(*passed_arg)) {
                        if (!ASRUtils::types_equal(expected_arg_type, passed_arg_type, expected_arg, passed_arg, true)) {
                            if (ASRUtils::is_array(expected_arg_type) &&
                                !ASRUtils::is_array(passed_arg_type)) {
                                ASR::ttype_t* expected_elem_type = ASRUtils::type_get_past_array(expected_arg_type);
                                if (ASRUtils::types_equal(expected_elem_type, passed_arg_type,
                                        expected_arg, passed_arg, true)) {
                                    // Sequence association (F2018 15.5.2.11):
                                    // an array element may be passed to an
                                    // explicit-shape or assumed-size array dummy.
                                    continue;
                                }
                            }
                            throw CodeGenError("Type mismatch in subroutine call, expected `" + ASRUtils::type_to_str_python_expr(expected_arg_type, expected_arg)
                                    + "`, passed `" + ASRUtils::type_to_str_python_expr(passed_arg_type, passed_arg) + "`", x.m_args[i].m_value->base.loc);
                        }
                    }
                }
            }
            builder->CreateCall(fn, args);
            fixup_descriptor_after_cchar_bind_c_call(x, s, args);
            fixup_scalar_alloc_after_bind_c_call(x, s, args);
            for (auto& fc : pending_flat_copybacks) {
                copy_flat_back_to_struct(fc.flat_buf, fc.orig_struct_ptr,
                    fc.struct_llvm_type, fc.layout);
            }
            pending_flat_copybacks.clear();
        }
    }

    void fixup_descriptor_after_cchar_bind_c_call(
            const ASR::SubroutineCall_t &x,
            ASR::Function_t* s,
            const std::vector<llvm::Value*>& call_args) {
        for (size_t i = 0; i < x.n_args; i++) {
            if (!x.m_args[i].m_value) continue;
            if (!ASR::is_a<ASR::StringPhysicalCast_t>(*x.m_args[i].m_value)) continue;
            ASR::StringPhysicalCast_t* cast =
                ASR::down_cast<ASR::StringPhysicalCast_t>(x.m_args[i].m_value);
            if (cast->m_old != ASR::string_physical_typeType::DescriptorString ||
                cast->m_new != ASR::string_physical_typeType::CChar) continue;
            size_t arg_idx = i;
            if (arg_idx >= s->n_args) continue;
            ASR::Variable_t* orig_arg = ASR::down_cast<ASR::Variable_t>(
                ASRUtils::symbol_get_past_external(
                    ASR::down_cast<ASR::Var_t>(s->m_args[arg_idx])->m_v));
            if (!ASRUtils::is_allocatable(orig_arg->m_type)) continue;

            // Get the original string descriptor
            this->visit_expr_load_wrapper(cast->m_arg, 0);
            llvm::Value* desc = tmp;
            llvm::Value* data_ptr = llvm_utils->create_gep2(
                llvm_utils->string_descriptor, desc, 0);

            // The callee received a CFI descriptor (created during
            // convert_call_args) and may have updated its base_addr
            // field (e.g., via _lfortran_strcpy_alloc for intent(out)
            // allocatable characters). Read back the new data pointer
            // from the CFI descriptor and store it into the original
            // string_descriptor so the caller sees the updated value.
            if (arg_idx < call_args.size()) {
                llvm::Value* cfi_desc = call_args[arg_idx];
                llvm::Type* char_llvm_type = llvm::Type::getInt8Ty(context);
                llvm::Type* cfi_desc_type = arr_descr->get_array_type(
                    ASRUtils::EXPR(ASR::make_Var_t(al,
                        orig_arg->base.base.loc, &orig_arg->base)),
                    ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(orig_arg->m_type)),
                    char_llvm_type, false);
                // The call arg may have been bitcast (e.g. %array* -> i8**)
                // to match the callee's declared parameter type. Cast back
                // to %array* so the GEP uses the correct struct type.
                if (cfi_desc->getType() != cfi_desc_type->getPointerTo()) {
                    cfi_desc = builder->CreateBitCast(cfi_desc,
                        cfi_desc_type->getPointerTo());
                }
                llvm::Value* cfi_base_addr = llvm_utils->CreateLoad2(
                    character_type,
                    llvm_utils->create_gep2(cfi_desc_type, cfi_desc, 0));
                builder->CreateStore(cfi_base_addr, data_ptr);
            }

            llvm::Value* data = builder->CreateLoad(character_type, data_ptr);
            llvm::Value* len_ptr = llvm_utils->create_gep2(
                llvm_utils->string_descriptor, desc, 1);
            llvm::Value* str_len = builder->CreateCall(
                module->getOrInsertFunction("strlen",
                    llvm::FunctionType::get(
                        llvm::Type::getInt64Ty(context),
                        {character_type}, false)),
                {data});
            builder->CreateStore(str_len, len_ptr);
        }
    }

    // After a bind(C) call, read back the base_addr from any CFI descriptor
    // that was created for a scalar allocatable/pointer argument and store
    // it into the original Fortran variable.
    void fixup_scalar_alloc_after_bind_c_call(
            const ASR::SubroutineCall_t &x,
            ASR::Function_t* s,
            const std::vector<llvm::Value*>& call_args) {
        ASR::FunctionType_t* fn_type = ASRUtils::get_FunctionType(s);
        if (fn_type->m_abi != ASR::abiType::BindC)
            return;
        for (size_t i = 0; i < x.n_args; i++) {
            if (!x.m_args[i].m_value) continue;
            if (!ASR::is_a<ASR::Var_t>(*x.m_args[i].m_value)) continue;
            size_t arg_idx = i;
            if (arg_idx >= s->n_args) continue;
            if (!ASR::is_a<ASR::Var_t>(*s->m_args[arg_idx])) continue;
            ASR::symbol_t* arg_sym = ASRUtils::symbol_get_past_external(
                    ASR::down_cast<ASR::Var_t>(s->m_args[arg_idx])->m_v);
            if (!ASR::is_a<ASR::Variable_t>(*arg_sym)) continue;
            ASR::Variable_t* orig_arg = ASR::down_cast<ASR::Variable_t>(arg_sym);
            if (ASRUtils::is_array(orig_arg->m_type)) continue;
            if (!LLVM::is_llvm_pointer(*orig_arg->m_type)) continue;
            if (ASRUtils::is_character(*ASRUtils::type_get_past_allocatable(
                    ASRUtils::type_get_past_pointer(orig_arg->m_type)))) continue;
            if (orig_arg->m_intent != ASR::intentType::Out &&
                orig_arg->m_intent != ASR::intentType::InOut) continue;

            // Get the descriptor that was passed to C
            if (arg_idx >= call_args.size()) continue;
            llvm::Value* cfi_desc = call_args[arg_idx];

            // Determine the descriptor type
            ASR::ttype_t* elem_asr_type = ASRUtils::type_get_past_allocatable(
                ASRUtils::type_get_past_pointer(orig_arg->m_type));
            llvm::Type* elem_llvm_type = llvm_utils->get_el_type(
                x.m_args[i].m_value, elem_asr_type, module.get());
            llvm::Type* desc_type = arr_descr->get_array_type(
                ASRUtils::EXPR(ASR::make_Var_t(al,
                    orig_arg->base.base.loc, &orig_arg->base)),
                elem_asr_type, elem_llvm_type, false);

            // Cast back to descriptor pointer if needed
            if (cfi_desc->getType() != desc_type->getPointerTo()) {
                cfi_desc = builder->CreateBitCast(cfi_desc,
                    desc_type->getPointerTo());
            }

            // Read base_addr from descriptor (field 0)
            llvm::Value* new_base = llvm_utils->CreateLoad2(
                elem_llvm_type->getPointerTo(),
                llvm_utils->create_gep2(desc_type, cfi_desc, 0));

            // Store back into the original Fortran variable
            ASR::Variable_t* actual_var = EXPR2VAR(x.m_args[i].m_value);
            uint32_t h = get_hash((ASR::asr_t*)actual_var);
            llvm::Value* var_ptr = llvm_symtab[h];
            builder->CreateStore(new_base, var_ptr);
        }
    }

    void handle_allocated(ASR::expr_t* arg) {
        ASR::ttype_t* asr_type = ASRUtils::expr_type(arg);
        int64_t ptr_loads_copy = ptr_loads;
        if (ASRUtils::is_class_type(ASRUtils::extract_type(asr_type))) {
            bool ref = false;
            ptr_loads = 1;
            if (ASR::is_a<ASR::StructInstanceMember_t>(*arg)) {
                ref = true;
            }
            visit_expr_wrapper(arg, ref);
        } else {
            ptr_loads = 1;
            visit_expr_wrapper(arg, true);
        }
        ptr_loads = ptr_loads_copy;
        int n_dims = ASRUtils::extract_n_dims_from_ttype(asr_type);
        if (ASRUtils::is_class_type(ASRUtils::extract_type(asr_type)) &&
                ASRUtils::is_unlimited_polymorphic_type(arg) && !ASR::is_a<ASR::StructInstanceMember_t>(*arg)) {
            // For unlimited polymorphic Var with new_classes, fetch_var returns
            // the raw alloca; load once to get the actual pointer value.
            // StructInstanceMember is already loaded via ref=true above.
            llvm::Type* class_type = llvm_utils->get_type_from_ttype_t_util(arg, asr_type, module.get());
            tmp = llvm_utils->CreateLoad2(class_type, tmp);
        }
        if( n_dims > 0 || ASRUtils::is_array(asr_type) ) {
            visit_expr_load_wrapper(arg, 1, true);
            tmp = arr_descr->get_is_allocated_flag(tmp, arg);
        } else if (ASRUtils::is_character(*expr_type(arg))) {
            ASR::String_t* str_type = ASR::down_cast<ASR::String_t>(
                ASRUtils::extract_type(expr_type(arg)));
            if (str_type->m_physical_type == ASR::CChar) {
                tmp = builder->CreateICmpNE(
                    builder->CreatePtrToInt(tmp, llvm::Type::getInt64Ty(context)),
                    llvm::ConstantInt::get(llvm::Type::getInt64Ty(context),
                        llvm::APInt(64, 0)));
            } else {
                // bind(C) allocatable string dummies have an extra pointer
                // level (string_descriptor** rather than string_descriptor*)
                // due to the BindC ABI. Use ptr_loads=1 to dereference to
                // string_descriptor* as get_string_data requires.
                bool is_bindc = ASR::is_a<ASR::Var_t>(*arg) &&
                    ASRUtils::EXPR2VAR(arg)->m_abi == ASR::abiType::BindC;
                visit_expr_load_wrapper(arg, is_bindc ? 1 : 0);
                tmp = builder->CreateICmpNE(
                    llvm_utils->get_string_data(str_type, tmp),
                    llvm::ConstantPointerNull::get(character_type));
            }
        } else {
            tmp = builder->CreateICmpNE(
                builder->CreatePtrToInt(tmp, llvm::Type::getInt64Ty(context)),
                    llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), llvm::APInt(64, 0)) );
        }
    }

    llvm::Value* CreatePointerToStructTypeReturnValue(llvm::FunctionType* fnty,
                                                  llvm::Value* return_value,
                                                  ASR::ttype_t* asr_return_type) {
        if( !LLVM::is_llvm_struct(asr_return_type) ) {
            return return_value;
        }

        // Call to LLVM APIs not needed to fetch the return type of the function.
        // We can use asr_return_type as well but anyways for compactness I did it here.
        llvm::Value* pointer_to_struct = llvm_utils->CreateAlloca(*builder, fnty->getReturnType());
        LLVM::CreateStore(*builder, return_value, pointer_to_struct);
        return pointer_to_struct;
    }

    llvm::Value* CreateCallUtil(llvm::FunctionType* fnty, llvm::Function* fn,
                                std::vector<llvm::Value*>& args,
                                ASR::ttype_t* asr_return_type) {
        if (fnty->getReturnType()->isVoidTy()) {
            // Windows complex(kind=8) uses a hidden first argument (sret-style):
            // the function returns `void` and writes the result into the first
            // argument. The ASR still models it as a function with a return
            // variable, so we materialize the return value here.
            if (ASR::is_a<ASR::Complex_t>(*asr_return_type) &&
                ASRUtils::extract_kind_from_ttype_t(asr_return_type) == 8 &&
                compiler_options.platform == Platform::Windows) {
                llvm::Value* sret_tmp = llvm_utils->CreateAlloca(complex_type_8, nullptr, "complex_ret_tmp");
                args.insert(args.begin(), sret_tmp);
                builder->CreateCall(fn, args);
                return llvm_utils->CreateLoad2(complex_type_8, sret_tmp);
            }
        }
        llvm::Value* return_value = builder->CreateCall(fn, args);
        return CreatePointerToStructTypeReturnValue(fnty, return_value,
                                                asr_return_type);
    }

    llvm::Value* CreateCallUtil(llvm::Function* fn, std::vector<llvm::Value*>& args,
                                ASR::ttype_t* asr_return_type) {
        return CreateCallUtil(fn->getFunctionType(), fn, args, asr_return_type);
    }

    void visit_RuntimePolymorphicSubroutineCall(const ASR::SubroutineCall_t& x, std::string proc_sym_name) {
        bool is_pointer = LLVM::is_llvm_pointer(*ASRUtils::expr_type(x.m_dt));
        uint64_t ptr_loads_copy = ptr_loads;
        ptr_loads = is_pointer;
        this->visit_expr_wrapper(x.m_dt, is_pointer);
        ptr_loads = ptr_loads_copy;
        llvm::Value* llvm_dt = tmp;
        ASR::symbol_t* func_sym = ASRUtils::symbol_get_past_StructMethodDeclaration(
            ASRUtils::symbol_get_past_external(x.m_name));
        ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(
            ASRUtils::get_struct_sym_from_struct_expr(x.m_dt));
        llvm::FunctionType* fnTy = llvm_utils->get_function_type(
            *ASR::down_cast<ASR::Function_t>(ASRUtils::symbol_get_past_external(func_sym)),
            module.get());
        llvm::PointerType *fnPtrTy = llvm::PointerType::get(fnTy, 0);
        llvm::PointerType *fnPtrPtrTy = llvm::PointerType::get(fnPtrTy, 0);
        llvm::PointerType *fnPtrPtrPtrTy = llvm::PointerType::get(fnPtrPtrTy, 0);

        // Convert function args
        std::vector<llvm::Value*> args;
        struct_api->create_new_vtable_for_struct_type(struct_sym, module.get());
        ASR::Struct_t* struct_type_t = ASR::down_cast<ASR::Struct_t>(struct_sym);
        ASR::symbol_t* s_class_proc = struct_type_t->m_symtab->get_symbol(proc_sym_name);
        while(!s_class_proc && struct_type_t->m_parent) {
            struct_type_t = ASR::down_cast<ASR::Struct_t>(
                ASRUtils::symbol_get_past_external(struct_type_t->m_parent));
            s_class_proc = struct_type_t->m_symtab->get_symbol(proc_sym_name);
        }
        LCOMPILERS_ASSERT(!ASR::is_a<ASR::ExternalSymbol_t>(*s_class_proc));
        ASR::StructMethodDeclaration_t* class_proc = ASR::down_cast<ASR::StructMethodDeclaration_t>(s_class_proc);
        ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(
            ASRUtils::symbol_get_past_external(class_proc->m_proc));
        if (!class_proc->m_is_nopass) {
            llvm::Type* target_struct_type = llvm_utils->get_type_from_ttype_t_util(func->m_args[0], 
                ASRUtils::extract_type(ASRUtils::expr_type(func->m_args[0])), module.get());
            llvm_dt = builder->CreateBitCast(llvm_dt, target_struct_type->getPointerTo());
            args.push_back(llvm_dt);
        }
        std::vector<llvm::Value *> args2 = convert_call_args(x, !class_proc->m_is_nopass /* skip_self */);
        args.insert(args.end(), args2.begin(), args2.end());

        // Get VTable pointer
        if (ASR::is_a<ASR::ArrayItem_t>(*x.m_dt)) {
            ASR::ArrayItem_t* array_item = ASR::down_cast<ASR::ArrayItem_t>(x.m_dt);
            int ptr_loads_copy = ptr_loads;
            ptr_loads = 1;
            this->visit_expr_wrapper(array_item->m_v, true);
            ptr_loads = ptr_loads_copy;
            llvm::Type* struct_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                array_item->m_v, ASRUtils::extract_type(array_item->m_type), module.get());
            if (ASRUtils::extract_physical_type(
                    ASRUtils::expr_type(array_item->m_v)) == ASR::array_physical_typeType::DescriptorArray) {
                llvm_dt = llvm_utils->CreateLoad2(
                    struct_llvm_type->getPointerTo(), arr_descr->get_pointer_to_data(
                    array_item->m_v, ASRUtils::expr_type(array_item->m_v), tmp, module.get()));
            } else {
                llvm_dt = tmp;
            }
        }
        llvm::Value* vtable_ptr = builder->CreateBitCast(llvm_dt, fnPtrPtrPtrTy);
        vtable_ptr = llvm_utils->CreateLoad2(fnPtrPtrTy, vtable_ptr);

        // Get function pointer from VTable
        llvm::Value* fn = (llvm_utils->create_ptr_gep2(fnPtrTy,
            vtable_ptr, struct_api->struct_vtab_function_offset[struct_sym][proc_sym_name]));
        fn = llvm_utils->CreateLoad2(fnPtrTy, fn);
        builder->CreateCall(fnTy, fn, args);
        return;
    }

    void visit_RuntimePolymorphicFunctionCall(const ASR::FunctionCall_t& x, std::string proc_sym_name) {
        bool is_pointer = LLVM::is_llvm_pointer(*ASRUtils::expr_type(x.m_dt));
        uint64_t ptr_loads_copy = ptr_loads;
        ptr_loads = is_pointer;
        this->visit_expr_wrapper(x.m_dt, is_pointer);
        ptr_loads = ptr_loads_copy;
        llvm::Value* llvm_dt = tmp;
        ASR::symbol_t* func_sym = ASRUtils::symbol_get_past_StructMethodDeclaration(
            ASRUtils::symbol_get_past_external(x.m_name));
        ASR::symbol_t* struct_sym = ASRUtils::symbol_get_past_external(
            ASRUtils::get_struct_sym_from_struct_expr(x.m_dt));
        llvm::FunctionType* fnTy = llvm_utils->get_function_type(
            *ASR::down_cast<ASR::Function_t>(ASRUtils::symbol_get_past_external(func_sym)),
            module.get());
        llvm::PointerType *fnPtrTy = llvm::PointerType::get(fnTy, 0);
        llvm::PointerType *fnPtrPtrTy = llvm::PointerType::get(fnPtrTy, 0);
        llvm::PointerType *fnPtrPtrPtrTy = llvm::PointerType::get(fnPtrPtrTy, 0);

        // Convert function args
        std::vector<llvm::Value*> args;
        struct_api->create_new_vtable_for_struct_type(struct_sym, module.get());
        ASR::Struct_t* struct_type_t = ASR::down_cast<ASR::Struct_t>(struct_sym);
        ASR::symbol_t* s_class_proc = struct_type_t->m_symtab->get_symbol(proc_sym_name);
        while(!s_class_proc && struct_type_t->m_parent) {
            struct_type_t = ASR::down_cast<ASR::Struct_t>(
                ASRUtils::symbol_get_past_external(struct_type_t->m_parent));
            s_class_proc = struct_type_t->m_symtab->get_symbol(proc_sym_name);
        }
        LCOMPILERS_ASSERT(!ASR::is_a<ASR::ExternalSymbol_t>(*s_class_proc));
        ASR::StructMethodDeclaration_t* class_proc = ASR::down_cast<ASR::StructMethodDeclaration_t>(s_class_proc);
        ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(
            ASRUtils::symbol_get_past_external(class_proc->m_proc));
        if (!class_proc->m_is_nopass) {
            llvm::Type* target_struct_type = llvm_utils->get_type_from_ttype_t_util(func->m_args[0],
                ASRUtils::extract_type(ASRUtils::expr_type(func->m_args[0])), module.get());
            llvm_dt = builder->CreateBitCast(llvm_dt, target_struct_type->getPointerTo());

            // If the parameter is a POINTER, we need an extra level of indirection
            if (LLVM::is_llvm_pointer(*ASRUtils::expr_type(func->m_args[0]))) {
                // Allocate space for a pointer on the stack
                llvm::Value* ptr_storage = llvm_utils->CreateAlloca(*builder, target_struct_type->getPointerTo());
                // Store the loaded class descriptor pointer into it
                builder->CreateStore(llvm_dt, ptr_storage);
                // Pass the address of the storage (which is now a pointer-to-pointer)
                args.push_back(ptr_storage);
            } else {
                args.push_back(llvm_dt);
            }
        }
        std::vector<llvm::Value *> args2 = convert_call_args(x, !class_proc->m_is_nopass /* skip_self */);
        args.insert(args.end(), args2.begin(), args2.end());

        // Get Runtime VTable Pointer
        if (ASR::is_a<ASR::ArrayItem_t>(*x.m_dt)) {
            ASR::ArrayItem_t* array_item = ASR::down_cast<ASR::ArrayItem_t>(x.m_dt);
            ptr_loads_copy = ptr_loads;
            ptr_loads = LLVM::is_llvm_pointer(*ASRUtils::expr_type(array_item->m_v));
            this->visit_expr_wrapper(array_item->m_v, true);
            ptr_loads = ptr_loads_copy;
            llvm::Type* struct_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                array_item->m_v, ASRUtils::extract_type(array_item->m_type), module.get());
            if (ASRUtils::extract_physical_type(
                    ASRUtils::expr_type(array_item->m_v)) == ASR::array_physical_typeType::DescriptorArray) {
                llvm_dt = llvm_utils->CreateLoad2(
                    struct_llvm_type->getPointerTo(), arr_descr->get_pointer_to_data(
                    array_item->m_v, ASRUtils::expr_type(array_item->m_v), tmp, module.get()));
            } else {
                llvm_dt = tmp;
            }
        }
        llvm::Value* vtable_ptr = builder->CreateBitCast(llvm_dt, fnPtrPtrPtrTy);
        vtable_ptr = llvm_utils->CreateLoad2(fnPtrPtrTy, vtable_ptr);

        // Get function pointer from VTable
        llvm::Value* fn = (llvm_utils->create_ptr_gep2(fnPtrTy,
            vtable_ptr, struct_api->struct_vtab_function_offset[struct_sym][proc_sym_name]));
        fn = llvm_utils->CreateLoad2(fnPtrTy, fn);
        tmp = builder->CreateCall(fnTy, fn, args);
        if (func->m_return_var) {
            ASR::ttype_t* ret_ty = ASRUtils::EXPR2VAR(func->m_return_var)->m_type;
            if (ASR::is_a<ASR::Complex_t>(*ret_ty)) {
                tmp = llvm_utils->complex_function_return_abi_to_internal(tmp, ret_ty);
            }
        }
        return;
    }

    void visit_FunctionCall(const ASR::FunctionCall_t &x) {
        if ( compiler_options.emit_debug_info ) debug_emit_loc(x);
        if( ASRUtils::is_intrinsic_optimization(x.m_name) ) {
            ASR::Function_t* routine = ASR::down_cast<ASR::Function_t>(
                        ASRUtils::symbol_get_past_external(x.m_name));
            if( generate_optimization_instructions(routine, x.m_args) ) {
                return ;
            }
        }
        if (x.m_value) {
            this->visit_expr_wrapper(x.m_value, true);
            return ;
        }

        // Generate runtime error if array arguments' shape doesn't match
        if (compiler_options.po.bounds_checking) {
            bounds_check_call(x);
        }

        std::vector<llvm::Value*> args;
        if( x.m_dt && ASR::is_a<ASR::StructInstanceMember_t>(*x.m_dt) &&
            ASR::is_a<ASR::Variable_t>(*ASRUtils::symbol_get_past_external(x.m_name)) &&
            ASR::is_a<ASR::FunctionType_t>(*ASRUtils::type_get_past_pointer(ASRUtils::symbol_type(x.m_name))) ) {
            const ASR::Function_t* func = ASRUtils::get_function(x.m_name);

            uint64_t ptr_loads_copy = ptr_loads;
            ptr_loads = 1;
            this->visit_expr(*x.m_dt);
            ptr_loads = ptr_loads_copy;
            navigate_class_descriptor_to_proc_ptr(x.m_dt, x.m_name);

            llvm::Value* callee = llvm_utils->CreateLoad2(
                llvm_utils->get_function_type(*func, module.get())->getPointerTo(), tmp);

            // Self argument has been inserted into args by the semantic
            // layer (with a value distinct from m_dt — see
            // make_FunctionCall_t_util).  Let convert_call_args handle
            // all args including self with proper class wrapping.
            args = convert_call_args(x, false);
            llvm::FunctionType* fntype = llvm_utils->get_function_type(
                *func, module.get());
            tmp = builder->CreateCall(fntype, callee, args);
            return ;
        }

        const ASR::symbol_t *proc_sym = symbol_get_past_external(x.m_name);
        std::string proc_sym_name = "";
        bool is_deferred = false;
        bool is_nopass = false;
        if( ASR::is_a<ASR::StructMethodDeclaration_t>(*proc_sym) ) {
            ASR::StructMethodDeclaration_t* class_proc =
                ASR::down_cast<ASR::StructMethodDeclaration_t>(proc_sym);
            is_deferred = ASRUtils::is_class_type(ASRUtils::extract_type(ASRUtils::expr_type(x.m_dt)));
            proc_sym_name = class_proc->m_name;
            is_nopass = class_proc->m_is_nopass;
        }
        if( is_deferred ) {
            visit_RuntimePolymorphicFunctionCall(x, proc_sym_name);
            return ;
        }

        ASR::Function_t *s = nullptr;
        if (ASR::is_a<ASR::Function_t>(*proc_sym)) {
            s = ASR::down_cast<ASR::Function_t>(proc_sym);
        } else if (ASR::is_a<ASR::StructMethodDeclaration_t>(*proc_sym)) {
            ASR::StructMethodDeclaration_t *clss_proc = ASR::down_cast<
                ASR::StructMethodDeclaration_t>(proc_sym);
            s = ASR::down_cast<ASR::Function_t>(clss_proc->m_proc);
            proc_sym = clss_proc->m_proc;
        } else if (ASR::is_a<ASR::Variable_t>(*proc_sym)) {
            ASR::Variable_t* proc_var = ASR::down_cast<ASR::Variable_t>(proc_sym);
            ASR::symbol_t *type_decl = proc_var->m_type_declaration;
            if (type_decl == nullptr) {
                throw CodeGenError("Procedure variable '" + std::string(proc_var->m_name)
                    + "' has no interface. Add explicit interface or ensure it is called somewhere.");
            }
            s = ASR::down_cast<ASR::Function_t>(
                ASRUtils::symbol_get_past_external(type_decl));
            is_nopass = true;  // procedure pointer variables are always nopass
        } else {
            throw CodeGenError("FunctionCall: Symbol type not supported");
        }
        if( s == nullptr ) {
            s = ASR::down_cast<ASR::Function_t>(symbol_get_past_external(x.m_name));
        }
        bool is_method = false;
        if (x.m_dt && (!is_nopass)) {
            is_method = true;
        }

        bool intrinsic_function = ASRUtils::is_intrinsic_function2(s);
        uint32_t h;
        ASR::FunctionType_t* s_func_type = ASR::down_cast<ASR::FunctionType_t>(s->m_function_signature);
        if (s_func_type->m_abi == ASR::abiType::Source && !intrinsic_function) {
            h = get_hash((ASR::asr_t*)proc_sym);
        } else if (s_func_type->m_abi == ASR::abiType::LFortranModule) {
            throw CodeGenError("Function LCompilers interfaces not implemented yet");
        } else if (s_func_type->m_abi == ASR::abiType::ExternalUndefined) {
            h = get_hash((ASR::asr_t*)proc_sym);
        } else if (s_func_type->m_abi == ASR::abiType::BindC) {
            h = get_hash((ASR::asr_t*)proc_sym);
        } else if (s_func_type->m_abi == ASR::abiType::Intrinsic || intrinsic_function) {
            std::string func_name = s->m_name;
            if( fname2arg_type.find(func_name) != fname2arg_type.end() ) {
                h = get_hash((ASR::asr_t*)proc_sym);
            } else {
                if (func_name == "len") {
                    throw LCompilersException("Unhandled");
                } else if (func_name == "command_argument_count") {
                    llvm::Function *fn = module->getFunction("_lfortran_get_argc");
                    if(!fn) {
                        llvm::FunctionType *function_type = llvm::FunctionType::get(
                            llvm::Type::getInt32Ty(context), {}, false);
                        fn = llvm::Function::Create(function_type,
                            llvm::Function::ExternalLinkage, "_lfortran_get_argc", module.get());
                    }
                    tmp = builder->CreateCall(fn, {});
                    return;
                } else if (func_name == "achar") {
                    // TODO: make achar just StringChr
                    this->visit_expr_wrapper(x.m_args[0].m_value, true);
                    tmp = lfortran_str_chr(tmp);
                    return;
                }
                if( ASRUtils::get_FunctionType(s)->m_deftype == ASR::deftypeType::Interface ) {
                    throw CodeGenError("Intrinsic '" + func_name + "' not implemented yet and compile time value is not available.");
                } else {
                    h = get_hash((ASR::asr_t*)proc_sym);
                }
            }
        } else {
            throw CodeGenError("ABI type not implemented yet.");
        }
        if (llvm_symtab_fn_arg.find(h) != llvm_symtab_fn_arg.end()) {
            // Check if this is a callback function
            llvm::Value* fn = llvm_symtab_fn_arg[h];
            if (llvm_symtab_fn.find(h) == llvm_symtab_fn.end()) {
                throw CodeGenError("The callback function not found in llvm_symtab_fn");
            }
            llvm::FunctionType* fntype = llvm_symtab_fn[h]->getFunctionType();
            std::string m_name = std::string(((ASR::Function_t*)(&(x.m_name->base)))->m_name);
            // For procedure pointer variables (Pointer_t(FunctionType_t)), the argument
            // is passed by reference (fntype**), so we need to load to get fntype*.
            if (ASR::is_a<ASR::Variable_t>(*proc_sym) &&
                    ASRUtils::is_pointer(ASR::down_cast<ASR::Variable_t>(proc_sym)->m_type)) {
                fn = llvm_utils->CreateLoad2(fntype->getPointerTo(), fn);
            }
            args = convert_call_args(x, is_method /* skip_self */);

            tmp = builder->CreateCall(fntype, fn, args);
        } else if (ASRUtils::is_symbol_procedure_variable(ASRUtils::symbol_get_past_external(proc_sym)) && llvm_symtab.find(h) != llvm_symtab.end()) {
            // This is the case were a function pointer ( procedure variable ) is associated and used
            llvm::FunctionType* fntype = llvm_utils->get_function_type(*s, module.get());
            ASR::expr_t* proc_sym_expr = ASRUtils::EXPR(ASR::make_Var_t(al, x.base.base.loc, (ASR::symbol_t*) s));
            llvm::Type* fn_type = llvm_utils->get_type_from_ttype_t_util(proc_sym_expr,
                s->m_function_signature, module.get());
            llvm::Value* fn = llvm_symtab[h];
            fn = llvm_utils->CreateLoad2(fn_type, fn);
            args = convert_call_args(x, is_method /* skip_self */);

            tmp = builder->CreateCall(fntype, fn, args);
        } else if (llvm_symtab_fn.find(h) == llvm_symtab_fn.end()) {
            throw CodeGenError("Function code not generated for '"
                + std::string(s->m_name) + "'");
        } else {
            llvm::Function *fn = llvm_symtab_fn[h];
            std::string m_name = std::string(((ASR::Function_t*)(&(x.m_name->base)))->m_name);
            args = convert_call_args(x, false);
            ASR::ttype_t *return_var_type0 = EXPR2VAR(s->m_return_var)->m_type;
            if (ASRUtils::get_FunctionType(s)->m_abi == ASR::abiType::BindC) {
                if (is_a<ASR::Complex_t>(*return_var_type0)) {
                    int a_kind = down_cast<ASR::Complex_t>(return_var_type0)->m_kind;
                    if (a_kind == 8) {
                        if (compiler_options.platform == Platform::Windows) {
                            tmp = llvm_utils->CreateAlloca(complex_type_8, nullptr, "complex_ret_tmp");
                            args.insert(args.begin(), tmp);
                            builder->CreateCall(fn, args);
                            // Convert {double,double}* to {double,double}
                            tmp = llvm_utils->CreateLoad2(complex_type_8, tmp);
                        } else {
                            tmp = builder->CreateCall(fn, args);
                        }
                    } else {
                        tmp = builder->CreateCall(fn, args);
                    }
                } else {
                    tmp = builder->CreateCall(fn, args);
                }
            } else {
                tmp = CreateCallUtil(fn, args, return_var_type0);
            }
            // The convention we use is that any strings is a pointer to the underlying physicalType
            // Example of StringPhysicalTypes -> `string_descriptor*`, `i8*`
            if(ASRUtils::is_string_only(return_var_type0)){
                // Make sure to use `alloca` at the entry point.
                llvm::Value* string_ptr = llvm_utils->CreateAlloca(
                                            llvm_utils->get_StringType(return_var_type0),
                                            nullptr,
                                            "string_ret_const"
                                        );
                builder->CreateStore(tmp, string_ptr);
                tmp = string_ptr;
            }
        }
        // Convert complex return value from platform ABI to internal representation
        if (s->m_return_var) {
            ASR::ttype_t *return_var_type0 = EXPR2VAR(s->m_return_var)->m_type;
            if (is_a<ASR::Complex_t>(*return_var_type0)) {
                tmp = llvm_utils->complex_function_return_abi_to_internal(tmp,
                    return_var_type0);
            }
        }
        if (ASRUtils::is_character(*x.m_type)) {
            strings_to_be_deallocated.push_back(al, tmp);
        }
    }

    void load_array_size_deep_copy(ASR::expr_t* x) {
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 2;
        if (x != nullptr &&  ASR::is_a<ASR::Var_t>(*x)) {
            ASR::Var_t* x_var = ASR::down_cast<ASR::Var_t>(x);
            ASR::symbol_t* x_sym = ASRUtils::symbol_get_past_external(x_var->m_v);
            if (x_sym != nullptr && ASR::is_a<ASR::Variable_t>(*x_sym)) {
                ASR::Variable_t* x_sym_variable = ASR::down_cast<ASR::Variable_t>(x_sym);
                uint32_t x_sym_variable_h = get_hash((ASR::asr_t*)x_sym_variable);
                if (llvm_symtab_deep_copy.find({x_sym_variable_h, current_scope}) != llvm_symtab_deep_copy.end()) {
                    ASR::ttype_t* x_type = ASRUtils::expr_type(x);
                    llvm::Type* llvm_type = llvm_utils->get_type_from_ttype_t_util(x,
                        ASRUtils::type_get_past_allocatable_pointer(x_type), module.get());
                    tmp = llvm_utils->CreateLoad2(llvm_type,
                        llvm_symtab_deep_copy[{x_sym_variable_h, current_scope}]);
                } else {
                    this->visit_expr_wrapper(x, true);
                }
            } else {
                this->visit_expr_wrapper(x, true);
            }
        } else if (x != nullptr) {
            this->visit_expr_wrapper(x, true);
        } else {
            ptr_loads = ptr_loads_copy;
            throw CodeGenError("x is nullptr in load_array_size_deep_copy()");
        }
        ptr_loads = ptr_loads_copy;
    }

    void visit_ArraySizeUtil(ASR::expr_t* m_v, ASR::ttype_t* m_type,
        ASR::expr_t* m_dim=nullptr, ASR::expr_t* m_value=nullptr) {
        if( m_value ) {
            visit_expr_wrapper(m_value, true);
            return ;
        }

        m_v = ASRUtils::get_expr_size_expr(m_v);
        LCOMPILERS_ASSERT(m_v);

        if (ASR::is_a<ASR::ArraySection_t>(*m_v)) {
            ASR::expr_t* size_expr = ASRUtils::EXPR(ASRUtils::make_ArraySize_t_util(
                al, m_v->base.loc, m_v, m_dim, m_type, nullptr, false));
            visit_expr_wrapper(size_expr, true);
            return;
        }

        int output_kind = ASRUtils::extract_kind_from_ttype_t(m_type);
        int dim_kind = 4;
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 2 - // Sync: instead of 2 - , should this be ptr_loads_copy -
                    LLVM::is_llvm_pointer(*ASRUtils::expr_type(m_v));
        visit_expr_wrapper(m_v);
        ptr_loads = ptr_loads_copy;
        ASR::ttype_t* x_mv_type = ASRUtils::expr_type(m_v);
        LCOMPILERS_ASSERT(ASRUtils::is_array(x_mv_type));
        llvm::Type* array_type = llvm_utils->get_type_from_ttype_t_util(m_v,
                ASRUtils::type_get_past_allocatable(ASRUtils::type_get_past_pointer(x_mv_type)), module.get());
        if (is_a<ASR::StructInstanceMember_t>(*m_v)) {
            tmp = llvm_utils->CreateLoad2(array_type->getPointerTo(), tmp);
        }
        llvm::Value* llvm_arg = tmp;

        llvm::Value* llvm_dim = nullptr;
        if( m_dim ) {
            visit_expr_wrapper(m_dim, true);
            dim_kind = ASRUtils::extract_kind_from_ttype_t(ASRUtils::expr_type(m_dim));
            llvm_dim = tmp;
        }

        ASR::array_physical_typeType physical_type = ASRUtils::extract_physical_type(x_mv_type);
        if (physical_type == ASR::array_physical_typeType::StringArraySinglePointer) {
            if (ASRUtils::is_fixed_size_array(x_mv_type)) {
                physical_type = ASR::array_physical_typeType::FixedSizeArray;
            } else {
                physical_type = ASR::array_physical_typeType::DescriptorArray;
            }
        }
        switch( physical_type ) {
            case ASR::array_physical_typeType::AssumedRankArray:
            case ASR::array_physical_typeType::DescriptorArray: {
                tmp = arr_descr->get_array_size(array_type, llvm_arg, llvm_dim, output_kind, dim_kind);
                break;
            }
            case ASR::array_physical_typeType::PointerArray:
            case ASR::array_physical_typeType::SIMDArray:
            case ASR::array_physical_typeType::FixedSizeArray: {
                    llvm::Type* target_type = llvm_utils->get_type_from_ttype_t_util(m_v,
                        ASRUtils::type_get_past_allocatable(
                            ASRUtils::type_get_past_pointer(m_type)), module.get());


                    ASR::dimension_t* m_dims = nullptr;
                    int n_dims = ASRUtils::extract_dimensions_from_ttype(x_mv_type, m_dims);
                    if( llvm_dim ) {
                    llvm::AllocaInst *target = llvm_utils->CreateAlloca(
                        target_type, nullptr, "array_size");
                    llvm::BasicBlock *mergeBB = llvm::BasicBlock::Create(context, "ifcont");
                    for( int i = 0; i < n_dims; i++ ) {
                        llvm::Function *fn = builder->GetInsertBlock()->getParent();

                        llvm::BasicBlock *thenBB = llvm::BasicBlock::Create(context, "then", fn);
                        llvm::BasicBlock *elseBB = llvm::BasicBlock::Create(context, "else");

                        llvm::Value* cond = builder->CreateICmpEQ(llvm_dim,
                            llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, i + 1)));
                        builder->CreateCondBr(cond, thenBB, elseBB);
                        builder->SetInsertPoint(thenBB);
                        {
                            load_array_size_deep_copy(m_dims[i].m_length);

                            // Make dimension length and return size compatible.
                            if(ASRUtils::extract_kind_from_ttype_t(
                                ASRUtils::expr_type(m_dims[i].m_length)) > output_kind){
                                    tmp = builder->CreateTrunc(tmp, llvm::IntegerType::get(context, 8 * output_kind));
                            } else if (ASRUtils::extract_kind_from_ttype_t(
                                ASRUtils::expr_type(m_dims[i].m_length)) < output_kind){
                                tmp = builder->CreateSExt(tmp, llvm::IntegerType::get(context, 8 * output_kind));
                            }
                            builder->CreateStore(tmp, target);
                        }
                        builder->CreateBr(mergeBB);

                        start_new_block(elseBB);
                    }
                    start_new_block(mergeBB);
                    tmp = llvm_utils->CreateLoad2(target_type, target);
                } else {
                    if( physical_type == ASR::array_physical_typeType::FixedSizeArray ) {
                        int64_t size = ASRUtils::get_fixed_size_of_array(m_dims, n_dims);
                        tmp = llvm::ConstantInt::get(target_type, llvm::APInt(8 * output_kind, size));
                    } else {
                        llvm::Value* llvm_size = llvm::ConstantInt::get(target_type, llvm::APInt(8 * output_kind, 1));
                        int ptr_loads_copy = ptr_loads;
                        ptr_loads = 2;
                        for( int i = 0; i < n_dims; i++ ) {
                            load_array_size_deep_copy(m_dims[i].m_length);

                            // Make dimension length and return size compatible.
                            if(ASRUtils::extract_kind_from_ttype_t(
                                ASRUtils::expr_type(m_dims[i].m_length)) > output_kind){
                                    tmp = builder->CreateTrunc(tmp, llvm::IntegerType::get(context, 8 * output_kind));
                            } else if (ASRUtils::extract_kind_from_ttype_t(
                                ASRUtils::expr_type(m_dims[i].m_length)) < output_kind){
                                tmp = builder->CreateSExt(tmp, llvm::IntegerType::get(context, 8 * output_kind));
                            }

                            llvm_size = builder->CreateMul(tmp, llvm_size);
                        }
                        ptr_loads = ptr_loads_copy;
                        tmp = llvm_size;
                    }
                }
                break;
            }
            default: {
                LCOMPILERS_ASSERT(false);
            }
        }
    }

    void visit_ArraySize(const ASR::ArraySize_t& x) {
        visit_ArraySizeUtil(x.m_v, x.m_type, x.m_dim, x.m_value);
    }

    void visit_ArrayRank(const ASR::ArrayRank_t& x) {
        if( x.m_value ) {
            this->visit_expr_wrapper(x.m_value, true);
            return ;
        }
        ASR::expr_t* m_arg = x.m_v;
        this->visit_expr_wrapper(m_arg, false);
        llvm::Value *arg = tmp;
        llvm::Type* arr_type = llvm_utils->get_type_from_ttype_t_util(m_arg, ASRUtils::expr_type(m_arg), module.get());
        tmp = arr_descr->get_rank(arr_type, arg);
    }

    void visit_ArrayBound(const ASR::ArrayBound_t& x) {
        if (x.m_value) {
            this->visit_expr(*x.m_value);
            return;
        }
        ASR::expr_t* array_value = ASRUtils::expr_value(x.m_v);
        if( array_value && ASR::is_a<ASR::ArrayConstant_t>(*array_value) && ASRUtils::is_value_constant(x.m_dim) ) {
            int kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
            size_t bound_value = 0;
            ASR::dimension_t* m_dims = nullptr;
            ASRUtils::extract_dimensions_from_ttype(ASRUtils::expr_type(array_value), m_dims);
            int req_dim;
            ASRUtils::extract_value(x.m_dim, req_dim);
            req_dim--;

            size_t lbound = 1;
            if (m_dims[req_dim].m_start) {
                ASRUtils::extract_value(m_dims[req_dim].m_start, lbound);
            }
            size_t length = 0;
            bool has_length = m_dims[req_dim].m_length != nullptr &&
                ASRUtils::extract_value(m_dims[req_dim].m_length, length);
            // Per Fortran 2018 (16.9.109/16.9.197), if dimension DIM has zero
            // extent, LBOUND returns 1 and UBOUND returns 0 regardless of
            // the array's declared bounds.
            if( x.m_bound == ASR::arrayboundType::LBound ) {
                bound_value = (has_length && length == 0) ? 1 : lbound;
            } else if( x.m_bound == ASR::arrayboundType::UBound ) {
                LCOMPILERS_ASSERT(has_length);
                bound_value = (length == 0) ? 0 : length + lbound - 1;
            } else {
                LCOMPILERS_ASSERT(false);
            }
            tmp = llvm::ConstantInt::get(context, llvm::APInt(kind * 8, bound_value));
            return ;
        }

        ASR::ttype_t* x_mv_type = ASRUtils::expr_type(x.m_v);
        llvm::Type* array_type = llvm_utils->get_type_from_ttype_t_util(x.m_v,
                ASRUtils::type_get_past_allocatable(ASRUtils::type_get_past_pointer(x_mv_type)), module.get());
        int64_t ptr_loads_copy = ptr_loads;
        ptr_loads = 2 - // Sync: instead of 2 - , should this be ptr_loads_copy -
                    (LLVM::is_llvm_pointer(*ASRUtils::expr_type(x.m_v)));
        visit_expr_wrapper(x.m_v);
        ptr_loads = ptr_loads_copy;
        if (is_a<ASR::StructInstanceMember_t>(*x.m_v)) {
            tmp = llvm_utils->CreateLoad2(array_type->getPointerTo(), tmp);
        }
        llvm::Value* llvm_arg1 = tmp;
        visit_expr_wrapper(x.m_dim, true);
        llvm::Value* dim_val = tmp;

        ASR::array_physical_typeType physical_type = ASRUtils::extract_physical_type(x_mv_type);
        switch( physical_type ) {
            case ASR::array_physical_typeType::AssumedRankArray:
            case ASR::array_physical_typeType::DescriptorArray: {
                llvm::Value* dim_des_val = arr_descr->get_pointer_to_dimension_descriptor_array(array_type, llvm_arg1);
                llvm::Value* const_1 = llvm::ConstantInt::get(context, llvm::APInt(32, 1));
                dim_val = builder->CreateSub(dim_val, const_1);
                llvm::Value* dim_struct = arr_descr->get_pointer_to_dimension_descriptor(dim_des_val, dim_val);
                llvm::Value* res = nullptr;
                // Per Fortran 2018 (16.9.109/16.9.197), if dimension DIM has zero
                // extent, LBOUND returns 1 and UBOUND returns 0 regardless of the
                // array's declared bounds.
                llvm::Value* dim_size = arr_descr->get_dimension_size(dim_des_val, dim_val, true);
                llvm::Value* is_zero_extent = builder->CreateICmpEQ(dim_size,
                    llvm::ConstantInt::get(dim_size->getType(), 0));
                if( x.m_bound == ASR::arrayboundType::LBound ) {
                    llvm::Value* lb = arr_descr->get_lower_bound(dim_struct);
                    res = builder->CreateSelect(is_zero_extent,
                        llvm::ConstantInt::get(lb->getType(), 1), lb);
                } else if( x.m_bound == ASR::arrayboundType::UBound ) {
                    llvm::Value* ub = arr_descr->get_upper_bound(dim_struct);
                    res = builder->CreateSelect(is_zero_extent,
                        llvm::ConstantInt::get(ub->getType(), 0), ub);
                }
                // Cast to match the declared return type of the ArrayBound node
                llvm::Type* target_type = llvm_utils->get_type_from_ttype_t_util(x.m_v,
                    ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(x.m_type)), module.get());
                tmp = builder->CreateSExtOrTrunc(res, target_type);
                break;
            }
            case ASR::array_physical_typeType::FixedSizeArray:
            case ASR::array_physical_typeType::PointerArray:
            case ASR::array_physical_typeType::UnboundedPointerArray: {
                llvm::Type* target_type = llvm_utils->get_type_from_ttype_t_util(x.m_v,
                    ASRUtils::type_get_past_allocatable(
                        ASRUtils::type_get_past_pointer(x.m_type)), module.get());
                llvm::AllocaInst *target = llvm_utils->CreateAlloca(
                    target_type, nullptr, "array_bound");
                llvm::BasicBlock *mergeBB = llvm::BasicBlock::Create(context, "ifcont");
                ASR::dimension_t* m_dims = nullptr;
                int n_dims = ASRUtils::extract_dimensions_from_ttype(x_mv_type, m_dims);
                for( int i = 0; i < n_dims; i++ ) {
                    llvm::Function *fn = builder->GetInsertBlock()->getParent();

                    llvm::BasicBlock *thenBB = llvm::BasicBlock::Create(context, "then", fn);
                    llvm::BasicBlock *elseBB = llvm::BasicBlock::Create(context, "else");

                    llvm::Value* cond = builder->CreateICmpEQ(dim_val,
                        llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, i + 1)));
                    builder->CreateCondBr(cond, thenBB, elseBB);
                    builder->SetInsertPoint(thenBB);
                    {
                        // Per Fortran 2018 (16.9.109/16.9.197), if dimension
                        // DIM has zero extent, LBOUND returns 1 and UBOUND
                        // returns 0 regardless of the array's declared bounds.
                        llvm::Value *length = nullptr;
                        if (m_dims[i].m_length) {
                            load_array_size_deep_copy(m_dims[i].m_length);
                            length = tmp;
                        }
                        if( x.m_bound == ASR::arrayboundType::LBound ) {
                            llvm::Value *lbound = nullptr;
                            if (m_dims[i].m_start) {
                                this->visit_expr_wrapper(m_dims[i].m_start, true);
                                lbound = tmp;
                            } else {
                                lbound = llvm::ConstantInt::get(target_type, 1);
                            }
                            llvm::Value *res = builder->CreateSExtOrTrunc(lbound, target_type);
                            if (length) {
                                llvm::Value *length_cast = builder->CreateSExtOrTrunc(length, target_type);
                                llvm::Value *is_zero = builder->CreateICmpEQ(
                                    length_cast, llvm::ConstantInt::get(target_type, 0));
                                res = builder->CreateSelect(is_zero,
                                    llvm::ConstantInt::get(target_type, 1), res);
                            }
                            builder->CreateStore(res, target);
                        } else if( x.m_bound == ASR::arrayboundType::UBound ) {
                            llvm::Value *lbound = nullptr;
                            if (m_dims[i].m_start) {
                                this->visit_expr_wrapper(m_dims[i].m_start, true);
                                lbound = tmp;
                            } else {
                                lbound = llvm::ConstantInt::get(target_type, 1);
                            }
                            if (length) {
                                unsigned target_bit_width = target_type->getIntegerBitWidth();
                                llvm::Value *ub = builder->CreateSub(
                                    builder->CreateSExtOrTrunc(builder->CreateAdd(length, lbound), target_type),
                                    llvm::ConstantInt::get(context, llvm::APInt(target_bit_width, 1)));
                                llvm::Value *length_cast = builder->CreateSExtOrTrunc(length, target_type);
                                llvm::Value *is_zero = builder->CreateICmpEQ(
                                    length_cast, llvm::ConstantInt::get(target_type, 0));
                                ub = builder->CreateSelect(is_zero,
                                    llvm::ConstantInt::get(target_type, 0), ub);
                                builder->CreateStore(ub, target);
                            } else {
                                // Assumed-size array: last dimension has no length
                                builder->CreateStore(builder->CreateSExtOrTrunc(lbound, target_type), target);
                            }
                        }
                    }
                    builder->CreateBr(mergeBB);

                    start_new_block(elseBB);
                }
                start_new_block(mergeBB);
                tmp = llvm_utils->CreateLoad2(target_type, target);
                break;
            }
            case ASR::array_physical_typeType::SIMDArray: {
                int kind = ASRUtils::extract_kind_from_ttype_t(x.m_type);
                unsigned bit_width = kind * 8;
                if( x.m_bound == ASR::arrayboundType::LBound ) {
                    tmp = llvm::ConstantInt::get(context, llvm::APInt(bit_width, 1));
                } else if( x.m_bound == ASR::arrayboundType::UBound ) {
                    int64_t size = ASRUtils::get_fixed_size_of_array(ASRUtils::expr_type(x.m_v));
                    tmp = llvm::ConstantInt::get(context, llvm::APInt(bit_width, size));
                }
                break;
            }
            case ASR::array_physical_typeType::StringArraySinglePointer: {
                ASR::dimension_t* m_dims = nullptr;
                int n_dims = ASRUtils::extract_dimensions_from_ttype(x_mv_type, m_dims);
                if (ASRUtils::is_dimension_empty(m_dims, n_dims)) {
                    // treat it as DescriptorArray
                    llvm::Value* dim_des_val = arr_descr->get_pointer_to_dimension_descriptor_array(array_type, llvm_arg1);
                    llvm::Value* const_1 = llvm::ConstantInt::get(context, llvm::APInt(32, 1));
                    dim_val = builder->CreateSub(dim_val, const_1);
                    llvm::Value* dim_struct = arr_descr->get_pointer_to_dimension_descriptor(dim_des_val, dim_val);
                    llvm::Value* res = nullptr;
                    // Per Fortran 2018 (16.9.109/16.9.197), if dimension DIM has
                    // zero extent, LBOUND returns 1 and UBOUND returns 0.
                    llvm::Value* dim_size = arr_descr->get_dimension_size(dim_des_val, dim_val, true);
                    llvm::Value* is_zero_extent = builder->CreateICmpEQ(dim_size,
                        llvm::ConstantInt::get(dim_size->getType(), 0));
                    if( x.m_bound == ASR::arrayboundType::LBound ) {
                        llvm::Value* lb = arr_descr->get_lower_bound(dim_struct);
                        res = builder->CreateSelect(is_zero_extent,
                            llvm::ConstantInt::get(lb->getType(), 1), lb);
                    } else if( x.m_bound == ASR::arrayboundType::UBound ) {
                        llvm::Value* ub = arr_descr->get_upper_bound(dim_struct);
                        res = builder->CreateSelect(is_zero_extent,
                            llvm::ConstantInt::get(ub->getType(), 0), ub);
                    }
                    // Cast to match the declared return type of the ArrayBound node
                    llvm::Type* target_type = llvm_utils->get_type_from_ttype_t_util(x.m_v,
                        ASRUtils::type_get_past_allocatable(
                            ASRUtils::type_get_past_pointer(x.m_type)), module.get());
                    tmp = builder->CreateSExtOrTrunc(res, target_type);
                    break;
                } else if (ASRUtils::is_fixed_size_array(x_mv_type)) {
                    llvm::Type* target_type = llvm_utils->get_type_from_ttype_t_util(x.m_v,
                        ASRUtils::type_get_past_allocatable(
                            ASRUtils::type_get_past_pointer(x.m_type)), module.get());
                    llvm::AllocaInst *target = llvm_utils->CreateAlloca(
                        target_type, nullptr, "array_bound");
                    llvm::BasicBlock *mergeBB = llvm::BasicBlock::Create(context, "ifcont");
                    ASR::dimension_t* m_dims = nullptr;
                    int n_dims = ASRUtils::extract_dimensions_from_ttype(x_mv_type, m_dims);
                    for( int i = 0; i < n_dims; i++ ) {
                        llvm::Function *fn = builder->GetInsertBlock()->getParent();

                        llvm::BasicBlock *thenBB = llvm::BasicBlock::Create(context, "then", fn);
                        llvm::BasicBlock *elseBB = llvm::BasicBlock::Create(context, "else");

                        llvm::Value* cond = builder->CreateICmpEQ(dim_val,
                            llvm::ConstantInt::get(llvm::Type::getInt32Ty(context), llvm::APInt(32, i + 1)));
                        builder->CreateCondBr(cond, thenBB, elseBB);
                        builder->SetInsertPoint(thenBB);
                        {
                            if( x.m_bound == ASR::arrayboundType::LBound ) {
                                this->visit_expr_wrapper(m_dims[i].m_start, true);
                                builder->CreateStore(tmp, target);
                            } else if( x.m_bound == ASR::arrayboundType::UBound ) {
                                llvm::Value *lbound = nullptr, *length = nullptr;
                                this->visit_expr_wrapper(m_dims[i].m_start, true);
                                lbound = tmp;
                                load_array_size_deep_copy(m_dims[i].m_length);
                                length = tmp;
                                unsigned target_bit_width = target_type->getIntegerBitWidth();
                                builder->CreateStore(
                                    builder->CreateSub(builder->CreateSExtOrTrunc(builder->CreateAdd(length, lbound), target_type),
                                        llvm::ConstantInt::get(context, llvm::APInt(target_bit_width, 1))),
                                    target);
                            }
                        }
                        builder->CreateBr(mergeBB);

                        start_new_block(elseBB);
                    }
                    start_new_block(mergeBB);
                    tmp = llvm_utils->CreateLoad2(target_type, target);
                    break;
                } else {
                    LCOMPILERS_ASSERT(false);
                    break;
                }
            }
            default: {
                LCOMPILERS_ASSERT(false);
            }
        }
    }

    void visit_StringFormat(const ASR::StringFormat_t& x) {
        // TODO: Handle some things at compile time if possible:
        //ASR::expr_t* fmt_value = ASRUtils::expr_value(x.m_fmt);
        // if (fmt_value) ...
        if (x.m_kind == ASR::string_format_kindType::FormatFortran) {
            std::vector<llvm::Value *> args;
            // Push fmt string.
            if(x.m_fmt == nullptr){ // default formatting
                llvm::Type* int8Type = builder->getInt8Ty();
                llvm::PointerType* charPtrType = int8Type->getPointerTo();
                llvm::Constant* nullCharPtr = llvm::ConstantPointerNull::get(charPtrType);
                args.push_back(nullCharPtr);
                args.push_back(llvm::ConstantInt::get(llvm::Type::getInt64Ty(context), 0));
            } else {
                llvm::Value* fmt_data, *fmt_len;
                std::tie(fmt_data, fmt_len) = get_string_data_and_length(x.m_fmt);
                // When the format is a character array, the total format
                // string is all elements concatenated (contiguous in memory).
                // Multiply element length by total array size.
                if (ASRUtils::is_array(ASRUtils::expr_type(x.m_fmt))) {
                    ASR::expr_t* array_size_expr = ASRUtils::EXPR(
                        ASR::make_ArraySize_t(al, x.m_fmt->base.loc, x.m_fmt,
                        nullptr, ASRUtils::TYPE(ASR::make_Integer_t(
                            al, x.m_fmt->base.loc, 8)),
                        ASRUtils::get_compile_time_array_size(al,
                            ASRUtils::expr_type(x.m_fmt))));
                    visit_expr(*array_size_expr);
                    fmt_len = builder->CreateMul(fmt_len, tmp);
                }
                args.push_back(fmt_data);
                args.push_back(fmt_len);
            }
            // Push Serialization;
            llvm::Value* serialization_info = SerializeExprTypes(x.m_args, x.n_args);
            args.push_back(serialization_info);

            // Create and Push a pointer to int64 to store the result size in
            llvm::Value *result_size_ptr = llvm_utils->CreateAlloca(*builder, llvm::Type::getInt64Ty(context));
            args.push_back(result_size_ptr);

            // Check unallocated arguments
            if (compiler_options.po.bounds_checking) {
                for (size_t i = 0; i < x.n_args; i++) {
                    ASR::ttype_t* arg_expr_type = ASRUtils::expr_type(x.m_args[i]);
                    bool is_alloc_string = ASRUtils::is_allocatable(arg_expr_type) &&
                        ASRUtils::is_character(*ASRUtils::type_get_past_allocatable_pointer(arg_expr_type));
                    if (ASRUtils::is_allocatable(arg_expr_type) && !is_alloc_string) {
                        llvm_utils->generate_runtime_error(expr_is_unallocated(x.m_args[i]),
                                "Argument %d is unallocated.",
                                {LLVMUtils::RuntimeLabel("This is unallocated", {x.m_args[i]->base.loc})},
                                infile,
                                location_manager,
                                llvm::ConstantInt::get(llvm_utils->getIntType(4), llvm::APInt(32, i + 1)));
                    }
                }
            }

            //Push serialization of sizes and n_size
            size_t ArraySizesCnt = 0;
            for (size_t i=0; i<x.n_args; i++) {
                if(ASRUtils::is_array(ASRUtils::expr_type(x.m_args[i]))){
                    ASR::expr_t* ArraySizeExpr = ASRUtils::EXPR
                        (ASR::make_ArraySize_t(al, x.m_args[i]->base.loc, x.m_args[i],
                        nullptr, ASRUtils::TYPE(ASR::make_Integer_t(al, x.m_args[i]->base.loc, 8)),
                        ASRUtils::get_compile_time_array_size(al,
                            ASRUtils::expr_type(x.m_args[i])))) ;
                    visit_expr(*ArraySizeExpr);
                    args.push_back(tmp);
                    ArraySizesCnt++;
                }
            }

            args.insert(args.end()-ArraySizesCnt,
                llvm::ConstantInt::get(llvm::Type::getInt32Ty(context),
                    ArraySizesCnt));
            // Push runtime-length of strings and n_length
            size_t string_lengths_cnt = 0;
            for(size_t i=0; i<x.n_args; i++) {
                // Only loop on string types.
                if(!ASRUtils::is_character(*
                    ASRUtils::extract_type(
                        expr_type(x.m_args[i])))){continue;}

                ASR::String_t* str_type =
                    ASR::down_cast<ASR::String_t>(
                        ASRUtils::extract_type(
                            expr_type(x.m_args[i])));
                int64_t len; // dummy to use below.
                if(ASRUtils::extract_value(str_type->m_len, len)){
                    // It's serialized at compile time (`S-len`).
                } else {
                    args.push_back(get_string_length(x.m_args[i]));
                    ++string_lengths_cnt;
                }
            }
            args.insert(args.end()- string_lengths_cnt - ArraySizesCnt,
                llvm::ConstantInt::get(llvm::Type::getInt32Ty(context),
                    string_lengths_cnt));

            // This vector is used to keep track of all contiguous buffer copies created for arrays
            // which are allocated on heap, and need to be freed after the function call.
            std::vector<llvm::Value*> contiguous_copies_to_free;
            // Push the args.
            for (size_t i=0; i<x.n_args; i++) {
                // Push the args as raw pointers
                int64_t ptr_load_copy = ptr_loads;
                ptr_loads = (ASR::is_a<ASR::Var_t>(*x.m_args[i]) && !ASRUtils::is_character(*expr_type(x.m_args[i]))) ? 0 : 1;
                // Special Hanlding to pass appropriate pointer to the backend.
                if(ASRUtils::is_array(expr_type(x.m_args[i]))){ // Arrays need a cast to pointerToDataArray physicalType.
                    ASR::Array_t* arr = ASR::down_cast<ASR::Array_t>(
                        ASRUtils::type_get_past_allocatable_pointer(
                            ASRUtils::expr_type(x.m_args[i])));
                    //Copy only when we have DescriptorArray Type, where not referencing through pointer
                    //TODO: Check for non-unit strides, and create a copy only when having non-unit strides.
                    bool copy_needed = (arr->m_physical_type == ASR::array_physical_typeType::DescriptorArray) &&
                                        ASR::is_a<ASR::Var_t>(*x.m_args[i]);
                    if(copy_needed){
                        // For DescriptorArray, we need to create a contiguous copy
                        // because the descriptor may have non-unit strides (e.g., from array sections)
                        // and the string format runtime function expects contiguous arrays for inputs
                        ASR::ttype_t* source_array_type = ASRUtils::expr_type(x.m_args[i]);
                        // Visit the expression to get the descriptor
                        int64_t ptr_loads_copy2 = ptr_loads;
                        ptr_loads = 1 - !LLVM::is_llvm_pointer(*source_array_type);
                        visit_expr_wrapper(x.m_args[i]);
                        llvm::Value* source_desc = tmp;
                        llvm::Type* source_llvm_type = llvm_utils->get_type_from_ttype_t_util(
                            x.m_args[i], ASRUtils::type_get_past_allocatable_pointer(source_array_type), module.get());
                        ptr_loads = ptr_loads_copy2;
                        llvm::Type* elem_type = llvm_utils->get_type_from_ttype_t_util(
                            x.m_args[i], ASRUtils::extract_type(source_array_type), module.get());
                        int rank = ASRUtils::extract_n_dims_from_ttype(source_array_type);
                        // Create contiguous copy using the array descriptor utility
                        tmp = arr_descr->create_contiguous_copy_from_descriptor(
                            source_llvm_type, source_desc, elem_type, rank, module.get());
                        // Track this heap allocation to free after function call
                        contiguous_copies_to_free.push_back(tmp);
                    } else if(arr->m_physical_type != ASR::array_physical_typeType::PointerArray){
                        ASR::ttype_t* array_type = ASRUtils::TYPE(
                                                    ASR::make_Array_t(al, arr->base.base.loc,arr->m_type,
                                                    arr->m_dims, arr->n_dims,ASR::array_physical_typeType::FixedSizeArray));
                        ASR::expr_t* array_casted_to_pointer = ASRUtils::EXPR(
                                                                ASR::make_ArrayPhysicalCast_t(al, arr->base.base.loc,
                                                                x.m_args[i],arr->m_physical_type,
                                                                ASR::array_physical_typeType::PointerArray,
                                                                array_type, nullptr));
                        this->visit_expr(*array_casted_to_pointer);
                    } else {
                        this->visit_expr(*x.m_args[i]);
                    }
                } else if (ASRUtils::is_character(*expr_type(x.m_args[i])) &&
                    ASRUtils::get_string_type(expr_type(x.m_args[i]))->m_physical_type == ASR::CChar) {
                    visit_expr_load_wrapper(x.m_args[i], 0);
                    LCOMPILERS_ASSERT(llvm_utils->get_StringType(expr_type(x.m_args[i])) == llvm::Type::getInt8Ty(context))
                    LCOMPILERS_ASSERT(tmp->getType()->isPointerTy()); // atleast not `char`
                    llvm::Value* tmp_ptr = builder->CreateAlloca(llvm::Type::getInt8Ty(context)->getPointerTo()->getPointerTo());
                    builder->CreateStore(tmp, tmp_ptr);
                    tmp = tmp_ptr;
                } else {
                    ptr_loads = ptr_loads + LLVM::is_llvm_pointer(*expr_type(x.m_args[i]));
                    this->visit_expr_wrapper(x.m_args[i], true);
                }
                // Handling for polymorphic class variables in print statements with --new-classes
                if (ASR::is_a<ASR::Var_t>(*x.m_args[i]) &&
                    ASRUtils::is_class_type(ASRUtils::extract_type(ASRUtils::expr_type(x.m_args[i])))) {

                    // Try to get struct symbol for this expression (returns nullptr if not applicable)
                    ASR::symbol_t* maybe_struct =
                        ASRUtils::symbol_get_past_external(
                            ASRUtils::get_struct_sym_from_struct_expr(x.m_args[i])
                        );

                    // If the argument is not a struct/class, skip this block (important for REPL)
                    if (maybe_struct != nullptr) {

                        ASR::Struct_t* struct_t = ASR::down_cast<ASR::Struct_t>(maybe_struct);

                        llvm::Type* polymorphic_struct_type = llvm_utils->getClassType(struct_t);
                        llvm::Type* actual_struct_type = llvm_utils->getStructType(struct_t, module.get(), true);

                        // GEP to the "data" field of the wrapper: wrapper = { typeid, data_ptr }
                        llvm::Value* data_ptr = llvm_utils->create_gep2(
                            polymorphic_struct_type, tmp, 1);

                        if (ASRUtils::is_unlimited_polymorphic_type(x.m_args[i])) {
                            // For class(*), load the void* from wrapper
                            tmp = llvm_utils->CreateLoad2(llvm_utils->i8_ptr, data_ptr);
                        } else {
                            // For a concrete CLASS(T), data_ptr is already the pointer we need
                            tmp = llvm_utils->CreateLoad2(actual_struct_type, data_ptr);
                        }
                    }
                }
                if(!tmp->getType()->isPointerTy() ||
                    ASR::is_a<ASR::PointerToCPtr_t>(*x.m_args[i])){
                    tmp = logical_store_val(tmp, x.m_args[i]);
                    llvm::Value* tmp_ptr = builder->CreateAlloca(tmp->getType());
                    builder->CreateStore(tmp, tmp_ptr);
                    tmp = tmp_ptr;
                }
                // bind(C)/SEQUENCE inline char member: tmp is [len x i8]* but
                // format expects string_descriptor*. Materialize a temp descriptor.
                if (ASRUtils::is_character(*expr_type(x.m_args[i])) &&
                    ASRUtils::get_string_type(expr_type(x.m_args[i]))->m_physical_type
                        == ASR::DescriptorString &&
                    ASR::is_a<ASR::StructInstanceMember_t>(*x.m_args[i])) {
                    ASR::StructInstanceMember_t* sim = ASR::down_cast<
                        ASR::StructInstanceMember_t>(x.m_args[i]);
                    ASR::symbol_t* member_sym = ASRUtils::symbol_get_past_external(sim->m_m);
                    ASR::Variable_t* member_var = ASR::down_cast<ASR::Variable_t>(member_sym);
                    ASR::symbol_t* struct_sym = ASR::down_cast<ASR::symbol_t>(
                        member_var->m_parent_symtab->asr_owner);
                    struct_sym = ASRUtils::symbol_get_past_external(struct_sym);
                    if (ASR::is_a<ASR::Struct_t>(*struct_sym) &&
                        (ASR::down_cast<ASR::Struct_t>(struct_sym)->m_abi
                            == ASR::abiType::BindC ||
                         ASR::down_cast<ASR::Struct_t>(struct_sym)->m_is_sequence) &&
                        !ASR::is_a<ASR::Pointer_t>(*sim->m_type) &&
                        !ASR::is_a<ASR::Allocatable_t>(*sim->m_type)) {
                        ASR::String_t* str_ty = ASR::down_cast<ASR::String_t>(
                            ASRUtils::extract_type(sim->m_type));
                        int64_t slen = 1;
                        if (str_ty->m_len) {
                            ASRUtils::extract_value(str_ty->m_len, slen);
                        }
                        if (slen < 1) slen = 1;
                        llvm::Value* desc = builder->CreateAlloca(
                            llvm_utils->string_descriptor);
                        llvm::Value* data_field = llvm_utils->create_gep2(
                            llvm_utils->string_descriptor, desc, 0);
                        llvm::Value* len_field = llvm_utils->create_gep2(
                            llvm_utils->string_descriptor, desc, 1);
                        llvm::Value* data_ptr = builder->CreateBitCast(
                            tmp, llvm::Type::getInt8Ty(context)->getPointerTo());
                        builder->CreateStore(data_ptr, data_field);
                        builder->CreateStore(llvm::ConstantInt::get(
                            context, llvm::APInt(64, slen)), len_field);
                        tmp = desc;
                    }
                }
                args.push_back(tmp);
                ptr_loads = ptr_load_copy;
            }
            tmp = llvm_utils->string_format_fortran(args, this->current_decimal_mode, this->current_sign_mode, this->current_round_mode);
            // Free contiguous copies that were heap-allocated
            for (llvm::Value* copy_ptr : contiguous_copies_to_free) {
                llvm_utils->lfortran_free(copy_ptr);
            }

            // Load result size
            llvm::Value *result_size = llvm_utils->CreateLoad2(llvm::Type::getInt64Ty(context), result_size_ptr);

            tmp = llvm_utils->create_string_descriptor(tmp, result_size,"stringFormat_desc");
        } else {
            throw CodeGenError("Only FormatFortran string formatting implemented so far.");
        }
    }

    void visit_ArrayBroadcast(const ASR::ArrayBroadcast_t &x) {
        this->visit_expr_wrapper(x.m_array, true);
        llvm::Value *value = tmp;
        llvm::Type* ele_type = llvm_utils->get_el_type(
            x.m_array, ASRUtils::type_get_past_array(x.m_type), module.get());
        size_t n_eles = ASRUtils::get_fixed_size_of_array(x.m_type);
        llvm::Type* vec_type = nullptr;
        if (n_eles == 0) {
            vec_type = llvm::ArrayType::get(ele_type, 0);
        } else {
            vec_type = FIXED_VECTOR_TYPE::get(ele_type, n_eles);
        }
        llvm::AllocaInst *vec = llvm_utils->CreateAlloca(*builder, vec_type);
        for (size_t i=0; i < n_eles; i++) {
            llvm::Value* store_value = value;
            if (ASRUtils::is_logical(*ASRUtils::type_get_past_array(x.m_type)) &&
                store_value->getType()->isIntegerTy(1)) {
                store_value = builder->CreateZExt(store_value, llvm::Type::getInt8Ty(context));
            }
            builder->CreateStore(store_value, llvm_utils->create_gep2(vec_type, vec, i));
        }
        tmp = llvm_utils->CreateLoad2(vec_type, vec);
    }
};

/// Utility used by any llvm_util member function.
/// defined here to have access to ASRToLLVMVisitor member definition (specifically `visit_expr_wrapper()`, `tmp`).
llvm::Value* LLVMUtils::get_array_size(llvm::Value* array_ptr, llvm::Type* array_llvm_type, ASR::ttype_t* array_asr_type, ASRToLLVMVisitor *asr_to_llvm_visitor){
    const int RESULT_KIND = 8;
    auto const array_t = ASR::down_cast<ASR::Array_t>(array_asr_type);
    if(ASRUtils::is_fixed_size_array(array_asr_type)){
        const auto array_size = ASRUtils::get_fixed_size_of_array(array_asr_type);
        return llvm::ConstantInt::get(context, llvm::APInt(RESULT_KIND * 8, array_size));
    } else if(array_t->m_physical_type == ASR::DescriptorArray) {
        return arr_api->get_array_size(array_llvm_type, array_ptr, nullptr, RESULT_KIND);
    } else {
        llvm::Value* llvm_size = llvm::ConstantInt::get(context, llvm::APInt(8 * RESULT_KIND, 1));
        auto const n_dims = array_t->n_dims;
        auto const m_dims = array_t->m_dims;
        for( size_t i = 0; i < n_dims; i++ ) {
            asr_to_llvm_visitor->visit_expr_wrapper(m_dims[i].m_length, true);

            // Make dimension length and return size compatible.
            if(ASRUtils::extract_kind_from_ttype_t(
                ASRUtils::expr_type(m_dims[i].m_length)) > RESULT_KIND){
                    asr_to_llvm_visitor->tmp = builder->CreateTrunc(asr_to_llvm_visitor->tmp, llvm::IntegerType::get(context, 8 * RESULT_KIND));
            } else if (ASRUtils::extract_kind_from_ttype_t(
                ASRUtils::expr_type(m_dims[i].m_length)) < RESULT_KIND){
                asr_to_llvm_visitor->tmp = builder->CreateSExt(asr_to_llvm_visitor->tmp, llvm::IntegerType::get(context, 8 * RESULT_KIND));
            }
            llvm_size = builder->CreateMul(asr_to_llvm_visitor->tmp, llvm_size);
        }
        return llvm_size;
    }
}

Result<std::unique_ptr<LLVMModule>> asr_to_llvm(ASR::TranslationUnit_t &asr,
        diag::Diagnostics &diagnostics,
        llvm::LLVMContext &context, Allocator &al,
        LCompilers::PassManager& pass_manager,
        CompilerOptions &co, const std::string &run_fn, const std::string &/*global_underscore*/,
        const std::string &infile, LocationManager &lm)
{
    ASRToLLVMVisitor v(al, context, infile, co, diagnostics, lm);

    std::vector<int64_t> skip_optimization_func_instantiation;
    skip_optimization_func_instantiation.push_back(static_cast<int64_t>(
                    ASRUtils::IntrinsicElementalFunctions::FlipSign));
    skip_optimization_func_instantiation.push_back(static_cast<int64_t>(
                    ASRUtils::IntrinsicElementalFunctions::FMA));
    skip_optimization_func_instantiation.push_back(static_cast<int64_t>(
                    ASRUtils::IntrinsicElementalFunctions::SignFromValue));

    co.po.run_fun = run_fn;
    co.po.always_run = false;
    co.po.skip_optimization_func_instantiation = skip_optimization_func_instantiation;
    pass_manager.rtlib = co.rtlib;
    auto t1 = std::chrono::high_resolution_clock::now();
    pass_manager.apply_passes(al, &asr, co.po, diagnostics);
    auto t2 = std::chrono::high_resolution_clock::now();

    if (co.time_report) {
        int time_take_to_run_asr_passes = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
        double time_in_milliseconds = (double) time_take_to_run_asr_passes / 1000.0;
        std::string message = "";
        if ( time_in_milliseconds >= 1.0 ) {
            message = "ASR -> ASR passes: " + std::to_string(time_take_to_run_asr_passes / 1000) + "." + std::to_string(time_take_to_run_asr_passes % 1000) + " ms";
        } else {
            message = "ASR -> ASR passes: " + std::to_string(time_in_milliseconds) + " ms";
        }
        co.po.vector_of_time_report.push_back(message);
    }

    // Uncomment for debugging the ASR after the transformation
    // std::cout << LCompilers::pickle(asr, true, false, false) << std::endl;

    // GPU Metal: generate Metal shader source after passes have created GpuKernelFunction nodes
    // Always regenerate for each translation unit so that separate compilation
    // picks up the correct kernel names for every file.
    if (co.gpu_backend == "metal") {
        diag::Diagnostics metal_diag;
        Result<std::string> metal_res = asr_to_metal(al, asr, metal_diag, co);
        if (metal_res.ok) {
            co.gpu_metal_source = metal_res.result;
        }
    }

    // GPU CUDA: generate CUDA kernel source after passes have created GpuKernelFunction nodes
    if (co.gpu_backend == "cuda" && co.gpu_cuda_source.empty()) {
        diag::Diagnostics cuda_diag;
        Result<std::string> cuda_res = asr_to_cuda(al, asr, cuda_diag, co);
        if (cuda_res.ok) {
            co.gpu_cuda_source = cuda_res.result;
        }
    }

    t1 = std::chrono::high_resolution_clock::now();
    try {
        v.visit_asr((ASR::asr_t&)asr);
    } catch (const CodeGenError &e) {
        Error error;
        diagnostics.diagnostics.push_back(e.d);
        return error;
    } catch (const CodeGenAbort &) {
        LCOMPILERS_ASSERT(diagnostics.has_error())
        Error error;
        return error;
    }
    std::string msg;
    llvm::raw_string_ostream err(msg);
    if (llvm::verifyModule(*v.module, &err)) {
        std::string buf;
        llvm::raw_string_ostream os(buf);
        v.module->print(os, nullptr);
        std::cout << os.str();
        msg = "asr_to_llvm: module failed verification. Error:\n" + err.str();
        std::cout << msg << std::endl;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
        diagnostics.diagnostics.push_back(diag::Diagnostic(msg,
            diag::Level::Error, diag::Stage::CodeGen));
        Error error;
        return error;
    };

    std::unique_ptr<LLVMModule> res = std::make_unique<LLVMModule>(std::move(v.module));
    t2 = std::chrono::high_resolution_clock::now();

    if (co.time_report) {
        int time_take_to_generate_llvm_ir = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
        std::string message = "LLVM IR creation: " + std::to_string(time_take_to_generate_llvm_ir / 1000) + "." + std::to_string(time_take_to_generate_llvm_ir % 1000) + " ms";
        co.po.vector_of_time_report.push_back(message);
    }

    return res;
}

} // namespace LCompilers
