Affix

 view release on metacpan or  search on metacpan

infix/src/arch/riscv/abi_riscv64.c  view on Meta::CPAN

 *   - 2xXLEN (16-byte) floating-point scalars (`long double`) are passed in an
 *     even-aligned FPR pair.
 *
 * - **Aggregate Classification:**
 *   - Aggregates larger than 2xXLEN (16 bytes) are passed by reference (a pointer
 *     in a GPR). Larger return values are returned via a hidden pointer in a0.
 *   - Aggregates of at most 2xXLEN bits containing only floating-point members are
 *     passed in FPRs, one FPR per member, in order.
 *   - All other aggregates are passed in GPRs.
 *
 * - **Variadic Calls:**
 *   - Variadic arguments use the integer calling convention: floats and doubles are
 *     passed in GPRs, not FPRs.
 *   - Once any variadic argument has been placed on the stack, all subsequent
 *     variadic arguments are passed on the stack.
 *
 * - **Return Values:**
 *   - Scalars, 2xXLEN scalars, and aggregates up to 16 bytes are returned in
 *     registers (a0/a1, fa0/fa1). Larger aggregates use the memory strategy.
 *
 * - **Stack Alignment:** The stack pointer is always 16-byte aligned; stack argument
 *   slots are 8-byte wide (16-byte arguments are 16-byte aligned).
 * @endinternal
 */
#include "arch/riscv/abi_riscv64_common.h"
#include "arch/riscv/abi_riscv64_emitters.h"
#include "common/infix_internals.h"
#include "common/utility.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

/** @internal The GPRs used for the first 8 integer/pointer arguments (a0-a7). */
static const riscv_gpr GPR_ARGS[] = {X_A0_REG, X_A1_REG, X_A2_REG, X_A3_REG, X_A4_REG, X_A5_REG, X_A6_REG, X_A7_REG};
/** @internal The FPRs used for the first 8 floating-point arguments (fa0-fa7). */
static const riscv_fpr FPR_ARGS[] = {
    F_FA0_REG, F_FA1_REG, F_FA2_REG, F_FA3_REG, F_FA4_REG, F_FA5_REG, F_FA6_REG, F_FA7_REG};
/** @internal The number of GPRs available for argument passing. */
#define RV_NUM_GPR_ARGS 8
/** @internal The number of FPRs available for argument passing. */
#define RV_NUM_FPR_ARGS 8
/** @internal A safety limit on the number of FP members to classify in an aggregate. */
#define RV_MAX_FLATTENED_FIELDS 32
/** @internal A safety limit on the recursion depth when flattening an aggregate. */
#define RV_MAX_FLATTEN_DEPTH 32
/** @internal Stack space reserved for the three callee-saved context registers (s1/s2/s3). */
#define RV_FWD_SAVED_SIZE 32
/** @internal Stack space reserved for the saved return address in a reverse stub. */
#define RV_REV_SAVED_SIZE 16

/** @internal The classification result for a single argument. */
typedef struct {
    infix_arg_location_type type; /**< The physical location (GPR/FPR/Pair/Stack). */
    uint8_t reg_index;            /**< The first register used (GPR base for MIXED). */
    uint8_t reg_index2;           /**< The second register (FPR base for MIXED). */
    uint32_t num_regs;            /**< Number of registers (for FPR aggregates / 2xXLEN). */
    uint32_t stack_offset;        /**< Byte offset from the stack pointer. */
} rv64_arg_class;

/** @internal The v-table of RISC-V functions for generating forward trampolines. */
static infix_status prepare_forward_call_frame_riscv64(infix_arena_t * arena,
                                                       infix_call_frame_layout ** out_layout,
                                                       infix_type * ret_type,
                                                       infix_type ** arg_types,
                                                       size_t num_args,
                                                       size_t num_fixed_args,
                                                       void * target_fn);
static infix_status generate_forward_prologue_riscv64(code_buffer * buf, infix_call_frame_layout * layout);
static infix_status generate_forward_argument_moves_riscv64(code_buffer * buf,
                                                            infix_call_frame_layout * layout,
                                                            infix_type ** arg_types,
                                                            size_t num_args,
                                                            c23_maybe_unused size_t num_fixed_args);
static infix_status generate_forward_call_instruction_riscv64(code_buffer *, infix_call_frame_layout *);
static infix_status generate_forward_epilogue_riscv64(code_buffer * buf,
                                                      infix_call_frame_layout * layout,
                                                      infix_type * ret_type);
const infix_forward_abi_spec g_riscv64_forward_spec = {
    .prepare_forward_call_frame = prepare_forward_call_frame_riscv64,
    .generate_forward_prologue = generate_forward_prologue_riscv64,
    .generate_forward_argument_moves = generate_forward_argument_moves_riscv64,
    .generate_forward_call_instruction = generate_forward_call_instruction_riscv64,
    .generate_forward_epilogue = generate_forward_epilogue_riscv64};

/** @internal The v-table of RISC-V functions for generating reverse trampolines. */
static infix_status prepare_reverse_call_frame_riscv64(infix_arena_t * arena,
                                                       infix_reverse_call_frame_layout ** out_layout,
                                                       infix_reverse_t * context);
static infix_status generate_reverse_prologue_riscv64(code_buffer * buf, infix_reverse_call_frame_layout * layout);
static infix_status generate_reverse_argument_marshalling_riscv64(code_buffer * buf,
                                                                  infix_reverse_call_frame_layout * layout,
                                                                  infix_reverse_t * context);
static infix_status generate_reverse_dispatcher_call_riscv64(code_buffer * buf,
                                                             infix_reverse_call_frame_layout * layout,
                                                             infix_reverse_t * context);
static infix_status generate_reverse_epilogue_riscv64(code_buffer * buf,
                                                      infix_reverse_call_frame_layout * layout,
                                                      infix_reverse_t * context);
const infix_reverse_abi_spec g_riscv64_reverse_spec = {
    .prepare_reverse_call_frame = prepare_reverse_call_frame_riscv64,
    .generate_reverse_prologue = generate_reverse_prologue_riscv64,
    .generate_reverse_argument_marshalling = generate_reverse_argument_marshalling_riscv64,
    .generate_reverse_dispatcher_call = generate_reverse_dispatcher_call_riscv64,
    .generate_reverse_epilogue = generate_reverse_epilogue_riscv64};

/** @internal The v-table for the new Direct Marshalling ABI. */
static infix_status prepare_direct_forward_call_frame_riscv64(infix_arena_t * arena,
                                                              infix_direct_call_frame_layout ** out_layout,
                                                              infix_type * ret_type,
                                                              infix_type ** arg_types,
                                                              size_t num_args,
                                                              infix_direct_arg_handler_t * handlers,
                                                              void * target_fn);
static infix_status generate_direct_forward_prologue_riscv64(code_buffer * buf,
                                                             infix_direct_call_frame_layout * layout);
static infix_status generate_direct_forward_argument_moves_riscv64(code_buffer * buf,
                                                                   infix_direct_call_frame_layout * layout);
static infix_status generate_direct_forward_call_instruction_riscv64(code_buffer * buf,
                                                                     infix_direct_call_frame_layout * layout);
static infix_status generate_direct_forward_epilogue_riscv64(code_buffer * buf,
                                                             infix_direct_call_frame_layout * layout,
                                                             infix_type * ret_type);
const infix_direct_forward_abi_spec g_riscv64_direct_forward_spec = {
    .prepare_direct_forward_call_frame = prepare_direct_forward_call_frame_riscv64,
    .generate_direct_forward_prologue = generate_direct_forward_prologue_riscv64,
    .generate_direct_forward_argument_moves = generate_direct_forward_argument_moves_riscv64,
    .generate_direct_forward_call_instruction = generate_direct_forward_call_instruction_riscv64,
    .generate_direct_forward_epilogue = generate_direct_forward_epilogue_riscv64};

//
// Low-level helpers
//

/**
 * @internal
 * @brief Emit `rd = rs_base + offset`, materializing the offset via the `li` expansion
 *        when it does not fit in a 12-bit immediate.
 */
static void rv64_emit_compute_addr(code_buffer * buf, uint8_t rd, uint8_t rs_base, int32_t offset) {
    if (offset >= -2048 && offset <= 2047) {
        infix_riscv64_emit_addi(buf, rd, rs_base, offset);
        return;
    }
    infix_riscv64_emit_load_u64_immediate(buf, rd, (uint64_t)(int64_t)offset);
    infix_riscv64_emit_add(buf, rd, rs_base, rd);
}

/** @internal Wide-offset `ld rd, offset(rs_base)`. */
static void rv64_mem_ld(code_buffer * buf, uint8_t rd, uint8_t rs_base, int32_t off) {
    if (off >= -2048 && off <= 2047) {
        infix_riscv64_emit_ld(buf, rd, rs_base, off);
        return;
    }
    rv64_emit_compute_addr(buf, RV_SCRATCH0_REG, rs_base, off);
    infix_riscv64_emit_ld(buf, rd, RV_SCRATCH0_REG, 0);
}

/** @internal Wide-offset `sd data, offset(rs_base)`. */
static void rv64_mem_sd(code_buffer * buf, uint8_t rs_base, uint8_t data, int32_t off) {
    if (off >= -2048 && off <= 2047) {
        infix_riscv64_emit_sd(buf, rs_base, data, off);
        return;
    }
    rv64_emit_compute_addr(buf, RV_SCRATCH0_REG, rs_base, off);
    infix_riscv64_emit_sd(buf, RV_SCRATCH0_REG, data, 0);
}

/** @internal Wide-offset `lw rd, offset(rs_base)`. */
static void rv64_mem_lw(code_buffer * buf, uint8_t rd, uint8_t rs_base, int32_t off) {
    if (off >= -2048 && off <= 2047) {
        infix_riscv64_emit_lw(buf, rd, rs_base, off);
        return;
    }
    rv64_emit_compute_addr(buf, RV_SCRATCH0_REG, rs_base, off);
    infix_riscv64_emit_lw(buf, rd, RV_SCRATCH0_REG, 0);
}

/** @internal Wide-offset `sw data, offset(rs_base)`. */
static void rv64_mem_sw(code_buffer * buf, uint8_t rs_base, uint8_t data, int32_t off) {
    if (off >= -2048 && off <= 2047) {
        infix_riscv64_emit_sw(buf, rs_base, data, off);
        return;
    }
    rv64_emit_compute_addr(buf, RV_SCRATCH0_REG, rs_base, off);

infix/src/arch/riscv/abi_riscv64.c  view on Meta::CPAN

        rv64_all_leaf_list leaves;
        if (rv64_flatten_all(type, &leaves)) {
            size_t fp = 0;
            size_t int_idx = SIZE_MAX;
            size_t long_leaf = SIZE_MAX;
            for (size_t j = 0; j < leaves.count; ++j) {
                const rv64_all_leaf * leaf = &leaves.leaves[j];
                if (leaf->is_fp && leaf->size <= 8)
                    fp++;
                else if (!leaf->is_fp && leaf->size <= 8)
                    int_idx = j;
                else
                    long_leaf = j;
            }
            // One FP real + one integer (either order): FPR + GPR.
            if (fp == 1 && int_idx != SIZE_MAX && long_leaf == SIZE_MAX && leaves.count == 2) {
                if (*vpr_count < RV_NUM_FPR_ARGS && *gpr_count < RV_NUM_GPR_ARGS) {
                    cls.type = ARG_LOCATION_MIXED;
                    cls.reg_index = (uint8_t)(*gpr_count)++;   // GPR for the integer leaf.
                    cls.reg_index2 = (uint8_t)(*vpr_count)++;  // FPR for the FP leaf.
                    cls.num_regs = (1 << 8) | 1;
                    return cls;
                }
                return rv64_classify_integer(type, false, gpr_count, stack_offset, variadic_stack_mode);
            }
            // Two FP reals (or a `_Complex` scalar): two FPRs in memory order.
            if (fp == 2 && int_idx == SIZE_MAX && long_leaf == SIZE_MAX) {
                if (*vpr_count + 2 <= RV_NUM_FPR_ARGS) {
                    cls.type = ARG_LOCATION_VPR;
                    cls.reg_index = (uint8_t)*vpr_count;
                    cls.num_regs = 2;
                    *vpr_count += 2;
                    return cls;
                }
                return rv64_classify_integer(type, false, gpr_count, stack_offset, variadic_stack_mode);
            }
            // A single FP real (a one-member FP struct).
            if (fp == 1 && int_idx == SIZE_MAX && long_leaf == SIZE_MAX) {
                if (*vpr_count < RV_NUM_FPR_ARGS) {
                    cls.type = ARG_LOCATION_VPR;
                    cls.reg_index = (uint8_t)(*vpr_count)++;
                    cls.num_regs = 1;
                    return cls;
                }
                return rv64_classify_integer(type, false, gpr_count, stack_offset, variadic_stack_mode);
            }
        }
    }

    // Everything else (integers, enums, pointers, unions, 3+ leaf or 4-float
    // aggregates, {long double}) is integer.
    return rv64_classify_integer(type, false, gpr_count, stack_offset, variadic_stack_mode);
}

//
// Forward trampolines
//

/**
 * @internal
 * @brief Stage 1 (Forward): Analyzes a signature and creates a call frame layout.
 */
static infix_status prepare_forward_call_frame_riscv64(infix_arena_t * arena,
                                                       infix_call_frame_layout ** out_layout,
                                                       infix_type * ret_type,
                                                       infix_type ** arg_types,
                                                       size_t num_args,
                                                       size_t num_fixed_args,
                                                       void * target_fn) {
    if (out_layout == nullptr)
        return INFIX_ERROR_INVALID_ARGUMENT;
    infix_call_frame_layout * layout =
        infix_arena_calloc(arena, 1, sizeof(infix_call_frame_layout), _Alignof(infix_call_frame_layout));
    if (layout == nullptr) {
        *out_layout = nullptr;
        return INFIX_ERROR_ALLOCATION_FAILED;
    }
    layout->arg_locations =
        infix_arena_calloc(arena, num_args, sizeof(infix_arg_location), _Alignof(infix_arg_location));
    if (layout->arg_locations == nullptr && num_args > 0) {
        *out_layout = nullptr;
        return INFIX_ERROR_ALLOCATION_FAILED;
    }
    layout->is_variadic = (num_fixed_args < num_args);
    layout->target_fn = target_fn;
    layout->num_args = num_args;
    layout->num_stack_args = 0;

    layout->return_value_in_memory = rv64_return_in_memory(ret_type);
    // When the target returns by memory, a0 holds the hidden sret pointer.
    size_t gpr_count = layout->return_value_in_memory ? 1 : 0;
    size_t vpr_count = 0;
    uint32_t stack_offset = 0;
    bool variadic_stack_mode = false;

    for (size_t i = 0; i < num_args; ++i) {
        infix_type * type = arg_types[i];
        if (type->size > INFIX_MAX_ARG_SIZE) {
            *out_layout = nullptr;
            return INFIX_ERROR_LAYOUT_FAILED;
        }
        bool is_variadic_arg = (i >= num_fixed_args);
        rv64_arg_class cls =
            rv64_classify_arg(type, is_variadic_arg, &gpr_count, &vpr_count, &stack_offset, &variadic_stack_mode);
        layout->arg_locations[i].type = cls.type;
        layout->arg_locations[i].reg_index = cls.reg_index;
        layout->arg_locations[i].reg_index2 = cls.reg_index2;
        layout->arg_locations[i].num_regs = cls.num_regs;
        layout->arg_locations[i].stack_offset = cls.stack_offset;
        if (cls.type == ARG_LOCATION_STACK)
            layout->num_stack_args++;
    }

    layout->total_stack_alloc = (stack_offset + 15) & ~15;
    layout->num_gpr_args = (uint8_t)gpr_count;
    layout->num_vpr_args = (uint8_t)vpr_count;
    if (layout->total_stack_alloc > INFIX_MAX_STACK_ALLOC) {
        *out_layout = nullptr;
        return INFIX_ERROR_LAYOUT_FAILED;
    }
    *out_layout = layout;
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 2 (Forward): Generates the function prologue.
 * @details Saves the callee-saved context registers (s1 = target, s2 = return
 *          buffer, s3 = args array), moves the trampoline's own arguments into
 *          them, and allocates space for the outgoing stack arguments.
 */
static infix_status generate_forward_prologue_riscv64(code_buffer * buf, infix_call_frame_layout * layout) {
    infix_riscv64_emit_addi(buf, X_SP_REG, X_SP_REG, -RV_FWD_SAVED_SIZE);
    rv64_mem_sd(buf, X_SP_REG, RV_CTX_TARGET_REG, 0);
    rv64_mem_sd(buf, X_SP_REG, RV_CTX_RET_REG, 8);
    rv64_mem_sd(buf, X_SP_REG, RV_CTX_ARGS_REG, 16);
    rv64_mem_sd(buf, X_SP_REG, X_RA_REG, 24);

    layout->prologue_size = (uint32_t)buf->size;

    if (layout->target_fn == nullptr) {  // Unbound trampoline args: (target_fn, ret_ptr, args_ptr) in a0, a1, a2.
        infix_riscv64_emit_addi(buf, RV_CTX_TARGET_REG, X_A0_REG, 0);
        infix_riscv64_emit_addi(buf, RV_CTX_RET_REG, X_A1_REG, 0);
        infix_riscv64_emit_addi(buf, RV_CTX_ARGS_REG, X_A2_REG, 0);
    }
    else {  // Bound trampoline args: (ret_ptr, args_ptr) in a0, a1.
        infix_riscv64_emit_addi(buf, RV_CTX_RET_REG, X_A0_REG, 0);
        infix_riscv64_emit_addi(buf, RV_CTX_ARGS_REG, X_A1_REG, 0);
    }
    rv64_emit_stack_sub(buf, (uint32_t)layout->total_stack_alloc);
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 3 (Forward): Generates code to move arguments into their native locations.
 */
static infix_status generate_forward_argument_moves_riscv64(code_buffer * buf,
                                                            infix_call_frame_layout * layout,
                                                            infix_type ** arg_types,
                                                            size_t num_args,
                                                            c23_maybe_unused size_t num_fixed_args) {
    // Hidden pointer for large struct returns is passed in a0.
    if (layout->return_value_in_memory)
        infix_riscv64_emit_addi(buf, X_A0_REG, RV_CTX_RET_REG, 0);

    for (size_t i = 0; i < num_args; ++i) {
        infix_arg_location * loc = &layout->arg_locations[i];
        infix_type * type = arg_types[i];
        // t0 = args_array[i]
        rv64_mem_ld(buf, RV_SCRATCH0_REG, RV_CTX_ARGS_REG, (int32_t)(i * sizeof(void *)));

        switch (loc->type) {
        case ARG_LOCATION_GPR:
            if (type->category == INFIX_TYPE_ARRAY) {
                infix_riscv64_emit_addi(buf, GPR_ARGS[loc->reg_index], RV_SCRATCH0_REG, 0);
                break;
            }
            rv64_emit_load_gpr_value(buf, GPR_ARGS[loc->reg_index], RV_SCRATCH0_REG, type);
            break;
        case ARG_LOCATION_GPR_PAIR:
            rv64_mem_ld(buf, GPR_ARGS[loc->reg_index], RV_SCRATCH0_REG, 0);
            rv64_mem_ld(buf, GPR_ARGS[loc->reg_index + 1], RV_SCRATCH0_REG, 8);
            break;
        case ARG_LOCATION_GPR_STACK_SPLIT:
            // 2xXLEN value with exactly one register left: low half in the
            // register, high half in the outgoing stack slot.
            rv64_mem_ld(buf, GPR_ARGS[loc->reg_index], RV_SCRATCH0_REG, 0);
            rv64_mem_ld(buf, RV_SCRATCH1_REG, RV_SCRATCH0_REG, 8);
            rv64_mem_sd(buf, X_SP_REG, RV_SCRATCH1_REG, (int32_t)loc->stack_offset);
            break;
        case ARG_LOCATION_GPR_REFERENCE:
            infix_riscv64_emit_addi(buf, GPR_ARGS[loc->reg_index], RV_SCRATCH0_REG, 0);
            break;
        case ARG_LOCATION_VPR:
            if (type->category == INFIX_TYPE_STRUCT || type->category == INFIX_TYPE_COMPLEX)
                rv64_emit_load_fp_aggregate(buf, RV_SCRATCH0_REG, FPR_ARGS[loc->reg_index], type);
            else
                rv64_emit_load_fp_value(buf, RV_SCRATCH0_REG, FPR_ARGS[loc->reg_index], 0, type->size);
            break;
        case ARG_LOCATION_MIXED:
            rv64_emit_load_mixed(buf, RV_SCRATCH0_REG, GPR_ARGS[loc->reg_index], FPR_ARGS[loc->reg_index2], type);
            break;
        case ARG_LOCATION_STACK:
            if (type->size > 16 || type->category == INFIX_TYPE_ARRAY) {
                // By-reference / array argument: the pointer itself is the value.
                rv64_mem_sd(buf, X_SP_REG, RV_SCRATCH0_REG, (int32_t)loc->stack_offset);
                break;
            }
            if ((type->category == INFIX_TYPE_PRIMITIVE || type->category == INFIX_TYPE_POINTER ||
                 type->category == INFIX_TYPE_ENUM) &&
                type->size <= 8) {
                // Narrow scalars are extended into a full 8-byte stack slot.
                rv64_emit_load_gpr_value(buf, RV_SCRATCH1_REG, RV_SCRATCH0_REG, type);
                rv64_mem_sd(buf, X_SP_REG, RV_SCRATCH1_REG, (int32_t)loc->stack_offset);
                break;
            }
            // Aggregates up to 16 bytes and 2xXLEN scalars copy their raw bytes.
            rv64_emit_copy_memory(buf, X_SP_REG, (int32_t)loc->stack_offset, RV_SCRATCH0_REG, 0, type->size);
            break;
        default:
            break;
        }
    }
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 3.5 (Forward): Generates the call instruction.
 * @details Null-checks the target pointer (crashing via `ebreak` if null) and
 *          emits `jalr ra, target, 0`.
 */
static infix_status generate_forward_call_instruction_riscv64(code_buffer * buf,
                                                              c23_maybe_unused infix_call_frame_layout * layout) {
    uint8_t target_reg = RV_SCRATCH0_REG;
    if (layout->target_fn)
        infix_riscv64_emit_load_u64_immediate(buf, target_reg, (uint64_t)layout->target_fn);
    else
        infix_riscv64_emit_addi(buf, target_reg, RV_CTX_TARGET_REG, 0);
    // A non-null target skips the ebreak and calls the target; a null target falls
    // through into the ebreak and traps. The target returns to the epilogue.
    infix_riscv64_emit_bne(buf, target_reg, X_ZERO_REG, 8);
    infix_riscv64_emit_ebreak(buf);
    infix_riscv64_emit_jalr(buf, X_RA_REG, target_reg, 0);
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 4 (Forward): Generates the function epilogue.
 * @details Copies the return value from a0/a1/fa0/fa1 into the user's return
 *          buffer, deallocates the frame, restores the context registers, and
 *          returns to the caller.
 */
static infix_status generate_forward_epilogue_riscv64(code_buffer * buf,
                                                      infix_call_frame_layout * layout,
                                                      infix_type * ret_type) {
    layout->epilogue_offset = (uint32_t)buf->size;
    if (ret_type->category != INFIX_TYPE_VOID && !layout->return_value_in_memory) {
        rv64_return_class rc = rv64_classify_return(ret_type);
        if (rc.mixed) {
            // One integer leaf in a0 and one FP leaf in fa0.
            rv64_emit_store_fp_value(buf, RV_CTX_RET_REG, F_FA0_REG, rc.fp_offsets[0], rc.fp_sizes[0]);
            rv64_emit_store_gpr_low_bytes(buf, RV_CTX_RET_REG, X_A0_REG, rc.int_offset, rc.int_size);
        }
        else if (rc.fp_count == 2) {
            rv64_emit_store_fp_value(buf, RV_CTX_RET_REG, F_FA0_REG, rc.fp_offsets[0], rc.fp_sizes[0]);
            rv64_emit_store_fp_value(buf, RV_CTX_RET_REG, F_FA1_REG, rc.fp_offsets[1], rc.fp_sizes[1]);
        }
        else if (rc.fp_count == 1) {
            rv64_emit_store_fp_value(buf, RV_CTX_RET_REG, F_FA0_REG, rc.fp_offsets[0], rc.fp_sizes[0]);
        }
        else {
            rv64_emit_store_gpr_return(buf, RV_CTX_RET_REG, ret_type->size);
        }
    }
    rv64_emit_stack_add(buf, (uint32_t)layout->total_stack_alloc);
    rv64_mem_ld(buf, RV_CTX_TARGET_REG, X_SP_REG, 0);
    rv64_mem_ld(buf, RV_CTX_RET_REG, X_SP_REG, 8);
    rv64_mem_ld(buf, RV_CTX_ARGS_REG, X_SP_REG, 16);
    rv64_mem_ld(buf, X_RA_REG, X_SP_REG, 24);
    infix_riscv64_emit_addi(buf, X_SP_REG, X_SP_REG, RV_FWD_SAVED_SIZE);
    infix_riscv64_emit_jalr(buf, X_ZERO_REG, X_RA_REG, 0);
    return INFIX_SUCCESS;
}

//
// Reverse trampolines
//

/**
 * @internal
 * @brief Stage 1 (Reverse): Calculates the stack layout for a reverse trampoline stub.
 */
static infix_status prepare_reverse_call_frame_riscv64(infix_arena_t * arena,
                                                       infix_reverse_call_frame_layout ** out_layout,
                                                       infix_reverse_t * context) {
    infix_reverse_call_frame_layout * layout = infix_arena_calloc(
        arena, 1, sizeof(infix_reverse_call_frame_layout), _Alignof(infix_reverse_call_frame_layout));
    if (layout == nullptr)
        return INFIX_ERROR_ALLOCATION_FAILED;
    if (context->return_type->size > INFIX_MAX_ARG_SIZE) {
        *out_layout = nullptr;
        return INFIX_ERROR_LAYOUT_FAILED;
    }
    size_t return_size = (context->return_type->size + 15) & ~15;
    size_t args_array_size = (context->num_args * sizeof(void *) + 15) & ~15;
    size_t saved_args_data_size = 0;
    for (size_t i = 0; i < context->num_args; ++i) {
        if (context->arg_types[i]->size > INFIX_MAX_ARG_SIZE) {
            *out_layout = nullptr;
            return INFIX_ERROR_LAYOUT_FAILED;
        }
        saved_args_data_size += (context->arg_types[i]->size + 15) & ~15;
    }
    if (saved_args_data_size > INFIX_MAX_ARG_SIZE) {
        *out_layout = nullptr;
        return INFIX_ERROR_LAYOUT_FAILED;
    }
    size_t total_local_space = return_size + args_array_size + saved_args_data_size;
    if (total_local_space > INFIX_MAX_STACK_ALLOC) {
        *out_layout = nullptr;
        return INFIX_ERROR_LAYOUT_FAILED;
    }
    layout->total_stack_alloc = (total_local_space + 15) & ~15;
    layout->return_buffer_offset = 0;
    layout->args_array_offset = layout->return_buffer_offset + (int32_t)return_size;
    layout->saved_args_offset = layout->args_array_offset + (int32_t)args_array_size;
    *out_layout = layout;
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 2 (Reverse): Generates the prologue for the reverse trampoline stub.
 */
static infix_status generate_reverse_prologue_riscv64(code_buffer * buf, infix_reverse_call_frame_layout * layout) {
    infix_riscv64_emit_addi(buf, X_SP_REG, X_SP_REG, -RV_REV_SAVED_SIZE);
    rv64_mem_sd(buf, X_SP_REG, X_RA_REG, 0);
    layout->prologue_size = (uint32_t)buf->size;
    rv64_emit_stack_sub(buf, (uint32_t)layout->total_stack_alloc);
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 3 (Reverse): Generates code to marshal arguments into the `void**` array.
 * @details Copies each incoming argument (from GPRs, FPRs, or the caller's stack)
 *          into a saved-data area on the stub's local stack and populates
 *          `args_array` with pointers to it.
 */
static infix_status generate_reverse_argument_marshalling_riscv64(code_buffer * buf,
                                                                  infix_reverse_call_frame_layout * layout,
                                                                  infix_reverse_t * context) {
    bool return_in_memory = rv64_return_in_memory(context->return_type);
    // a0 holds the hidden sret pointer when the return value is in memory; the
    // dispatcher needs it again when it writes the return value back out, so
    // stash it in the return-buffer slot before anything clobbers a0.
    if (return_in_memory)
        rv64_mem_sd(buf, X_SP_REG, X_A0_REG, layout->return_buffer_offset);
    // a0 holds the hidden sret pointer when the return value is in memory.
    size_t gpr_idx = return_in_memory ? 1 : 0;
    size_t vpr_idx = 0;
    uint32_t stack_offset = 0;
    bool variadic_stack_mode = false;
    size_t current_saved_data_offset = 0;
    // The caller's stack arguments sit directly above our saved return address.
    const int32_t caller_stack_base = RV_REV_SAVED_SIZE + (int32_t)layout->total_stack_alloc;

    for (size_t i = 0; i < context->num_args; ++i) {
        infix_type * type = context->arg_types[i];
        bool is_variadic_arg = (i >= context->num_fixed_args);
        rv64_arg_class cls =
            rv64_classify_arg(type, is_variadic_arg, &gpr_idx, &vpr_idx, &stack_offset, &variadic_stack_mode);

        int32_t arg_save_loc = (int32_t)(layout->saved_args_offset + current_saved_data_offset);

        bool raw_pointer = false;
        switch (cls.type) {
        case ARG_LOCATION_GPR:
            rv64_mem_sd(buf, X_SP_REG, GPR_ARGS[cls.reg_index], arg_save_loc);
            break;
        case ARG_LOCATION_GPR_PAIR:
            rv64_mem_sd(buf, X_SP_REG, GPR_ARGS[cls.reg_index], arg_save_loc);
            rv64_mem_sd(buf, X_SP_REG, GPR_ARGS[cls.reg_index + 1], arg_save_loc + 8);
            break;
        case ARG_LOCATION_GPR_STACK_SPLIT:
            // 2xXLEN value split across a register and the caller's stack.
            rv64_mem_sd(buf, X_SP_REG, GPR_ARGS[cls.reg_index], arg_save_loc);
            rv64_mem_ld(buf, RV_SCRATCH0_REG, X_SP_REG, caller_stack_base + (int32_t)cls.stack_offset);
            rv64_mem_sd(buf, X_SP_REG, RV_SCRATCH0_REG, arg_save_loc + 8);
            break;
        case ARG_LOCATION_GPR_REFERENCE:
            // Large aggregate passed by reference: the argument is the pointer
            // itself, which the dispatcher dereferences. Store the raw pointer
            // straight into args_array[i].
            raw_pointer = true;
            break;
        case ARG_LOCATION_VPR:
            if (cls.num_regs > 1) {
                rv64_emit_compute_addr(buf, RV_SCRATCH0_REG, X_SP_REG, arg_save_loc);
                rv64_emit_store_fp_aggregate(buf, RV_SCRATCH0_REG, FPR_ARGS[cls.reg_index], type);
            }
            else {
                rv64_emit_store_fp_value(buf, X_SP_REG, FPR_ARGS[cls.reg_index], arg_save_loc, type->size);
            }
            break;
        case ARG_LOCATION_MIXED:
            rv64_emit_compute_addr(buf, RV_SCRATCH0_REG, X_SP_REG, arg_save_loc);
            rv64_emit_store_mixed(buf, RV_SCRATCH0_REG, GPR_ARGS[cls.reg_index], FPR_ARGS[cls.reg_index2], type);
            break;
        case ARG_LOCATION_STACK:
            if (type->size > 16) {
                // By-reference pointer passed on the caller's stack.
                rv64_mem_ld(buf, RV_SCRATCH0_REG, X_SP_REG, caller_stack_base + (int32_t)cls.stack_offset);
                rv64_mem_sd(buf, X_SP_REG, RV_SCRATCH0_REG, arg_save_loc);
            }
            else {
                rv64_emit_copy_memory(
                    buf, X_SP_REG, arg_save_loc, X_SP_REG, caller_stack_base + (int32_t)cls.stack_offset, type->size);
            }
            break;
        default:
            break;
        }

        if (raw_pointer) {
            // args_array[i] = the raw pointer value from the argument register.
            rv64_mem_sd(
                buf, X_SP_REG, GPR_ARGS[cls.reg_index], layout->args_array_offset + (int32_t)(i * sizeof(void *)));
        }
        else {
            // args_array[i] = sp + arg_save_loc
            rv64_emit_compute_addr(buf, RV_SCRATCH0_REG, X_SP_REG, arg_save_loc);
            rv64_mem_sd(buf, X_SP_REG, RV_SCRATCH0_REG, layout->args_array_offset + (int32_t)(i * sizeof(void *)));
        }

        current_saved_data_offset += (type->size + 15) & ~15;
    }
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 4 (Reverse): Generates the call to the C dispatcher.
 * @details Loads `(context, return_buffer_ptr, args_array_ptr)` into a0/a1/a2 and
 *          calls the dispatcher via `jalr`.
 */
static infix_status generate_reverse_dispatcher_call_riscv64(code_buffer * buf,
                                                             infix_reverse_call_frame_layout * layout,
                                                             infix_reverse_t * context) {
    infix_riscv64_emit_load_u64_immediate(buf, X_A0_REG, (uint64_t)context);
    if (rv64_return_in_memory(context->return_type))
        rv64_mem_ld(buf, X_A1_REG, X_SP_REG, layout->return_buffer_offset);
    else
        rv64_emit_compute_addr(buf, X_A1_REG, X_SP_REG, layout->return_buffer_offset);
    rv64_emit_compute_addr(buf, X_A2_REG, X_SP_REG, layout->args_array_offset);
    infix_riscv64_emit_load_u64_immediate(buf, RV_SCRATCH0_REG, (uint64_t)context->internal_dispatcher);
    infix_riscv64_emit_jalr(buf, X_RA_REG, RV_SCRATCH0_REG, 0);
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 5 (Reverse): Generates the epilogue for the reverse trampoline stub.
 */
static infix_status generate_reverse_epilogue_riscv64(code_buffer * buf,
                                                      infix_reverse_call_frame_layout * layout,
                                                      infix_reverse_t * context) {
    bool return_in_memory = rv64_return_in_memory(context->return_type);
    if (context->return_type->category != INFIX_TYPE_VOID && !return_in_memory) {
        rv64_return_class rc = rv64_classify_return(context->return_type);
        if (rc.mixed) {
            // One integer leaf in a0 and one FP leaf in fa0.
            rv64_emit_load_fp_value(
                buf, X_SP_REG, F_FA0_REG, layout->return_buffer_offset + rc.fp_offsets[0], rc.fp_sizes[0]);
            rv64_emit_compute_addr(buf, RV_SCRATCH0_REG, X_SP_REG, layout->return_buffer_offset + rc.int_offset);
            rv64_emit_load_gpr_value_sized(buf, X_A0_REG, RV_SCRATCH0_REG, rc.int_size, rc.int_signed);
        }
        else if (rc.fp_count == 2) {
            rv64_emit_load_fp_value(
                buf, X_SP_REG, F_FA0_REG, layout->return_buffer_offset + rc.fp_offsets[0], rc.fp_sizes[0]);
            rv64_emit_load_fp_value(
                buf, X_SP_REG, F_FA1_REG, layout->return_buffer_offset + rc.fp_offsets[1], rc.fp_sizes[1]);
        }
        else if (rc.fp_count == 1) {
            rv64_emit_load_fp_value(
                buf, X_SP_REG, F_FA0_REG, layout->return_buffer_offset + rc.fp_offsets[0], rc.fp_sizes[0]);
        }
        else {
            rv64_emit_compute_addr(buf, RV_SCRATCH0_REG, X_SP_REG, layout->return_buffer_offset);
            rv64_emit_load_gpr_value(buf, X_A0_REG, RV_SCRATCH0_REG, context->return_type);
            if (context->return_type->size > 8)
                rv64_mem_ld(buf, X_A1_REG, RV_SCRATCH0_REG, 8);
        }
    }
    rv64_emit_stack_add(buf, (uint32_t)layout->total_stack_alloc);
    rv64_mem_ld(buf, X_RA_REG, X_SP_REG, 0);
    infix_riscv64_emit_addi(buf, X_SP_REG, X_SP_REG, RV_REV_SAVED_SIZE);
    infix_riscv64_emit_jalr(buf, X_ZERO_REG, X_RA_REG, 0);
    return INFIX_SUCCESS;
}

//
// Direct marshalling trampolines
//

/** @internal Scratch-space requirements for a single direct argument. */
typedef struct {
    bool needs_scratch;
    size_t size;
    size_t align;
} rv64_direct_scratch_info;

/** @internal Compute the scratch-space needs for a direct argument. */
static rv64_direct_scratch_info rv64_direct_scratch(const infix_direct_arg_layout * arg) {
    rv64_direct_scratch_info info = {false, 0, 0};
    if (arg->handler->aggregate_marshaller) {
        info.needs_scratch = true;
        info.size = arg->type->size;
        info.align = arg->type->alignment;
    }
    else if (arg->handler->scalar_marshaller) {
        info.needs_scratch = true;
        info.size = 16;
        info.align = 16;
    }
    else if (arg->handler->writeback_handler) {
        const infix_type * pointee =
            (arg->type->category == INFIX_TYPE_POINTER) ? arg->type->meta.pointer_info.pointee_type : arg->type;
        info.needs_scratch = true;
        info.size = pointee->size;
        info.align = pointee->alignment;
    }
    return info;
}

/** @internal Recompute the standard (outgoing stack args) allocation size. */
static size_t rv64_direct_standard_alloc(const infix_direct_call_frame_layout * layout) {
    size_t stack_offset = 0;
    for (size_t i = 0; i < layout->num_args; ++i) {
        const infix_arg_location * loc = &layout->args[i].location;
        if (loc->type == ARG_LOCATION_STACK) {
            size_t s = layout->args[i].type->size;
            if (s > 16)
                s = 8;  // By-reference pointers occupy a single 8-byte slot.
            size_t end = loc->stack_offset + ((s + 7) & ~7);
            if (end > stack_offset)
                stack_offset = end;
        }
        else if (loc->type == ARG_LOCATION_GPR_STACK_SPLIT) {
            // The high half of a split 2xXLEN value occupies an 8-byte stack slot.
            size_t end = loc->stack_offset + 8;
            if (end > stack_offset)
                stack_offset = end;
        }
    }
    return (stack_offset + 15) & ~15;
}

/**
 * @internal
 * @brief Stage 1 (Direct): Analyzes a signature and creates a direct call frame layout.
 */
static infix_status prepare_direct_forward_call_frame_riscv64(infix_arena_t * arena,
                                                              infix_direct_call_frame_layout ** out_layout,
                                                              infix_type * ret_type,
                                                              infix_type ** arg_types,
                                                              size_t num_args,
                                                              infix_direct_arg_handler_t * handlers,
                                                              void * target_fn) {
    infix_call_frame_layout * standard_layout = nullptr;
    infix_status status =
        prepare_forward_call_frame_riscv64(arena, &standard_layout, ret_type, arg_types, num_args, num_args, target_fn);
    if (status != INFIX_SUCCESS)
        return status;

    infix_direct_call_frame_layout * layout =
        infix_arena_calloc(arena, 1, sizeof(infix_direct_call_frame_layout), _Alignof(infix_direct_call_frame_layout));
    if (layout == nullptr)
        return INFIX_ERROR_ALLOCATION_FAILED;
    layout->args =
        infix_arena_calloc(arena, num_args, sizeof(infix_direct_arg_layout), _Alignof(infix_direct_arg_layout));
    if (layout->args == nullptr && num_args > 0)
        return INFIX_ERROR_ALLOCATION_FAILED;

    layout->num_args = num_args;
    layout->target_fn = target_fn;
    layout->return_value_in_memory = standard_layout->return_value_in_memory;

    size_t scratch_space_needed = 0;
    for (size_t i = 0; i < num_args; ++i) {
        layout->args[i].location = standard_layout->arg_locations[i];
        layout->args[i].type = arg_types[i];
        layout->args[i].handler = &handlers[i];
        rv64_direct_scratch_info info = rv64_direct_scratch(&layout->args[i]);
        if (info.needs_scratch) {
            scratch_space_needed = _infix_align_up(scratch_space_needed, info.align);
            scratch_space_needed += info.size;
        }
    }

    size_t total_needed = standard_layout->total_stack_alloc + scratch_space_needed;
    layout->total_stack_alloc = (total_needed + 15) & ~15;

    *out_layout = layout;
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 2 (Direct): Generates the direct trampoline prologue.
 */
static infix_status generate_direct_forward_prologue_riscv64(code_buffer * buf,
                                                             infix_direct_call_frame_layout * layout) {
    infix_riscv64_emit_addi(buf, X_SP_REG, X_SP_REG, -RV_FWD_SAVED_SIZE);
    rv64_mem_sd(buf, X_SP_REG, RV_CTX_TARGET_REG, 0);
    rv64_mem_sd(buf, X_SP_REG, RV_CTX_RET_REG, 8);
    rv64_mem_sd(buf, X_SP_REG, RV_CTX_ARGS_REG, 16);
    rv64_mem_sd(buf, X_SP_REG, X_RA_REG, 24);
    layout->prologue_size = (uint32_t)buf->size;
    // The direct CIF is called with (ret_ptr, lang_args) in a0, a1.
    infix_riscv64_emit_addi(buf, RV_CTX_RET_REG, X_A0_REG, 0);
    infix_riscv64_emit_addi(buf, RV_CTX_ARGS_REG, X_A1_REG, 0);
    rv64_emit_stack_sub(buf, (uint32_t)layout->total_stack_alloc);
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 3 (Direct): Generates code to call marshallers and place arguments.
 */
static infix_status generate_direct_forward_argument_moves_riscv64(code_buffer * buf,
                                                                   infix_direct_call_frame_layout * layout) {
    if (layout->return_value_in_memory)
        infix_riscv64_emit_addi(buf, X_A0_REG, RV_CTX_RET_REG, 0);

    const size_t scratch_base_from_sp = rv64_direct_standard_alloc(layout);
    size_t current_scratch_offset = 0;

    // PHASE 1: MARSHALL & SAVE TO STACK

    for (size_t i = 0; i < layout->num_args; ++i) {
        const infix_direct_arg_layout * arg = &layout->args[i];
        rv64_direct_scratch_info info = rv64_direct_scratch(arg);
        int32_t my_scratch_offset = -1;
        if (info.needs_scratch) {
            current_scratch_offset = _infix_align_up(current_scratch_offset, info.align);
            my_scratch_offset = (int32_t)(scratch_base_from_sp + current_scratch_offset);
            current_scratch_offset += info.size;
        }
        if (!info.needs_scratch || (!arg->handler->aggregate_marshaller && !arg->handler->scalar_marshaller))
            continue;

        // a0 = language object
        rv64_mem_ld(buf, X_A0_REG, RV_CTX_ARGS_REG, (int32_t)(i * sizeof(void *)));

        if (arg->handler->aggregate_marshaller) {
            rv64_emit_compute_addr(buf, X_A1_REG, X_SP_REG, my_scratch_offset);
            infix_riscv64_emit_load_u64_immediate(buf, X_A2_REG, (uint64_t)arg->type);
            infix_riscv64_emit_load_u64_immediate(buf, RV_SCRATCH0_REG, (uint64_t)arg->handler->aggregate_marshaller);
            infix_riscv64_emit_jalr(buf, X_RA_REG, RV_SCRATCH0_REG, 0);
        }
        else {
            infix_riscv64_emit_load_u64_immediate(buf, RV_SCRATCH0_REG, (uint64_t)arg->handler->scalar_marshaller);
            infix_riscv64_emit_jalr(buf, X_RA_REG, RV_SCRATCH0_REG, 0);
            rv64_mem_sd(buf, X_SP_REG, X_A0_REG, my_scratch_offset);
        }
    }

    // PHASE 2: PLACE (Stack -> Registers)

    current_scratch_offset = 0;

    for (size_t i = 0; i < layout->num_args; ++i) {
        const infix_direct_arg_layout * arg = &layout->args[i];
        rv64_direct_scratch_info info = rv64_direct_scratch(arg);
        int32_t my_scratch_offset = -1;
        if (info.needs_scratch) {
            current_scratch_offset = _infix_align_up(current_scratch_offset, info.align);
            my_scratch_offset = (int32_t)(scratch_base_from_sp + current_scratch_offset);
            current_scratch_offset += info.size;
        }
        if (!info.needs_scratch)
            continue;

        bool pass_address = (arg->type->category == INFIX_TYPE_POINTER);
        bool is_value_move =
            arg->handler->aggregate_marshaller || (arg->handler->writeback_handler && !arg->handler->scalar_marshaller);

        if (is_value_move) {
            switch (arg->location.type) {
            case ARG_LOCATION_GPR_REFERENCE:
                rv64_emit_compute_addr(buf, GPR_ARGS[arg->location.reg_index], X_SP_REG, my_scratch_offset);

infix/src/arch/riscv/abi_riscv64.c  view on Meta::CPAN

                break;
            case ARG_LOCATION_MIXED:
                rv64_emit_compute_addr(buf, RV_SCRATCH0_REG, X_SP_REG, my_scratch_offset);
                rv64_emit_load_mixed(buf,
                                     RV_SCRATCH0_REG,
                                     GPR_ARGS[arg->location.reg_index],
                                     FPR_ARGS[arg->location.reg_index2],
                                     arg->type);
                break;
            case ARG_LOCATION_STACK:
                if (pass_address) {
                    rv64_emit_compute_addr(buf, RV_SCRATCH1_REG, X_SP_REG, my_scratch_offset);
                    rv64_mem_sd(buf, X_SP_REG, RV_SCRATCH1_REG, (int32_t)arg->location.stack_offset);
                }
                else {
                    rv64_emit_copy_memory(buf,
                                          X_SP_REG,
                                          (int32_t)arg->location.stack_offset,
                                          X_SP_REG,
                                          my_scratch_offset,
                                          arg->type->size);
                }
                break;
            default:
                break;
            }
        }
        else if (arg->handler->scalar_marshaller) {
            switch (arg->location.type) {
            case ARG_LOCATION_GPR:
                rv64_mem_ld(buf, GPR_ARGS[arg->location.reg_index], X_SP_REG, my_scratch_offset);
                break;
            case ARG_LOCATION_VPR:
                if (is_float(arg->type)) {
                    // The scalar marshaller returns the value as a double; narrow it.
                    rv64_mem_fld(buf, FPR_ARGS[arg->location.reg_index], X_SP_REG, my_scratch_offset);
                    infix_riscv64_emit_fcvt_s_d(
                        buf, FPR_ARGS[arg->location.reg_index], FPR_ARGS[arg->location.reg_index]);
                }
                else {
                    rv64_mem_fld(buf, FPR_ARGS[arg->location.reg_index], X_SP_REG, my_scratch_offset);
                }
                break;
            case ARG_LOCATION_STACK:
                rv64_mem_ld(buf, RV_SCRATCH1_REG, X_SP_REG, my_scratch_offset);
                rv64_mem_sd(buf, X_SP_REG, RV_SCRATCH1_REG, (int32_t)arg->location.stack_offset);
                break;
            default:
                break;
            }
        }
    }
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 3.5 (Direct): Generates the call instruction.
 */
static infix_status generate_direct_forward_call_instruction_riscv64(
    code_buffer * buf, c23_maybe_unused infix_direct_call_frame_layout * layout) {
    infix_riscv64_emit_load_u64_immediate(buf, RV_SCRATCH0_REG, (uint64_t)layout->target_fn);
    // A non-null target skips the ebreak and calls the target; a null target falls
    // through into the ebreak and traps. The target returns to the epilogue.
    infix_riscv64_emit_bne(buf, RV_SCRATCH0_REG, X_ZERO_REG, 8);
    infix_riscv64_emit_ebreak(buf);
    infix_riscv64_emit_jalr(buf, X_RA_REG, RV_SCRATCH0_REG, 0);
    return INFIX_SUCCESS;
}

/**
 * @internal
 * @brief Stage 4 (Direct): Generates the epilogue, including write-back calls.
 */
static infix_status generate_direct_forward_epilogue_riscv64(code_buffer * buf,
                                                             infix_direct_call_frame_layout * layout,
                                                             infix_type * ret_type) {
    layout->epilogue_offset = (uint32_t)buf->size;
    if (ret_type->category != INFIX_TYPE_VOID && !layout->return_value_in_memory) {
        rv64_return_class rc = rv64_classify_return(ret_type);
        if (rc.mixed) {
            rv64_emit_store_fp_value(buf, RV_CTX_RET_REG, F_FA0_REG, rc.fp_offsets[0], rc.fp_sizes[0]);
            rv64_emit_store_gpr_low_bytes(buf, RV_CTX_RET_REG, X_A0_REG, rc.int_offset, rc.int_size);
        }
        else if (rc.fp_count == 2) {
            rv64_emit_store_fp_value(buf, RV_CTX_RET_REG, F_FA0_REG, rc.fp_offsets[0], rc.fp_sizes[0]);
            rv64_emit_store_fp_value(buf, RV_CTX_RET_REG, F_FA1_REG, rc.fp_offsets[1], rc.fp_sizes[1]);
        }
        else if (rc.fp_count == 1) {
            rv64_emit_store_fp_value(buf, RV_CTX_RET_REG, F_FA0_REG, rc.fp_offsets[0], rc.fp_sizes[0]);
        }
        else {
            rv64_emit_store_gpr_return(buf, RV_CTX_RET_REG, ret_type->size);
        }
    }

    const size_t scratch_base_from_sp = rv64_direct_standard_alloc(layout);
    size_t epilogue_scratch_offset = 0;

    for (size_t i = 0; i < layout->num_args; ++i) {
        const infix_direct_arg_layout * arg = &layout->args[i];
        rv64_direct_scratch_info info = rv64_direct_scratch(arg);
        int32_t my_scratch_offset = -1;
        if (info.needs_scratch) {
            epilogue_scratch_offset = _infix_align_up(epilogue_scratch_offset, info.align);
            my_scratch_offset = (int32_t)(scratch_base_from_sp + epilogue_scratch_offset);
            epilogue_scratch_offset += info.size;
        }

        if (arg->handler->writeback_handler) {
            // Save the C return value before calling out.
            infix_riscv64_emit_addi(buf, X_SP_REG, X_SP_REG, -32);
            rv64_mem_sd(buf, X_SP_REG, X_A0_REG, 0);
            rv64_mem_sd(buf, X_SP_REG, X_A1_REG, 8);
            rv64_mem_fsd(buf, X_SP_REG, F_FA0_REG, 16);

            // a0 = language object
            rv64_mem_ld(buf, X_A0_REG, RV_CTX_ARGS_REG, (int32_t)(i * sizeof(void *)));
            // a1 = c_data_ptr (the scratch slot, 32 bytes above the current SP)
            rv64_emit_compute_addr(buf, X_A1_REG, X_SP_REG, 32 + my_scratch_offset);
            // a2 = type
            infix_riscv64_emit_load_u64_immediate(buf, X_A2_REG, (uint64_t)arg->type);
            // Call the handler.
            infix_riscv64_emit_load_u64_immediate(buf, RV_SCRATCH0_REG, (uint64_t)arg->handler->writeback_handler);
            infix_riscv64_emit_jalr(buf, X_RA_REG, RV_SCRATCH0_REG, 0);

            // Restore the C return value.
            rv64_mem_fld(buf, F_FA0_REG, X_SP_REG, 16);
            rv64_mem_ld(buf, X_A1_REG, X_SP_REG, 8);
            rv64_mem_ld(buf, X_A0_REG, X_SP_REG, 0);
            infix_riscv64_emit_addi(buf, X_SP_REG, X_SP_REG, 32);
        }
    }

    rv64_emit_stack_add(buf, (uint32_t)layout->total_stack_alloc);
    rv64_mem_ld(buf, RV_CTX_TARGET_REG, X_SP_REG, 0);



( run in 0.358 second using v1.01-cache-2.11-cpan-2e0ccfb7a10 )