#include "ccc.h"
#include "codegen.h"
#include "scope.h"
#include "register.h"
#include "dseg.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define CGEN_PANIC(format, ...) {\
    fprintf(\
        stderr,\
        "ccc: code gen error: " format "\n" __VA_OPT__(,)\
        __VA_ARGS__);\
    exit(1);\
}

struct lval_def {
    const struct type* type;
    struct storage_location loc;
};

static struct reg* RV_REG = &RAX;
static struct reg* MULDIV_REG = &RAX;
static struct reg* MULDIV_OVERFLOW_REG = &RDX;

#define RETURN_LABEL_FMT "%s@ret"
#define FULL_REG_SZ 8
#define WORD_SZ 2

static struct dseg dseg;
static struct scope* scope;
static const struct fn_decl_node* active_fn;
static integral_t branch_counter = 0;
static integral_t loop_counter = 0;

static void enter_scope(
    struct scope* child_scope,
    integral_t bp_offset
) {
    if (child_scope == NULL ||
            child_scope->next_out != scope)
        CGEN_PANIC("enter_scope: scopes are misaligned");

    scope = child_scope;
    scope->bp_offset = bp_offset;
}

static void exit_scope(struct scope* child_scope, bool save_bp_offset) {
    if (child_scope != scope || child_scope->next_out == NULL)
        CGEN_PANIC("exit_scope: scopes are misaligned");

    scope = child_scope->next_out;
    if (save_bp_offset) scope->bp_offset = child_scope->bp_offset;
}

static struct reg* allocate_register() {
    for (integral_t i = 0; DATA_REGS[i] != NULL; i++) {
        if (!DATA_REGS[i]->is_occupied) {
            DATA_REGS[i]->is_occupied = true;
            return DATA_REGS[i];
        }
    }
    return NULL;
}

static void release_register(struct reg* reg) {
    reg->is_occupied = false;
}

static void spill_register(FILE* outfile, struct reg* reg) {
    fprintf(outfile, "\tpush %s\n", reg->qword);
    reg->is_occupied = false;
    scope->bp_offset += FULL_REG_SZ;
}

static void unspill_register(FILE* outfile, struct reg* reg) {
    fprintf(outfile, "\tpop %s\n", reg->qword);
    reg->is_occupied = true;
    scope->bp_offset -= FULL_REG_SZ;
}

static const struct data_type* get_effective_data_type(
    const struct type* type
) {
    switch (type->type) {
        case TP_DATA:
            return type->data.data_type;
        case TP_PTR:
            return &long_long_type;
    }
    CGEN_PANIC("unhandled type of type case");
}

static struct lval_def allocate_stack(
    FILE* outfile,
    const struct type* type
) {
    integral_t type_sz = get_effective_data_type(type)->sz;
    fprintf(outfile, "\tsub rsp, 0x%llx\n", type_sz);
    scope->bp_offset += type_sz;
    return (struct lval_def) {
        .loc = {
            .type = STO_STACK,
            .bp_offset = scope->bp_offset,
        },
        .type = type,
    };
}

static struct lval_def allocate_temporary(
    FILE* outfile,
    const struct type* type
) {
    struct reg* reg = allocate_register();
    if (reg == NULL) return allocate_stack(outfile, type);
    return (struct lval_def) {
        .type = type,
        .loc = {
            .type = STO_REG,
            .reg = reg,
        },
    };
}

static void release_temporary(FILE* outfile, const struct lval_def* tmp) {
    switch (tmp->loc.type) {
        case STO_REG:
            release_register(tmp->loc.reg);
            break;
        case STO_STACK:
        case STO_IMM:
        case STO_FN:
        case STO_LABEL:
            break;
        case STO_UNRESOLVED:
            CGEN_PANIC("can't release unresolved storage");
    }
}

static void emit_storage_loc(
    FILE* outfile,
    const struct storage_location* loc,
    integral_t sz
) {
    switch (loc->type) {
        case STO_LABEL:
            fprintf(outfile, "%s", loc->label);
            break;
        case STO_FN:
            fprintf(outfile, "%s", loc->decl->name);
            break;
        case STO_REG:
            if (sz > 4) fprintf(outfile, "%s", loc->reg->qword);
            else if (sz > 2) fprintf(outfile, "%s", loc->reg->dword);
            else if (sz > 1) fprintf(outfile, "%s", loc->reg->word);
            else fprintf(outfile, "%s", loc->reg->byte);
            break;
        case STO_STACK:
            if (loc->bp_offset < 0)
                fprintf(outfile, "[rbp + 0x%llx]", -loc->bp_offset);
            else if (loc->bp_offset > 0)
                fprintf(outfile, "[rbp - 0x%llx]", loc->bp_offset);
            else
                fprintf(outfile, "[rbp]");
            break;
        case STO_IMM:
            fprintf(outfile, "0x%llx", loc->value);
            break;
        case STO_UNRESOLVED:
            CGEN_PANIC("can't emit unresolved storage location");
    }
}

static bool locs_equal(
    const struct storage_location* a,
    const struct storage_location* b
) {
    if (a->type != b->type) return false;

    switch (a->type) {
        case STO_IMM:
            return a->value == b->value;
        case STO_REG:
            return strcmp(a->reg->qword, b->reg->qword) == 0;
        case STO_STACK:
            return a->bp_offset == b->bp_offset;
        case STO_LABEL:
            return strcmp(a->label, b->label) == 0;
        case STO_FN:
            return a->decl == b->decl;
        case STO_UNRESOLVED:
            return false;
    }
    CGEN_PANIC("unhandled storage type case");
}

static void emit_size_const(FILE* outfile, integral_t sz) {
    if (sz > 4) fprintf(outfile, "qword ");
    else if (sz > 2) fprintf(outfile, "dword ");
    else if (sz > 1) fprintf(outfile, "word ");
    else fprintf(outfile, "byte ");
}

static void emit_mov(
    FILE* outfile,
    const struct lval_def* dst,
    const struct storage_location* src
) {
    /* first optimization: if dst == src, emit nothing */
    if (locs_equal(&dst->loc, src)) return;

    integral_t dst_sz = get_effective_data_type(dst->type)->sz;
    switch (dst->loc.type) {
        case STO_REG:
            if (src->type == STO_REG && dst_sz < 4) {
                fprintf(outfile, "\tmovzx ");
                emit_storage_loc(outfile, &dst->loc, FULL_REG_SZ);
            } else {
                fprintf(outfile, "\tmov ");
                emit_storage_loc(outfile, &dst->loc, dst_sz);
            }

            fprintf(outfile, ", ");
            emit_storage_loc(outfile, src, dst_sz);
            break;
        case STO_STACK:
            if (src->type == STO_STACK) {
                /* `mov mem, mem` is illegal in x86_64 */
                if (dst_sz == FULL_REG_SZ || dst_sz == WORD_SZ) {
                    /* if we can swing it, use the stack as the intermediary */
                    fprintf(outfile, "\tpush ");
                    emit_size_const(outfile, dst_sz);
                    emit_storage_loc(outfile, &dst->loc, dst_sz);
                    fprintf(outfile, "\n\tpop ");
                    emit_size_const(outfile, dst_sz);
                    emit_storage_loc(outfile, src, dst_sz);
                    break;
                }

                struct reg* tmp_reg = allocate_register();
                bool spill_reg = tmp_reg == NULL;
                if (spill_reg) {
                    spill_register(outfile, &RCX);
                    tmp_reg = &RCX;
                }

                struct lval_def tmp = {
                    .type = dst->type,
                    .loc = {
                        .type = STO_REG,
                        .reg = tmp_reg,
                    }
                };
                emit_mov(outfile, &tmp, src);
                emit_mov(outfile, dst, &tmp.loc);

                if (spill_reg) unspill_register(outfile, &RCX);
                else release_register(tmp_reg);
                return;
            }

            fprintf(outfile, "\tmov ");
            if (src->type == STO_IMM) emit_size_const(outfile, dst_sz);

            emit_storage_loc(outfile, &dst->loc, dst_sz);
            fprintf(outfile, ", ");
            emit_storage_loc(outfile, src, dst_sz);
            break;
        case STO_LABEL:
        case STO_IMM:
        case STO_FN:
        case STO_UNRESOLVED:
            CGEN_PANIC("can't move value into storage type");
    }
    fprintf(outfile, "\n");
}

static void emit_cmp_zero(FILE* outfile, const struct lval_def* lval) {
    fprintf(outfile, "\tcmp ");
    integral_t type_sz = get_effective_data_type(lval->type)->sz;
    switch (lval->loc.type) {
        case STO_REG:
            emit_storage_loc(outfile, &lval->loc, type_sz);
            break;
        case STO_STACK:
            emit_size_const(outfile, type_sz);
            emit_storage_loc(outfile, &lval->loc, type_sz);
            break;
        case STO_LABEL:
        case STO_IMM:
        case STO_FN:
        case STO_UNRESOLVED:
            CGEN_PANIC("can't compare this storage type")
    }
    fprintf(outfile, ", 0\n");
}

static void emit_expr(
    FILE* outfile,
    const struct expr_node* node,
    const struct lval_def* dst);

static void emit_int_lit(
    FILE* outfile,
    const struct int_lit_node* node,
    const struct lval_def* dst
) {
    if (dst != NULL)
        emit_mov(
            outfile,
            dst,
            &(struct storage_location) {
                .type = STO_IMM,
                .value = node->val,
            });
}

static void emit_float_lit(
    FILE* outfile,
    const struct float_lit_node* node,
    const struct lval_def* dst
) {
    if (dst != NULL) {
        CGEN_PANIC("float literals are not implemented");        
    }
}

static void emit_char_lit(
    FILE* outfile,
    const struct char_lit_node* node,
    const struct lval_def* dst
) {
    if (dst != NULL) {
        emit_mov(
            outfile,
            dst,
            &(struct storage_location) {
                .type = STO_IMM,
                .value = (integral_t) node->val
            });
    }
}

static void emit_str_lit(
    FILE* outfile,
    const struct str_lit_node* node,
    const struct lval_def* dst
) {
    if (dst != NULL) {
        const char* dseg_sym = dseg_put(&dseg, (struct dseg_entry) {
            .type = ENT_STRING,
            .key.string = node->val,
        });
        emit_mov(outfile, dst, &(struct storage_location) {
            .type = STO_LABEL,
            .label = dseg_sym,
        });
    }
}

static void emit_var_ref(
    FILE* outfile,
    const struct var_ref_node* node,
    const struct lval_def* dst
) {
    if (dst != NULL) {
        emit_mov(outfile, dst, &node->def_ref->loc);
    }
}

static void emit_stmt(FILE* outfile, const struct stmt_node* node);

static void emit_decl(
    FILE* outfile,
    const struct decl_node* node
) {
    struct lval_def var_dst =
        allocate_stack(outfile, node->def_ref->type);
    node->def_ref->loc = var_dst.loc;

    fprintf(outfile, "\t; %s\n", node->def_ref->name);

    if (node->initial_value != NULL)
        emit_expr(outfile, node->initial_value, &var_dst);
}

static void emit_decl_list(
    FILE* outfile,
    const struct decl_list_node* node
) {
    for (const struct decl_node* cur = node->head;
            cur != NULL;
            cur = cur->next)
        emit_decl(outfile, cur);
}

static void emit_assignment(
    FILE* outfile,
    const struct assign_node* node,
    const struct lval_def* dst
) {
    struct lval_def lval_def;
    switch (node->lval->type) {
        case EXPR_VAR_REF:
            struct var_ref_node* var_ref = &node->lval->inner.var_ref;
            lval_def = (struct lval_def) {
                .type = var_ref->def_ref->type,
                .loc = var_ref->def_ref->loc,
            };
            break;
        default:
            CGEN_PANIC("expression is not assignable");
    }
    emit_expr(outfile, node->rval, &lval_def);
    if (dst != NULL) emit_mov(outfile, dst, &lval_def.loc);
}

static integral_t push_stack_args(FILE* outfile, struct expr_list_node* arg) {
    if (arg == NULL) return 0;

    integral_t args_sz =
        get_effective_data_type(arg->resolved_type)->sz
        + push_stack_args(outfile, arg->next);

    struct lval_def arg_dst = allocate_stack(outfile, arg->resolved_type);
    emit_expr(outfile, arg->expr, &arg_dst);

    return args_sz;
}

static void emit_call(
    FILE* outfile,
    const struct call_node* node,
    const struct lval_def* dst
) {
    /* 1. spill all existing data registers besides dst */
    const struct reg* dst_reg = dst->loc.type == STO_REG ? dst->loc.reg : NULL;
    integral_t reg_occupied = 0, n_data_regs = 0;
    for (; DATA_REGS[n_data_regs] != NULL; n_data_regs++) {
        if (!DATA_REGS[n_data_regs]->is_occupied) continue;
        if (DATA_REGS[n_data_regs] == dst_reg) continue;

        reg_occupied |= 1 << n_data_regs;
        spill_register(outfile, DATA_REGS[n_data_regs]);
    }

    /* 2. evaluate arguments in reverse order into their respective locations */
    struct expr_list_node* arg = node->args;    
    for (integral_t i = 0;
            arg != NULL && CALLING_CONV[i] != NULL;
            arg = arg->next, i++) {
        CALLING_CONV[i]->is_occupied = true;
        emit_expr(outfile, arg->expr, &(struct lval_def) {
            .type = arg->resolved_type,
            .loc = {
                .type = STO_REG,
                .reg = CALLING_CONV[i],
            }
        });
    }
    integral_t arg_stack_space = push_stack_args(outfile, arg);

    /* 3. `call <label>` */
    fprintf(outfile, "\tcall %s\n", node->fn_ref->name);

    /* 4. `mov dst, rax` */
    emit_mov(outfile, dst, &(struct storage_location) {
        .type = STO_REG,
        .reg = &RAX,
    });

    /* 5. destroy stack-based arg holders */
    if (arg_stack_space > 0)
        fprintf(outfile, "\tadd rsp, %llu\n", arg_stack_space);

    /* 6. unspill all data registers in reverse order */
    for (integral_t i = n_data_regs; i > 0; i--) {
        integral_t reg_idx = i - 1;
        DATA_REGS[reg_idx]->is_occupied = false;
        if (!(reg_occupied & (1 << reg_idx))) continue;
        unspill_register(outfile, DATA_REGS[reg_idx]);
    }
}

static void emit_unary(
    FILE* outfile,
    const struct unary_node* node,
    const struct lval_def* dst
) {
    emit_expr(outfile, node->expr, dst);
    if (dst == NULL) return;

    integral_t dst_sz = get_effective_data_type(dst->type)->sz;
    switch (node->op) {
        case UNARY_NEG:
            fprintf(outfile, "\tneg ");

            if (dst->loc.type == STO_STACK)
                emit_size_const(outfile, dst_sz);

            emit_storage_loc(outfile, &dst->loc, dst_sz);
            fprintf(outfile, "\n");
            break;
    }
}

/* TODO: tighten up/enforce with regard to up-casting smaller operands */
static void emit_binary(
    FILE* outfile,
    const struct binary_node* node,
    const struct lval_def* dst
) {
    if (dst == NULL) {
        emit_expr(outfile, node->lhs, NULL);
        emit_expr(outfile, node->rhs, NULL);
        return;
    }

    /* LHS goes in RAX explicitly because imul and idiv are weird */
    bool clobber_dst = dst->loc.type == STO_REG && dst->loc.reg == MULDIV_REG;
    bool spill_numerator = MULDIV_REG->is_occupied && !clobber_dst;
    if (spill_numerator) spill_register(outfile, MULDIV_REG);
    MULDIV_REG->is_occupied = true;
    struct lval_def lhs_dst = {
        .type = dst->type,
        .loc = {
            .type = STO_REG,
            .reg = MULDIV_REG,
        }
    };
    emit_expr(outfile, node->lhs, &lhs_dst);

    /* overflow is going to clobber RDX */
    bool spill_overflow = MULDIV_OVERFLOW_REG->is_occupied;
    if (spill_overflow) spill_register(outfile, MULDIV_OVERFLOW_REG);
    MULDIV_OVERFLOW_REG->is_occupied = true;
    struct storage_location overflow_loc = {
        .type = STO_REG,
        .reg = MULDIV_OVERFLOW_REG,
    };

    /* RHS can go wherever */
    struct lval_def rhs_dst = allocate_temporary(outfile, dst->type);
    emit_expr(outfile, node->rhs, &rhs_dst);

    integral_t dst_sz = get_effective_data_type(dst->type)->sz;
    switch (node->op) {
        case BINARY_ADD:
            fprintf(outfile, "\tadd ");
            emit_storage_loc(outfile, &lhs_dst.loc, dst_sz);
            fprintf(outfile, ", ");
            break;
        case BINARY_SUB:
            fprintf(outfile, "\tsub ");
            emit_storage_loc(outfile, &lhs_dst.loc, dst_sz);
            fprintf(outfile, ", ");
            break;
        case BINARY_MUL:
            fprintf(outfile, "\timul ");
            if (rhs_dst.loc.type == STO_STACK)
                emit_size_const(outfile, dst_sz);
            break;
        case BINARY_DIV:
            /* nothing in the top half reg */
            fprintf(outfile, "\txor ");
            emit_storage_loc(outfile, &overflow_loc, FULL_REG_SZ);
            fprintf(outfile, ", ");
            emit_storage_loc(outfile, &overflow_loc, FULL_REG_SZ);
            fprintf(outfile, "\n");

            fprintf(outfile, "\tidiv ");
            if (rhs_dst.loc.type == STO_STACK)
                emit_size_const(outfile, dst_sz);
            break;
    }

    emit_storage_loc(outfile, &rhs_dst.loc, dst_sz);
    fprintf(outfile, "\n");

    /* we don't care about overflow */
    if (spill_overflow) unspill_register(outfile, MULDIV_OVERFLOW_REG);
    else release_register(MULDIV_OVERFLOW_REG);

    /* answer's already in RAX */
    if (clobber_dst) return;
    emit_mov(outfile, dst, &lhs_dst.loc);

    if (spill_numerator) unspill_register(outfile, MULDIV_REG);
    else release_register(MULDIV_REG);
}

static void emit_expr_list(
    FILE* outfile,
    const struct expr_list_node* node,
    const struct lval_def* dst
) {
    for (; node != NULL; node = node->next)
        emit_expr(outfile, node->expr, node->next == NULL ? dst : NULL);
}

static void emit_cast(
    FILE* outfile,
    const struct cast_node* node,
    const struct lval_def* dst
) {
    /* TODO: for anything but a reinterpret cast this is garbage */
    emit_expr(outfile, node->expr, dst);
}

static void emit_expr(
    FILE* outfile,
    const struct expr_node* node,
    const struct lval_def* dst
) {
    switch (node->type) {
        case EXPR_INT_LIT:
            emit_int_lit(outfile, &node->inner.int_lit, dst);
            break;
        case EXPR_FLOAT_LIT:
            emit_float_lit(outfile, &node->inner.float_lit, dst);
            break;
        case EXPR_CHAR_LIT:
            emit_char_lit(outfile, &node->inner.char_lit, dst);
            break;
        case EXPR_STR_LIT:
            emit_str_lit(outfile, &node->inner.str_lit, dst);
            break;
        case EXPR_VAR_REF:
            emit_var_ref(outfile, &node->inner.var_ref, dst);
            break;
        case EXPR_ASSIGN:
            emit_assignment(outfile, &node->inner.assign, dst);
            break;
        case EXPR_CALL:
            emit_call(outfile, &node->inner.call, dst);
            break;
        case EXPR_UNARY:
            emit_unary(outfile, &node->inner.unary, dst);
            break;
        case EXPR_BINARY:
            emit_binary(outfile, &node->inner.binary, dst);
            break;
        case EXPR_PAREN:
            emit_expr_list(outfile, node->inner.paren.expr_list, dst);
            break;
        case EXPR_CAST:
            emit_cast(outfile, &node->inner.cast, dst);
            break;
    }
}

static void emit_return(FILE* outfile, const struct return_node* node) {
    if (active_fn == NULL) CGEN_PANIC("must be inside a function to return");
    if (node->ret_val != NULL)
        emit_expr_list(
            outfile,
            node->ret_val,
            &(struct lval_def) {
                .type = &active_fn->return_type,
                .loc = {
                    .type = STO_REG,
                    .reg = RV_REG,
                },
            });

    fprintf(outfile, "\tjmp " RETURN_LABEL_FMT "\n", active_fn->name);
}

static void emit_group_contents(FILE* outfile, const struct group_node* node) {
    const struct stmt_node* body_node = node->head;
    while (body_node != NULL) {
        emit_stmt(outfile, body_node);
        body_node = body_node->next;
    }
}

static void emit_group(FILE* outfile, const struct group_node* node) {
    enter_scope(node->scope, scope->bp_offset);

    emit_group_contents(outfile, node);

    /* don't reset sp because alloca needs to work */
    scope->next_out->bp_offset = scope->bp_offset;
    exit_scope(node->scope, true);
}

static void emit_if(FILE* outfile, const struct if_node* node) {
    enter_scope(node->scope, scope->bp_offset);

    struct lval_def cond_result =
        allocate_temporary(outfile, node->cond->resolved_type);
    emit_expr_list(outfile, node->cond, &cond_result);
    emit_cmp_zero(outfile, &cond_result);
    release_temporary(outfile, &cond_result);

    integral_t branch_num = ++branch_counter;
    fprintf(outfile, "\tjz branch_false@%lld\n", branch_num);
    emit_stmt(outfile, node->true_branch);

    if (node->false_branch == NULL) {
        fprintf(outfile, "branch_false@%lld:\n", branch_num);
    } else {
        fprintf(outfile, "\tjmp branch_done@%lld\n", branch_num);
        fprintf(outfile, "branch_false@%lld:\n", branch_num);
        emit_stmt(outfile, node->false_branch);
        fprintf(outfile, "branch_done@%lld:\n", branch_num);
    }

    exit_scope(node->scope, true);
}

static void emit_loop_init(FILE* outfile, const struct loop_init_node* node) {
    switch (node->type) {
        case INIT_EXPR_LIST:
            emit_expr_list(outfile, node->expr_list, NULL);
            break;
        case INIT_DECL_LIST:
            emit_decl_list(outfile, node->decl_list);
            break;
    }
}

static void emit_loop(FILE* outfile, const struct loop_node* node) {
    enter_scope(node->scope, scope->bp_offset);

    if (node->init != NULL) emit_loop_init(outfile, node->init);

    integral_t loop_num = ++loop_counter;
    if (node->cond != NULL) {
        struct lval_def cond_dst =
            allocate_temporary(outfile, node->cond->resolved_type);
        fprintf(outfile, "loop_head@%lld:\n", loop_num);
        emit_expr_list(outfile, node->cond, &cond_dst);
        emit_cmp_zero(outfile, &cond_dst);
        release_temporary(outfile, &cond_dst);
        fprintf(outfile, "\tjz loop_done@%lld\n", loop_num);
    } else {
        fprintf(outfile, "loop_head@%lld:\n", loop_num);
    }

    emit_stmt(outfile, node->body);
    if (node->incr != NULL) emit_expr_list(outfile, node->incr, NULL);

    fprintf(outfile, "\tjmp loop_head@%lld\n", loop_num);
    fprintf(outfile, "loop_done@%lld:\n", loop_num);

    exit_scope(node->scope, true);
}

static void emit_stmt(FILE* outfile, const struct stmt_node* node) {
    switch (node->type) {
        case STMT_EMPTY:
            break;
        case STMT_DECL_LIST:
            emit_decl_list(outfile, &node->inner.decl_list);
            break;
        case STMT_RETURN:
            emit_return(outfile, &node->inner.return_);
            break;
        case STMT_EXPR_LIST:
            emit_expr_list(outfile, &node->inner.expr_list, NULL);
            break;
        case STMT_GROUP:
            emit_group(outfile, &node->inner.group);
            break;
        case STMT_IF:
            emit_if(outfile, &node->inner.if_);
            break;
        case STMT_LOOP:
            emit_loop(outfile, &node->inner.loop);
            break;
    }
}

static void emit_fn_decl(FILE* outfile, const struct fn_decl_node* node) {
    if (active_fn != NULL)
        CGEN_PANIC(
            "can't define function %s inside function %s",
            node->name,
            active_fn->name);
    active_fn = node;

    /* TODO: we need to account for the base pointer moving in var locs */
    fprintf(outfile, "%s:\n", node->name);
    fprintf(outfile, "\tpush rbp\n");
    fprintf(outfile, "\tmov rbp, rsp\n");

    enter_scope(node->scope, 0);

    sintegral_t spilled_bp_ofs = -16; // return address + spilled rbp
    struct arg_decl_node* arg_decl = node->args;
    for (integral_t i = 0; arg_decl != NULL; arg_decl = arg_decl->next, i++) {
        struct var_def* arg_def = arg_decl->def_ref;
        fprintf(outfile, "\t; %s\n", arg_def->name);
        struct lval_def arg_dst =
            allocate_stack(outfile, arg_def->type);
        arg_def->loc = arg_dst.loc;

        struct storage_location arg_src;
        if (CALLING_CONV[i] != NULL) {
            arg_src = (struct storage_location) {
                .type = STO_REG,
                .reg = CALLING_CONV[i]
            };
            CALLING_CONV[i]->is_occupied = false;
        } else {
            arg_src = (struct storage_location) {
                .type = STO_STACK,
                .bp_offset = spilled_bp_ofs,
            };
            spilled_bp_ofs -= get_effective_data_type(arg_def->type)->sz;
        }
        emit_mov(outfile, &arg_dst, &arg_src);
    }

    enter_scope(node->body.scope, scope->bp_offset);
    emit_group_contents(outfile, &node->body);
    exit_scope(node->body.scope, true);

    fprintf(outfile, RETURN_LABEL_FMT ":\n", node->name);
    fprintf(outfile, "\tmov rsp, rbp\n");
    fprintf(outfile, "\tpop rbp\n");
    fprintf(outfile, "\tret\n");

    active_fn = NULL;
    exit_scope(node->scope, false);
}

static void emit_root_node(FILE* outfile, const struct root_node* node) {
    switch (node->type) {
        case ROOT_FN_DECL:
            emit_fn_decl(outfile, &node->inner.fn_decl);
            break;
    }
}

void emit_code(struct ast* ast, const char* path) {
    FILE* outfile = fopen(path, "w");
    if (outfile == NULL) CCC_PANIC;

    dseg_init(&dseg);
    scope = ast->root_scope;

    /* output all function declarations in the root scope as globals */
    const struct root_node* node = ast->root_node;
    for (; node != NULL; node = node->next) {
        if (node->type != ROOT_FN_DECL) continue;
        fprintf(outfile, "global %s\n", node->inner.fn_decl.name);
    }
    fprintf(outfile, "\n");

    /* text (code) segment */
    fprintf(outfile, "section .text\n");
    node = ast->root_node;
    while (node != NULL) {
        emit_root_node(outfile, node);
        if (node->next != NULL) fprintf(outfile, "\n");
        node = node->next;
    }

    /* data segment */
    fprintf(outfile, "\n");
    emit_dseg(outfile, &dseg);

    dseg_destroy(&dseg);
    fclose(outfile);
}
