From 93fcab82459e692a81063f90398607799733a893 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sun, 14 Dec 2025 15:15:08 -0800 Subject: [PATCH 01/20] initial commit of llvm-ir backend --- CMakeLists.txt | 81 +++ src/llvm/aot_runtime.c | 427 +++++++++++++ src/llvm/hl2llvm_main.c | 327 ++++++++++ src/llvm/llvm_codegen.c | 1097 ++++++++++++++++++++++++++++++++ src/llvm/llvm_codegen.h | 225 +++++++ src/llvm/llvm_ops_arith.c | 259 ++++++++ src/llvm/llvm_ops_calls.c | 448 +++++++++++++ src/llvm/llvm_ops_closures.c | 125 ++++ src/llvm/llvm_ops_constants.c | 94 +++ src/llvm/llvm_ops_control.c | 372 +++++++++++ src/llvm/llvm_ops_enums.c | 166 +++++ src/llvm/llvm_ops_exceptions.c | 223 +++++++ src/llvm/llvm_ops_memory.c | 339 ++++++++++ src/llvm/llvm_ops_misc.c | 72 +++ src/llvm/llvm_ops_objects.c | 216 +++++++ src/llvm/llvm_ops_refs.c | 104 +++ src/llvm/llvm_ops_types.c | 303 +++++++++ src/llvm/llvm_runtime.c | 332 ++++++++++ src/llvm/llvm_types.c | 135 ++++ src/std/types.c | 1 + 20 files changed, 5346 insertions(+) create mode 100644 src/llvm/aot_runtime.c create mode 100644 src/llvm/hl2llvm_main.c create mode 100644 src/llvm/llvm_codegen.c create mode 100644 src/llvm/llvm_codegen.h create mode 100644 src/llvm/llvm_ops_arith.c create mode 100644 src/llvm/llvm_ops_calls.c create mode 100644 src/llvm/llvm_ops_closures.c create mode 100644 src/llvm/llvm_ops_constants.c create mode 100644 src/llvm/llvm_ops_control.c create mode 100644 src/llvm/llvm_ops_enums.c create mode 100644 src/llvm/llvm_ops_exceptions.c create mode 100644 src/llvm/llvm_ops_memory.c create mode 100644 src/llvm/llvm_ops_misc.c create mode 100644 src/llvm/llvm_ops_objects.c create mode 100644 src/llvm/llvm_ops_refs.c create mode 100644 src/llvm/llvm_ops_types.c create mode 100644 src/llvm/llvm_runtime.c create mode 100644 src/llvm/llvm_types.c diff --git a/CMakeLists.txt b/CMakeLists.txt index a78b53316..1b39055b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|aarch64" AND (NOT CMAKE_OSX_ARCHITECTURES endif() option(WITH_VM "Whether to build the Hashlink virtual machine" ${WITH_VM_DEFAULT}) +option(WITH_LLVM_AOT "Whether to build the hl2llvm AOT compiler" OFF) option(BUILD_SHARED_LIBS "Build using shared libraries" ON) if(BUILD_SHARED_LIBS) # ensure third-party static libs are built with PIC @@ -236,6 +237,83 @@ else() endif() endif() +##################### +# LLVM AOT Compiler (hl2llvm) +if(WITH_LLVM_AOT) + find_package(LLVM REQUIRED CONFIG) + message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") + message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}") + + # LLVM definitions and includes + add_definitions(${LLVM_DEFINITIONS}) + + # Source files for hl2llvm + set(HL2LLVM_SOURCES + src/llvm/hl2llvm_main.c + src/llvm/llvm_codegen.c + src/llvm/llvm_types.c + src/llvm/llvm_runtime.c + src/llvm/llvm_ops_constants.c + src/llvm/llvm_ops_arith.c + src/llvm/llvm_ops_control.c + src/llvm/llvm_ops_memory.c + src/llvm/llvm_ops_calls.c + src/llvm/llvm_ops_closures.c + src/llvm/llvm_ops_types.c + src/llvm/llvm_ops_objects.c + src/llvm/llvm_ops_enums.c + src/llvm/llvm_ops_refs.c + src/llvm/llvm_ops_exceptions.c + src/llvm/llvm_ops_misc.c + src/code.c + ) + + add_executable(hl2llvm ${HL2LLVM_SOURCES}) + + # AOT runtime library (provides module loading for AOT binaries) + add_library(aot_runtime STATIC + src/llvm/aot_runtime.c + src/module.c + src/code.c + ) + target_include_directories(aot_runtime PRIVATE src) + target_link_libraries(aot_runtime libhl) + + # Make hl2llvm depend on aot_runtime so both are built together + add_dependencies(hl2llvm aot_runtime) + + target_include_directories(hl2llvm + PRIVATE + src + ${LLVM_INCLUDE_DIRS} + ) + + # Get LLVM libraries + llvm_map_components_to_libnames(LLVM_LIBS + core + analysis + bitwriter + target + ${LLVM_TARGETS_TO_BUILD} + ) + + target_link_libraries(hl2llvm + libhl + ${LLVM_LIBS} + ) + + if(APPLE) + set_target_properties(hl2llvm PROPERTIES + INSTALL_RPATH "@executable_path;@executable_path/../${CMAKE_INSTALL_LIBDIR}" + ) + elseif(UNIX) + set_target_properties(hl2llvm PROPERTIES + INSTALL_RPATH "$ORIGIN;$ORIGIN/../${CMAKE_INSTALL_LIBDIR}" + ) + endif() + +endif() + if(BUILD_TESTING) find_program( @@ -442,6 +520,9 @@ set(INSTALL_TARGETS libhl) if (WITH_VM) list(APPEND INSTALL_TARGETS hl) endif() +if (WITH_LLVM_AOT) + list(APPEND INSTALL_TARGETS hl2llvm) +endif() install( TARGETS diff --git a/src/llvm/aot_runtime.c b/src/llvm/aot_runtime.c new file mode 100644 index 000000000..fb77e6c2c --- /dev/null +++ b/src/llvm/aot_runtime.c @@ -0,0 +1,427 @@ +/* + * AOT Runtime Helper + * Provides module loading functions for AOT-compiled binaries + */ +#include +#include +#include +#include +#include + +/* + * Global types array - set after module initialization. + * + * AOT-compiled code needs access to runtime type information for: + * - Object allocation (hl_alloc_obj needs hl_type with initialized obj->rt) + * - Type casting and checks + * - Virtual method dispatch + * + * This global points to the types array from the loaded module, which has + * all the runtime type info (obj->rt) properly initialized by hl_module_init(). + * AOT code accesses types as &aot_types[type_idx]. + */ +hl_type *aot_types = NULL; +int aot_ntypes = 0; + +/* + * Global variables storage - set after module initialization. + * + * HashLink globals are stored in a byte buffer (globals_data) with + * per-global offsets (globals_indexes). hl_module_init() initializes + * these arrays. + */ +static unsigned char *aot_globals_data = NULL; +static int *aot_globals_indexes = NULL; +static int aot_nglobals = 0; + +/* + * Get a type pointer by index. + * Used by AOT-compiled code to get properly initialized type pointers. + */ +void *aot_get_type(int idx) { + if (idx >= 0 && idx < aot_ntypes && aot_types != NULL) { + return (void *)&aot_types[idx]; + } + return NULL; +} + +/* + * Get a global variable pointer by index. + * Returns a pointer to the global's storage location in globals_data. + */ +void *aot_get_global(int idx) { + if (idx >= 0 && idx < aot_nglobals && aot_globals_data != NULL && aot_globals_indexes != NULL) { + return (void*)(aot_globals_data + aot_globals_indexes[idx]); + } + return NULL; +} + +/* + * JIT stub functions for AOT runtime. + * + * AOT-compiled binaries don't need JIT compilation, but hl_module_init() in + * module.c calls these functions. We provide minimal stubs that: + * - Return non-NULL from hl_jit_alloc() so init doesn't fail + * - Return success (>=0) from hl_jit_function() for each function + * - Return a dummy code pointer from hl_jit_code() + * + * The actual function code is already AOT-compiled into the binary, so + * we don't need to generate anything. The module init will set up + * functions_ptrs based on the "JIT code" we return, but for AOT we'll + * override those pointers later or use our own dispatch. + */ +static int dummy_jit_ctx; /* Non-NULL pointer for hl_jit_alloc */ +static char dummy_code[16] = {0}; /* Dummy code buffer */ + +jit_ctx *hl_jit_alloc(void) { return (jit_ctx *)&dummy_jit_ctx; } +void hl_jit_init(jit_ctx *ctx, hl_module *m) { (void)ctx; (void)m; } +int hl_jit_function(jit_ctx *ctx, hl_module *m, hl_function *f) { + (void)ctx; (void)m; (void)f; + return 0; /* Return offset 0 - all functions point to start of dummy_code */ +} +void hl_jit_free(jit_ctx *ctx, h_bool can_reset) { (void)ctx; (void)can_reset; } +void *hl_jit_code(jit_ctx *ctx, hl_module *m, int *size, hl_debug_infos **dbg, hl_module *prev) { + (void)ctx; (void)m; (void)prev; + if (size) *size = sizeof(dummy_code); + if (dbg) *dbg = NULL; + return dummy_code; /* Return pointer to dummy code buffer */ +} +void hl_jit_reset(jit_ctx *ctx, hl_module *m) { (void)ctx; (void)m; } +void hl_jit_patch_method(void *old_fun, void **new_fun) { (void)old_fun; (void)new_fun; } + +/* + * C2HL trampoline for dynamic function calls (AArch64). + * + * This is the equivalent of jit_c2hl in the JIT. It takes: + * X0 = function pointer to call + * X1 = pointer to register args (X0-X7, then D0-D7) + * X2 = pointer to stack args end + * + * It loads arguments from the prepared buffers and calls the function. + */ +#if defined(__aarch64__) +__asm__ ( + ".global aot_c2hl_trampoline\n" + ".type aot_c2hl_trampoline, %function\n" + "aot_c2hl_trampoline:\n" + /* Save frame */ + "stp x29, x30, [sp, #-16]!\n" + "mov x29, sp\n" + + /* Save function pointer and stack args */ + "mov x9, x0\n" /* X9 = function to call */ + "mov x10, x1\n" /* X10 = reg args ptr */ + "mov x11, x2\n" /* X11 = stack args end */ + + /* Load integer registers X0-X7 from [X10] */ + "ldp x0, x1, [x10, #0]\n" + "ldp x2, x3, [x10, #16]\n" + "ldp x4, x5, [x10, #32]\n" + "ldp x6, x7, [x10, #48]\n" + + /* Load FP registers D0-D7 from [X10 + 64] */ + "ldp d0, d1, [x10, #64]\n" + "ldp d2, d3, [x10, #80]\n" + "ldp d4, d5, [x10, #96]\n" + "ldp d6, d7, [x10, #112]\n" + + /* Push stack args: X11 points past end, X10+128 points to start */ + /* Stack args are between [X10+128, X11) */ + "add x12, x10, #128\n" /* X12 = start of stack args */ + "1:\n" + "cmp x12, x11\n" + "b.ge 2f\n" + "ldr x13, [x11, #-8]!\n" + "str x13, [sp, #-16]!\n" + "b 1b\n" + "2:\n" + + /* Call the function */ + "blr x9\n" + + /* Restore frame and return */ + "mov sp, x29\n" + "ldp x29, x30, [sp], #16\n" + "ret\n" +); +extern void *aot_c2hl_trampoline(void *func, void *regs, void *stack_end); +#else +/* Stub for non-AArch64 platforms */ +static void *aot_c2hl_trampoline(void *f, void *regs, void *stack) { + (void)f; (void)regs; (void)stack; + hl_error("AOT C2HL trampoline not implemented for this platform"); + return NULL; +} +#endif + +/* Number of register arguments (X0-X7 for ints, D0-D7 for floats) */ +#define CALL_NREGS 8 +#define MAX_ARGS 64 + +/* + * Select which register to use for an argument (C2HL direction). + * Returns register index (0-7 for int, 8-15 for FP) or -1 for stack. + */ +static int select_call_reg_c2hl(int *nextCpu, int *nextFpu, hl_type *t) { + switch (t->kind) { + case HF32: + case HF64: + if (*nextFpu < CALL_NREGS) return CALL_NREGS + (*nextFpu)++; + return -1; + default: + if (*nextCpu < CALL_NREGS) return (*nextCpu)++; + return -1; + } +} + +/* + * Get stack size for a type. + */ +static int stack_size_c2hl(hl_type *t) { + switch (t->kind) { + case HUI8: + case HBOOL: + return 1; + case HUI16: + return 2; + case HI32: + case HF32: + return 4; + default: + return 8; + } +} + +/* + * Callback for dynamic function calls (C -> HL direction). + * Called by hl_dyn_call/hl_call_method to invoke AOT functions dynamically. + */ +static void *aot_callback_c2hl(void **f, hl_type *t, void **args, vdynamic *ret) { + unsigned char stack[MAX_ARGS * 16]; + int nextCpu = 0, nextFpu = 0; + int mappedRegs[MAX_ARGS]; + + memset(stack, 0, sizeof(stack)); + + if (t->fun->nargs > MAX_ARGS) + hl_error("Too many arguments for dynamic call"); + + /* First pass: determine register assignments and stack size */ + int i, size = 0; + for (i = 0; i < t->fun->nargs; i++) { + hl_type *at = t->fun->args[i]; + int creg = select_call_reg_c2hl(&nextCpu, &nextFpu, at); + mappedRegs[i] = creg; + if (creg < 0) { + int tsize = stack_size_c2hl(at); + if (tsize < 8) tsize = 8; + size += tsize; + } + } + + /* Align stack size to 16 bytes */ + int pad = (-size) & 15; + size += pad; + + /* Second pass: copy arguments to appropriate locations */ + /* stack layout: [0..64) = X0-X7, [64..128) = D0-D7, [128..) = stack args */ + /* + * args[i] points to a vdynamic struct: { hl_type *t; union v; } + * The actual value is in the 'v' union at offset 8, not at offset 0. + * We need to read from the correct offset based on the expected type. + */ + int pos = 128; /* Stack args start after register save area */ + for (i = 0; i < t->fun->nargs; i++) { + hl_type *at = t->fun->args[i]; + vdynamic *dyn = (vdynamic*)args[i]; + int creg = mappedRegs[i]; + void *store; + + if (creg >= 0) { + store = stack + creg * 8; + } else { + store = stack + pos; + pos += 8; + } + + switch (at->kind) { + case HUI8: + case HBOOL: + *(int*)store = dyn->v.ui8; + break; + case HUI16: + *(int*)store = dyn->v.ui16; + break; + case HI32: + *(int*)store = dyn->v.i; + break; + case HI64: + *(int_val*)store = dyn->v.i64; + break; + case HF32: + *(float*)store = dyn->v.f; + break; + case HF64: + *(double*)store = dyn->v.d; + break; + default: + *(void**)store = dyn->v.ptr; + break; + } + } + + /* Call through trampoline */ + /* The casts for float/double are intentional - the trampoline returns + * in D0 for FP types, and the cast tells the compiler to read from D0. */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wbad-function-cast" +#pragma GCC diagnostic ignored "-Wcast-function-type" +#pragma GCC diagnostic ignored "-Wincompatible-pointer-types" + switch (t->fun->ret->kind) { + case HVOID: + aot_c2hl_trampoline(*f, stack, stack + pos); + return NULL; + case HUI8: + case HBOOL: + case HUI16: + case HI32: + ret->v.i = (int)(int_val)aot_c2hl_trampoline(*f, stack, stack + pos); + return &ret->v.i; + case HI64: + ret->v.i64 = (int_val)aot_c2hl_trampoline(*f, stack, stack + pos); + return &ret->v.i64; + case HF32: { + float (*fp)(void*,void*,void*) = (float(*)(void*,void*,void*))(void*)aot_c2hl_trampoline; + ret->v.f = fp(*f, stack, stack + pos); + return &ret->v.f; + } + case HF64: { + double (*dp)(void*,void*,void*) = (double(*)(void*,void*,void*))(void*)aot_c2hl_trampoline; + ret->v.d = dp(*f, stack, stack + pos); + return &ret->v.d; + } + default: + return aot_c2hl_trampoline(*f, stack, stack + pos); + } +#pragma GCC diagnostic pop +} + +/* + * Get wrapper function for a given function index. + * For AOT, we return NULL since we don't support HL->C wrapping yet. + */ +static void *aot_get_wrapper(int findex) { + (void)findex; + return NULL; +} + +/* Export wrapper functions that can be linked */ +static hl_code *aot_code_read(const char *path, char **error_msg) { + FILE *f = fopen(path, "rb"); + if (!f) { + if (error_msg) *error_msg = "Cannot open file"; + return NULL; + } + fseek(f, 0, SEEK_END); + int size = (int)ftell(f); + fseek(f, 0, SEEK_SET); + unsigned char *data = (unsigned char *)malloc(size); + if (fread(data, 1, size, f) != (size_t)size) { + free(data); + fclose(f); + if (error_msg) *error_msg = "Failed to read file"; + return NULL; + } + fclose(f); + hl_code *code = hl_code_read(data, size, error_msg); + free(data); + return code; +} + +/* + * AOT function pointer table - generated by LLVM codegen. + * Maps findex -> function pointer for HL functions. + * Native function entries are NULL (resolved by hl_module_init). + */ +extern void *aot_function_table[]; +extern int aot_function_count; + +/* + * Initialize module from embedded bytecode data. + * + * AOT-compiled binaries embed the .hl bytecode directly in the executable + * as a global byte array. This allows the binary to be fully standalone + * without needing the original .hl file at runtime. + * + * The bytecode is needed at runtime because it contains type metadata + * (hl_type structures, vtables, field layouts) that the runtime uses for: + * - Object allocation (needs type size and layout) + * - Type casting and runtime type checks + * - Dynamic dispatch through vtables + * - Field access offset calculations + * + * While the function *code* is AOT-compiled to native instructions, + * the type *metadata* is loaded from the embedded bytecode and used + * to initialize the runtime type system. + */ +int aot_init_module_data(const unsigned char *data, int size) { + char *error_msg = NULL; + hl_code *code = hl_code_read(data, size, &error_msg); + if (!code) { + fprintf(stderr, "Failed to read embedded HL code: %s\n", error_msg ? error_msg : "unknown error"); + return 0; + } + hl_module *m = hl_module_alloc(code); + if (!m) { + fprintf(stderr, "Failed to allocate module\n"); + return 0; + } + /* hl_module_init(module, hot_reload, vtune_later) */ + if (!hl_module_init(m, 0, 0)) { + fprintf(stderr, "Failed to initialize module\n"); + return 0; + } + + /* + * Export the types and globals arrays for AOT code to access. + * After hl_module_init(), all types have their runtime info (obj->rt) + * properly initialized, and globals are allocated/initialized. + */ + aot_types = code->types; + aot_ntypes = code->ntypes; + + aot_globals_data = m->globals_data; + aot_globals_indexes = m->globals_indexes; + aot_nglobals = code->nglobals; + + /* + * Patch functions_ptrs with AOT-compiled function addresses. + * + * After hl_module_init(), m->functions_ptrs contains: + * - For natives: properly resolved function pointers from shared libraries + * - For HL functions: pointers to dummy_code (useless) + * + * We patch the HL function entries with the actual AOT-compiled addresses + * from aot_function_table. Native entries (NULL in the table) are left alone. + * + * This is critical for closures and method dispatch to work correctly. + */ + for (int i = 0; i < aot_function_count; i++) { + if (aot_function_table[i] != NULL) { + m->functions_ptrs[i] = aot_function_table[i]; + } + } + + /* + * Set up callbacks for dynamic function calls. + * + * hl_dyn_call and hl_call_method use hlc_static_call to invoke functions + * dynamically. We provide our AOT-compatible callback that uses a trampoline + * to call AOT functions with the correct calling convention. + * + * The flags=1 tells the runtime to use cl->fun directly (not &cl->fun). + */ + hl_setup_callbacks2(aot_callback_c2hl, aot_get_wrapper, 1); + + return 1; +} diff --git a/src/llvm/hl2llvm_main.c b/src/llvm/hl2llvm_main.c new file mode 100644 index 000000000..da9644bce --- /dev/null +++ b/src/llvm/hl2llvm_main.c @@ -0,0 +1,327 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/* + * hl2llvm - HashLink bytecode to LLVM IR AOT compiler + * + * Usage: hl2llvm [options] input.hl -o output + * + * Options: + * -o Output file (required) + * --emit-llvm Output LLVM IR text (.ll) + * --emit-bc Output LLVM bitcode (.bc) + * --emit-asm Output native assembly (.s) + * --emit-obj Output object file (.o) [default] + * -O0 No optimization + * -O1 Light optimization + * -O2 Default optimization [default] + * -O3 Aggressive optimization + * -g Emit debug info + * -v Verbose output + * --help Show this help + */ + +#include +#include +#include +#include +#include "llvm_codegen.h" + +static void print_usage(const char *prog) { + printf("HashLink bytecode to LLVM IR AOT compiler\n\n"); + printf("Usage: %s [options] input.hl -o output\n\n", prog); + printf("Options:\n"); + printf(" -o Output file (required)\n"); + printf(" --emit-llvm Output LLVM IR text (.ll)\n"); + printf(" --emit-bc Output LLVM bitcode (.bc)\n"); + printf(" --emit-asm Output native assembly (.s)\n"); + printf(" --emit-obj Output object file (.o) [default]\n"); + printf(" -O0 No optimization\n"); + printf(" -O1 Light optimization\n"); + printf(" -O2 Default optimization [default]\n"); + printf(" -O3 Aggressive optimization\n"); + printf(" -g Emit debug info\n"); + printf(" -v Verbose output\n"); + printf(" --help Show this help\n"); +} + +int main(int argc, char **argv) { + const char *input_file = NULL; + const char *output_file = NULL; + llvm_output_format format = LLVM_OUTPUT_OBJECT; + llvm_opt_level opt_level = LLVM_OPT_DEFAULT; + bool emit_debug = false; + bool verbose = false; + + /* Parse command line arguments */ + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + print_usage(argv[0]); + return 0; + } else if (strcmp(argv[i], "-o") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "Error: -o requires an argument\n"); + return 1; + } + output_file = argv[++i]; + } else if (strcmp(argv[i], "--emit-llvm") == 0) { + format = LLVM_OUTPUT_LLVM_IR; + } else if (strcmp(argv[i], "--emit-bc") == 0) { + format = LLVM_OUTPUT_BITCODE; + } else if (strcmp(argv[i], "--emit-asm") == 0) { + format = LLVM_OUTPUT_ASSEMBLY; + } else if (strcmp(argv[i], "--emit-obj") == 0) { + format = LLVM_OUTPUT_OBJECT; + } else if (strcmp(argv[i], "-O0") == 0) { + opt_level = LLVM_OPT_NONE; + } else if (strcmp(argv[i], "-O1") == 0) { + opt_level = LLVM_OPT_LESS; + } else if (strcmp(argv[i], "-O2") == 0) { + opt_level = LLVM_OPT_DEFAULT; + } else if (strcmp(argv[i], "-O3") == 0) { + opt_level = LLVM_OPT_AGGRESSIVE; + } else if (strcmp(argv[i], "-g") == 0) { + emit_debug = true; + } else if (strcmp(argv[i], "-v") == 0) { + verbose = true; + } else if (argv[i][0] == '-') { + fprintf(stderr, "Unknown option: %s\n", argv[i]); + return 1; + } else { + if (input_file != NULL) { + fprintf(stderr, "Error: Multiple input files specified\n"); + return 1; + } + input_file = argv[i]; + } + } + + if (input_file == NULL) { + fprintf(stderr, "Error: No input file specified\n"); + print_usage(argv[0]); + return 1; + } + + if (output_file == NULL) { + fprintf(stderr, "Error: No output file specified (use -o)\n"); + return 1; + } + + /* Initialize HashLink runtime (needed for hl_code_read) */ + hl_global_init(); + + /* Load the HashLink bytecode file */ + if (verbose) { + printf("Loading %s...\n", input_file); + } + + FILE *f = fopen(input_file, "rb"); + if (!f) { + fprintf(stderr, "Error: Cannot open file %s\n", input_file); + return 1; + } + + fseek(f, 0, SEEK_END); + int size = (int)ftell(f); + fseek(f, 0, SEEK_SET); + + char *fdata = (char *)malloc(size); + if (!fdata) { + fprintf(stderr, "Error: Out of memory\n"); + fclose(f); + return 1; + } + + if (fread(fdata, 1, size, f) != (size_t)size) { + fprintf(stderr, "Error: Failed to read %s\n", input_file); + free(fdata); + fclose(f); + return 1; + } + fclose(f); + + char *error_msg = NULL; + hl_code *code = hl_code_read((unsigned char *)fdata, size, &error_msg); + /* Keep fdata around - we'll embed it in the binary */ + + if (code == NULL) { + free(fdata); + fprintf(stderr, "Error: Failed to load %s: %s\n", input_file, + error_msg ? error_msg : "unknown error"); + return 1; + } + + if (verbose) { + printf("Loaded bytecode: %d functions, %d types, %d globals\n", + code->nfunctions, code->ntypes, code->nglobals); + } + + /* Create minimal module context - needed for hl_get_obj_rt() during compilation. + * We don't use hl_module_alloc() because it's not exported from libhl and + * pulls in JIT dependencies we don't need. */ + static hl_module_context module_ctx; + memset(&module_ctx, 0, sizeof(module_ctx)); + hl_alloc_init(&module_ctx.alloc); + + /* Set up functions_types array - needed by hl_get_obj_rt() for method lookups */ + int total_functions = code->nfunctions + code->nnatives; + module_ctx.functions_types = (hl_type **)malloc(sizeof(hl_type *) * total_functions); + memset(module_ctx.functions_types, 0, sizeof(hl_type *) * total_functions); + for (int i = 0; i < code->nfunctions; i++) { + hl_function *fn = &code->functions[i]; + module_ctx.functions_types[fn->findex] = fn->type; + } + for (int i = 0; i < code->nnatives; i++) { + hl_native *n = &code->natives[i]; + module_ctx.functions_types[n->findex] = n->t; + } + + /* Set module context on object types so hl_get_obj_rt() can compute field offsets */ + for (int i = 0; i < code->ntypes; i++) { + hl_type *t = &code->types[i]; + if ((t->kind == HOBJ || t->kind == HSTRUCT) && t->obj) { + t->obj->m = &module_ctx; + } + } + + /* Create LLVM context */ + llvm_ctx *ctx = llvm_create_context(); + if (ctx == NULL) { + fprintf(stderr, "Error: Failed to create LLVM context\n"); + hl_code_free(code); + return 1; + } + + ctx->opt_level = opt_level; + ctx->emit_debug_info = emit_debug; + ctx->bytecode_data = (unsigned char *)fdata; + ctx->bytecode_size = size; + + /* Initialize the module */ + if (verbose) { + printf("Initializing LLVM module...\n"); + } + + if (!llvm_init_module(ctx, code, input_file)) { + fprintf(stderr, "Error: Failed to initialize module: %s\n", + ctx->error_msg ? ctx->error_msg : "unknown error"); + llvm_destroy_context(ctx); + hl_code_free(code); + return 1; + } + + /* Compile all functions */ + if (verbose) { + printf("Compiling %d functions...\n", code->nfunctions); + } + + for (int i = 0; i < code->nfunctions; i++) { + hl_function *f = &code->functions[i]; + if (verbose && (i % 100 == 0 || i == code->nfunctions - 1)) { + printf(" Compiling function %d/%d...\n", i + 1, code->nfunctions); + } + if (!llvm_compile_function(ctx, f)) { + fprintf(stderr, "Error: Failed to compile function %d: %s\n", + i, ctx->error_msg ? ctx->error_msg : "unknown error"); + llvm_destroy_context(ctx); + hl_code_free(code); + return 1; + } + } + + /* Generate entry point */ + if (verbose) { + printf("Generating entry point...\n"); + } + + if (!llvm_generate_entry_point(ctx, code->entrypoint)) { + fprintf(stderr, "Error: Failed to generate entry point: %s\n", + ctx->error_msg ? ctx->error_msg : "unknown error"); + llvm_destroy_context(ctx); + hl_code_free(code); + return 1; + } + + /* Finalize the module */ + if (!llvm_finalize_module(ctx)) { + fprintf(stderr, "Error: Failed to finalize module: %s\n", + ctx->error_msg ? ctx->error_msg : "unknown error"); + llvm_destroy_context(ctx); + hl_code_free(code); + return 1; + } + + /* Verify the module */ + if (verbose) { + printf("Verifying module...\n"); + } + + if (!llvm_verify(ctx)) { + fprintf(stderr, "Error: Module verification failed: %s\n", + ctx->error_msg ? ctx->error_msg : "unknown error"); + llvm_destroy_context(ctx); + hl_code_free(code); + return 1; + } + + /* Optimize if requested */ + if (opt_level > LLVM_OPT_NONE) { + if (verbose) { + printf("Optimizing (level %d)...\n", opt_level); + } + llvm_optimize(ctx); + } + + /* Write output */ + if (verbose) { + const char *format_name; + switch (format) { + case LLVM_OUTPUT_LLVM_IR: format_name = "LLVM IR"; break; + case LLVM_OUTPUT_BITCODE: format_name = "bitcode"; break; + case LLVM_OUTPUT_ASSEMBLY: format_name = "assembly"; break; + case LLVM_OUTPUT_OBJECT: format_name = "object"; break; + default: format_name = "unknown"; break; + } + printf("Writing %s to %s...\n", format_name, output_file); + } + + if (!llvm_output(ctx, output_file, format)) { + fprintf(stderr, "Error: Failed to write output: %s\n", + ctx->error_msg ? ctx->error_msg : "unknown error"); + llvm_destroy_context(ctx); + hl_code_free(code); + return 1; + } + + if (verbose) { + printf("Done!\n"); + } + + /* Cleanup */ + llvm_destroy_context(ctx); + hl_free(&module_ctx.alloc); + free(module_ctx.functions_types); + free(fdata); + hl_code_free(code); + + return 0; +} diff --git a/src/llvm/llvm_codegen.c b/src/llvm/llvm_codegen.c new file mode 100644 index 000000000..eb7333ece --- /dev/null +++ b/src/llvm/llvm_codegen.c @@ -0,0 +1,1097 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include "llvm_codegen.h" +#include +#include + +/* Forward declarations */ +static void compile_opcode(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +static void scan_for_blocks(llvm_ctx *ctx, hl_function *f); +static void create_basic_blocks(llvm_ctx *ctx, hl_function *f); +static void create_function_allocas(llvm_ctx *ctx, hl_function *f); + +llvm_ctx *llvm_create_context(void) { + llvm_ctx *ctx = (llvm_ctx *)calloc(1, sizeof(llvm_ctx)); + if (!ctx) return NULL; + + /* Initialize LLVM targets */ + LLVMInitializeNativeTarget(); + LLVMInitializeNativeAsmPrinter(); + LLVMInitializeNativeAsmParser(); + + /* Create LLVM context */ + ctx->context = LLVMContextCreate(); + if (!ctx->context) { + free(ctx); + return NULL; + } + + ctx->builder = LLVMCreateBuilderInContext(ctx->context); + + /* Initialize common types */ + ctx->void_type = LLVMVoidTypeInContext(ctx->context); + ctx->i1_type = LLVMInt1TypeInContext(ctx->context); + ctx->i8_type = LLVMInt8TypeInContext(ctx->context); + ctx->i16_type = LLVMInt16TypeInContext(ctx->context); + ctx->i32_type = LLVMInt32TypeInContext(ctx->context); + ctx->i64_type = LLVMInt64TypeInContext(ctx->context); + ctx->f32_type = LLVMFloatTypeInContext(ctx->context); + ctx->f64_type = LLVMDoubleTypeInContext(ctx->context); + ctx->ptr_type = LLVMPointerTypeInContext(ctx->context, 0); + + ctx->opt_level = LLVM_OPT_DEFAULT; + ctx->emit_debug_info = false; + + return ctx; +} + +void llvm_destroy_context(llvm_ctx *ctx) { + if (!ctx) return; + + if (ctx->error_msg) free(ctx->error_msg); + if (ctx->basic_blocks) free(ctx->basic_blocks); + if (ctx->vreg_allocs) free(ctx->vreg_allocs); + if (ctx->is_block_start) free(ctx->is_block_start); + if (ctx->hl_type_cache) free(ctx->hl_type_cache); + if (ctx->functions) free(ctx->functions); + if (ctx->function_types) free(ctx->function_types); + if (ctx->global_refs) free(ctx->global_refs); + if (ctx->string_constants) free(ctx->string_constants); + if (ctx->bytes_constants) free(ctx->bytes_constants); + if (ctx->type_constants) free(ctx->type_constants); + + if (ctx->target_machine) LLVMDisposeTargetMachine(ctx->target_machine); + if (ctx->builder) LLVMDisposeBuilder(ctx->builder); + if (ctx->module) LLVMDisposeModule(ctx->module); + if (ctx->context) LLVMContextDispose(ctx->context); + + free(ctx); +} + +bool llvm_init_module(llvm_ctx *ctx, hl_code *code, const char *module_name) { + ctx->code = code; + + /* Create module */ + ctx->module = LLVMModuleCreateWithNameInContext(module_name, ctx->context); + if (!ctx->module) { + ctx->error_msg = strdup("Failed to create LLVM module"); + return false; + } + + /* Set target triple */ + char *triple = LLVMGetDefaultTargetTriple(); + LLVMSetTarget(ctx->module, triple); + + /* Create target machine */ + char *error = NULL; + LLVMTargetRef target; + if (LLVMGetTargetFromTriple(triple, &target, &error) != 0) { + ctx->error_msg = error; + LLVMDisposeMessage(triple); + return false; + } + + /* Get native CPU and features for best performance */ + char *cpu = LLVMGetHostCPUName(); + char *features = LLVMGetHostCPUFeatures(); + + ctx->target_machine = LLVMCreateTargetMachine( + target, triple, cpu, features, + (LLVMCodeGenOptLevel)ctx->opt_level, + LLVMRelocDefault, + LLVMCodeModelDefault + ); + + LLVMDisposeMessage(cpu); + LLVMDisposeMessage(features); + LLVMDisposeMessage(triple); + + if (!ctx->target_machine) { + ctx->error_msg = strdup("Failed to create target machine"); + return false; + } + + ctx->target_data = LLVMCreateTargetDataLayout(ctx->target_machine); + LLVMSetModuleDataLayout(ctx->module, ctx->target_data); + + /* Allocate type cache */ + ctx->num_types = code->ntypes; + ctx->hl_type_cache = (LLVMTypeRef *)calloc(code->ntypes, sizeof(LLVMTypeRef)); + + /* Allocate function arrays */ + ctx->num_functions = code->nfunctions + code->nnatives; + ctx->functions = (LLVMValueRef *)calloc(ctx->num_functions, sizeof(LLVMValueRef)); + ctx->function_types = (LLVMTypeRef *)calloc(ctx->num_functions, sizeof(LLVMTypeRef)); + + /* Allocate globals */ + ctx->num_globals = code->nglobals; + ctx->global_refs = (LLVMValueRef *)calloc(code->nglobals, sizeof(LLVMValueRef)); + + /* Allocate string/bytes constants */ + ctx->num_strings = code->nstrings; + ctx->string_constants = (LLVMValueRef *)calloc(code->nstrings, sizeof(LLVMValueRef)); + ctx->num_bytes = code->nbytes; + ctx->bytes_constants = (LLVMValueRef *)calloc(code->nbytes, sizeof(LLVMValueRef)); + + /* Allocate type constants */ + ctx->type_constants = (LLVMValueRef *)calloc(code->ntypes, sizeof(LLVMValueRef)); + + /* Declare runtime functions */ + llvm_declare_runtime(ctx); + + /* Create function declarations for all HL functions */ + for (int i = 0; i < code->nfunctions; i++) { + hl_function *f = &code->functions[i]; + LLVMTypeRef fn_type = llvm_get_function_type(ctx, f->type); + ctx->function_types[f->findex] = fn_type; + + char name[64]; + snprintf(name, sizeof(name), "hl_fun_%d", f->findex); + ctx->functions[f->findex] = LLVMAddFunction(ctx->module, name, fn_type); + } + + /* Create declarations for native functions */ + for (int i = 0; i < code->nnatives; i++) { + hl_native *n = &code->natives[i]; + + /* Map native function names to actual symbols: + * - "std" library functions use "hl_" prefix (built into libhl.so) + * - Other libraries use their lib name as prefix (in hdll files) + */ + char name[256]; + if (strcmp(n->lib, "std") == 0) { + snprintf(name, sizeof(name), "hl_%s", n->name); + } else { + snprintf(name, sizeof(name), "%s_%s", n->lib, n->name); + } + /* Check if function already declared by runtime declarations. + * If so, use existing to avoid LLVM creating suffixed symbols. + * Also use the existing function's type for consistency. */ + LLVMValueRef existing = LLVMGetNamedFunction(ctx->module, name); + if (existing) { + ctx->functions[n->findex] = existing; + ctx->function_types[n->findex] = LLVMGlobalGetValueType(existing); + } else { + LLVMTypeRef fn_type = llvm_get_function_type(ctx, n->t); + ctx->function_types[n->findex] = fn_type; + ctx->functions[n->findex] = LLVMAddFunction(ctx->module, name, fn_type); + LLVMSetLinkage(ctx->functions[n->findex], LLVMExternalLinkage); + } + } + + /* Create global variables storage */ + LLVMTypeRef globals_type = LLVMArrayType(ctx->i8_type, code->nglobals * 8); + ctx->globals_base = LLVMAddGlobal(ctx->module, globals_type, "hl_globals"); + LLVMSetInitializer(ctx->globals_base, LLVMConstNull(globals_type)); + + /* Create pointers to individual globals (each global is at offset i*8 in the array) */ + for (int i = 0; i < code->nglobals; i++) { + LLVMValueRef indices[] = { + LLVMConstInt(ctx->i64_type, 0, false), + LLVMConstInt(ctx->i64_type, i * 8, false) + }; + ctx->global_refs[i] = LLVMConstGEP2(globals_type, ctx->globals_base, indices, 2); + } + + /* Create string constants */ + for (int i = 0; i < code->nstrings; i++) { + const char *str = code->strings[i]; + int len = code->strings_lens[i]; + char name[32]; + snprintf(name, sizeof(name), ".str.%d", i); + + /* Create global string constant */ + LLVMValueRef str_val = LLVMConstStringInContext(ctx->context, str, len, 1); + LLVMValueRef global = LLVMAddGlobal(ctx->module, LLVMTypeOf(str_val), name); + LLVMSetInitializer(global, str_val); + LLVMSetLinkage(global, LLVMPrivateLinkage); + LLVMSetGlobalConstant(global, 1); + ctx->string_constants[i] = global; + } + + /* Create bytes constants */ + for (int i = 0; i < code->nbytes; i++) { + int pos = code->bytes_pos[i]; + int len = (i + 1 < code->nbytes) ? code->bytes_pos[i + 1] - pos : 0; + char name[32]; + snprintf(name, sizeof(name), ".bytes.%d", i); + + LLVMValueRef bytes_val = LLVMConstStringInContext( + ctx->context, code->bytes + pos, len, 1); + LLVMValueRef global = LLVMAddGlobal(ctx->module, LLVMTypeOf(bytes_val), name); + LLVMSetInitializer(global, bytes_val); + LLVMSetLinkage(global, LLVMPrivateLinkage); + LLVMSetGlobalConstant(global, 1); + ctx->bytes_constants[i] = global; + } + + /* + * Type access: instead of embedding type pointers as globals (which would need + * runtime relocation), we call aot_get_type(idx) at runtime to get properly + * initialized type pointers from the loaded module. + * + * The type_constants array is no longer used - llvm_get_type_ptr() generates + * calls to aot_get_type() instead. + */ + + return true; +} + +bool llvm_compile_function(llvm_ctx *ctx, hl_function *f) { + LLVMValueRef func = ctx->functions[f->findex]; + if (!func) { + ctx->error_msg = strdup("Function not declared"); + return false; + } + + ctx->current_function = func; + ctx->num_vregs = f->nregs; + + /* Scan opcodes to find basic block boundaries */ + scan_for_blocks(ctx, f); + + /* Create basic blocks */ + create_basic_blocks(ctx, f); + + /* Create a dedicated entry block for allocas (LLVM entry block can't have predecessors) */ + ctx->entry_block = LLVMAppendBasicBlockInContext( + ctx->context, ctx->current_function, "entry"); + /* Move entry block to the front */ + LLVMMoveBasicBlockBefore(ctx->entry_block, ctx->basic_blocks[0]); + + /* Position at entry block and create allocas */ + LLVMPositionBuilderAtEnd(ctx->builder, ctx->entry_block); + create_function_allocas(ctx, f); + + /* Store function parameters into their allocas */ + hl_type_fun *ft = f->type->fun; + for (int i = 0; i < ft->nargs; i++) { + LLVMValueRef param = LLVMGetParam(func, i); + LLVMBuildStore(ctx->builder, param, ctx->vreg_allocs[i]); + } + + /* Branch from entry to first code block */ + LLVMBuildBr(ctx->builder, ctx->basic_blocks[0]); + + /* Position at first code block */ + LLVMPositionBuilderAtEnd(ctx->builder, ctx->basic_blocks[0]); + + /* Track if current block is already terminated */ + bool block_terminated = false; + + /* Compile each opcode */ + for (int i = 0; i < f->nops; i++) { + /* Switch to appropriate basic block if this starts one */ + if (ctx->is_block_start[i] && i > 0) { + LLVMBasicBlockRef block = ctx->basic_blocks[i]; + if (block) { + /* Add fallthrough branch if previous block not terminated */ + if (!block_terminated) { + LLVMBuildBr(ctx->builder, block); + } + /* Position at the new block */ + LLVMPositionBuilderAtEnd(ctx->builder, block); + block_terminated = false; + } + } + + /* Skip instructions if block already terminated (dead code) */ + if (block_terminated) { + continue; + } + + compile_opcode(ctx, f, &f->ops[i], i); + + /* Check if block actually has a terminator now */ + LLVMBasicBlockRef current = LLVMGetInsertBlock(ctx->builder); + if (current && LLVMGetBasicBlockTerminator(current)) { + block_terminated = true; + } + } + + /* Ensure final block is terminated */ + if (!block_terminated) { + llvm_ensure_block_terminated(ctx, NULL); + } + + /* Clean up per-function state */ + free(ctx->basic_blocks); + ctx->basic_blocks = NULL; + free(ctx->vreg_allocs); + ctx->vreg_allocs = NULL; + free(ctx->is_block_start); + ctx->is_block_start = NULL; + ctx->current_function = NULL; + + return true; +} + +static void scan_for_blocks(llvm_ctx *ctx, hl_function *f) { + ctx->is_block_start = (bool *)calloc(f->nops + 1, sizeof(bool)); + ctx->is_block_start[0] = true; /* Entry block */ + + for (int i = 0; i < f->nops; i++) { + hl_opcode *op = &f->ops[i]; + switch (op->op) { + case OLabel: + ctx->is_block_start[i] = true; + break; + + case OJTrue: + case OJFalse: + case OJNull: + case OJNotNull: + ctx->is_block_start[i + 1] = true; + ctx->is_block_start[i + 1 + op->p2] = true; + break; + + case OJSLt: + case OJSGte: + case OJSGt: + case OJSLte: + case OJULt: + case OJUGte: + case OJNotLt: + case OJNotGte: + case OJEq: + case OJNotEq: + ctx->is_block_start[i + 1] = true; + ctx->is_block_start[i + 1 + op->p3] = true; + break; + + case OJAlways: + ctx->is_block_start[i + 1 + op->p1] = true; + if (i + 1 < f->nops) + ctx->is_block_start[i + 1] = true; + break; + + case OSwitch: { + int ncases = op->p2; + for (int j = 0; j < ncases; j++) { + int target = i + 1 + op->extra[j]; + if (target >= 0 && target < f->nops) + ctx->is_block_start[target] = true; + } + /* Default case is next instruction */ + if (i + 1 < f->nops) + ctx->is_block_start[i + 1] = true; + break; + } + + case OTrap: + ctx->is_block_start[i + 1] = true; + ctx->is_block_start[i + 1 + op->p2] = true; + break; + + case ORet: + case OThrow: + case ORethrow: + if (i + 1 < f->nops) + ctx->is_block_start[i + 1] = true; + break; + + default: + break; + } + } + +} + +static void create_basic_blocks(llvm_ctx *ctx, hl_function *f) { + ctx->num_blocks = f->nops + 1; /* Array size, indexed by opcode position */ + ctx->basic_blocks = (LLVMBasicBlockRef *)calloc(ctx->num_blocks, sizeof(LLVMBasicBlockRef)); + + for (int i = 0; i <= f->nops; i++) { + if (ctx->is_block_start[i]) { + char name[32]; + snprintf(name, sizeof(name), "bb%d", i); + ctx->basic_blocks[i] = LLVMAppendBasicBlockInContext( + ctx->context, ctx->current_function, name); + } + } +} + +static void create_function_allocas(llvm_ctx *ctx, hl_function *f) { + ctx->vreg_allocs = (LLVMValueRef *)calloc(f->nregs, sizeof(LLVMValueRef)); + + for (int i = 0; i < f->nregs; i++) { + hl_type *t = f->regs[i]; + + /* Skip void types - can't allocate void */ + if (!t || t->kind == HVOID) { + ctx->vreg_allocs[i] = NULL; + continue; + } + + LLVMTypeRef llvm_type = llvm_get_type(ctx, t); + + char name[32]; + snprintf(name, sizeof(name), "r%d", i); + ctx->vreg_allocs[i] = LLVMBuildAlloca(ctx->builder, llvm_type, name); + } +} + +static void compile_opcode(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + /* Constants */ + case OMov: + case OInt: + case OFloat: + case OBool: + case OBytes: + case OString: + case ONull: + llvm_emit_constants(ctx, f, op, op_idx); + break; + + /* Arithmetic */ + case OAdd: + case OSub: + case OMul: + case OSDiv: + case OUDiv: + case OSMod: + case OUMod: + case OShl: + case OSShr: + case OUShr: + case OAnd: + case OOr: + case OXor: + case ONeg: + case ONot: + case OIncr: + case ODecr: + llvm_emit_arithmetic(ctx, f, op, op_idx); + break; + + /* Control flow */ + case OLabel: + case ORet: + case OJTrue: + case OJFalse: + case OJNull: + case OJNotNull: + case OJSLt: + case OJSGte: + case OJSGt: + case OJSLte: + case OJULt: + case OJUGte: + case OJNotLt: + case OJNotGte: + case OJEq: + case OJNotEq: + case OJAlways: + case OSwitch: + llvm_emit_control_flow(ctx, f, op, op_idx); + break; + + /* Memory */ + case OGetGlobal: + case OSetGlobal: + case OField: + case OSetField: + case OGetThis: + case OSetThis: + case OGetI8: + case OGetI16: + case OGetMem: + case OGetArray: + case OSetI8: + case OSetI16: + case OSetMem: + case OSetArray: + llvm_emit_memory(ctx, f, op, op_idx); + break; + + /* Calls */ + case OCall0: + case OCall1: + case OCall2: + case OCall3: + case OCall4: + case OCallN: + case OCallMethod: + case OCallThis: + case OCallClosure: + llvm_emit_calls(ctx, f, op, op_idx); + break; + + /* Closures */ + case OStaticClosure: + case OInstanceClosure: + case OVirtualClosure: + llvm_emit_closures(ctx, f, op, op_idx); + break; + + /* Type operations */ + case OToDyn: + case OToSFloat: + case OToUFloat: + case OToInt: + case OSafeCast: + case OUnsafeCast: + case OToVirtual: + case OType: + case OGetType: + case OGetTID: + llvm_emit_types(ctx, f, op, op_idx); + break; + + /* Null check is control flow (branches) */ + case ONullCheck: + llvm_emit_control_flow(ctx, f, op, op_idx); + break; + + /* Objects */ + case ONew: + case OArraySize: + case ODynGet: + case ODynSet: + llvm_emit_objects(ctx, f, op, op_idx); + break; + + /* Enums */ + case OMakeEnum: + case OEnumAlloc: + case OEnumIndex: + case OEnumField: + case OSetEnumField: + llvm_emit_enums(ctx, f, op, op_idx); + break; + + /* References */ + case ORef: + case OUnref: + case OSetref: + case ORefData: + case ORefOffset: + llvm_emit_refs(ctx, f, op, op_idx); + break; + + /* Exceptions */ + case OThrow: + case ORethrow: + case OTrap: + case OEndTrap: + case OCatch: + llvm_emit_exceptions(ctx, f, op, op_idx); + break; + + /* Miscellaneous */ + case OAssert: + case ONop: + case OPrefetch: + case OAsm: + llvm_emit_misc(ctx, f, op, op_idx); + break; + + default: + fprintf(stderr, "Warning: Unhandled opcode %d at position %d\n", op->op, op_idx); + break; + } +} + +bool llvm_finalize_module(llvm_ctx *ctx) { + return true; +} + +bool llvm_verify(llvm_ctx *ctx) { + char *error = NULL; + if (LLVMVerifyModule(ctx->module, LLVMReturnStatusAction, &error) != 0) { + ctx->error_msg = error; + return false; + } + if (error) LLVMDisposeMessage(error); + return true; +} + +void llvm_optimize(llvm_ctx *ctx) { + if (ctx->opt_level == LLVM_OPT_NONE) return; + + /* Use the new pass manager (LLVM 13+) via LLVMRunPasses */ + const char *passes; + switch (ctx->opt_level) { + case LLVM_OPT_LESS: + passes = "default"; + break; + case LLVM_OPT_DEFAULT: + passes = "default"; + break; + case LLVM_OPT_AGGRESSIVE: + passes = "default"; + break; + default: + return; + } + + LLVMPassBuilderOptionsRef options = LLVMCreatePassBuilderOptions(); + LLVMErrorRef err = LLVMRunPasses(ctx->module, passes, ctx->target_machine, options); + if (err) { + char *msg = LLVMGetErrorMessage(err); + fprintf(stderr, "Optimization error: %s\n", msg); + LLVMDisposeErrorMessage(msg); + } + LLVMDisposePassBuilderOptions(options); +} + +bool llvm_output(llvm_ctx *ctx, const char *filename, llvm_output_format format) { + char *error = NULL; + + switch (format) { + case LLVM_OUTPUT_LLVM_IR: + if (LLVMPrintModuleToFile(ctx->module, filename, &error) != 0) { + ctx->error_msg = error; + return false; + } + break; + + case LLVM_OUTPUT_BITCODE: + if (LLVMWriteBitcodeToFile(ctx->module, filename) != 0) { + ctx->error_msg = strdup("Failed to write bitcode"); + return false; + } + break; + + case LLVM_OUTPUT_ASSEMBLY: + if (LLVMTargetMachineEmitToFile(ctx->target_machine, ctx->module, + (char *)filename, LLVMAssemblyFile, &error) != 0) { + ctx->error_msg = error; + return false; + } + break; + + case LLVM_OUTPUT_OBJECT: + if (LLVMTargetMachineEmitToFile(ctx->target_machine, ctx->module, + (char *)filename, LLVMObjectFile, &error) != 0) { + ctx->error_msg = error; + return false; + } + break; + } + + return true; +} + +/* Helper functions */ + +LLVMValueRef llvm_load_vreg(llvm_ctx *ctx, hl_function *f, int vreg_idx) { + if (vreg_idx < 0 || vreg_idx >= f->nregs) { + fprintf(stderr, "Invalid vreg index: %d\n", vreg_idx); + return LLVMConstNull(ctx->ptr_type); + } + + /* Void types have no alloca - return undef */ + if (!ctx->vreg_allocs[vreg_idx]) { + return LLVMGetUndef(ctx->ptr_type); + } + + LLVMTypeRef type = llvm_get_type(ctx, f->regs[vreg_idx]); + return LLVMBuildLoad2(ctx->builder, type, ctx->vreg_allocs[vreg_idx], ""); +} + +void llvm_store_vreg(llvm_ctx *ctx, hl_function *f, int vreg_idx, LLVMValueRef value) { + if (vreg_idx < 0 || vreg_idx >= f->nregs) { + fprintf(stderr, "Invalid vreg index: %d\n", vreg_idx); + return; + } + + /* Void types have no alloca - skip store */ + if (!ctx->vreg_allocs[vreg_idx]) { + return; + } + + LLVMBuildStore(ctx->builder, value, ctx->vreg_allocs[vreg_idx]); +} + +LLVMBasicBlockRef llvm_get_block_for_offset(llvm_ctx *ctx, int current_op, int offset) { + int target = current_op + 1 + offset; + if (target >= 0 && target < ctx->num_blocks && ctx->basic_blocks[target]) { + return ctx->basic_blocks[target]; + } + return NULL; +} + +void llvm_ensure_block_terminated(llvm_ctx *ctx, LLVMBasicBlockRef next_block) { + LLVMBasicBlockRef current = LLVMGetInsertBlock(ctx->builder); + if (!current) return; + + LLVMValueRef terminator = LLVMGetBasicBlockTerminator(current); + if (!terminator) { + if (next_block) { + LLVMBuildBr(ctx->builder, next_block); + } else { + /* End of function - add appropriate return */ + LLVMTypeRef fn_type = LLVMGlobalGetValueType(ctx->current_function); + LLVMTypeRef ret_type = LLVMGetReturnType(fn_type); + if (LLVMGetTypeKind(ret_type) == LLVMVoidTypeKind) { + LLVMBuildRetVoid(ctx->builder); + } else { + /* Return undef for non-void functions that fall through */ + LLVMBuildRet(ctx->builder, LLVMGetUndef(ret_type)); + } + } + } +} + +LLVMValueRef llvm_get_function_ptr(llvm_ctx *ctx, int findex) { + if (findex >= 0 && findex < ctx->num_functions) { + return ctx->functions[findex]; + } + return NULL; +} + +LLVMValueRef llvm_get_type_ptr(llvm_ctx *ctx, int type_idx) { + /* + * Call aot_get_type(type_idx) to get the type pointer at runtime. + * This returns &aot_types[type_idx] where aot_types points to the + * module's types array (initialized by aot_init_module_data). + */ + LLVMValueRef idx = LLVMConstInt(ctx->i32_type, type_idx, false); + LLVMValueRef args[] = { idx }; + LLVMTypeRef fn_type = LLVMFunctionType(ctx->ptr_type, (LLVMTypeRef[]){ ctx->i32_type }, 1, false); + return LLVMBuildCall2(ctx->builder, fn_type, ctx->rt_aot_get_type, args, 1, "type_ptr"); +} + +LLVMValueRef llvm_get_string(llvm_ctx *ctx, int str_idx) { + if (str_idx >= 0 && str_idx < ctx->num_strings) { + return ctx->string_constants[str_idx]; + } + return LLVMConstNull(ctx->ptr_type); +} + +LLVMValueRef llvm_get_bytes(llvm_ctx *ctx, int bytes_idx) { + if (bytes_idx >= 0 && bytes_idx < ctx->num_bytes) { + return ctx->bytes_constants[bytes_idx]; + } + return LLVMConstNull(ctx->ptr_type); +} + +LLVMValueRef llvm_create_entry_alloca(llvm_ctx *ctx, LLVMTypeRef type, const char *name) { + /* + * Create an alloca in the entry block to avoid stack growth in loops. + * This is the LLVM-recommended pattern for temporary stack allocations. + */ + LLVMBasicBlockRef current_block = LLVMGetInsertBlock(ctx->builder); + + /* Position at end of entry block (before the terminator) */ + LLVMValueRef terminator = LLVMGetBasicBlockTerminator(ctx->entry_block); + if (terminator) { + LLVMPositionBuilderBefore(ctx->builder, terminator); + } else { + LLVMPositionBuilderAtEnd(ctx->builder, ctx->entry_block); + } + + /* Create the alloca */ + LLVMValueRef alloca = LLVMBuildAlloca(ctx->builder, type, name); + + /* Restore builder position */ + LLVMPositionBuilderAtEnd(ctx->builder, current_block); + + return alloca; +} + +bool llvm_generate_entry_point(llvm_ctx *ctx, int entry_findex) { + /* + * Generate main() entry point that: + * 1. Calls hl_global_init() to initialize GC + * 2. Calls hl_sys_init() to initialize runtime + * 3. Calls hl_register_thread() to register current thread + * 4. Loads the .hl module for type information + * 5. Patches function pointers for AOT functions + * 6. Calls the HashLink entry point function + * 7. Returns 0 + */ + + /* + * Generate the AOT function pointer table. + * This maps findex -> function pointer for HL functions. + * Native function entries are NULL (resolved by runtime). + * + * The table is used by aot_init_module_data to patch m->functions_ptrs + * so that closures and method dispatch work correctly. + */ + int total_funcs = ctx->code->nfunctions + ctx->code->nnatives; + LLVMTypeRef func_table_type = LLVMArrayType(ctx->ptr_type, total_funcs); + LLVMValueRef func_table = LLVMAddGlobal(ctx->module, func_table_type, "aot_function_table"); + LLVMSetLinkage(func_table, LLVMExternalLinkage); + + /* Build initializer - HL functions get their pointer, natives get NULL */ + LLVMValueRef *func_ptrs = malloc(sizeof(LLVMValueRef) * total_funcs); + for (int i = 0; i < total_funcs; i++) { + func_ptrs[i] = LLVMConstNull(ctx->ptr_type); /* Default to NULL (natives) */ + } + + /* Fill in HL function pointers */ + for (int i = 0; i < ctx->code->nfunctions; i++) { + hl_function *f = ctx->code->functions + i; + if (ctx->functions[f->findex]) { + func_ptrs[f->findex] = ctx->functions[f->findex]; + } + } + + LLVMValueRef table_init = LLVMConstArray(ctx->ptr_type, func_ptrs, total_funcs); + LLVMSetInitializer(func_table, table_init); + free(func_ptrs); + + /* Generate the function count constant */ + LLVMValueRef func_count = LLVMAddGlobal(ctx->module, ctx->i32_type, "aot_function_count"); + LLVMSetLinkage(func_count, LLVMExternalLinkage); + LLVMSetInitializer(func_count, LLVMConstInt(ctx->i32_type, total_funcs, false)); + + /* Declare external runtime init functions */ + LLVMTypeRef void_fn_type = LLVMFunctionType(ctx->void_type, NULL, 0, false); + + LLVMValueRef sys_init = LLVMAddFunction(ctx->module, "hl_sys_init", + LLVMFunctionType(ctx->void_type, (LLVMTypeRef[]){ ctx->ptr_type, ctx->i32_type, ctx->ptr_type }, 3, false)); + LLVMSetLinkage(sys_init, LLVMExternalLinkage); + + LLVMValueRef register_thread = LLVMAddFunction(ctx->module, "hl_register_thread", + LLVMFunctionType(ctx->void_type, (LLVMTypeRef[]){ ctx->ptr_type }, 1, false)); + LLVMSetLinkage(register_thread, LLVMExternalLinkage); + + LLVMValueRef global_init = LLVMAddFunction(ctx->module, "hl_global_init", + void_fn_type); + LLVMSetLinkage(global_init, LLVMExternalLinkage); + + /* AOT runtime helper function - initializes module from embedded bytecode */ + LLVMValueRef aot_init = LLVMAddFunction(ctx->module, "aot_init_module_data", + LLVMFunctionType(ctx->i32_type, (LLVMTypeRef[]){ ctx->ptr_type, ctx->i32_type }, 2, false)); + LLVMSetLinkage(aot_init, LLVMExternalLinkage); + + /* + * Embed the .hl bytecode as a global constant array. + * + * This makes the AOT binary fully standalone - it doesn't need the original + * .hl file at runtime. The bytecode contains type metadata (hl_type structures, + * vtables, field layouts) that the runtime needs for: + * - Object allocation (type size and layout) + * - Type casting and runtime type checks + * - Dynamic dispatch through vtables + * - Field access offset calculations + * + * While function code is AOT-compiled to native instructions, the type metadata + * is parsed from this embedded bytecode at startup to initialize the runtime. + */ + LLVMValueRef bytecode_global = NULL; + if (ctx->bytecode_data && ctx->bytecode_size > 0) { + LLVMTypeRef bytecode_type = LLVMArrayType(ctx->i8_type, ctx->bytecode_size); + bytecode_global = LLVMAddGlobal(ctx->module, bytecode_type, "hl_bytecode"); + LLVMSetLinkage(bytecode_global, LLVMPrivateLinkage); + LLVMSetGlobalConstant(bytecode_global, 1); + + /* Create initializer from bytecode data */ + LLVMValueRef *bytes = malloc(sizeof(LLVMValueRef) * ctx->bytecode_size); + for (int i = 0; i < ctx->bytecode_size; i++) { + bytes[i] = LLVMConstInt(ctx->i8_type, ctx->bytecode_data[i], 0); + } + LLVMValueRef init = LLVMConstArray(ctx->i8_type, bytes, ctx->bytecode_size); + LLVMSetInitializer(bytecode_global, init); + free(bytes); + } + + /* Create main function */ + LLVMTypeRef main_params[] = { ctx->i32_type, ctx->ptr_type }; + LLVMTypeRef main_type = LLVMFunctionType(ctx->i32_type, main_params, 2, false); + LLVMValueRef main_func = LLVMAddFunction(ctx->module, "main", main_type); + + LLVMBasicBlockRef entry = LLVMAppendBasicBlockInContext(ctx->context, main_func, "entry"); + LLVMPositionBuilderAtEnd(ctx->builder, entry); + + /* Get argc and argv */ + LLVMValueRef argc = LLVMGetParam(main_func, 0); + LLVMValueRef argv = LLVMGetParam(main_func, 1); + + /* Allocate thread info on stack */ + LLVMTypeRef thread_info_type = LLVMArrayType(ctx->i8_type, 512); /* Approximate size */ + LLVMValueRef thread_info = LLVMBuildAlloca(ctx->builder, thread_info_type, "thread_info"); + + /* Call hl_global_init() first - initializes GC mutex needed by other functions */ + LLVMBuildCall2(ctx->builder, void_fn_type, global_init, NULL, 0, ""); + + /* Call hl_sys_init(NULL, argc, argv) */ + LLVMValueRef null_ptr = LLVMConstNull(ctx->ptr_type); + LLVMValueRef init_args[] = { null_ptr, argc, argv }; + LLVMBuildCall2(ctx->builder, + LLVMFunctionType(ctx->void_type, (LLVMTypeRef[]){ ctx->ptr_type, ctx->i32_type, ctx->ptr_type }, 3, false), + sys_init, init_args, 3, ""); + + /* Call hl_register_thread(thread_info) */ + LLVMValueRef thread_ptr = LLVMBuildBitCast(ctx->builder, thread_info, ctx->ptr_type, ""); + LLVMValueRef register_args[] = { thread_ptr }; + LLVMBuildCall2(ctx->builder, + LLVMFunctionType(ctx->void_type, (LLVMTypeRef[]){ ctx->ptr_type }, 1, false), + register_thread, register_args, 1, ""); + + /* Initialize module from embedded bytecode */ + LLVMValueRef init_result; + if (bytecode_global) { + /* Call aot_init_module_data(bytecode_ptr, bytecode_size) */ + LLVMValueRef bytecode_ptr = LLVMBuildBitCast(ctx->builder, bytecode_global, ctx->ptr_type, ""); + LLVMValueRef bytecode_size = LLVMConstInt(ctx->i32_type, ctx->bytecode_size, false); + LLVMValueRef aot_args[] = { bytecode_ptr, bytecode_size }; + init_result = LLVMBuildCall2(ctx->builder, + LLVMFunctionType(ctx->i32_type, (LLVMTypeRef[]){ ctx->ptr_type, ctx->i32_type }, 2, false), + aot_init, aot_args, 2, "init_result"); + } else { + /* No bytecode embedded - fail initialization */ + init_result = LLVMConstInt(ctx->i32_type, 0, false); + } + + /* Check if init failed, exit with error code 1 */ + LLVMValueRef init_failed = LLVMBuildICmp(ctx->builder, LLVMIntEQ, init_result, + LLVMConstInt(ctx->i32_type, 0, false), "init_failed"); + LLVMBasicBlockRef fail_bb = LLVMAppendBasicBlockInContext(ctx->context, main_func, "init_fail"); + LLVMBasicBlockRef cont_bb = LLVMAppendBasicBlockInContext(ctx->context, main_func, "init_ok"); + LLVMBuildCondBr(ctx->builder, init_failed, fail_bb, cont_bb); + + /* Fail block: return 1 */ + LLVMPositionBuilderAtEnd(ctx->builder, fail_bb); + LLVMBuildRet(ctx->builder, LLVMConstInt(ctx->i32_type, 1, false)); + + /* Continue block */ + LLVMPositionBuilderAtEnd(ctx->builder, cont_bb); + + /* Call entry point function */ + LLVMValueRef entry_func = ctx->functions[entry_findex]; + if (!entry_func) { + ctx->error_msg = strdup("Entry point function not found"); + return false; + } + + /* + * Set up exception trap around entry point call. + * This catches any uncaught exceptions and prints them nicely. + * + * Equivalent to: + * hl_trap_ctx trap; + * hl_trap(trap, exc, on_exception); + * entry_func(); + * hl_endtrap(trap); + * return 0; + * on_exception: + * hl_print_uncaught_exception(exc); + * return 1; + */ + + /* Compute offsets using NULL pointer trick */ + hl_trap_ctx *t = NULL; + hl_thread_info *tinf = NULL; + int offset_trap_current = (int)(int_val)&tinf->trap_current; + int offset_exc_value = (int)(int_val)&tinf->exc_value; + int offset_prev = (int)(int_val)&t->prev; + int offset_tcheck = (int)(int_val)&t->tcheck; + + /* Allocate hl_trap_ctx on stack */ + int trap_size = (sizeof(hl_trap_ctx) + 15) & ~15; + LLVMTypeRef trap_type = LLVMArrayType(ctx->i8_type, trap_size); + LLVMValueRef trap = LLVMBuildAlloca(ctx->builder, trap_type, "trap_ctx"); + + /* Get thread info */ + LLVMValueRef thread = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_get_thread), + ctx->rt_get_thread, NULL, 0, "thread"); + + /* trap->tcheck = NULL */ + LLVMValueRef tcheck_offset = LLVMConstInt(ctx->i64_type, offset_tcheck, false); + LLVMValueRef tcheck_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + trap, &tcheck_offset, 1, "tcheck_ptr"); + LLVMBuildStore(ctx->builder, LLVMConstNull(ctx->ptr_type), tcheck_ptr); + + /* trap->prev = thread->trap_current */ + LLVMValueRef trap_current_offset = LLVMConstInt(ctx->i64_type, offset_trap_current, false); + LLVMValueRef trap_current_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + thread, &trap_current_offset, 1, "trap_current_ptr"); + LLVMValueRef old_trap = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, trap_current_ptr, "old_trap"); + + LLVMValueRef prev_offset = LLVMConstInt(ctx->i64_type, offset_prev, false); + LLVMValueRef prev_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + trap, &prev_offset, 1, "prev_ptr"); + LLVMBuildStore(ctx->builder, old_trap, prev_ptr); + + /* thread->trap_current = trap */ + LLVMBuildStore(ctx->builder, trap, trap_current_ptr); + + /* Call setjmp */ + LLVMValueRef setjmp_args[] = { trap }; + LLVMTypeRef setjmp_fn_type = LLVMFunctionType(ctx->i32_type, + (LLVMTypeRef[]){ ctx->ptr_type }, 1, false); + LLVMValueRef setjmp_result = LLVMBuildCall2(ctx->builder, setjmp_fn_type, + ctx->rt_setjmp, setjmp_args, 1, "setjmp_result"); + + /* Branch based on setjmp result */ + LLVMValueRef zero = LLVMConstInt(ctx->i32_type, 0, false); + LLVMValueRef caught = LLVMBuildICmp(ctx->builder, LLVMIntNE, setjmp_result, zero, "caught"); + + LLVMBasicBlockRef normal_bb = LLVMAppendBasicBlockInContext(ctx->context, main_func, "normal"); + LLVMBasicBlockRef exception_bb = LLVMAppendBasicBlockInContext(ctx->context, main_func, "exception"); + LLVMBuildCondBr(ctx->builder, caught, exception_bb, normal_bb); + + /* Normal path: call entry function, clean up trap, return 0 */ + LLVMPositionBuilderAtEnd(ctx->builder, normal_bb); + + LLVMTypeRef entry_fn_type = ctx->function_types[entry_findex]; + LLVMBuildCall2(ctx->builder, entry_fn_type, entry_func, NULL, 0, ""); + + /* hl_endtrap: thread->trap_current = trap->prev */ + LLVMValueRef thread2 = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_get_thread), + ctx->rt_get_thread, NULL, 0, "thread2"); + LLVMValueRef trap_current_ptr2 = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + thread2, &trap_current_offset, 1, "trap_current_ptr2"); + LLVMValueRef prev_trap = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, prev_ptr, "prev_trap"); + LLVMBuildStore(ctx->builder, prev_trap, trap_current_ptr2); + + LLVMBuildRet(ctx->builder, LLVMConstInt(ctx->i32_type, 0, false)); + + /* Exception path: print exception, return 1 */ + LLVMPositionBuilderAtEnd(ctx->builder, exception_bb); + + /* Declare hl_print_uncaught_exception if needed */ + LLVMValueRef print_exc = LLVMGetNamedFunction(ctx->module, "hl_print_uncaught_exception"); + if (!print_exc) { + LLVMTypeRef print_params[] = { ctx->ptr_type }; + LLVMTypeRef print_type = LLVMFunctionType(ctx->void_type, print_params, 1, false); + print_exc = LLVMAddFunction(ctx->module, "hl_print_uncaught_exception", print_type); + LLVMSetLinkage(print_exc, LLVMExternalLinkage); + } + + /* Get exception value from thread */ + LLVMValueRef thread3 = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_get_thread), + ctx->rt_get_thread, NULL, 0, "thread3"); + LLVMValueRef exc_value_offset = LLVMConstInt(ctx->i64_type, offset_exc_value, false); + LLVMValueRef exc_value_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + thread3, &exc_value_offset, 1, "exc_value_ptr"); + LLVMValueRef exc_value = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, exc_value_ptr, "exc_value"); + + /* Call hl_print_uncaught_exception(exc) */ + LLVMValueRef print_args[] = { exc_value }; + LLVMBuildCall2(ctx->builder, + LLVMFunctionType(ctx->void_type, (LLVMTypeRef[]){ ctx->ptr_type }, 1, false), + print_exc, print_args, 1, ""); + + LLVMBuildRet(ctx->builder, LLVMConstInt(ctx->i32_type, 1, false)); + + return true; +} diff --git a/src/llvm/llvm_codegen.h b/src/llvm/llvm_codegen.h new file mode 100644 index 000000000..9550650cb --- /dev/null +++ b/src/llvm/llvm_codegen.h @@ -0,0 +1,225 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#ifndef LLVM_CODEGEN_H +#define LLVM_CODEGEN_H + +#include +#include +#include +#include +#include +#include +#include + +#include "../hl.h" +#include "../hlmodule.h" + +/* Output format options */ +typedef enum { + LLVM_OUTPUT_LLVM_IR, /* Text LLVM IR (.ll) */ + LLVM_OUTPUT_BITCODE, /* LLVM bitcode (.bc) */ + LLVM_OUTPUT_ASSEMBLY, /* Native assembly (.s) */ + LLVM_OUTPUT_OBJECT /* Object file (.o) */ +} llvm_output_format; + +/* Optimization level */ +typedef enum { + LLVM_OPT_NONE = 0, + LLVM_OPT_LESS = 1, + LLVM_OPT_DEFAULT = 2, + LLVM_OPT_AGGRESSIVE = 3 +} llvm_opt_level; + +/* LLVM codegen context */ +typedef struct { + /* LLVM core objects */ + LLVMContextRef context; + LLVMModuleRef module; + LLVMBuilderRef builder; + LLVMTargetMachineRef target_machine; + LLVMTargetDataRef target_data; + + /* Current function compilation state */ + LLVMValueRef current_function; + LLVMBasicBlockRef entry_block; /* Entry block for allocas */ + LLVMBasicBlockRef *basic_blocks; /* Array indexed by opcode position */ + int num_blocks; + LLVMValueRef *vreg_allocs; /* Stack allocas for each vreg */ + int num_vregs; + bool *is_block_start; /* Marks which opcodes start blocks */ + + /* HashLink module being compiled */ + hl_code *code; + + /* Type caches */ + LLVMTypeRef *hl_type_cache; /* LLVM type for each hl_type index */ + int num_types; + + /* Function references */ + LLVMValueRef *functions; /* LLVMValueRef for each function */ + LLVMTypeRef *function_types; /* LLVMTypeRef for each function */ + int num_functions; + + /* Global variable storage */ + LLVMValueRef globals_base; /* Pointer to globals data */ + LLVMValueRef *global_refs; /* Individual global pointers */ + int num_globals; + + /* String/bytes constants */ + LLVMValueRef *string_constants; + int num_strings; + LLVMValueRef *bytes_constants; + int num_bytes; + + /* Type constant pointers - no longer used, types accessed via aot_get_type */ + LLVMValueRef *type_constants; + + /* AOT runtime accessors */ + LLVMValueRef rt_aot_get_type; + LLVMValueRef rt_aot_get_global; + + /* Runtime function declarations */ + LLVMValueRef rt_alloc_obj; + LLVMValueRef rt_alloc_array; + LLVMValueRef rt_alloc_enum; + LLVMValueRef rt_alloc_virtual; + LLVMValueRef rt_alloc_dynobj; + LLVMValueRef rt_alloc_closure_void; + LLVMValueRef rt_alloc_closure_ptr; + LLVMValueRef rt_alloc_bytes; + LLVMValueRef rt_throw; + LLVMValueRef rt_rethrow; + LLVMValueRef rt_null_access; + LLVMValueRef rt_get_obj_rt; + LLVMValueRef rt_to_virtual; + LLVMValueRef rt_safe_cast; + LLVMValueRef rt_make_dyn; + LLVMValueRef rt_dyn_call; + LLVMValueRef rt_dyn_call_safe; + LLVMValueRef rt_get_thread; + + /* Dynamic field access */ + LLVMValueRef rt_dyn_geti; + LLVMValueRef rt_dyn_geti64; + LLVMValueRef rt_dyn_getf; + LLVMValueRef rt_dyn_getd; + LLVMValueRef rt_dyn_getp; + LLVMValueRef rt_dyn_seti; + LLVMValueRef rt_dyn_seti64; + LLVMValueRef rt_dyn_setf; + LLVMValueRef rt_dyn_setd; + LLVMValueRef rt_dyn_setp; + LLVMValueRef rt_dyn_casti; + LLVMValueRef rt_dyn_casti64; + LLVMValueRef rt_dyn_castf; + LLVMValueRef rt_dyn_castd; + LLVMValueRef rt_dyn_castp; + + /* Hash functions */ + LLVMValueRef rt_hash; + LLVMValueRef rt_hash_gen; + + /* setjmp/longjmp for exceptions */ + LLVMValueRef rt_setjmp; + LLVMValueRef rt_longjmp; + + /* Common LLVM types */ + LLVMTypeRef void_type; + LLVMTypeRef i1_type; + LLVMTypeRef i8_type; + LLVMTypeRef i16_type; + LLVMTypeRef i32_type; + LLVMTypeRef i64_type; + LLVMTypeRef f32_type; + LLVMTypeRef f64_type; + LLVMTypeRef ptr_type; + + /* Struct types for runtime objects */ + LLVMTypeRef vdynamic_type; + LLVMTypeRef vclosure_type; + LLVMTypeRef varray_type; + LLVMTypeRef venum_type; + LLVMTypeRef vvirtual_type; + + /* Options */ + llvm_opt_level opt_level; + bool emit_debug_info; + + /* Embedded bytecode for standalone binary */ + const unsigned char *bytecode_data; + int bytecode_size; + + /* Error handling */ + char *error_msg; +} llvm_ctx; + +/* Main API */ +llvm_ctx *llvm_create_context(void); +void llvm_destroy_context(llvm_ctx *ctx); + +bool llvm_init_module(llvm_ctx *ctx, hl_code *code, const char *module_name); +bool llvm_compile_function(llvm_ctx *ctx, hl_function *f); +bool llvm_finalize_module(llvm_ctx *ctx); + +bool llvm_output(llvm_ctx *ctx, const char *filename, llvm_output_format format); +bool llvm_verify(llvm_ctx *ctx); +void llvm_optimize(llvm_ctx *ctx); + +/* Type mapping */ +LLVMTypeRef llvm_get_type(llvm_ctx *ctx, hl_type *t); +LLVMTypeRef llvm_get_function_type(llvm_ctx *ctx, hl_type *t); +int llvm_type_size(llvm_ctx *ctx, hl_type *t); +bool llvm_is_float_type(hl_type *t); +bool llvm_is_ptr_type(hl_type *t); + +/* Runtime declarations */ +void llvm_declare_runtime(llvm_ctx *ctx); + +/* Opcode handlers - organized by category */ +void llvm_emit_constants(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +void llvm_emit_arithmetic(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +void llvm_emit_control_flow(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +void llvm_emit_memory(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +void llvm_emit_calls(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +void llvm_emit_closures(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +void llvm_emit_types(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +void llvm_emit_objects(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +void llvm_emit_enums(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +void llvm_emit_refs(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +void llvm_emit_exceptions(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); +void llvm_emit_misc(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx); + +/* Helpers */ +LLVMValueRef llvm_load_vreg(llvm_ctx *ctx, hl_function *f, int vreg_idx); +void llvm_store_vreg(llvm_ctx *ctx, hl_function *f, int vreg_idx, LLVMValueRef value); +LLVMBasicBlockRef llvm_get_block_for_offset(llvm_ctx *ctx, int current_op, int offset); +void llvm_ensure_block_terminated(llvm_ctx *ctx, LLVMBasicBlockRef next_block); +LLVMValueRef llvm_get_function_ptr(llvm_ctx *ctx, int findex); +LLVMValueRef llvm_get_type_ptr(llvm_ctx *ctx, int type_idx); +LLVMValueRef llvm_get_string(llvm_ctx *ctx, int str_idx); +LLVMValueRef llvm_get_bytes(llvm_ctx *ctx, int bytes_idx); +LLVMValueRef llvm_create_entry_alloca(llvm_ctx *ctx, LLVMTypeRef type, const char *name); + +/* Entry point generation */ +bool llvm_generate_entry_point(llvm_ctx *ctx, int entry_findex); + +#endif /* LLVM_CODEGEN_H */ diff --git a/src/llvm/llvm_ops_arith.c b/src/llvm/llvm_ops_arith.c new file mode 100644 index 000000000..7d489ca02 --- /dev/null +++ b/src/llvm/llvm_ops_arith.c @@ -0,0 +1,259 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Arithmetic and Logic Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_arithmetic(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case OAdd: { + /* dst = a + b */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + hl_type *t = f->regs[dst]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result; + if (llvm_is_float_type(t)) { + result = LLVMBuildFAdd(ctx->builder, a, b, ""); + } else { + result = LLVMBuildAdd(ctx->builder, a, b, ""); + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OSub: { + /* dst = a - b */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + hl_type *t = f->regs[dst]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result; + if (llvm_is_float_type(t)) { + result = LLVMBuildFSub(ctx->builder, a, b, ""); + } else { + result = LLVMBuildSub(ctx->builder, a, b, ""); + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OMul: { + /* dst = a * b */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + hl_type *t = f->regs[dst]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result; + if (llvm_is_float_type(t)) { + result = LLVMBuildFMul(ctx->builder, a, b, ""); + } else { + result = LLVMBuildMul(ctx->builder, a, b, ""); + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OSDiv: { + /* dst = a / b (signed) */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + hl_type *t = f->regs[dst]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result; + if (llvm_is_float_type(t)) { + result = LLVMBuildFDiv(ctx->builder, a, b, ""); + } else { + result = LLVMBuildSDiv(ctx->builder, a, b, ""); + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OUDiv: { + /* dst = a / b (unsigned) */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result = LLVMBuildUDiv(ctx->builder, a, b, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OSMod: { + /* dst = a % b (signed) */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + hl_type *t = f->regs[dst]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result; + if (llvm_is_float_type(t)) { + result = LLVMBuildFRem(ctx->builder, a, b, ""); + } else { + result = LLVMBuildSRem(ctx->builder, a, b, ""); + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OUMod: { + /* dst = a % b (unsigned) */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result = LLVMBuildURem(ctx->builder, a, b, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OShl: { + /* dst = a << b */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result = LLVMBuildShl(ctx->builder, a, b, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OSShr: { + /* dst = a >> b (signed/arithmetic) */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result = LLVMBuildAShr(ctx->builder, a, b, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OUShr: { + /* dst = a >>> b (unsigned/logical) */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result = LLVMBuildLShr(ctx->builder, a, b, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OAnd: { + /* dst = a & b */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result = LLVMBuildAnd(ctx->builder, a, b, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OOr: { + /* dst = a | b */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result = LLVMBuildOr(ctx->builder, a, b, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OXor: { + /* dst = a ^ b */ + int dst = op->p1; + int ra = op->p2; + int rb = op->p3; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef result = LLVMBuildXor(ctx->builder, a, b, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case ONeg: { + /* dst = -src */ + int dst = op->p1; + int src = op->p2; + hl_type *t = f->regs[dst]; + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + LLVMValueRef result; + if (llvm_is_float_type(t)) { + result = LLVMBuildFNeg(ctx->builder, val, ""); + } else { + result = LLVMBuildNeg(ctx->builder, val, ""); + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case ONot: { + /* dst = ~src (bitwise not) */ + int dst = op->p1; + int src = op->p2; + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + LLVMValueRef minus_one = LLVMConstInt(LLVMTypeOf(val), -1, true); + LLVMValueRef result = LLVMBuildXor(ctx->builder, val, minus_one, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OIncr: { + /* dst++ (in-place increment) */ + int dst = op->p1; + hl_type *t = f->regs[dst]; + LLVMValueRef val = llvm_load_vreg(ctx, f, dst); + LLVMValueRef result; + if (llvm_is_float_type(t)) { + LLVMValueRef one = LLVMConstReal(llvm_get_type(ctx, t), 1.0); + result = LLVMBuildFAdd(ctx->builder, val, one, ""); + } else { + LLVMValueRef one = LLVMConstInt(llvm_get_type(ctx, t), 1, false); + result = LLVMBuildAdd(ctx->builder, val, one, ""); + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case ODecr: { + /* dst-- (in-place decrement) */ + int dst = op->p1; + hl_type *t = f->regs[dst]; + LLVMValueRef val = llvm_load_vreg(ctx, f, dst); + LLVMValueRef result; + if (llvm_is_float_type(t)) { + LLVMValueRef one = LLVMConstReal(llvm_get_type(ctx, t), 1.0); + result = LLVMBuildFSub(ctx->builder, val, one, ""); + } else { + LLVMValueRef one = LLVMConstInt(llvm_get_type(ctx, t), 1, false); + result = LLVMBuildSub(ctx->builder, val, one, ""); + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_ops_calls.c b/src/llvm/llvm_ops_calls.c new file mode 100644 index 000000000..4fdb96e4d --- /dev/null +++ b/src/llvm/llvm_ops_calls.c @@ -0,0 +1,448 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Function Call Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_calls(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case OCall0: { + /* dst = func() */ + int dst = op->p1; + int findex = op->p2; + LLVMValueRef func = llvm_get_function_ptr(ctx, findex); + LLVMTypeRef fn_type = ctx->function_types[findex]; + LLVMTypeRef ret_type = LLVMGetReturnType(fn_type); + bool returns_void = (LLVMGetTypeKind(ret_type) == LLVMVoidTypeKind); + LLVMValueRef result = LLVMBuildCall2(ctx->builder, fn_type, func, NULL, 0, ""); + if (f->regs[dst]->kind != HVOID && !returns_void) { + llvm_store_vreg(ctx, f, dst, result); + } + break; + } + + case OCall1: { + /* dst = func(arg0) */ + int dst = op->p1; + int findex = op->p2; + int arg0 = op->p3; + LLVMValueRef func = llvm_get_function_ptr(ctx, findex); + LLVMTypeRef fn_type = ctx->function_types[findex]; + LLVMTypeRef ret_type = LLVMGetReturnType(fn_type); + bool returns_void = (LLVMGetTypeKind(ret_type) == LLVMVoidTypeKind); + LLVMValueRef args[] = { llvm_load_vreg(ctx, f, arg0) }; + LLVMValueRef result = LLVMBuildCall2(ctx->builder, fn_type, func, args, 1, ""); + if (f->regs[dst]->kind != HVOID && !returns_void) { + llvm_store_vreg(ctx, f, dst, result); + } + break; + } + + case OCall2: { + /* dst = func(arg0, arg1) */ + int dst = op->p1; + int findex = op->p2; + int arg0 = op->p3; + int arg1 = (int)(int_val)op->extra; /* extra is direct int, not array */ + LLVMValueRef func = llvm_get_function_ptr(ctx, findex); + LLVMTypeRef fn_type = ctx->function_types[findex]; + LLVMTypeRef ret_type = LLVMGetReturnType(fn_type); + bool returns_void = (LLVMGetTypeKind(ret_type) == LLVMVoidTypeKind); + LLVMValueRef args[] = { + llvm_load_vreg(ctx, f, arg0), + llvm_load_vreg(ctx, f, arg1) + }; + LLVMValueRef result = LLVMBuildCall2(ctx->builder, fn_type, func, args, 2, ""); + if (f->regs[dst]->kind != HVOID && !returns_void) { + llvm_store_vreg(ctx, f, dst, result); + } + break; + } + + case OCall3: { + /* dst = func(arg0, arg1, arg2) */ + int dst = op->p1; + int findex = op->p2; + int arg0 = op->p3; + int arg1 = op->extra[0]; + int arg2 = op->extra[1]; + LLVMValueRef func = llvm_get_function_ptr(ctx, findex); + LLVMTypeRef fn_type = ctx->function_types[findex]; + LLVMTypeRef ret_type = LLVMGetReturnType(fn_type); + bool returns_void = (LLVMGetTypeKind(ret_type) == LLVMVoidTypeKind); + LLVMValueRef args[] = { + llvm_load_vreg(ctx, f, arg0), + llvm_load_vreg(ctx, f, arg1), + llvm_load_vreg(ctx, f, arg2) + }; + LLVMValueRef result = LLVMBuildCall2(ctx->builder, fn_type, func, args, 3, ""); + if (f->regs[dst]->kind != HVOID && !returns_void) { + llvm_store_vreg(ctx, f, dst, result); + } + break; + } + + case OCall4: { + /* dst = func(arg0, arg1, arg2, arg3) */ + int dst = op->p1; + int findex = op->p2; + int arg0 = op->p3; + int arg1 = op->extra[0]; + int arg2 = op->extra[1]; + int arg3 = op->extra[2]; + LLVMValueRef func = llvm_get_function_ptr(ctx, findex); + LLVMTypeRef fn_type = ctx->function_types[findex]; + LLVMTypeRef ret_type = LLVMGetReturnType(fn_type); + bool returns_void = (LLVMGetTypeKind(ret_type) == LLVMVoidTypeKind); + LLVMValueRef args[] = { + llvm_load_vreg(ctx, f, arg0), + llvm_load_vreg(ctx, f, arg1), + llvm_load_vreg(ctx, f, arg2), + llvm_load_vreg(ctx, f, arg3) + }; + LLVMValueRef result = LLVMBuildCall2(ctx->builder, fn_type, func, args, 4, ""); + if (f->regs[dst]->kind != HVOID && !returns_void) { + llvm_store_vreg(ctx, f, dst, result); + } + break; + } + + case OCallN: { + /* dst = func(args...) */ + int dst = op->p1; + int findex = op->p2; + int nargs = op->p3; + LLVMValueRef func = llvm_get_function_ptr(ctx, findex); + LLVMTypeRef fn_type = ctx->function_types[findex]; + LLVMTypeRef ret_type = LLVMGetReturnType(fn_type); + bool returns_void = (LLVMGetTypeKind(ret_type) == LLVMVoidTypeKind); + + LLVMValueRef *args = NULL; + if (nargs > 0) { + args = (LLVMValueRef *)malloc(sizeof(LLVMValueRef) * nargs); + for (int i = 0; i < nargs; i++) { + args[i] = llvm_load_vreg(ctx, f, op->extra[i]); + } + } + + LLVMValueRef result = LLVMBuildCall2(ctx->builder, fn_type, func, args, nargs, ""); + if (f->regs[dst]->kind != HVOID && !returns_void) { + llvm_store_vreg(ctx, f, dst, result); + } + + if (args) free(args); + break; + } + + case OCallMethod: { + /* dst = obj.method(args...) via vtable */ + int dst = op->p1; + int method_idx = op->p2; + int nargs = op->p3; + int obj_reg = op->extra[0]; + + hl_type *obj_type = f->regs[obj_reg]; + LLVMValueRef obj = llvm_load_vreg(ctx, f, obj_reg); + + /* Get method return type if possible */ + hl_type *method_type = NULL; + if (obj_type->kind == HOBJ && obj_type->obj) { + if (method_idx < obj_type->obj->nproto) { + int findex = obj_type->obj->proto[method_idx].findex; + if (findex >= 0 && findex < ctx->code->nfunctions) { + method_type = ctx->code->functions[findex].type; + } + } + } else if (obj_type->kind == HVIRTUAL && obj_type->virt) { + if (method_idx < obj_type->virt->nfields) { + method_type = obj_type->virt->fields[method_idx].t; + } + } + + /* Build function type matching actual call arguments */ + LLVMTypeRef ret_llvm_type = ctx->ptr_type; /* Default return type */ + if (method_type && method_type->kind == HFUN && method_type->fun) { + ret_llvm_type = llvm_get_type(ctx, method_type->fun->ret); + } + + if (obj_type->kind == HVIRTUAL) { + /* + * HVIRTUAL method call: + * vvirtual layout: hl_type* t (0), vdynamic* value (8), vvirtual* next (16), vfields[...] (24+) + * vfield[method_idx] is at offset 24 + method_idx * 8 + * If vfield is not NULL, call it with obj->value as first arg + * If vfield is NULL, call hl_dyn_call_obj (not implemented - will crash) + */ + int vfield_offset = 24 + method_idx * 8; + LLVMValueRef vfield_off_val = LLVMConstInt(ctx->i64_type, vfield_offset, false); + LLVMValueRef vfield_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + obj, &vfield_off_val, 1, ""); + LLVMValueRef vfield = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, vfield_ptr, "vfield"); + + /* Load obj->value (at offset 8) - this is the actual object to pass */ + LLVMValueRef value_off = LLVMConstInt(ctx->i64_type, 8, false); + LLVMValueRef value_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + obj, &value_off, 1, ""); + LLVMValueRef obj_value = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, value_ptr, "obj_value"); + + /* Build args array with obj->value as first arg */ + LLVMValueRef *args = (LLVMValueRef *)malloc(sizeof(LLVMValueRef) * nargs); + args[0] = obj_value; + for (int i = 1; i < nargs; i++) { + args[i] = llvm_load_vreg(ctx, f, op->extra[i]); + } + + /* Build param types - first is ptr (obj->value), rest from vregs */ + LLVMTypeRef *param_types = (LLVMTypeRef *)malloc(sizeof(LLVMTypeRef) * nargs); + param_types[0] = ctx->ptr_type; + for (int i = 1; i < nargs; i++) { + param_types[i] = llvm_get_type(ctx, f->regs[op->extra[i]]); + } + LLVMTypeRef fn_type = LLVMFunctionType(ret_llvm_type, param_types, nargs, false); + free(param_types); + + LLVMTypeRef ret_type = LLVMGetReturnType(fn_type); + bool returns_void = (LLVMGetTypeKind(ret_type) == LLVMVoidTypeKind); + LLVMValueRef result = LLVMBuildCall2(ctx->builder, fn_type, vfield, args, nargs, ""); + if (f->regs[dst]->kind != HVOID && !returns_void) { + llvm_store_vreg(ctx, f, dst, result); + } + + free(args); + } else { + /* HOBJ method call via hl_get_obj_rt */ + LLVMValueRef *args = (LLVMValueRef *)malloc(sizeof(LLVMValueRef) * nargs); + for (int i = 0; i < nargs; i++) { + args[i] = llvm_load_vreg(ctx, f, op->extra[i]); + } + + /* Build param types from actual argument vregs */ + LLVMTypeRef *param_types = (LLVMTypeRef *)malloc(sizeof(LLVMTypeRef) * nargs); + for (int i = 0; i < nargs; i++) { + param_types[i] = llvm_get_type(ctx, f->regs[op->extra[i]]); + } + LLVMTypeRef fn_type = LLVMFunctionType(ret_llvm_type, param_types, nargs, false); + free(param_types); + + /* Load type pointer from object */ + LLVMValueRef type_ptr_offset = LLVMConstInt(ctx->i64_type, 0, false); + LLVMValueRef type_ptr = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, + LLVMBuildGEP2(ctx->builder, ctx->i8_type, obj, &type_ptr_offset, 1, ""), ""); + + /* Get hl_runtime_obj from type */ + LLVMValueRef rt_args[] = { type_ptr }; + LLVMValueRef rt = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_get_obj_rt), + ctx->rt_get_obj_rt, rt_args, 1, ""); + + /* Load method from proto array */ + /* hl_runtime_obj has proto array, each entry has fptr */ + /* Offset to proto: depends on structure layout */ + int proto_offset = 8 + 4 * 7; /* Approximate offset to proto array pointer */ + LLVMValueRef proto_off_val = LLVMConstInt(ctx->i64_type, proto_offset, false); + LLVMValueRef proto_ptr = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, + LLVMBuildGEP2(ctx->builder, ctx->i8_type, rt, &proto_off_val, 1, ""), ""); + + /* Each proto entry is 24 bytes (name, findex, pindex, t, hashed_name) + fptr */ + int entry_size = 32; /* Approximate */ + LLVMValueRef method_off = LLVMConstInt(ctx->i64_type, method_idx * entry_size, false); + LLVMValueRef method_entry = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + proto_ptr, &method_off, 1, ""); + + /* fptr is at offset 0 or near start of proto entry */ + LLVMValueRef fptr = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, method_entry, ""); + + LLVMTypeRef ret_type = LLVMGetReturnType(fn_type); + bool returns_void = (LLVMGetTypeKind(ret_type) == LLVMVoidTypeKind); + LLVMValueRef result = LLVMBuildCall2(ctx->builder, fn_type, fptr, args, nargs, ""); + if (f->regs[dst]->kind != HVOID && !returns_void) { + llvm_store_vreg(ctx, f, dst, result); + } + + free(args); + } + break; + } + + case OCallThis: { + /* dst = this.method(args...) where this is R(0) */ + int dst = op->p1; + int method_idx = op->p2; + int extra_nargs = op->p3; /* Args beyond 'this' */ + int nargs = extra_nargs + 1; /* Include 'this' */ + + hl_type *this_type = f->regs[0]; + LLVMValueRef this_obj = llvm_load_vreg(ctx, f, 0); + + LLVMValueRef *args = (LLVMValueRef *)malloc(sizeof(LLVMValueRef) * nargs); + args[0] = this_obj; + for (int i = 0; i < extra_nargs; i++) { + args[i + 1] = llvm_load_vreg(ctx, f, op->extra[i]); + } + + /* Get method return type */ + hl_type *method_type = NULL; + if (this_type->kind == HOBJ && this_type->obj) { + if (method_idx < this_type->obj->nproto) { + int findex = this_type->obj->proto[method_idx].findex; + if (findex >= 0 && findex < ctx->code->nfunctions) { + method_type = ctx->code->functions[findex].type; + } + } + } + + /* Build function type matching actual call arguments */ + LLVMTypeRef ret_llvm_type = ctx->ptr_type; + if (method_type && method_type->kind == HFUN && method_type->fun) { + ret_llvm_type = llvm_get_type(ctx, method_type->fun->ret); + } + + /* Build param types from actual argument vregs */ + LLVMTypeRef *param_types = (LLVMTypeRef *)malloc(sizeof(LLVMTypeRef) * nargs); + param_types[0] = llvm_get_type(ctx, f->regs[0]); /* this */ + for (int i = 0; i < extra_nargs; i++) { + param_types[i + 1] = llvm_get_type(ctx, f->regs[op->extra[i]]); + } + LLVMTypeRef fn_type = LLVMFunctionType(ret_llvm_type, param_types, nargs, false); + free(param_types); + + /* Load method pointer via vtable */ + LLVMValueRef type_ptr = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, this_obj, ""); + LLVMValueRef rt_args[] = { type_ptr }; + LLVMValueRef rt = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_get_obj_rt), + ctx->rt_get_obj_rt, rt_args, 1, ""); + + int proto_offset = 8 + 4 * 7; + LLVMValueRef proto_off_val = LLVMConstInt(ctx->i64_type, proto_offset, false); + LLVMValueRef proto_ptr = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, + LLVMBuildGEP2(ctx->builder, ctx->i8_type, rt, &proto_off_val, 1, ""), ""); + + int entry_size = 32; + LLVMValueRef method_off = LLVMConstInt(ctx->i64_type, method_idx * entry_size, false); + LLVMValueRef method_entry = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + proto_ptr, &method_off, 1, ""); + LLVMValueRef fptr = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, method_entry, ""); + + LLVMTypeRef ret_type = LLVMGetReturnType(fn_type); + bool returns_void = (LLVMGetTypeKind(ret_type) == LLVMVoidTypeKind); + LLVMValueRef result = LLVMBuildCall2(ctx->builder, fn_type, fptr, args, nargs, ""); + if (f->regs[dst]->kind != HVOID && !returns_void) { + llvm_store_vreg(ctx, f, dst, result); + } + + free(args); + break; + } + + case OCallClosure: { + /* dst = closure(args...) */ + int dst = op->p1; + int closure_reg = op->p2; + int nargs = op->p3; + + LLVMValueRef closure = llvm_load_vreg(ctx, f, closure_reg); + + /* vclosure structure: + * hl_type* t; + * void* fun; + * int hasValue; + * void* value; + */ + + /* Load function pointer from closure */ + LLVMValueRef fun_offset = LLVMConstInt(ctx->i64_type, 8, false); /* After hl_type* */ + LLVMValueRef fun_ptr = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, + LLVMBuildGEP2(ctx->builder, ctx->i8_type, closure, &fun_offset, 1, ""), ""); + + /* Check hasValue and get value pointer */ + LLVMValueRef has_val_offset = LLVMConstInt(ctx->i64_type, 16, false); + LLVMValueRef has_val = LLVMBuildLoad2(ctx->builder, ctx->i32_type, + LLVMBuildGEP2(ctx->builder, ctx->i8_type, closure, &has_val_offset, 1, ""), ""); + + LLVMValueRef val_offset = LLVMConstInt(ctx->i64_type, 24, false); + LLVMValueRef closure_val = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, + LLVMBuildGEP2(ctx->builder, ctx->i8_type, closure, &val_offset, 1, ""), ""); + + /* Build argument list */ + int actual_nargs = nargs; + LLVMValueRef zero = LLVMConstInt(ctx->i32_type, 0, false); + LLVMValueRef has_closure_arg = LLVMBuildICmp(ctx->builder, LLVMIntNE, has_val, zero, ""); + + /* For simplicity, we'll use hl_dyn_call for closure calls */ + /* This handles the hasValue case properly */ + + /* + * hl_dyn_call expects vdynamic** args - an array of pointers to vdynamic structs. + * We need to wrap each argument value in a vdynamic struct. + * vdynamic layout: { hl_type* t; union { ..., void* ptr, ... } v; } + */ + + /* Allocate vdynamic array on stack (16 bytes each: type ptr + value) */ + /* Use entry block alloca to avoid stack growth if this is in a loop */ + LLVMTypeRef vdyn_type = LLVMArrayType(ctx->i8_type, 16); + LLVMTypeRef vdyn_array_type = LLVMArrayType(vdyn_type, nargs > 0 ? nargs : 1); + LLVMValueRef vdyn_array = llvm_create_entry_alloca(ctx, vdyn_array_type, "vdyn_array"); + + /* Allocate pointer array to point to each vdynamic */ + LLVMTypeRef args_array_type = LLVMArrayType(ctx->ptr_type, nargs > 0 ? nargs : 1); + LLVMValueRef args_array = llvm_create_entry_alloca(ctx, args_array_type, "args_array"); + + for (int i = 0; i < nargs; i++) { + int arg_reg = op->extra[i]; + hl_type *arg_type = f->regs[arg_reg]; + LLVMValueRef arg = llvm_load_vreg(ctx, f, arg_reg); + + /* Get pointer to this vdynamic slot using two-index GEP for nested array */ + LLVMValueRef vdyn_indices[] = { + LLVMConstInt(ctx->i32_type, 0, false), /* Dereference the array pointer */ + LLVMConstInt(ctx->i32_type, i, false) /* Index into the array */ + }; + LLVMValueRef vdyn_ptr = LLVMBuildGEP2(ctx->builder, vdyn_array_type, vdyn_array, vdyn_indices, 2, ""); + + /* Store type pointer at offset 0 */ + /* Find type index */ + int type_idx = -1; + for (int ti = 0; ti < ctx->code->ntypes; ti++) { + if (ctx->code->types + ti == arg_type) { + type_idx = ti; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) + : LLVMConstNull(ctx->ptr_type); + LLVMValueRef type_slot = LLVMBuildBitCast(ctx->builder, vdyn_ptr, ctx->ptr_type, ""); + LLVMBuildStore(ctx->builder, type_ptr, type_slot); + + /* Store value at offset 8 */ + LLVMValueRef val_offset = LLVMConstInt(ctx->i64_type, 8, false); + LLVMValueRef val_slot = LLVMBuildGEP2(ctx->builder, ctx->i8_type, vdyn_ptr, &val_offset, 1, ""); + LLVMValueRef val_slot_typed = LLVMBuildBitCast(ctx->builder, val_slot, + LLVMPointerType(LLVMTypeOf(arg), 0), ""); + LLVMBuildStore(ctx->builder, arg, val_slot_typed); + + /* Store pointer to vdynamic in args array using two-index GEP */ + LLVMValueRef args_indices[] = { + LLVMConstInt(ctx->i32_type, 0, false), + LLVMConstInt(ctx->i32_type, i, false) + }; + LLVMValueRef args_slot = LLVMBuildGEP2(ctx->builder, args_array_type, args_array, args_indices, 2, ""); + LLVMBuildStore(ctx->builder, vdyn_ptr, args_slot); + } + + /* Call hl_dyn_call */ + LLVMValueRef call_args[] = { closure, args_array, LLVMConstInt(ctx->i32_type, nargs, false) }; + LLVMValueRef result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_dyn_call), + ctx->rt_dyn_call, call_args, 3, ""); + + if (f->regs[dst]->kind != HVOID) { + llvm_store_vreg(ctx, f, dst, result); + } + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_ops_closures.c b/src/llvm/llvm_ops_closures.c new file mode 100644 index 000000000..a395c2c04 --- /dev/null +++ b/src/llvm/llvm_ops_closures.c @@ -0,0 +1,125 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Closure Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_closures(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case OStaticClosure: { + /* dst = closure for function findex (no captured value) */ + int dst = op->p1; + int findex = op->p2; + + /* Get function pointer */ + LLVMValueRef func = llvm_get_function_ptr(ctx, findex); + + /* Get type pointer from destination register - this is the closure's type + * as determined by the Haxe compiler, not the function's internal type */ + hl_type *closure_type = f->regs[dst]; + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == closure_type) { + type_idx = i; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) : LLVMConstNull(ctx->ptr_type); + + /* Call hl_alloc_closure_void(type, fun) */ + LLVMValueRef args[] = { type_ptr, func }; + LLVMValueRef result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_alloc_closure_void), + ctx->rt_alloc_closure_void, args, 2, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OInstanceClosure: { + /* dst = closure for function findex with captured object */ + int dst = op->p1; + int findex = op->p2; + int obj = op->p3; + + /* Get function pointer */ + LLVMValueRef func = llvm_get_function_ptr(ctx, findex); + + /* Get captured object */ + LLVMValueRef obj_val = llvm_load_vreg(ctx, f, obj); + + /* Get type pointer from destination register - this is the closure's type + * as determined by the Haxe compiler, not the function's internal type */ + hl_type *closure_type = f->regs[dst]; + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == closure_type) { + type_idx = i; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) : LLVMConstNull(ctx->ptr_type); + + /* Call hl_alloc_closure_ptr(type, fun, obj) */ + LLVMValueRef args[] = { type_ptr, func, obj_val }; + LLVMValueRef result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_alloc_closure_ptr), + ctx->rt_alloc_closure_ptr, args, 3, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OVirtualClosure: { + /* dst = virtual method closure with captured object */ + int dst = op->p1; + int obj = op->p2; + int method_idx = op->p3; + + LLVMValueRef obj_val = llvm_load_vreg(ctx, f, obj); + hl_type *obj_type = f->regs[obj]; + + /* Load type pointer from object */ + LLVMValueRef type_ptr = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, obj_val, ""); + + /* Get runtime object */ + LLVMValueRef rt_args[] = { type_ptr }; + LLVMValueRef rt = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_get_obj_rt), + ctx->rt_get_obj_rt, rt_args, 1, ""); + + /* Load method from proto array */ + int proto_offset = 8 + 4 * 7; /* Approximate offset to proto array */ + LLVMValueRef proto_off_val = LLVMConstInt(ctx->i64_type, proto_offset, false); + LLVMValueRef proto_ptr = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, + LLVMBuildGEP2(ctx->builder, ctx->i8_type, rt, &proto_off_val, 1, ""), ""); + + int entry_size = 32; + LLVMValueRef method_off = LLVMConstInt(ctx->i64_type, method_idx * entry_size, false); + LLVMValueRef method_entry = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + proto_ptr, &method_off, 1, ""); + LLVMValueRef fptr = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, method_entry, ""); + + /* Get closure type from destination register - this is the correct type + * as determined by the Haxe compiler */ + hl_type *closure_type = f->regs[dst]; + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == closure_type) { + type_idx = i; + break; + } + } + LLVMValueRef closure_type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) : type_ptr; + + /* Call hl_alloc_closure_ptr(type, fun, obj) */ + LLVMValueRef args[] = { closure_type_ptr, fptr, obj_val }; + LLVMValueRef result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_alloc_closure_ptr), + ctx->rt_alloc_closure_ptr, args, 3, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_ops_constants.c b/src/llvm/llvm_ops_constants.c new file mode 100644 index 000000000..26268909c --- /dev/null +++ b/src/llvm/llvm_ops_constants.c @@ -0,0 +1,94 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Constant and Movement Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_constants(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case OMov: { + /* dst = src */ + int dst = op->p1; + int src = op->p2; + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + llvm_store_vreg(ctx, f, dst, val); + break; + } + + case OInt: { + /* dst = ints[p2] */ + int dst = op->p1; + int val = ctx->code->ints[op->p2]; + LLVMValueRef const_val = LLVMConstInt(ctx->i32_type, val, true); + llvm_store_vreg(ctx, f, dst, const_val); + break; + } + + case OFloat: { + /* dst = floats[p2] */ + int dst = op->p1; + double val = ctx->code->floats[op->p2]; + hl_type *t = f->regs[dst]; + LLVMValueRef const_val; + if (t->kind == HF32) { + const_val = LLVMConstReal(ctx->f32_type, val); + } else { + const_val = LLVMConstReal(ctx->f64_type, val); + } + llvm_store_vreg(ctx, f, dst, const_val); + break; + } + + case OBool: { + /* dst = p2 (0 or 1) */ + int dst = op->p1; + int val = op->p2; + LLVMValueRef const_val = LLVMConstInt(ctx->i8_type, val ? 1 : 0, false); + llvm_store_vreg(ctx, f, dst, const_val); + break; + } + + case OBytes: { + /* dst = bytes[p2] */ + int dst = op->p1; + int idx = op->p2; + LLVMValueRef bytes_ptr = llvm_get_bytes(ctx, idx); + /* Get pointer to first element */ + LLVMValueRef indices[] = { + LLVMConstInt(ctx->i32_type, 0, false), + LLVMConstInt(ctx->i32_type, 0, false) + }; + LLVMValueRef ptr = LLVMBuildGEP2(ctx->builder, + LLVMGetElementType(LLVMTypeOf(bytes_ptr)), bytes_ptr, indices, 2, ""); + llvm_store_vreg(ctx, f, dst, ptr); + break; + } + + case OString: { + /* dst = strings[p2] (as UTF-16) */ + int dst = op->p1; + int idx = op->p2; + LLVMValueRef str_ptr = llvm_get_string(ctx, idx); + /* Get pointer to first element */ + LLVMValueRef indices[] = { + LLVMConstInt(ctx->i32_type, 0, false), + LLVMConstInt(ctx->i32_type, 0, false) + }; + LLVMValueRef ptr = LLVMBuildGEP2(ctx->builder, + LLVMGetElementType(LLVMTypeOf(str_ptr)), str_ptr, indices, 2, ""); + llvm_store_vreg(ctx, f, dst, ptr); + break; + } + + case ONull: { + /* dst = null */ + int dst = op->p1; + LLVMValueRef null_val = LLVMConstNull(ctx->ptr_type); + llvm_store_vreg(ctx, f, dst, null_val); + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_ops_control.c b/src/llvm/llvm_ops_control.c new file mode 100644 index 000000000..bd098ca26 --- /dev/null +++ b/src/llvm/llvm_ops_control.c @@ -0,0 +1,372 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Control Flow Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_control_flow(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case OLabel: { + /* Label is handled by basic block creation in pre-scan */ + /* Just ensure we're in the right block */ + break; + } + + case ORet: { + /* Return from function */ + int src = op->p1; + hl_type *ret_type = f->type->fun->ret; + if (ret_type->kind == HVOID) { + LLVMBuildRetVoid(ctx->builder); + } else { + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + LLVMBuildRet(ctx->builder, val); + } + break; + } + + case OJAlways: { + /* Unconditional jump */ + int offset = op->p1; + LLVMBasicBlockRef target = llvm_get_block_for_offset(ctx, op_idx, offset); + if (target) { + LLVMBuildBr(ctx->builder, target); + } + break; + } + + case OJTrue: { + /* Jump if true (non-zero) */ + int cond_reg = op->p1; + int offset = op->p2; + LLVMValueRef cond = llvm_load_vreg(ctx, f, cond_reg); + LLVMValueRef zero = LLVMConstInt(LLVMTypeOf(cond), 0, false); + LLVMValueRef cmp = LLVMBuildICmp(ctx->builder, LLVMIntNE, cond, zero, ""); + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJFalse: { + /* Jump if false (zero) */ + int cond_reg = op->p1; + int offset = op->p2; + LLVMValueRef cond = llvm_load_vreg(ctx, f, cond_reg); + LLVMValueRef zero = LLVMConstInt(LLVMTypeOf(cond), 0, false); + LLVMValueRef cmp = LLVMBuildICmp(ctx->builder, LLVMIntEQ, cond, zero, ""); + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJNull: { + /* Jump if null */ + int src = op->p1; + int offset = op->p2; + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + LLVMValueRef null_val = LLVMConstNull(ctx->ptr_type); + LLVMValueRef cmp = LLVMBuildICmp(ctx->builder, LLVMIntEQ, val, null_val, ""); + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJNotNull: { + /* Jump if not null */ + int src = op->p1; + int offset = op->p2; + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + LLVMValueRef null_val = LLVMConstNull(ctx->ptr_type); + LLVMValueRef cmp = LLVMBuildICmp(ctx->builder, LLVMIntNE, val, null_val, ""); + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJEq: { + /* Jump if a == b */ + int ra = op->p1; + int rb = op->p2; + int offset = op->p3; + hl_type *t = f->regs[ra]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef cmp; + if (llvm_is_float_type(t)) { + cmp = LLVMBuildFCmp(ctx->builder, LLVMRealOEQ, a, b, ""); + } else { + cmp = LLVMBuildICmp(ctx->builder, LLVMIntEQ, a, b, ""); + } + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJNotEq: { + /* Jump if a != b */ + int ra = op->p1; + int rb = op->p2; + int offset = op->p3; + hl_type *t = f->regs[ra]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef cmp; + if (llvm_is_float_type(t)) { + cmp = LLVMBuildFCmp(ctx->builder, LLVMRealONE, a, b, ""); + } else { + cmp = LLVMBuildICmp(ctx->builder, LLVMIntNE, a, b, ""); + } + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJSLt: { + /* Jump if a < b (signed) */ + int ra = op->p1; + int rb = op->p2; + int offset = op->p3; + hl_type *t = f->regs[ra]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef cmp; + if (llvm_is_float_type(t)) { + cmp = LLVMBuildFCmp(ctx->builder, LLVMRealOLT, a, b, ""); + } else { + cmp = LLVMBuildICmp(ctx->builder, LLVMIntSLT, a, b, ""); + } + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJSGte: { + /* Jump if a >= b (signed) */ + int ra = op->p1; + int rb = op->p2; + int offset = op->p3; + hl_type *t = f->regs[ra]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef cmp; + if (llvm_is_float_type(t)) { + cmp = LLVMBuildFCmp(ctx->builder, LLVMRealOGE, a, b, ""); + } else { + cmp = LLVMBuildICmp(ctx->builder, LLVMIntSGE, a, b, ""); + } + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJSGt: { + /* Jump if a > b (signed) */ + int ra = op->p1; + int rb = op->p2; + int offset = op->p3; + hl_type *t = f->regs[ra]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef cmp; + if (llvm_is_float_type(t)) { + cmp = LLVMBuildFCmp(ctx->builder, LLVMRealOGT, a, b, ""); + } else { + cmp = LLVMBuildICmp(ctx->builder, LLVMIntSGT, a, b, ""); + } + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJSLte: { + /* Jump if a <= b (signed) */ + int ra = op->p1; + int rb = op->p2; + int offset = op->p3; + hl_type *t = f->regs[ra]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef cmp; + if (llvm_is_float_type(t)) { + cmp = LLVMBuildFCmp(ctx->builder, LLVMRealOLE, a, b, ""); + } else { + cmp = LLVMBuildICmp(ctx->builder, LLVMIntSLE, a, b, ""); + } + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJULt: { + /* Jump if a < b (unsigned) */ + int ra = op->p1; + int rb = op->p2; + int offset = op->p3; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef cmp = LLVMBuildICmp(ctx->builder, LLVMIntULT, a, b, ""); + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJUGte: { + /* Jump if a >= b (unsigned) */ + int ra = op->p1; + int rb = op->p2; + int offset = op->p3; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef cmp = LLVMBuildICmp(ctx->builder, LLVMIntUGE, a, b, ""); + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJNotLt: { + /* Jump if !(a < b) - NaN-aware for floats */ + int ra = op->p1; + int rb = op->p2; + int offset = op->p3; + hl_type *t = f->regs[ra]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef cmp; + if (llvm_is_float_type(t)) { + /* For floats, use unordered-or-greater-or-equal for NaN handling */ + cmp = LLVMBuildFCmp(ctx->builder, LLVMRealUGE, a, b, ""); + } else { + cmp = LLVMBuildICmp(ctx->builder, LLVMIntSGE, a, b, ""); + } + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OJNotGte: { + /* Jump if !(a >= b) - NaN-aware for floats */ + int ra = op->p1; + int rb = op->p2; + int offset = op->p3; + hl_type *t = f->regs[ra]; + LLVMValueRef a = llvm_load_vreg(ctx, f, ra); + LLVMValueRef b = llvm_load_vreg(ctx, f, rb); + LLVMValueRef cmp; + if (llvm_is_float_type(t)) { + /* For floats, use unordered-or-less-than for NaN handling */ + cmp = LLVMBuildFCmp(ctx->builder, LLVMRealULT, a, b, ""); + } else { + cmp = LLVMBuildICmp(ctx->builder, LLVMIntSLT, a, b, ""); + } + LLVMBasicBlockRef then_bb = llvm_get_block_for_offset(ctx, op_idx, offset); + LLVMBasicBlockRef else_bb = llvm_get_block_for_offset(ctx, op_idx, 0); /* fallthrough */ + if (then_bb && else_bb) { + LLVMBuildCondBr(ctx->builder, cmp, then_bb, else_bb); + } + break; + } + + case OSwitch: { + /* Multi-way branch */ + int src = op->p1; + int ncases = op->p2; + int default_offset = op->p3; + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + /* Ensure switch value is i32 */ + LLVMTypeRef val_type = LLVMTypeOf(val); + if (LLVMGetTypeKind(val_type) != LLVMIntegerTypeKind || + LLVMGetIntTypeWidth(val_type) != 32) { + val = LLVMBuildIntCast2(ctx->builder, val, ctx->i32_type, false, ""); + } + + LLVMBasicBlockRef default_bb = llvm_get_block_for_offset(ctx, op_idx, default_offset); + if (!default_bb) { + /* Create fallback default block with unreachable */ + default_bb = LLVMAppendBasicBlockInContext(ctx->context, + ctx->current_function, "switch.default"); + LLVMBasicBlockRef current = LLVMGetInsertBlock(ctx->builder); + LLVMPositionBuilderAtEnd(ctx->builder, default_bb); + LLVMBuildUnreachable(ctx->builder); + LLVMPositionBuilderAtEnd(ctx->builder, current); + } + + LLVMValueRef switch_inst = LLVMBuildSwitch(ctx->builder, val, default_bb, ncases); + for (int i = 0; i < ncases; i++) { + int case_offset = op->extra[i]; + LLVMBasicBlockRef case_bb = llvm_get_block_for_offset(ctx, op_idx, case_offset); + if (case_bb) { + LLVMValueRef case_val = LLVMConstInt(ctx->i32_type, i, false); + LLVMAddCase(switch_inst, case_val, case_bb); + } + } + break; + } + + case ONullCheck: { + /* Null pointer check - call hl_null_access if null */ + int src = op->p1; + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + LLVMValueRef null_val = LLVMConstNull(ctx->ptr_type); + LLVMValueRef is_null = LLVMBuildICmp(ctx->builder, LLVMIntEQ, val, null_val, ""); + + LLVMBasicBlockRef null_bb = LLVMAppendBasicBlockInContext(ctx->context, + ctx->current_function, "nullcheck.fail"); + LLVMBasicBlockRef ok_bb = LLVMAppendBasicBlockInContext(ctx->context, + ctx->current_function, "nullcheck.ok"); + + LLVMBuildCondBr(ctx->builder, is_null, null_bb, ok_bb); + + LLVMPositionBuilderAtEnd(ctx->builder, null_bb); + LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_null_access), + ctx->rt_null_access, NULL, 0, ""); + LLVMBuildUnreachable(ctx->builder); + + LLVMPositionBuilderAtEnd(ctx->builder, ok_bb); + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_ops_enums.c b/src/llvm/llvm_ops_enums.c new file mode 100644 index 000000000..dc49ed9a0 --- /dev/null +++ b/src/llvm/llvm_ops_enums.c @@ -0,0 +1,166 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Enum Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_enums(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case OMakeEnum: { + /* dst = make enum with constructor and args */ + int dst = op->p1; + int construct_idx = op->p2; + int nargs = op->p3; + hl_type *enum_type = f->regs[dst]; + + /* Get type pointer */ + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == enum_type) { + type_idx = i; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) + : LLVMConstNull(ctx->ptr_type); + + /* Call hl_alloc_enum */ + LLVMValueRef alloc_args[] = { type_ptr, LLVMConstInt(ctx->i32_type, construct_idx, false) }; + LLVMValueRef enum_obj = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_alloc_enum), + ctx->rt_alloc_enum, alloc_args, 2, ""); + + /* Set constructor args */ + /* venum layout: hl_type* t, int index, then args */ + int args_offset = 16; /* After type ptr (8) and index (4) + padding (4) */ + + if (enum_type->tenum && construct_idx < enum_type->tenum->nconstructs) { + hl_enum_construct *c = &enum_type->tenum->constructs[construct_idx]; + int offset = args_offset; + for (int i = 0; i < nargs && i < c->nparams; i++) { + hl_type *param_type = c->params[i]; + LLVMValueRef arg = llvm_load_vreg(ctx, f, op->extra[i]); + LLVMValueRef off_val = LLVMConstInt(ctx->i64_type, offset, false); + LLVMValueRef field_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + enum_obj, &off_val, 1, ""); + LLVMBuildStore(ctx->builder, arg, field_ptr); + offset += llvm_type_size(ctx, param_type); + /* Align to 8 bytes */ + if (offset % 8 != 0) offset += 8 - (offset % 8); + } + } + + llvm_store_vreg(ctx, f, dst, enum_obj); + break; + } + + case OEnumAlloc: { + /* dst = allocate enum with constructor index */ + int dst = op->p1; + int construct_idx = op->p2; + hl_type *enum_type = f->regs[dst]; + + /* Get type pointer */ + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == enum_type) { + type_idx = i; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) + : LLVMConstNull(ctx->ptr_type); + + /* Call hl_alloc_enum */ + LLVMValueRef args[] = { type_ptr, LLVMConstInt(ctx->i32_type, construct_idx, false) }; + LLVMValueRef result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_alloc_enum), + ctx->rt_alloc_enum, args, 2, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OEnumIndex: { + /* dst = enum.index */ + int dst = op->p1; + int src = op->p2; + LLVMValueRef enum_obj = llvm_load_vreg(ctx, f, src); + + /* venum layout: hl_type* t (8 bytes), int index (4 bytes) */ + LLVMValueRef idx_offset = LLVMConstInt(ctx->i64_type, 8, false); + LLVMValueRef idx_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + enum_obj, &idx_offset, 1, ""); + LLVMValueRef idx = LLVMBuildLoad2(ctx->builder, ctx->i32_type, idx_ptr, ""); + llvm_store_vreg(ctx, f, dst, idx); + break; + } + + case OEnumField: { + /* dst = enum.field[field_idx] for constructor construct_idx */ + /* NOTE: extra is used as a direct integer value, not an array pointer */ + int dst = op->p1; + int src = op->p2; + int construct_idx = op->p3; + int field_idx = (int)(int_val)op->extra; + hl_type *enum_type = f->regs[src]; + hl_type *dst_type = f->regs[dst]; + + LLVMValueRef enum_obj = llvm_load_vreg(ctx, f, src); + LLVMTypeRef field_llvm_type = llvm_get_type(ctx, dst_type); + + /* Calculate field offset */ + int offset = 16; /* After type ptr and index */ + if (enum_type->tenum && construct_idx < enum_type->tenum->nconstructs) { + hl_enum_construct *c = &enum_type->tenum->constructs[construct_idx]; + for (int i = 0; i < field_idx && i < c->nparams; i++) { + offset += llvm_type_size(ctx, c->params[i]); + if (offset % 8 != 0) offset += 8 - (offset % 8); + } + } + + LLVMValueRef off_val = LLVMConstInt(ctx->i64_type, offset, false); + LLVMValueRef field_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + enum_obj, &off_val, 1, ""); + LLVMValueRef val = LLVMBuildLoad2(ctx->builder, field_llvm_type, field_ptr, ""); + llvm_store_vreg(ctx, f, dst, val); + break; + } + + case OSetEnumField: { + /* enum.field[field_idx] = val */ + int enum_reg = op->p1; + int field_idx = op->p2; + int src = op->p3; + hl_type *enum_type = f->regs[enum_reg]; + + LLVMValueRef enum_obj = llvm_load_vreg(ctx, f, enum_reg); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + /* Get constructor index from enum object */ + LLVMValueRef idx_offset = LLVMConstInt(ctx->i64_type, 8, false); + LLVMValueRef idx_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + enum_obj, &idx_offset, 1, ""); + LLVMValueRef construct_idx = LLVMBuildLoad2(ctx->builder, ctx->i32_type, idx_ptr, ""); + + /* For simplicity, calculate offset assuming constructor 0 */ + /* In practice, we'd need to handle multiple constructors */ + int offset = 16; /* After type ptr and index */ + if (enum_type->tenum && enum_type->tenum->nconstructs > 0) { + hl_enum_construct *c = &enum_type->tenum->constructs[0]; + for (int i = 0; i < field_idx && i < c->nparams; i++) { + offset += llvm_type_size(ctx, c->params[i]); + if (offset % 8 != 0) offset += 8 - (offset % 8); + } + } + + LLVMValueRef off_val = LLVMConstInt(ctx->i64_type, offset, false); + LLVMValueRef field_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + enum_obj, &off_val, 1, ""); + LLVMBuildStore(ctx->builder, val, field_ptr); + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_ops_exceptions.c b/src/llvm/llvm_ops_exceptions.c new file mode 100644 index 000000000..e60d3f34b --- /dev/null +++ b/src/llvm/llvm_ops_exceptions.c @@ -0,0 +1,223 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Exception Handling Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_exceptions(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case OTrap: { + /* + * Setup exception trap using setjmp/longjmp. + * + * This mirrors the hl_trap macro from hl.h: + * ctx.tcheck = NULL; + * ctx.prev = __tinf->trap_current; + * __tinf->trap_current = &ctx; + * if (setjmp(ctx.buf)) { r = __tinf->exc_value; goto label; } + * + * hl_trap_ctx layout: + * jmp_buf buf; // offset 0 + * hl_trap_ctx *prev; // offset sizeof(jmp_buf) + * vdynamic *tcheck; // offset sizeof(jmp_buf) + 8 + */ + int exc_reg = op->p1; + int handler_offset = op->p2; + + /* Compute offsets using NULL pointer trick (like x86/aarch64 JIT) */ + hl_trap_ctx *t = NULL; + hl_thread_info *tinf = NULL; + int offset_trap_current = (int)(int_val)&tinf->trap_current; + int offset_exc_value = (int)(int_val)&tinf->exc_value; + int offset_prev = (int)(int_val)&t->prev; + int offset_tcheck = (int)(int_val)&t->tcheck; + + /* Allocate hl_trap_ctx on stack in entry block to avoid stack growth in loops */ + /* Use sizeof(hl_trap_ctx) rounded up to 16-byte alignment */ + int trap_size = (sizeof(hl_trap_ctx) + 15) & ~15; + LLVMTypeRef trap_type = LLVMArrayType(ctx->i8_type, trap_size); + LLVMValueRef trap = llvm_create_entry_alloca(ctx, trap_type, "trap_ctx"); + + /* Step 1: Call hl_get_thread() to get thread info pointer */ + LLVMValueRef thread = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_get_thread), + ctx->rt_get_thread, NULL, 0, "thread"); + + /* Step 2: trap->tcheck = NULL */ + LLVMValueRef tcheck_offset = LLVMConstInt(ctx->i64_type, offset_tcheck, false); + LLVMValueRef tcheck_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + trap, &tcheck_offset, 1, "tcheck_ptr"); + LLVMBuildStore(ctx->builder, LLVMConstNull(ctx->ptr_type), tcheck_ptr); + + /* Step 3: trap->prev = thread->trap_current */ + LLVMValueRef trap_current_offset = LLVMConstInt(ctx->i64_type, offset_trap_current, false); + LLVMValueRef trap_current_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + thread, &trap_current_offset, 1, "trap_current_ptr"); + LLVMValueRef old_trap = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, trap_current_ptr, "old_trap"); + + LLVMValueRef prev_offset = LLVMConstInt(ctx->i64_type, offset_prev, false); + LLVMValueRef prev_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + trap, &prev_offset, 1, "prev_ptr"); + LLVMBuildStore(ctx->builder, old_trap, prev_ptr); + + /* Step 4: thread->trap_current = trap */ + LLVMBuildStore(ctx->builder, trap, trap_current_ptr); + + /* Step 5: Call setjmp(trap->buf) - buf is at offset 0, so trap ptr = buf ptr */ + LLVMValueRef setjmp_args[] = { trap }; + LLVMTypeRef setjmp_fn_type = LLVMFunctionType(ctx->i32_type, + (LLVMTypeRef[]){ ctx->ptr_type }, 1, false); + LLVMValueRef setjmp_result = LLVMBuildCall2(ctx->builder, setjmp_fn_type, + ctx->rt_setjmp, setjmp_args, 1, "setjmp_result"); + + /* Mark setjmp call as returns_twice */ + LLVMSetInstructionCallConv(setjmp_result, LLVMCCallConv); + + /* Step 6: Branch based on setjmp result */ + /* If setjmp returns 0: continue normally */ + /* If setjmp returns non-zero: exception was caught */ + LLVMValueRef zero = LLVMConstInt(ctx->i32_type, 0, false); + LLVMValueRef caught = LLVMBuildICmp(ctx->builder, LLVMIntNE, setjmp_result, zero, "caught"); + + /* Create basic blocks for the two paths */ + /* We need to create new blocks because the current block is being terminated */ + LLVMBasicBlockRef caught_bb = LLVMAppendBasicBlockInContext(ctx->context, + ctx->current_function, "trap_caught"); + LLVMBasicBlockRef continue_bb = LLVMAppendBasicBlockInContext(ctx->context, + ctx->current_function, "trap_continue"); + LLVMBasicBlockRef handler_bb = llvm_get_block_for_offset(ctx, op_idx, handler_offset); + + LLVMBuildCondBr(ctx->builder, caught, caught_bb, continue_bb); + + /* Step 7: In caught_bb, load exception and jump to handler */ + LLVMPositionBuilderAtEnd(ctx->builder, caught_bb); + + /* Call hl_get_thread() again to get fresh thread pointer */ + LLVMValueRef thread2 = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_get_thread), + ctx->rt_get_thread, NULL, 0, "thread2"); + + /* Load exc_value from thread */ + LLVMValueRef exc_value_offset = LLVMConstInt(ctx->i64_type, offset_exc_value, false); + LLVMValueRef exc_value_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + thread2, &exc_value_offset, 1, "exc_value_ptr"); + LLVMValueRef exc_value = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, exc_value_ptr, "exc_value"); + + /* Store exception to destination register */ + llvm_store_vreg(ctx, f, exc_reg, exc_value); + + /* Jump to handler */ + if (handler_bb) { + LLVMBuildBr(ctx->builder, handler_bb); + } else { + LLVMBuildUnreachable(ctx->builder); + } + + /* Position builder at continue_bb for subsequent opcodes */ + LLVMPositionBuilderAtEnd(ctx->builder, continue_bb); + break; + } + + case OEndTrap: { + /* + * End exception trap - restore previous trap context. + * + * This mirrors the hl_endtrap macro from hl.h: + * hl_get_thread()->trap_current = ctx.prev + * + * We need to: + * 1. Get thread info + * 2. Load current trap from thread->trap_current + * 3. Load prev from trap->prev + * 4. Store prev to thread->trap_current + */ + + /* Compute offsets using NULL pointer trick */ + hl_trap_ctx *t = NULL; + hl_thread_info *tinf = NULL; + int offset_trap_current = (int)(int_val)&tinf->trap_current; + int offset_prev = (int)(int_val)&t->prev; + + /* Step 1: Call hl_get_thread() */ + LLVMValueRef thread = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_get_thread), + ctx->rt_get_thread, NULL, 0, "thread"); + + /* Step 2: Load current trap = thread->trap_current */ + LLVMValueRef trap_current_offset = LLVMConstInt(ctx->i64_type, offset_trap_current, false); + LLVMValueRef trap_current_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + thread, &trap_current_offset, 1, "trap_current_ptr"); + LLVMValueRef current_trap = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, trap_current_ptr, "current_trap"); + + /* Step 3: Load prev = trap->prev */ + LLVMValueRef prev_offset = LLVMConstInt(ctx->i64_type, offset_prev, false); + LLVMValueRef prev_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + current_trap, &prev_offset, 1, "prev_ptr"); + LLVMValueRef prev_trap = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, prev_ptr, "prev_trap"); + + /* Step 4: thread->trap_current = prev */ + LLVMBuildStore(ctx->builder, prev_trap, trap_current_ptr); + + /* Continue to next instruction (implicit fallthrough) */ + LLVMBasicBlockRef next_bb = llvm_get_block_for_offset(ctx, op_idx, 0); + if (next_bb) { + llvm_ensure_block_terminated(ctx, next_bb); + } + break; + } + + case OThrow: { + /* Throw exception - calls hl_throw (noreturn) */ + int exc = op->p1; + LLVMValueRef exc_val = llvm_load_vreg(ctx, f, exc); + + LLVMValueRef args[] = { exc_val }; + LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_throw), + ctx->rt_throw, args, 1, ""); + LLVMBuildUnreachable(ctx->builder); + break; + } + + case ORethrow: { + /* Rethrow exception - calls hl_rethrow (noreturn) */ + int exc = op->p1; + LLVMValueRef exc_val = llvm_load_vreg(ctx, f, exc); + + LLVMValueRef args[] = { exc_val }; + LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_rethrow), + ctx->rt_rethrow, args, 1, ""); + LLVMBuildUnreachable(ctx->builder); + break; + } + + case OCatch: { + /* + * Get caught exception from thread info. + * OCatch is used for typing purposes by OTrap - in most cases + * the exception is already loaded by OTrap's caught path. + * But we implement it properly in case it's used standalone. + */ + int dst = op->p1; + + /* Compute offset using NULL pointer trick */ + hl_thread_info *tinf = NULL; + int offset_exc_value = (int)(int_val)&tinf->exc_value; + + /* Call hl_get_thread() to get thread info */ + LLVMValueRef thread = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_get_thread), + ctx->rt_get_thread, NULL, 0, "thread"); + + /* Load exc_value from thread */ + LLVMValueRef exc_off_val = LLVMConstInt(ctx->i64_type, offset_exc_value, false); + LLVMValueRef exc_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + thread, &exc_off_val, 1, "exc_ptr"); + LLVMValueRef exc = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, exc_ptr, "exc_value"); + llvm_store_vreg(ctx, f, dst, exc); + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_ops_memory.c b/src/llvm/llvm_ops_memory.c new file mode 100644 index 000000000..131d0a178 --- /dev/null +++ b/src/llvm/llvm_ops_memory.c @@ -0,0 +1,339 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Memory Access Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_memory(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case OGetGlobal: { + /* + * dst = globals[idx] + * + * Call aot_get_global(idx) to get pointer to the global's storage. + * This returns &aot_globals[idx] where aot_globals points to the + * module's globals array (initialized by aot_init_module_data). + */ + int dst = op->p1; + int idx = op->p2; + hl_type *t = f->regs[dst]; + LLVMTypeRef llvm_type = llvm_get_type(ctx, t); + + /* Call aot_get_global(idx) to get pointer to global slot */ + LLVMValueRef idx_val = LLVMConstInt(ctx->i32_type, idx, false); + LLVMValueRef args[] = { idx_val }; + LLVMTypeRef fn_type = LLVMFunctionType(ctx->ptr_type, (LLVMTypeRef[]){ ctx->i32_type }, 1, false); + LLVMValueRef global_ptr = LLVMBuildCall2(ctx->builder, fn_type, + ctx->rt_aot_get_global, args, 1, "global_ptr"); + + /* Load the value from the global slot */ + LLVMValueRef val = LLVMBuildLoad2(ctx->builder, llvm_type, global_ptr, ""); + llvm_store_vreg(ctx, f, dst, val); + break; + } + + case OSetGlobal: { + /* + * globals[idx] = src + * + * Call aot_get_global(idx) to get pointer to the global's storage, + * then store the value there. + */ + int idx = op->p1; + int src = op->p2; + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + /* Call aot_get_global(idx) to get pointer to global slot */ + LLVMValueRef idx_val = LLVMConstInt(ctx->i32_type, idx, false); + LLVMValueRef args[] = { idx_val }; + LLVMTypeRef fn_type = LLVMFunctionType(ctx->ptr_type, (LLVMTypeRef[]){ ctx->i32_type }, 1, false); + LLVMValueRef global_ptr = LLVMBuildCall2(ctx->builder, fn_type, + ctx->rt_aot_get_global, args, 1, "global_ptr"); + + LLVMBuildStore(ctx->builder, val, global_ptr); + break; + } + + case OField: { + /* dst = obj.field[idx] */ + int dst = op->p1; + int obj = op->p2; + int field_idx = op->p3; + hl_type *obj_type = f->regs[obj]; + hl_type *dst_type = f->regs[dst]; + + LLVMValueRef obj_ptr = llvm_load_vreg(ctx, f, obj); + LLVMTypeRef field_type = llvm_get_type(ctx, dst_type); + + /* Calculate field offset based on type */ + int offset = 0; + if (obj_type->kind == HOBJ || obj_type->kind == HSTRUCT) { + /* For objects/structs, get runtime field offsets */ + hl_runtime_obj *rt = hl_get_obj_rt(obj_type); + if (rt && field_idx < rt->nfields) { + offset = rt->fields_indexes[field_idx]; + } + } else if (obj_type->kind == HVIRTUAL) { + /* For virtuals, use vfields */ + if (obj_type->virt && field_idx < obj_type->virt->nfields) { + /* vfields stores field offsets in the vvirtual structure */ + /* The actual data is after the vvirtual header */ + offset = sizeof(void*) * 2 + field_idx * 8; /* Approximate */ + } + } + + /* GEP to field */ + LLVMValueRef offset_val = LLVMConstInt(ctx->i64_type, offset, false); + LLVMValueRef field_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + obj_ptr, &offset_val, 1, ""); + LLVMValueRef val = LLVMBuildLoad2(ctx->builder, field_type, field_ptr, ""); + llvm_store_vreg(ctx, f, dst, val); + break; + } + + case OSetField: { + /* obj.field[idx] = src */ + int obj = op->p1; + int field_idx = op->p2; + int src = op->p3; + hl_type *obj_type = f->regs[obj]; + + LLVMValueRef obj_ptr = llvm_load_vreg(ctx, f, obj); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + /* Calculate field offset based on type */ + int offset = 0; + if (obj_type->kind == HOBJ || obj_type->kind == HSTRUCT) { + hl_runtime_obj *rt = hl_get_obj_rt(obj_type); + if (rt && field_idx < rt->nfields) { + offset = rt->fields_indexes[field_idx]; + } + } else if (obj_type->kind == HVIRTUAL) { + if (obj_type->virt && field_idx < obj_type->virt->nfields) { + offset = sizeof(void*) * 2 + field_idx * 8; + } + } + + /* GEP to field */ + LLVMValueRef offset_val = LLVMConstInt(ctx->i64_type, offset, false); + LLVMValueRef field_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + obj_ptr, &offset_val, 1, ""); + LLVMBuildStore(ctx->builder, val, field_ptr); + break; + } + + case OGetThis: { + /* dst = this.field[idx] (this is R(0)) */ + int dst = op->p1; + int field_idx = op->p2; + hl_type *this_type = f->regs[0]; + hl_type *dst_type = f->regs[dst]; + + LLVMValueRef this_ptr = llvm_load_vreg(ctx, f, 0); + LLVMTypeRef field_type = llvm_get_type(ctx, dst_type); + + int offset = 0; + if (this_type->kind == HOBJ || this_type->kind == HSTRUCT) { + hl_runtime_obj *rt = hl_get_obj_rt(this_type); + if (rt && field_idx < rt->nfields) { + offset = rt->fields_indexes[field_idx]; + } + } + + LLVMValueRef offset_val = LLVMConstInt(ctx->i64_type, offset, false); + LLVMValueRef field_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + this_ptr, &offset_val, 1, ""); + LLVMValueRef val = LLVMBuildLoad2(ctx->builder, field_type, field_ptr, ""); + llvm_store_vreg(ctx, f, dst, val); + break; + } + + case OSetThis: { + /* this.field[idx] = src (this is R(0)) */ + int field_idx = op->p1; + int src = op->p2; + hl_type *this_type = f->regs[0]; + + LLVMValueRef this_ptr = llvm_load_vreg(ctx, f, 0); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + int offset = 0; + if (this_type->kind == HOBJ || this_type->kind == HSTRUCT) { + hl_runtime_obj *rt = hl_get_obj_rt(this_type); + if (rt && field_idx < rt->nfields) { + offset = rt->fields_indexes[field_idx]; + } + } + + LLVMValueRef offset_val = LLVMConstInt(ctx->i64_type, offset, false); + LLVMValueRef field_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + this_ptr, &offset_val, 1, ""); + LLVMBuildStore(ctx->builder, val, field_ptr); + break; + } + + case OGetI8: { + /* dst = bytes[offset] as i8 */ + int dst = op->p1; + int bytes = op->p2; + int offset = op->p3; + LLVMValueRef base = llvm_load_vreg(ctx, f, bytes); + LLVMValueRef off = llvm_load_vreg(ctx, f, offset); + LLVMValueRef ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, base, &off, 1, ""); + LLVMValueRef val = LLVMBuildLoad2(ctx->builder, ctx->i8_type, ptr, ""); + /* Zero-extend to i32 */ + LLVMValueRef ext = LLVMBuildZExt(ctx->builder, val, ctx->i32_type, ""); + llvm_store_vreg(ctx, f, dst, ext); + break; + } + + case OGetI16: { + /* dst = bytes[offset] as i16 */ + int dst = op->p1; + int bytes = op->p2; + int offset = op->p3; + LLVMValueRef base = llvm_load_vreg(ctx, f, bytes); + LLVMValueRef off = llvm_load_vreg(ctx, f, offset); + LLVMValueRef ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, base, &off, 1, ""); + LLVMValueRef val = LLVMBuildLoad2(ctx->builder, ctx->i16_type, ptr, ""); + /* Zero-extend to i32 */ + LLVMValueRef ext = LLVMBuildZExt(ctx->builder, val, ctx->i32_type, ""); + llvm_store_vreg(ctx, f, dst, ext); + break; + } + + case OGetMem: { + /* dst = *(type*)(bytes + offset) */ + int dst = op->p1; + int bytes = op->p2; + int offset = op->p3; + hl_type *dst_type = f->regs[dst]; + LLVMTypeRef val_type = llvm_get_type(ctx, dst_type); + LLVMValueRef base = llvm_load_vreg(ctx, f, bytes); + LLVMValueRef off = llvm_load_vreg(ctx, f, offset); + LLVMValueRef ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, base, &off, 1, ""); + LLVMValueRef val = LLVMBuildLoad2(ctx->builder, val_type, ptr, ""); + llvm_store_vreg(ctx, f, dst, val); + break; + } + + case OGetArray: { + /* dst = array[index] */ + int dst = op->p1; + int arr = op->p2; + int idx = op->p3; + hl_type *arr_type = f->regs[arr]; + hl_type *elem_type = NULL; + + /* Get element type from array type */ + if (arr_type->kind == HARRAY && arr_type->tparam) { + elem_type = arr_type->tparam; + } else { + elem_type = f->regs[dst]; + } + + LLVMTypeRef val_type = llvm_get_type(ctx, elem_type); + int elem_size = llvm_type_size(ctx, elem_type); + + LLVMValueRef arr_ptr = llvm_load_vreg(ctx, f, arr); + LLVMValueRef index = llvm_load_vreg(ctx, f, idx); + + /* Data starts immediately after varray header */ + LLVMValueRef data_offset = LLVMConstInt(ctx->i64_type, sizeof(varray), false); + LLVMValueRef data_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + arr_ptr, &data_offset, 1, ""); + + /* Calculate element offset */ + LLVMValueRef elem_size_val = LLVMConstInt(ctx->i32_type, elem_size, false); + LLVMValueRef byte_offset = LLVMBuildMul(ctx->builder, index, elem_size_val, ""); + LLVMValueRef byte_offset64 = LLVMBuildZExt(ctx->builder, byte_offset, ctx->i64_type, ""); + LLVMValueRef elem_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + data_ptr, &byte_offset64, 1, ""); + + LLVMValueRef val = LLVMBuildLoad2(ctx->builder, val_type, elem_ptr, ""); + llvm_store_vreg(ctx, f, dst, val); + break; + } + + case OSetI8: { + /* bytes[offset] = val as i8 */ + int bytes = op->p1; + int offset = op->p2; + int src = op->p3; + LLVMValueRef base = llvm_load_vreg(ctx, f, bytes); + LLVMValueRef off = llvm_load_vreg(ctx, f, offset); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + LLVMValueRef ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, base, &off, 1, ""); + /* Truncate to i8 */ + LLVMValueRef trunc = LLVMBuildTrunc(ctx->builder, val, ctx->i8_type, ""); + LLVMBuildStore(ctx->builder, trunc, ptr); + break; + } + + case OSetI16: { + /* bytes[offset] = val as i16 */ + int bytes = op->p1; + int offset = op->p2; + int src = op->p3; + LLVMValueRef base = llvm_load_vreg(ctx, f, bytes); + LLVMValueRef off = llvm_load_vreg(ctx, f, offset); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + LLVMValueRef ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, base, &off, 1, ""); + /* Truncate to i16 */ + LLVMValueRef trunc = LLVMBuildTrunc(ctx->builder, val, ctx->i16_type, ""); + LLVMBuildStore(ctx->builder, trunc, ptr); + break; + } + + case OSetMem: { + /* *(type*)(bytes + offset) = val */ + int bytes = op->p1; + int offset = op->p2; + int src = op->p3; + LLVMValueRef base = llvm_load_vreg(ctx, f, bytes); + LLVMValueRef off = llvm_load_vreg(ctx, f, offset); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + LLVMValueRef ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, base, &off, 1, ""); + LLVMBuildStore(ctx->builder, val, ptr); + break; + } + + case OSetArray: { + /* array[index] = val */ + int arr = op->p1; + int idx = op->p2; + int src = op->p3; + hl_type *arr_type = f->regs[arr]; + hl_type *elem_type = NULL; + + if (arr_type->kind == HARRAY && arr_type->tparam) { + elem_type = arr_type->tparam; + } else { + elem_type = f->regs[src]; + } + + int elem_size = llvm_type_size(ctx, elem_type); + + LLVMValueRef arr_ptr = llvm_load_vreg(ctx, f, arr); + LLVMValueRef index = llvm_load_vreg(ctx, f, idx); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + /* Data starts immediately after varray header */ + LLVMValueRef data_offset = LLVMConstInt(ctx->i64_type, sizeof(varray), false); + LLVMValueRef data_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + arr_ptr, &data_offset, 1, ""); + + LLVMValueRef elem_size_val = LLVMConstInt(ctx->i32_type, elem_size, false); + LLVMValueRef byte_offset = LLVMBuildMul(ctx->builder, index, elem_size_val, ""); + LLVMValueRef byte_offset64 = LLVMBuildZExt(ctx->builder, byte_offset, ctx->i64_type, ""); + LLVMValueRef elem_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + data_ptr, &byte_offset64, 1, ""); + + LLVMBuildStore(ctx->builder, val, elem_ptr); + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_ops_misc.c b/src/llvm/llvm_ops_misc.c new file mode 100644 index 000000000..0792c939d --- /dev/null +++ b/src/llvm/llvm_ops_misc.c @@ -0,0 +1,72 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Miscellaneous Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_misc(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case ONop: { + /* No operation */ + break; + } + + case OAssert: { + /* Debug assertion - typically a breakpoint or trap */ + /* In release builds this is a no-op */ + /* For debug builds, we could emit a trap */ +#ifdef DEBUG + LLVMValueRef trap_fn = LLVMGetNamedFunction(ctx->module, "llvm.debugtrap"); + if (!trap_fn) { + LLVMTypeRef trap_fn_type = LLVMFunctionType(ctx->void_type, NULL, 0, false); + trap_fn = LLVMAddFunction(ctx->module, "llvm.debugtrap", trap_fn_type); + } + LLVMBuildCall2(ctx->builder, + LLVMFunctionType(ctx->void_type, NULL, 0, false), + trap_fn, NULL, 0, ""); +#endif + break; + } + + case OPrefetch: { + /* Memory prefetch hint - can be ignored or use LLVM prefetch intrinsic */ + int ptr_reg = op->p1; + /* int mode = op->p2; */ /* Read/write hint */ + /* int locality = op->p3; */ /* Cache locality hint */ + + LLVMValueRef ptr = llvm_load_vreg(ctx, f, ptr_reg); + + /* Get or declare llvm.prefetch intrinsic */ + LLVMValueRef prefetch_fn = LLVMGetNamedFunction(ctx->module, "llvm.prefetch.p0"); + if (!prefetch_fn) { + /* llvm.prefetch(ptr, rw, locality, cache_type) */ + LLVMTypeRef param_types[] = { ctx->ptr_type, ctx->i32_type, ctx->i32_type, ctx->i32_type }; + LLVMTypeRef prefetch_fn_type = LLVMFunctionType(ctx->void_type, param_types, 4, false); + prefetch_fn = LLVMAddFunction(ctx->module, "llvm.prefetch.p0", prefetch_fn_type); + } + + LLVMValueRef args[] = { + ptr, + LLVMConstInt(ctx->i32_type, 0, false), /* 0 = read */ + LLVMConstInt(ctx->i32_type, 3, false), /* locality: 3 = high */ + LLVMConstInt(ctx->i32_type, 1, false) /* cache type: 1 = data */ + }; + LLVMBuildCall2(ctx->builder, + LLVMFunctionType(ctx->void_type, + (LLVMTypeRef[]){ ctx->ptr_type, ctx->i32_type, ctx->i32_type, ctx->i32_type }, 4, false), + prefetch_fn, args, 4, ""); + break; + } + + case OAsm: { + /* Inline assembly - not supported in LLVM backend */ + /* This opcode is rarely used */ + /* We could potentially translate to LLVM inline asm, but it's complex */ + /* For now, emit a warning comment and continue */ + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_ops_objects.c b/src/llvm/llvm_ops_objects.c new file mode 100644 index 000000000..2fbc2ebae --- /dev/null +++ b/src/llvm/llvm_ops_objects.c @@ -0,0 +1,216 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Object and Dynamic Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_objects(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case ONew: { + /* dst = new object of type from R(dst) */ + int dst = op->p1; + hl_type *t = f->regs[dst]; + + /* Get type pointer */ + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == t) { + type_idx = i; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) + : LLVMConstNull(ctx->ptr_type); + + LLVMValueRef result; + switch (t->kind) { + case HOBJ: + case HSTRUCT: { + /* Call hl_alloc_obj(type) */ + LLVMValueRef args[] = { type_ptr }; + LLVMTypeRef fn_type = LLVMGlobalGetValueType(ctx->rt_alloc_obj); + result = LLVMBuildCall2(ctx->builder, fn_type, + ctx->rt_alloc_obj, args, 1, ""); + break; + } + case HDYNOBJ: { + /* Call hl_alloc_dynobj() - no arguments */ + LLVMTypeRef fn_type = LLVMGlobalGetValueType(ctx->rt_alloc_dynobj); + result = LLVMBuildCall2(ctx->builder, fn_type, + ctx->rt_alloc_dynobj, NULL, 0, ""); + break; + } + case HVIRTUAL: { + /* Call hl_alloc_virtual */ + LLVMValueRef args[] = { type_ptr }; + LLVMTypeRef fn_type = LLVMGlobalGetValueType(ctx->rt_alloc_virtual); + result = LLVMBuildCall2(ctx->builder, fn_type, + ctx->rt_alloc_virtual, args, 1, ""); + break; + } + default: + result = LLVMConstNull(ctx->ptr_type); + break; + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OArraySize: { + /* dst = array.length */ + int dst = op->p1; + int arr = op->p2; + LLVMValueRef arr_ptr = llvm_load_vreg(ctx, f, arr); + + /* varray layout: hl_type* t (0), hl_type* at (8), int size (16), int __pad (20) */ + LLVMValueRef size_offset = LLVMConstInt(ctx->i64_type, 16, false); + LLVMValueRef size_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + arr_ptr, &size_offset, 1, ""); + LLVMValueRef size = LLVMBuildLoad2(ctx->builder, ctx->i32_type, size_ptr, ""); + llvm_store_vreg(ctx, f, dst, size); + break; + } + + case ODynGet: { + /* dst = obj.field (dynamic field access by hash) */ + int dst = op->p1; + int obj = op->p2; + int field_hash = op->p3; + hl_type *dst_type = f->regs[dst]; + + LLVMValueRef obj_ptr = llvm_load_vreg(ctx, f, obj); + LLVMValueRef hash_val = LLVMConstInt(ctx->i32_type, field_hash, false); + + /* Choose the right getter based on destination type */ + LLVMValueRef result; + switch (dst_type->kind) { + case HI32: + case HUI8: + case HUI16: + case HBOOL: { + /* Get type pointer for dst */ + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == dst_type) { + type_idx = i; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) + : LLVMConstNull(ctx->ptr_type); + LLVMValueRef args[] = { obj_ptr, hash_val, type_ptr }; + result = LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_dyn_geti), + ctx->rt_dyn_geti, args, 3, ""); + break; + } + case HI64: { + LLVMValueRef args[] = { obj_ptr, hash_val }; + result = LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_dyn_geti64), + ctx->rt_dyn_geti64, args, 2, ""); + break; + } + case HF32: { + LLVMValueRef args[] = { obj_ptr, hash_val }; + result = LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_dyn_getf), + ctx->rt_dyn_getf, args, 2, ""); + break; + } + case HF64: { + LLVMValueRef args[] = { obj_ptr, hash_val }; + result = LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_dyn_getd), + ctx->rt_dyn_getd, args, 2, ""); + break; + } + default: { + /* Pointer type */ + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == dst_type) { + type_idx = i; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) + : LLVMConstNull(ctx->ptr_type); + LLVMValueRef args[] = { obj_ptr, hash_val, type_ptr }; + result = LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_dyn_getp), + ctx->rt_dyn_getp, args, 3, ""); + break; + } + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case ODynSet: { + /* obj.field = val (dynamic field set by hash) */ + int obj = op->p1; + int field_hash = op->p2; + int src = op->p3; + hl_type *src_type = f->regs[src]; + + LLVMValueRef obj_ptr = llvm_load_vreg(ctx, f, obj); + LLVMValueRef hash_val = LLVMConstInt(ctx->i32_type, field_hash, false); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + /* Choose the right setter based on source type */ + switch (src_type->kind) { + case HI32: + case HUI8: + case HUI16: + case HBOOL: { + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == src_type) { + type_idx = i; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) + : LLVMConstNull(ctx->ptr_type); + LLVMValueRef args[] = { obj_ptr, hash_val, type_ptr, val }; + LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_dyn_seti), + ctx->rt_dyn_seti, args, 4, ""); + break; + } + case HI64: { + LLVMValueRef args[] = { obj_ptr, hash_val, val }; + LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_dyn_seti64), + ctx->rt_dyn_seti64, args, 3, ""); + break; + } + case HF32: { + LLVMValueRef args[] = { obj_ptr, hash_val, val }; + LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_dyn_setf), + ctx->rt_dyn_setf, args, 3, ""); + break; + } + case HF64: { + LLVMValueRef args[] = { obj_ptr, hash_val, val }; + LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_dyn_setd), + ctx->rt_dyn_setd, args, 3, ""); + break; + } + default: { + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == src_type) { + type_idx = i; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) + : LLVMConstNull(ctx->ptr_type); + LLVMValueRef args[] = { obj_ptr, hash_val, type_ptr, val }; + LLVMBuildCall2(ctx->builder, LLVMGlobalGetValueType(ctx->rt_dyn_setp), + ctx->rt_dyn_setp, args, 4, ""); + break; + } + } + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_ops_refs.c b/src/llvm/llvm_ops_refs.c new file mode 100644 index 000000000..246d7e11d --- /dev/null +++ b/src/llvm/llvm_ops_refs.c @@ -0,0 +1,104 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Reference Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_refs(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case ORef: { + /* dst = &src (create reference to value) */ + int dst = op->p1; + int src = op->p2; + + /* References are represented as pointers to the vreg alloca */ + /* Get the address of the source vreg */ + LLVMValueRef ref_ptr = ctx->vreg_allocs[src]; + llvm_store_vreg(ctx, f, dst, ref_ptr); + break; + } + + case OUnref: { + /* dst = *src (dereference) */ + int dst = op->p1; + int src = op->p2; + hl_type *dst_type = f->regs[dst]; + LLVMTypeRef val_type = llvm_get_type(ctx, dst_type); + + LLVMValueRef ref_ptr = llvm_load_vreg(ctx, f, src); + LLVMValueRef val = LLVMBuildLoad2(ctx->builder, val_type, ref_ptr, ""); + llvm_store_vreg(ctx, f, dst, val); + break; + } + + case OSetref: { + /* *dst = src */ + int dst = op->p1; + int src = op->p2; + + LLVMValueRef ref_ptr = llvm_load_vreg(ctx, f, dst); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + LLVMBuildStore(ctx->builder, val, ref_ptr); + break; + } + + case ORefData: { + /* dst = bytes.data (get pointer to bytes/array data) */ + int dst = op->p1; + int src = op->p2; + hl_type *src_type = f->regs[src]; + + LLVMValueRef obj = llvm_load_vreg(ctx, f, src); + + /* For bytes, data is the pointer itself */ + /* For arrays, data starts at offset 16 (after type and size) */ + if (src_type->kind == HARRAY) { + LLVMValueRef data_offset = LLVMConstInt(ctx->i64_type, 16, false); + LLVMValueRef data_ptr = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + obj, &data_offset, 1, ""); + llvm_store_vreg(ctx, f, dst, data_ptr); + } else { + /* For bytes/other, just return the pointer */ + llvm_store_vreg(ctx, f, dst, obj); + } + break; + } + + case ORefOffset: { + /* dst = src + offset (pointer arithmetic) */ + int dst = op->p1; + int src = op->p2; + int offset_reg = op->p3; + hl_type *dst_type = f->regs[dst]; + + LLVMValueRef base = llvm_load_vreg(ctx, f, src); + LLVMValueRef offset = llvm_load_vreg(ctx, f, offset_reg); + + /* Determine element size from destination type */ + int elem_size = 1; /* Default to byte size */ + if (dst_type->kind == HREF && dst_type->tparam) { + elem_size = llvm_type_size(ctx, dst_type->tparam); + } + + /* Calculate byte offset */ + LLVMValueRef byte_offset; + if (elem_size == 1) { + byte_offset = offset; + } else { + LLVMValueRef size_val = LLVMConstInt(ctx->i32_type, elem_size, false); + byte_offset = LLVMBuildMul(ctx->builder, offset, size_val, ""); + } + + /* Extend to i64 for GEP */ + LLVMValueRef byte_offset64 = LLVMBuildSExt(ctx->builder, byte_offset, ctx->i64_type, ""); + + LLVMValueRef result = LLVMBuildGEP2(ctx->builder, ctx->i8_type, + base, &byte_offset64, 1, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_ops_types.c b/src/llvm/llvm_ops_types.c new file mode 100644 index 000000000..4e490d026 --- /dev/null +++ b/src/llvm/llvm_ops_types.c @@ -0,0 +1,303 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * LLVM Backend - Type Conversion and Casting Opcodes + */ +#include "llvm_codegen.h" + +void llvm_emit_types(llvm_ctx *ctx, hl_function *f, hl_opcode *op, int op_idx) { + switch (op->op) { + case OType: { + /* dst = type pointer for type index p2 */ + int dst = op->p1; + int type_idx = op->p2; + LLVMValueRef type_ptr = llvm_get_type_ptr(ctx, type_idx); + llvm_store_vreg(ctx, f, dst, type_ptr); + break; + } + + case OGetType: { + /* dst = obj->t (type of object) */ + int dst = op->p1; + int src = op->p2; + LLVMValueRef obj = llvm_load_vreg(ctx, f, src); + /* First field of any object is hl_type* t */ + LLVMValueRef type_ptr = LLVMBuildLoad2(ctx->builder, ctx->ptr_type, obj, ""); + llvm_store_vreg(ctx, f, dst, type_ptr); + break; + } + + case OGetTID: { + /* dst = type->kind */ + int dst = op->p1; + int src = op->p2; + LLVMValueRef type_ptr = llvm_load_vreg(ctx, f, src); + /* hl_type has kind as first field (int) */ + LLVMValueRef kind = LLVMBuildLoad2(ctx->builder, ctx->i32_type, type_ptr, ""); + llvm_store_vreg(ctx, f, dst, kind); + break; + } + + case OToDyn: { + /* dst = wrap value as dynamic */ + int dst = op->p1; + int src = op->p2; + hl_type *src_type = f->regs[src]; + + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + /* For pointer types, just use the value directly */ + if (llvm_is_ptr_type(src_type)) { + llvm_store_vreg(ctx, f, dst, val); + } else { + /* For value types, need to call hl_make_dyn */ + /* First store value to memory, then pass pointer */ + /* Use entry block alloca to avoid stack growth in loops */ + LLVMValueRef val_alloca = llvm_create_entry_alloca(ctx, + llvm_get_type(ctx, src_type), "todyn_tmp"); + LLVMBuildStore(ctx->builder, val, val_alloca); + + /* Get type pointer for the source type */ + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == src_type) { + type_idx = i; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) + : LLVMConstNull(ctx->ptr_type); + + LLVMValueRef args[] = { val_alloca, type_ptr }; + LLVMValueRef result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_make_dyn), + ctx->rt_make_dyn, args, 2, ""); + llvm_store_vreg(ctx, f, dst, result); + } + break; + } + + case OToSFloat: { + /* dst = (float)src - convert to float type */ + int dst = op->p1; + int src = op->p2; + hl_type *src_type = f->regs[src]; + hl_type *dst_type = f->regs[dst]; + LLVMTypeRef target_type = llvm_get_type(ctx, dst_type); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + LLVMValueRef result; + if (llvm_is_float_type(src_type)) { + /* Float to float - use fptrunc or fpext */ + if (src_type->kind == HF64 && dst_type->kind == HF32) { + result = LLVMBuildFPTrunc(ctx->builder, val, target_type, ""); + } else if (src_type->kind == HF32 && dst_type->kind == HF64) { + result = LLVMBuildFPExt(ctx->builder, val, target_type, ""); + } else { + result = val; /* Same type */ + } + } else { + /* Integer to float - use sitofp */ + result = LLVMBuildSIToFP(ctx->builder, val, target_type, ""); + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OToUFloat: { + /* dst = (float)src - convert to float type (unsigned source) */ + int dst = op->p1; + int src = op->p2; + hl_type *src_type = f->regs[src]; + hl_type *dst_type = f->regs[dst]; + LLVMTypeRef target_type = llvm_get_type(ctx, dst_type); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + LLVMValueRef result; + if (llvm_is_float_type(src_type)) { + /* Float to float - use fptrunc or fpext */ + if (src_type->kind == HF64 && dst_type->kind == HF32) { + result = LLVMBuildFPTrunc(ctx->builder, val, target_type, ""); + } else if (src_type->kind == HF32 && dst_type->kind == HF64) { + result = LLVMBuildFPExt(ctx->builder, val, target_type, ""); + } else { + result = val; /* Same type */ + } + } else { + /* Unsigned integer to float - use uitofp */ + result = LLVMBuildUIToFP(ctx->builder, val, target_type, ""); + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OToInt: { + /* dst = (int)src */ + int dst = op->p1; + int src = op->p2; + hl_type *src_type = f->regs[src]; + hl_type *dst_type = f->regs[dst]; + LLVMTypeRef target_type = llvm_get_type(ctx, dst_type); + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + LLVMValueRef result; + if (llvm_is_float_type(src_type)) { + result = LLVMBuildFPToSI(ctx->builder, val, target_type, ""); + } else { + /* Integer to integer - handle width differences */ + unsigned src_bits = LLVMGetIntTypeWidth(LLVMTypeOf(val)); + unsigned dst_bits = LLVMGetIntTypeWidth(target_type); + if (src_bits > dst_bits) { + result = LLVMBuildTrunc(ctx->builder, val, target_type, ""); + } else if (src_bits < dst_bits) { + result = LLVMBuildSExt(ctx->builder, val, target_type, ""); + } else { + result = val; + } + } + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OSafeCast: { + /* + * dst = safe_cast(src, target_type) + * + * Different cast functions are used based on destination type: + * - hl_dyn_casti(ptr, src_type, dst_type) -> i32 for HBOOL, HI32, HUI8, HUI16 + * - hl_dyn_casti64(ptr, src_type) -> i64 for HI64 + * - hl_dyn_castf(ptr, src_type) -> f32 for HF32 + * - hl_dyn_castd(ptr, src_type) -> f64 for HF64 + * - hl_dyn_castp(ptr, src_type, dst_type) -> ptr for pointer types + */ + int dst = op->p1; + int src = op->p2; + hl_type *src_type = f->regs[src]; + hl_type *dst_type = f->regs[dst]; + + /* Get source type pointer (static compile-time type) */ + int src_type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == src_type) { + src_type_idx = i; + break; + } + } + LLVMValueRef src_type_ptr = src_type_idx >= 0 ? llvm_get_type_ptr(ctx, src_type_idx) + : LLVMConstNull(ctx->ptr_type); + + /* Get destination type pointer (needed for some cast functions) */ + int dst_type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == dst_type) { + dst_type_idx = i; + break; + } + } + LLVMValueRef dst_type_ptr = dst_type_idx >= 0 ? llvm_get_type_ptr(ctx, dst_type_idx) + : LLVMConstNull(ctx->ptr_type); + + /* Get address of source vreg (pointer to stack slot containing the value) */ + LLVMValueRef src_addr = ctx->vreg_allocs[src]; + + LLVMValueRef result; + switch (dst_type->kind) { + case HF32: { + /* hl_dyn_castf(ptr, src_type) -> float */ + LLVMValueRef args[] = { src_addr, src_type_ptr }; + result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_dyn_castf), + ctx->rt_dyn_castf, args, 2, ""); + break; + } + case HF64: { + /* hl_dyn_castd(ptr, src_type) -> double */ + LLVMValueRef args[] = { src_addr, src_type_ptr }; + result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_dyn_castd), + ctx->rt_dyn_castd, args, 2, ""); + break; + } + case HI64: { + /* hl_dyn_casti64(ptr, src_type) -> i64 */ + LLVMValueRef args[] = { src_addr, src_type_ptr }; + result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_dyn_casti64), + ctx->rt_dyn_casti64, args, 2, ""); + break; + } + case HI32: + case HUI16: + case HUI8: + case HBOOL: { + /* hl_dyn_casti(ptr, src_type, dst_type) -> i32 */ + LLVMValueRef args[] = { src_addr, src_type_ptr, dst_type_ptr }; + LLVMValueRef i32_result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_dyn_casti), + ctx->rt_dyn_casti, args, 3, ""); + /* Truncate to actual destination size */ + LLVMTypeRef target_type = llvm_get_type(ctx, dst_type); + if (dst_type->kind == HBOOL || dst_type->kind == HUI8) { + result = LLVMBuildTrunc(ctx->builder, i32_result, target_type, ""); + } else if (dst_type->kind == HUI16) { + result = LLVMBuildTrunc(ctx->builder, i32_result, target_type, ""); + } else { + result = i32_result; + } + break; + } + default: { + /* hl_dyn_castp(ptr, src_type, dst_type) -> ptr */ + LLVMValueRef args[] = { src_addr, src_type_ptr, dst_type_ptr }; + result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_dyn_castp), + ctx->rt_dyn_castp, args, 3, ""); + break; + } + } + + llvm_store_vreg(ctx, f, dst, result); + break; + } + + case OUnsafeCast: { + /* dst = (dst_type)src - no runtime check */ + int dst = op->p1; + int src = op->p2; + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + /* Just pass through for pointer types */ + llvm_store_vreg(ctx, f, dst, val); + break; + } + + case OToVirtual: { + /* dst = to_virtual(src, virtual_type) */ + int dst = op->p1; + int src = op->p2; + hl_type *dst_type = f->regs[dst]; + + LLVMValueRef val = llvm_load_vreg(ctx, f, src); + + /* Get destination type pointer */ + int type_idx = -1; + for (int i = 0; i < ctx->code->ntypes; i++) { + if (ctx->code->types + i == dst_type) { + type_idx = i; + break; + } + } + LLVMValueRef type_ptr = type_idx >= 0 ? llvm_get_type_ptr(ctx, type_idx) + : LLVMConstNull(ctx->ptr_type); + + /* Call hl_to_virtual */ + LLVMValueRef args[] = { type_ptr, val }; + LLVMValueRef result = LLVMBuildCall2(ctx->builder, + LLVMGlobalGetValueType(ctx->rt_to_virtual), + ctx->rt_to_virtual, args, 2, ""); + llvm_store_vreg(ctx, f, dst, result); + break; + } + + default: + break; + } +} diff --git a/src/llvm/llvm_runtime.c b/src/llvm/llvm_runtime.c new file mode 100644 index 000000000..5998deaa8 --- /dev/null +++ b/src/llvm/llvm_runtime.c @@ -0,0 +1,332 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include "llvm_codegen.h" + +/* Helper to declare a function with specific signature. + * If function already exists, return the existing one to avoid + * LLVM creating duplicate symbols with suffixes like .1, .2, etc. */ +static LLVMValueRef declare_func(llvm_ctx *ctx, const char *name, + LLVMTypeRef ret_type, + LLVMTypeRef *param_types, int nparams, + bool is_vararg) { + LLVMValueRef existing = LLVMGetNamedFunction(ctx->module, name); + if (existing) return existing; + LLVMTypeRef fn_type = LLVMFunctionType(ret_type, param_types, nparams, is_vararg); + return LLVMAddFunction(ctx->module, name, fn_type); +} + +/* Add noreturn attribute to a function */ +static void add_noreturn(llvm_ctx *ctx, LLVMValueRef func) { + unsigned kind = LLVMGetEnumAttributeKindForName("noreturn", 8); + LLVMAttributeRef attr = LLVMCreateEnumAttribute(ctx->context, kind, 0); + LLVMAddAttributeAtIndex(func, LLVMAttributeFunctionIndex, attr); +} + +void llvm_declare_runtime(llvm_ctx *ctx) { + LLVMTypeRef ptr = ctx->ptr_type; + LLVMTypeRef i32 = ctx->i32_type; + LLVMTypeRef i64 = ctx->i64_type; + LLVMTypeRef f32 = ctx->f32_type; + LLVMTypeRef f64 = ctx->f64_type; + LLVMTypeRef void_t = ctx->void_type; + + /* hl_alloc_obj(hl_type*) -> vdynamic* */ + { + LLVMTypeRef params[] = { ptr }; + ctx->rt_alloc_obj = declare_func(ctx, "hl_alloc_obj", ptr, params, 1, false); + } + + /* hl_alloc_array(hl_type*, int) -> varray* */ + { + LLVMTypeRef params[] = { ptr, i32 }; + ctx->rt_alloc_array = declare_func(ctx, "hl_alloc_array", ptr, params, 2, false); + } + + /* hl_alloc_enum(hl_type*, int) -> venum* */ + { + LLVMTypeRef params[] = { ptr, i32 }; + ctx->rt_alloc_enum = declare_func(ctx, "hl_alloc_enum", ptr, params, 2, false); + } + + /* hl_alloc_virtual(hl_type*) -> vvirtual* */ + { + LLVMTypeRef params[] = { ptr }; + ctx->rt_alloc_virtual = declare_func(ctx, "hl_alloc_virtual", ptr, params, 1, false); + } + + /* hl_alloc_dynobj() -> vdynobj* */ + { + ctx->rt_alloc_dynobj = declare_func(ctx, "hl_alloc_dynobj", ptr, NULL, 0, false); + } + + /* hl_alloc_closure_void(hl_type*, void*) -> vclosure* */ + { + LLVMTypeRef params[] = { ptr, ptr }; + ctx->rt_alloc_closure_void = declare_func(ctx, "hl_alloc_closure_void", ptr, params, 2, false); + } + + /* hl_alloc_closure_ptr(hl_type*, void*, void*) -> vclosure* */ + { + LLVMTypeRef params[] = { ptr, ptr, ptr }; + ctx->rt_alloc_closure_ptr = declare_func(ctx, "hl_alloc_closure_ptr", ptr, params, 3, false); + } + + /* hl_alloc_bytes(int) -> vbyte* */ + { + LLVMTypeRef params[] = { i32 }; + ctx->rt_alloc_bytes = declare_func(ctx, "hl_alloc_bytes", ptr, params, 1, false); + } + + /* hl_throw(vdynamic*) -> noreturn */ + { + LLVMTypeRef params[] = { ptr }; + ctx->rt_throw = declare_func(ctx, "hl_throw", void_t, params, 1, false); + add_noreturn(ctx, ctx->rt_throw); + } + + /* hl_rethrow(vdynamic*) -> noreturn */ + { + LLVMTypeRef params[] = { ptr }; + ctx->rt_rethrow = declare_func(ctx, "hl_rethrow", void_t, params, 1, false); + add_noreturn(ctx, ctx->rt_rethrow); + } + + /* hl_null_access() -> noreturn */ + { + ctx->rt_null_access = declare_func(ctx, "hl_null_access", void_t, NULL, 0, false); + add_noreturn(ctx, ctx->rt_null_access); + } + + /* hl_get_obj_rt(hl_type*) -> hl_runtime_obj* */ + { + LLVMTypeRef params[] = { ptr }; + ctx->rt_get_obj_rt = declare_func(ctx, "hl_get_obj_rt", ptr, params, 1, false); + } + + /* hl_to_virtual(hl_type*, vdynamic*) -> vvirtual* */ + { + LLVMTypeRef params[] = { ptr, ptr }; + ctx->rt_to_virtual = declare_func(ctx, "hl_to_virtual", ptr, params, 2, false); + } + + /* hl_safe_cast(hl_type*, hl_type*) -> bool */ + { + LLVMTypeRef params[] = { ptr, ptr }; + ctx->rt_safe_cast = declare_func(ctx, "hl_safe_cast", ctx->i8_type, params, 2, false); + } + + /* hl_make_dyn(void*, hl_type*) -> vdynamic* */ + { + LLVMTypeRef params[] = { ptr, ptr }; + ctx->rt_make_dyn = declare_func(ctx, "hl_make_dyn", ptr, params, 2, false); + } + + /* hl_dyn_call(vclosure*, vdynamic**, int) -> vdynamic* */ + { + LLVMTypeRef params[] = { ptr, ptr, i32 }; + ctx->rt_dyn_call = declare_func(ctx, "hl_dyn_call", ptr, params, 3, false); + } + + /* hl_dyn_call_safe(vclosure*, vdynamic**, int, bool*) -> vdynamic* */ + { + LLVMTypeRef params[] = { ptr, ptr, i32, ptr }; + ctx->rt_dyn_call_safe = declare_func(ctx, "hl_dyn_call_safe", ptr, params, 4, false); + } + + /* hl_get_thread() -> hl_thread_info* */ + { + ctx->rt_get_thread = declare_func(ctx, "hl_get_thread", ptr, NULL, 0, false); + } + + /* Dynamic field getters */ + /* hl_dyn_geti(vdynamic*, int, hl_type*) -> int */ + { + LLVMTypeRef params[] = { ptr, i32, ptr }; + ctx->rt_dyn_geti = declare_func(ctx, "hl_dyn_geti", i32, params, 3, false); + } + + /* hl_dyn_geti64(vdynamic*, int) -> int64 */ + { + LLVMTypeRef params[] = { ptr, i32 }; + ctx->rt_dyn_geti64 = declare_func(ctx, "hl_dyn_geti64", i64, params, 2, false); + } + + /* hl_dyn_getf(vdynamic*, int) -> float */ + { + LLVMTypeRef params[] = { ptr, i32 }; + ctx->rt_dyn_getf = declare_func(ctx, "hl_dyn_getf", f32, params, 2, false); + } + + /* hl_dyn_getd(vdynamic*, int) -> double */ + { + LLVMTypeRef params[] = { ptr, i32 }; + ctx->rt_dyn_getd = declare_func(ctx, "hl_dyn_getd", f64, params, 2, false); + } + + /* hl_dyn_getp(vdynamic*, int, hl_type*) -> void* */ + { + LLVMTypeRef params[] = { ptr, i32, ptr }; + ctx->rt_dyn_getp = declare_func(ctx, "hl_dyn_getp", ptr, params, 3, false); + } + + /* Dynamic field setters */ + /* hl_dyn_seti(vdynamic*, int, hl_type*, int) */ + { + LLVMTypeRef params[] = { ptr, i32, ptr, i32 }; + ctx->rt_dyn_seti = declare_func(ctx, "hl_dyn_seti", void_t, params, 4, false); + } + + /* hl_dyn_seti64(vdynamic*, int, int64) */ + { + LLVMTypeRef params[] = { ptr, i32, i64 }; + ctx->rt_dyn_seti64 = declare_func(ctx, "hl_dyn_seti64", void_t, params, 3, false); + } + + /* hl_dyn_setf(vdynamic*, int, float) */ + { + LLVMTypeRef params[] = { ptr, i32, f32 }; + ctx->rt_dyn_setf = declare_func(ctx, "hl_dyn_setf", void_t, params, 3, false); + } + + /* hl_dyn_setd(vdynamic*, int, double) */ + { + LLVMTypeRef params[] = { ptr, i32, f64 }; + ctx->rt_dyn_setd = declare_func(ctx, "hl_dyn_setd", void_t, params, 3, false); + } + + /* hl_dyn_setp(vdynamic*, int, hl_type*, void*) */ + { + LLVMTypeRef params[] = { ptr, i32, ptr, ptr }; + ctx->rt_dyn_setp = declare_func(ctx, "hl_dyn_setp", void_t, params, 4, false); + } + + /* Dynamic cast functions */ + /* hl_dyn_casti(void*, hl_type*, hl_type*) -> int */ + { + LLVMTypeRef params[] = { ptr, ptr, ptr }; + ctx->rt_dyn_casti = declare_func(ctx, "hl_dyn_casti", i32, params, 3, false); + } + + /* hl_dyn_casti64(void*, hl_type*) -> int64 */ + { + LLVMTypeRef params[] = { ptr, ptr }; + ctx->rt_dyn_casti64 = declare_func(ctx, "hl_dyn_casti64", i64, params, 2, false); + } + + /* hl_dyn_castf(void*, hl_type*) -> float */ + { + LLVMTypeRef params[] = { ptr, ptr }; + ctx->rt_dyn_castf = declare_func(ctx, "hl_dyn_castf", f32, params, 2, false); + } + + /* hl_dyn_castd(void*, hl_type*) -> double */ + { + LLVMTypeRef params[] = { ptr, ptr }; + ctx->rt_dyn_castd = declare_func(ctx, "hl_dyn_castd", f64, params, 2, false); + } + + /* hl_dyn_castp(void*, hl_type*, hl_type*) -> void* */ + { + LLVMTypeRef params[] = { ptr, ptr, ptr }; + ctx->rt_dyn_castp = declare_func(ctx, "hl_dyn_castp", ptr, params, 3, false); + } + + /* Hash functions */ + /* hl_hash(vbyte*) -> int */ + { + LLVMTypeRef params[] = { ptr }; + ctx->rt_hash = declare_func(ctx, "hl_hash", i32, params, 1, false); + } + + /* hl_hash_gen(uchar*, bool) -> int */ + { + LLVMTypeRef params[] = { ptr, ctx->i8_type }; + ctx->rt_hash_gen = declare_func(ctx, "hl_hash_gen", i32, params, 2, false); + } + + /* setjmp/longjmp for exception handling */ + /* setjmp(jmp_buf) -> int */ + { + LLVMTypeRef params[] = { ptr }; + ctx->rt_setjmp = declare_func(ctx, "setjmp", i32, params, 1, false); + } + + /* longjmp(jmp_buf, int) -> noreturn */ + { + LLVMTypeRef params[] = { ptr, i32 }; + ctx->rt_longjmp = declare_func(ctx, "longjmp", void_t, params, 2, false); + add_noreturn(ctx, ctx->rt_longjmp); + } + + /* aot_get_type(int) -> void* - AOT runtime type accessor + * Mark as pure function so LLVM can CSE and hoist out of loops. + * The function just does: return &types_array[index]; */ + { + LLVMTypeRef params[] = { i32 }; + ctx->rt_aot_get_type = declare_func(ctx, "aot_get_type", ptr, params, 1, false); + /* memory(none) - function doesn't read or write any memory visible to caller. + * It returns a pointer computed from a global base + offset. */ + unsigned mem_kind = LLVMGetEnumAttributeKindForName("memory", 6); + LLVMAttributeRef mem_attr = LLVMCreateEnumAttribute(ctx->context, mem_kind, 0); + LLVMAddAttributeAtIndex(ctx->rt_aot_get_type, LLVMAttributeFunctionIndex, mem_attr); + /* nounwind - doesn't throw */ + unsigned nounwind_kind = LLVMGetEnumAttributeKindForName("nounwind", 8); + LLVMAttributeRef nounwind_attr = LLVMCreateEnumAttribute(ctx->context, nounwind_kind, 0); + LLVMAddAttributeAtIndex(ctx->rt_aot_get_type, LLVMAttributeFunctionIndex, nounwind_attr); + /* willreturn - always returns */ + unsigned willreturn_kind = LLVMGetEnumAttributeKindForName("willreturn", 10); + LLVMAttributeRef willreturn_attr = LLVMCreateEnumAttribute(ctx->context, willreturn_kind, 0); + LLVMAddAttributeAtIndex(ctx->rt_aot_get_type, LLVMAttributeFunctionIndex, willreturn_attr); + /* speculatable - can be safely speculated/hoisted */ + unsigned spec_kind = LLVMGetEnumAttributeKindForName("speculatable", 12); + if (spec_kind) { + LLVMAttributeRef spec_attr = LLVMCreateEnumAttribute(ctx->context, spec_kind, 0); + LLVMAddAttributeAtIndex(ctx->rt_aot_get_type, LLVMAttributeFunctionIndex, spec_attr); + } + } + + /* aot_get_global(int) -> void** - AOT runtime global accessor + * Mark as pure function so LLVM can CSE and hoist out of loops. + * The function just does: return &globals_array[index]; */ + { + LLVMTypeRef params[] = { i32 }; + ctx->rt_aot_get_global = declare_func(ctx, "aot_get_global", ptr, params, 1, false); + /* memory(none) - function doesn't read or write any memory visible to caller */ + unsigned mem_kind = LLVMGetEnumAttributeKindForName("memory", 6); + LLVMAttributeRef mem_attr = LLVMCreateEnumAttribute(ctx->context, mem_kind, 0); + LLVMAddAttributeAtIndex(ctx->rt_aot_get_global, LLVMAttributeFunctionIndex, mem_attr); + /* nounwind - doesn't throw */ + unsigned nounwind_kind = LLVMGetEnumAttributeKindForName("nounwind", 8); + LLVMAttributeRef nounwind_attr = LLVMCreateEnumAttribute(ctx->context, nounwind_kind, 0); + LLVMAddAttributeAtIndex(ctx->rt_aot_get_global, LLVMAttributeFunctionIndex, nounwind_attr); + /* willreturn - always returns */ + unsigned willreturn_kind = LLVMGetEnumAttributeKindForName("willreturn", 10); + LLVMAttributeRef willreturn_attr = LLVMCreateEnumAttribute(ctx->context, willreturn_kind, 0); + LLVMAddAttributeAtIndex(ctx->rt_aot_get_global, LLVMAttributeFunctionIndex, willreturn_attr); + /* speculatable - can be safely speculated/hoisted */ + unsigned spec_kind = LLVMGetEnumAttributeKindForName("speculatable", 12); + if (spec_kind) { + LLVMAttributeRef spec_attr = LLVMCreateEnumAttribute(ctx->context, spec_kind, 0); + LLVMAddAttributeAtIndex(ctx->rt_aot_get_global, LLVMAttributeFunctionIndex, spec_attr); + } + } +} diff --git a/src/llvm/llvm_types.c b/src/llvm/llvm_types.c new file mode 100644 index 000000000..52c87e9cc --- /dev/null +++ b/src/llvm/llvm_types.c @@ -0,0 +1,135 @@ +/* + * Copyright (C)2005-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include "llvm_codegen.h" + +LLVMTypeRef llvm_get_type(llvm_ctx *ctx, hl_type *t) { + if (!t) return ctx->ptr_type; + + switch (t->kind) { + case HVOID: + return ctx->void_type; + + case HUI8: + return ctx->i8_type; + + case HUI16: + return ctx->i16_type; + + case HI32: + return ctx->i32_type; + + case HI64: + return ctx->i64_type; + + case HF32: + return ctx->f32_type; + + case HF64: + return ctx->f64_type; + + case HBOOL: + return ctx->i8_type; + + /* All pointer/object types become opaque pointers */ + case HBYTES: + case HDYN: + case HFUN: + case HOBJ: + case HARRAY: + case HTYPE: + case HREF: + case HVIRTUAL: + case HDYNOBJ: + case HABSTRACT: + case HENUM: + case HNULL: + case HMETHOD: + case HSTRUCT: + case HPACKED: + case HGUID: + return ctx->ptr_type; + + default: + return ctx->ptr_type; + } +} + +LLVMTypeRef llvm_get_function_type(llvm_ctx *ctx, hl_type *t) { + if (!t || t->kind != HFUN) { + /* Default to void function */ + return LLVMFunctionType(ctx->void_type, NULL, 0, false); + } + + hl_type_fun *ft = t->fun; + + /* Get return type */ + LLVMTypeRef ret_type = llvm_get_type(ctx, ft->ret); + if (ft->ret->kind == HVOID) { + ret_type = ctx->void_type; + } + + /* Get parameter types */ + int nargs = ft->nargs; + LLVMTypeRef *param_types = NULL; + if (nargs > 0) { + param_types = (LLVMTypeRef *)malloc(sizeof(LLVMTypeRef) * nargs); + for (int i = 0; i < nargs; i++) { + param_types[i] = llvm_get_type(ctx, ft->args[i]); + } + } + + LLVMTypeRef fn_type = LLVMFunctionType(ret_type, param_types, nargs, false); + + if (param_types) free(param_types); + + return fn_type; +} + +int llvm_type_size(llvm_ctx *ctx, hl_type *t) { + if (!t) return 8; /* Pointer size */ + + switch (t->kind) { + case HVOID: + return 0; + case HUI8: + case HBOOL: + return 1; + case HUI16: + return 2; + case HI32: + case HF32: + return 4; + case HI64: + case HF64: + return 8; + default: + return 8; /* Pointer size */ + } +} + +bool llvm_is_float_type(hl_type *t) { + return t && (t->kind == HF32 || t->kind == HF64); +} + +bool llvm_is_ptr_type(hl_type *t) { + return t && t->kind >= HBYTES; +} diff --git a/src/std/types.c b/src/std/types.c index eaf228db6..165ea47c2 100644 --- a/src/std/types.c +++ b/src/std/types.c @@ -420,6 +420,7 @@ HL_PRIM void hl_init_enum( hl_type *et, hl_module_context *m ) { } HL_PRIM varray* hl_type_enum_fields( hl_type *t ) { + if( t->kind != HENUM ) return NULL; varray *a = hl_alloc_array(&hlt_bytes,t->tenum->nconstructs); int i; for( i=0; itenum->nconstructs;i++) From 12d3aba918db45b7712b2bc9b87b828c55305bad Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Thu, 4 Dec 2025 08:09:28 -0800 Subject: [PATCH 02/20] Initial commit of aarch64 support All unit tests pass except stack traces --- .gitignore | 3 + CMakeLists.txt | 93 +- Makefile | 20 +- libs/sdl/sdl/GL.hx | 1 + other/tests/minimal/Empty.hx | 4 + other/tests/minimal/FieldAccess.hx | 16 + other/tests/minimal/FuncCall.hx | 9 + other/tests/minimal/IntAdd.hx | 8 + other/tests/minimal/Makefile | 2 + other/tests/minimal/hldump.c | 287 + other/tests/minimal/test_array_ops.c | 384 ++ other/tests/minimal/test_binop_inplace.c | 670 ++ other/tests/minimal/test_bool_ops.c | 288 + other/tests/minimal/test_callbacks.c | 518 ++ other/tests/minimal/test_calls.c | 446 ++ other/tests/minimal/test_closures.c | 280 + other/tests/minimal/test_control_flow.c | 560 ++ other/tests/minimal/test_dynamic.c | 294 + other/tests/minimal/test_enum.c | 327 + other/tests/minimal/test_exceptions.c | 291 + other/tests/minimal/test_float_ops.c | 511 ++ other/tests/minimal/test_globals.c | 189 + other/tests/minimal/test_harness.h | 389 ++ other/tests/minimal/test_i64_ops.c | 545 ++ other/tests/minimal/test_instance_closure.c | 390 ++ other/tests/minimal/test_int_ops.c | 622 ++ other/tests/minimal/test_jumps_unsigned.c | 422 ++ other/tests/minimal/test_memory_ops.c | 448 ++ other/tests/minimal/test_methods.c | 330 + other/tests/minimal/test_native_field.c | 490 ++ other/tests/minimal/test_natives.c | 234 + other/tests/minimal/test_objects.c | 410 ++ other/tests/minimal/test_ref_ops.c | 474 ++ other/tests/minimal/test_strings.c | 205 + other/tests/minimal/test_switch.c | 367 + other/tests/minimal/test_type_ops.c | 290 + other/tests/minimal/test_unsigned_ops.c | 359 + other/tests/minimal/test_virtual_fields.c | 357 + src/hl.h | 8 +- src/jit_aarch64.c | 6807 +++++++++++++++++++ src/jit_aarch64_emit.c | 752 ++ src/jit_aarch64_emit.h | 182 + src/jit_common.h | 167 + src/jit_shared.c | 65 + src/{jit.c => jit_x86.c} | 133 +- src/llvm/aot_runtime.c | 19 +- src/llvm/llvm_ops_objects.c | 9 +- src/opcodes.h | 28 + src/profile.c | 6 +- src/std/cast.c | 2 +- src/std/types.c | 11 +- src/std/ucs2.c | 12 + 52 files changed, 19590 insertions(+), 144 deletions(-) create mode 100644 other/tests/minimal/Empty.hx create mode 100644 other/tests/minimal/FieldAccess.hx create mode 100644 other/tests/minimal/FuncCall.hx create mode 100644 other/tests/minimal/IntAdd.hx create mode 100644 other/tests/minimal/Makefile create mode 100644 other/tests/minimal/hldump.c create mode 100644 other/tests/minimal/test_array_ops.c create mode 100644 other/tests/minimal/test_binop_inplace.c create mode 100644 other/tests/minimal/test_bool_ops.c create mode 100644 other/tests/minimal/test_callbacks.c create mode 100644 other/tests/minimal/test_calls.c create mode 100644 other/tests/minimal/test_closures.c create mode 100644 other/tests/minimal/test_control_flow.c create mode 100644 other/tests/minimal/test_dynamic.c create mode 100644 other/tests/minimal/test_enum.c create mode 100644 other/tests/minimal/test_exceptions.c create mode 100644 other/tests/minimal/test_float_ops.c create mode 100644 other/tests/minimal/test_globals.c create mode 100644 other/tests/minimal/test_harness.h create mode 100644 other/tests/minimal/test_i64_ops.c create mode 100644 other/tests/minimal/test_instance_closure.c create mode 100644 other/tests/minimal/test_int_ops.c create mode 100644 other/tests/minimal/test_jumps_unsigned.c create mode 100644 other/tests/minimal/test_memory_ops.c create mode 100644 other/tests/minimal/test_methods.c create mode 100644 other/tests/minimal/test_native_field.c create mode 100644 other/tests/minimal/test_natives.c create mode 100644 other/tests/minimal/test_objects.c create mode 100644 other/tests/minimal/test_ref_ops.c create mode 100644 other/tests/minimal/test_strings.c create mode 100644 other/tests/minimal/test_switch.c create mode 100644 other/tests/minimal/test_type_ops.c create mode 100644 other/tests/minimal/test_unsigned_ops.c create mode 100644 other/tests/minimal/test_virtual_fields.c create mode 100644 src/jit_aarch64.c create mode 100644 src/jit_aarch64_emit.c create mode 100644 src/jit_aarch64_emit.h create mode 100644 src/jit_common.h create mode 100644 src/jit_shared.c rename src/{jit.c => jit_x86.c} (94%) diff --git a/.gitignore b/.gitignore index 8a662b2c5..9c68cc7d3 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ node_modules /*.tgz args.txt /other/benchs/hlc + +/CLAUDE.md +/.claude \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b39055b9..e316f954d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,9 +20,7 @@ include(FindPkgConfig) include(CTest) set(WITH_VM_DEFAULT ON) -if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|aarch64" AND (NOT CMAKE_OSX_ARCHITECTURES MATCHES "x86_64")) - set(WITH_VM_DEFAULT OFF) -endif() +# VM now supports x86, x86-64, and AArch64 architectures option(WITH_VM "Whether to build the Hashlink virtual machine" ${WITH_VM_DEFAULT}) option(WITH_LLVM_AOT "Whether to build the hl2llvm AOT compiler" OFF) @@ -200,9 +198,23 @@ set_target_properties(libhl ) if (WITH_VM) + # Select JIT backend based on architecture + if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") + set(JIT_SOURCES + src/jit_aarch64.c + src/jit_aarch64_emit.c + src/jit_shared.c + ) + else() + set(JIT_SOURCES + src/jit_x86.c + src/jit_shared.c + ) + endif() + add_executable(hl src/code.c - src/jit.c + ${JIT_SOURCES} src/main.c src/module.c src/debugger.c @@ -480,6 +492,79 @@ if(BUILD_TESTING) add_test(NAME uvsample.hl COMMAND hl ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/test/uvsample.hl 6001 ) + + ##################### + # Minimal JIT Tests + # These test individual opcodes without pulling in the Haxe stdlib + + # Common sources for all minimal JIT tests + set(MINIMAL_JIT_SOURCES + src/code.c + ${JIT_SOURCES} + src/module.c + src/debugger.c + src/profile.c + ) + + # Macro to add a minimal JIT test + macro(add_minimal_jit_test name) + add_executable(${name} + ${CMAKE_SOURCE_DIR}/other/tests/minimal/${name}.c + ${MINIMAL_JIT_SOURCES} + ) + target_include_directories(${name} + PRIVATE ${CMAKE_SOURCE_DIR}/other/tests/minimal + ) + target_link_libraries(${name} + libhl + ) + set_target_properties(${name} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/test/minimal + ) + add_test(NAME ${name} COMMAND ${name}) + endmacro() + + # Add all minimal JIT tests + add_minimal_jit_test(test_int_ops) + add_minimal_jit_test(test_float_ops) + add_minimal_jit_test(test_bool_ops) + add_minimal_jit_test(test_control_flow) + add_minimal_jit_test(test_i64_ops) + add_minimal_jit_test(test_calls) + add_minimal_jit_test(test_strings) + add_minimal_jit_test(test_globals) + add_minimal_jit_test(test_natives) + add_minimal_jit_test(test_closures) + add_minimal_jit_test(test_objects) + add_minimal_jit_test(test_dynamic) + add_minimal_jit_test(test_callbacks) + add_minimal_jit_test(test_native_field) + add_minimal_jit_test(test_binop_inplace) + add_minimal_jit_test(test_enum) + add_minimal_jit_test(test_instance_closure) + add_minimal_jit_test(test_memory_ops) + add_minimal_jit_test(test_array_ops) + add_minimal_jit_test(test_ref_ops) + add_minimal_jit_test(test_unsigned_ops) + add_minimal_jit_test(test_switch) + add_minimal_jit_test(test_jumps_unsigned) + add_minimal_jit_test(test_type_ops) + add_minimal_jit_test(test_exceptions) + add_minimal_jit_test(test_methods) + add_minimal_jit_test(test_virtual_fields) + + # Bytecode dump utility (needs code.c for hl_code_read) + add_executable(hldump + ${CMAKE_SOURCE_DIR}/other/tests/minimal/hldump.c + ${CMAKE_SOURCE_DIR}/src/code.c + ) + target_link_libraries(hldump libhl) + set_target_properties(hldump + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/test/minimal + ) + endif() add_test(NAME hello diff --git a/Makefile b/Makefile index 9ff8a6345..f4aed9a03 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,16 @@ STD = src/std/array.o src/std/buffer.o src/std/bytes.o src/std/cast.o src/std/da src/std/socket.o src/std/string.o src/std/sys.o src/std/types.o src/std/ucs2.o src/std/thread.o src/std/process.o \ src/std/track.o -HL = src/code.o src/jit.o src/main.o src/module.o src/debugger.o src/profile.o +# Conditional JIT backend selection based on architecture +ifeq ($(ARCH),aarch64) + HL_JIT = src/jit_aarch64.o src/jit_aarch64_emit.o src/jit_shared.o +else ifeq ($(ARCH),arm64) + HL_JIT = src/jit_aarch64.o src/jit_aarch64_emit.o src/jit_shared.o +else + HL_JIT = src/jit_x86.o src/jit_shared.o +endif + +HL = src/code.o $(HL_JIT) src/main.o src/module.o src/debugger.o src/profile.o FMT_INCLUDE = -I include/mikktspace -I include/minimp3 @@ -222,19 +231,12 @@ ifdef DEBUG CFLAGS += -g endif -all: libhl libs -ifeq ($(ARCH),arm64) - $(warning HashLink vm is not supported on arm64, skipping) -else -all: hl -endif +all: libhl libs hl install: $(UNAME)==Darwin && ${MAKE} uninstall -ifneq ($(ARCH),arm64) mkdir -p $(INSTALL_BIN_DIR) cp hl $(INSTALL_BIN_DIR) -endif mkdir -p $(INSTALL_LIB_DIR) cp *.hdll $(INSTALL_LIB_DIR) cp libhl.${LIBEXT} $(INSTALL_LIB_DIR) diff --git a/libs/sdl/sdl/GL.hx b/libs/sdl/sdl/GL.hx index a31e44a96..1ac3a2db7 100644 --- a/libs/sdl/sdl/GL.hx +++ b/libs/sdl/sdl/GL.hx @@ -701,6 +701,7 @@ class GL { public static inline var LUMINANCE_ALPHA = 0x190A; public static inline var BGRA = 0x80E1; + public static inline var RGB8 = 0x8051; public static inline var RGBA8 = 0x8058; public static inline var RGB10_A2 = 0x8059; diff --git a/other/tests/minimal/Empty.hx b/other/tests/minimal/Empty.hx new file mode 100644 index 000000000..0df354d52 --- /dev/null +++ b/other/tests/minimal/Empty.hx @@ -0,0 +1,4 @@ +// Test 1: Absolutely minimal - empty main +class Empty { + static function main() {} +} diff --git a/other/tests/minimal/FieldAccess.hx b/other/tests/minimal/FieldAccess.hx new file mode 100644 index 000000000..f8f902503 --- /dev/null +++ b/other/tests/minimal/FieldAccess.hx @@ -0,0 +1,16 @@ +// Test 4: Object field access +class Point { + public var x:Int; + public var y:Int; + public function new(x:Int, y:Int) { + this.x = x; + this.y = y; + } +} + +class FieldAccess { + static function main() { + var p = new Point(10, 20); + var sum = p.x + p.y; + } +} diff --git a/other/tests/minimal/FuncCall.hx b/other/tests/minimal/FuncCall.hx new file mode 100644 index 000000000..c6b4285c9 --- /dev/null +++ b/other/tests/minimal/FuncCall.hx @@ -0,0 +1,9 @@ +// Test 3: Function call +class FuncCall { + static function add(x:Int, y:Int):Int { + return x + y; + } + static function main() { + var result = add(3, 4); + } +} diff --git a/other/tests/minimal/IntAdd.hx b/other/tests/minimal/IntAdd.hx new file mode 100644 index 000000000..3d9d6e8ae --- /dev/null +++ b/other/tests/minimal/IntAdd.hx @@ -0,0 +1,8 @@ +// Test 2: Integer addition +class IntAdd { + static function main() { + var a = 1; + var b = 2; + var c = a + b; + } +} diff --git a/other/tests/minimal/Makefile b/other/tests/minimal/Makefile new file mode 100644 index 000000000..5f17a396e --- /dev/null +++ b/other/tests/minimal/Makefile @@ -0,0 +1,2 @@ +# NOTE: When adding a new test, add it to CMakeLists.txt in the project root +# (search for "add_minimal_jit_test") diff --git a/other/tests/minimal/hldump.c b/other/tests/minimal/hldump.c new file mode 100644 index 000000000..ed8669ffd --- /dev/null +++ b/other/tests/minimal/hldump.c @@ -0,0 +1,287 @@ +/* + * Simple HashLink bytecode dumper + * Dumps functions and opcodes from a .hl file + */ +#include +#include +#include +#include +#include + +/* Opcode names from opcodes.h */ +static const char *opcode_names[] = { + "OMov", "OInt", "OFloat", "OBool", "OBytes", "OString", "ONull", + "OAdd", "OSub", "OMul", "OSDiv", "OUDiv", "OSMod", "OUMod", + "OShl", "OSShr", "OUShr", "OAnd", "OOr", "OXor", + "ONeg", "ONot", "OIncr", "ODecr", + "OCall0", "OCall1", "OCall2", "OCall3", "OCall4", "OCallN", "OCallMethod", "OCallThis", "OCallClosure", + "OStaticClosure", "OInstanceClosure", "OVirtualClosure", + "OGetGlobal", "OSetGlobal", + "OField", "OSetField", "OGetThis", "OSetThis", + "ODynGet", "ODynSet", + "OJTrue", "OJFalse", "OJNull", "OJNotNull", "OJSLt", "OJSGte", "OJSGt", "OJSLte", "OJULt", "OJUGte", "OJNotLt", "OJNotGte", "OJEq", "OJNotEq", "OJAlways", + "OToDyn", "OToSFloat", "OToUFloat", "OToInt", "OSafeCast", "OUnsafeCast", "OToVirtual", + "OLabel", "ORet", "OThrow", "ORethrow", "OSwitch", "ONullCheck", "OTrap", "OEndTrap", + "OGetI8", "OGetI16", "OGetMem", "OGetArray", "OSetI8", "OSetI16", "OSetMem", "OSetArray", + "ONew", "OArraySize", "OType", "OGetType", "OGetTID", + "ORef", "OUnref", "OSetref", + "OMakeEnum", "OEnumAlloc", "OEnumIndex", "OEnumField", "OSetEnumField", + "OAssert", "ORefData", "ORefOffset", + "ONop", "OPrefetch", "OAsm", "OCatch" +}; + +static const char *type_kind_name(hl_type_kind k) { + switch (k) { + case HVOID: return "void"; + case HUI8: return "u8"; + case HUI16: return "u16"; + case HI32: return "i32"; + case HI64: return "i64"; + case HF32: return "f32"; + case HF64: return "f64"; + case HBOOL: return "bool"; + case HBYTES: return "bytes"; + case HDYN: return "dyn"; + case HFUN: return "fun"; + case HOBJ: return "obj"; + case HARRAY: return "array"; + case HTYPE: return "type"; + case HREF: return "ref"; + case HVIRTUAL: return "virtual"; + case HDYNOBJ: return "dynobj"; + case HABSTRACT: return "abstract"; + case HENUM: return "enum"; + case HNULL: return "null"; + case HMETHOD: return "method"; + case HSTRUCT: return "struct"; + case HPACKED: return "packed"; + default: return "???"; + } +} + +static void print_type(hl_type *t) { + if (!t) { + printf("null"); + return; + } + printf("%s", type_kind_name(t->kind)); + if (t->kind == HOBJ && t->obj && t->obj->name) { + printf("(%ls)", (wchar_t*)t->obj->name); + } else if (t->kind == HFUN && t->fun) { + printf("("); + for (int i = 0; i < t->fun->nargs; i++) { + if (i > 0) printf(","); + print_type(t->fun->args[i]); + } + printf(")->"); + print_type(t->fun->ret); + } +} + +static void dump_function(hl_code *c, hl_function *f, int verbose) { + printf("\n=== Function %d ===\n", f->findex); + printf(" Type: "); + print_type(f->type); + printf("\n"); + printf(" Registers: %d\n", f->nregs); + printf(" Opcodes: %d\n", f->nops); + + if (verbose) { + printf(" Register types:\n"); + for (int i = 0; i < f->nregs && i < 20; i++) { + printf(" r%d: ", i); + print_type(f->regs[i]); + printf("\n"); + } + if (f->nregs > 20) printf(" ... (%d more)\n", f->nregs - 20); + } + + printf(" Code:\n"); + for (int i = 0; i < f->nops; i++) { + hl_opcode *op = &f->ops[i]; + const char *name = (op->op < sizeof(opcode_names)/sizeof(opcode_names[0])) + ? opcode_names[op->op] : "???"; + printf(" %4d: %-16s %d, %d, %d", i, name, op->p1, op->p2, op->p3); + + /* Show extra info for some opcodes */ + switch (op->op) { + case OInt: + if (op->p2 >= 0 && op->p2 < c->nints) + printf(" ; r%d = %d", op->p1, c->ints[op->p2]); + break; + case OString: + if (op->p2 >= 0 && op->p2 < c->nstrings) + printf(" ; r%d = \"%s\"", op->p1, c->strings[op->p2]); + break; + case OBool: + printf(" ; r%d = %s", op->p1, op->p2 ? "true" : "false"); + break; + case OCall0: + case OCall1: + case OCall2: + case OCall3: + case OCall4: + case OCallN: + printf(" ; call F%d", op->p2); + break; + case OJAlways: + printf(" ; goto %d", (i + 1) + op->p1); + break; + case OJTrue: + case OJFalse: + case OJNull: + case OJNotNull: + printf(" ; if r%d goto %d", op->p1, (i + 1) + op->p2); + break; + case OJSLt: + case OJSGte: + case OJEq: + case OJNotEq: + printf(" ; if r%d,r%d goto %d", op->p1, op->p2, (i + 1) + op->p3); + break; + case ORet: + printf(" ; return r%d", op->p1); + break; + case OGetGlobal: + printf(" ; r%d = global[%d]", op->p1, op->p2); + break; + case OSetGlobal: + printf(" ; global[%d] = r%d", op->p2, op->p1); + break; + case OField: + printf(" ; r%d = r%d.field[%d]", op->p1, op->p2, op->p3); + break; + case OSetField: + printf(" ; r%d.field[%d] = r%d", op->p1, op->p2, op->p3); + break; + case ONew: + printf(" ; r%d = new", op->p1); + break; + default: + break; + } + printf("\n"); + } +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s [function_index | -a] [-v]\n", argv[0]); + fprintf(stderr, " -a: dump all functions\n"); + fprintf(stderr, " -v: verbose (show register types)\n"); + return 1; + } + + const char *filename = argv[1]; + int target_func = -1; /* -1 means entrypoint only */ + int dump_all = 0; + int verbose = 0; + + for (int i = 2; i < argc; i++) { + if (strcmp(argv[i], "-v") == 0) { + verbose = 1; + } else if (strcmp(argv[i], "-a") == 0) { + dump_all = 1; + } else { + target_func = atoi(argv[i]); + } + } + + /* Initialize HL */ + hl_global_init(); + + /* Load the bytecode */ + FILE *f = fopen(filename, "rb"); + if (!f) { + fprintf(stderr, "Cannot open %s\n", filename); + return 1; + } + + fseek(f, 0, SEEK_END); + int size = ftell(f); + fseek(f, 0, SEEK_SET); + + char *data = malloc(size); + fread(data, 1, size, f); + fclose(f); + + /* Parse bytecode */ + char *error_msg = NULL; + hl_code *code = hl_code_read((unsigned char*)data, size, &error_msg); + free(data); + + if (!code) { + fprintf(stderr, "Failed to parse bytecode: %s\n", error_msg ? error_msg : "unknown error"); + return 1; + } + + /* Print summary */ + printf("HashLink Bytecode: %s\n", filename); + printf(" Version: %d\n", code->version); + printf(" Entrypoint: F%d\n", code->entrypoint); + printf(" Types: %d\n", code->ntypes); + printf(" Globals: %d\n", code->nglobals); + printf(" Natives: %d\n", code->nnatives); + printf(" Functions: %d\n", code->nfunctions); + printf(" Strings: %d\n", code->nstrings); + printf(" Ints: %d\n", code->nints); + printf(" Floats: %d\n", code->nfloats); + + /* Print natives */ + if (code->nnatives > 0) { + printf("\n=== Natives ===\n"); + for (int i = 0; i < code->nnatives; i++) { + hl_native *n = &code->natives[i]; + printf(" F%d: %s@%s ", n->findex, n->name, n->lib); + print_type(n->t); + printf("\n"); + } + } + + /* Dump functions */ + if (dump_all) { + /* Dump all functions */ + printf("\n--- All Functions ---\n"); + for (int i = 0; i < code->nfunctions; i++) { + dump_function(code, &code->functions[i], verbose); + } + } else if (target_func >= 0) { + int found = 0; + /* Check if it's a native function first */ + for (int i = 0; i < code->nnatives; i++) { + if (code->natives[i].findex == target_func) { + hl_native *n = &code->natives[i]; + printf("\n=== Native %d ===\n", n->findex); + printf(" Library: %s\n", n->lib); + printf(" Name: %s\n", n->name); + printf(" Type: "); + print_type(n->t); + printf("\n"); + found = 1; + break; + } + } + /* Find and dump specific function */ + for (int i = 0; i < code->nfunctions; i++) { + if (code->functions[i].findex == target_func) { + dump_function(code, &code->functions[i], verbose); + found = 1; + break; + } + } + if (!found) { + printf("\nFunction F%d not found\n", target_func); + } + } else { + /* Dump entrypoint function */ + printf("\n--- Entrypoint Function ---\n"); + for (int i = 0; i < code->nfunctions; i++) { + if (code->functions[i].findex == code->entrypoint) { + dump_function(code, &code->functions[i], verbose); + break; + } + } + } + + return 0; +} diff --git a/other/tests/minimal/test_array_ops.c b/other/tests/minimal/test_array_ops.c new file mode 100644 index 000000000..02f7afa53 --- /dev/null +++ b/other/tests/minimal/test_array_ops.c @@ -0,0 +1,384 @@ +/* + * Test array operations for HashLink AArch64 JIT + * + * Tests: OGetArray, OSetArray, OArraySize + * + * OGetArray: dst = array[index] + * OSetArray: array[index] = value + * OArraySize: dst = array.length + */ +#include "test_harness.h" + +/* Helper to create an array type */ +static hl_type *create_array_type(hl_code *c, hl_type *elem_type) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types\n"); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HARRAY; + t->tparam = elem_type; + + return t; +} + +/* + * Test: OSetArray and OGetArray with i32 elements + * + * array = alloc_array(i32, 3) + * array[0] = 10 + * array[1] = 20 + * array[2] = 12 + * r0 = array[0] + array[1] + array[2] ; 10 + 20 + 12 = 42 + * return r0 + */ +TEST(array_i32_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 3, 0, 1, 2, 10, 20, 12 }; + test_init_ints(c, 7, ints); + + /* Create array type: Array */ + hl_type *array_i32 = create_array_type(c, &c->types[T_I32]); + + /* Native: hl_alloc_array(type, size) -> array */ + hl_type *alloc_args[] = { &c->types[T_TYPE], &c->types[T_I32] }; /* type pointer */ + hl_type *alloc_fn_type = test_alloc_fun_type(c, array_i32, 2, alloc_args); + test_add_native(c, 1, "std", "alloc_array", alloc_fn_type, (void*)hl_alloc_array); + + /* Function type: () -> i32 */ + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* + * Registers: + * r0: type pointer (for alloc) + * r1: size (3) + * r2: array + * r3-r5: indices (0, 1, 2) + * r6-r8: values (10, 20, 12) + * r9-r11: read values + * r12: sum + */ + hl_type *regs[] = { + &c->types[T_TYPE], /* r0 = type pointer */ + &c->types[T_I32], /* r1 = size */ + array_i32, /* r2 = array */ + &c->types[T_I32], /* r3 = idx 0 */ + &c->types[T_I32], /* r4 = idx 1 */ + &c->types[T_I32], /* r5 = idx 2 */ + &c->types[T_I32], /* r6 = val 10 */ + &c->types[T_I32], /* r7 = val 20 */ + &c->types[T_I32], /* r8 = val 12 */ + &c->types[T_I32], /* r9 = read[0] */ + &c->types[T_I32], /* r10 = read[1] */ + &c->types[T_I32], /* r11 = read[2] */ + &c->types[T_I32], /* r12 = sum */ + }; + + /* OType loads type at given index into register */ + hl_opcode ops[] = { + OP2(OType, 0, T_I32), /* r0 = type for i32 */ + OP2(OInt, 1, 0), /* r1 = 3 (size) */ + OP4_CALL2(OCall2, 2, 1, 0, 1), /* r2 = alloc_array(r0, r1) */ + OP2(OInt, 3, 1), /* r3 = 0 */ + OP2(OInt, 4, 2), /* r4 = 1 */ + OP2(OInt, 5, 3), /* r5 = 2 */ + OP2(OInt, 6, 4), /* r6 = 10 */ + OP2(OInt, 7, 5), /* r7 = 20 */ + OP2(OInt, 8, 6), /* r8 = 12 */ + OP3(OSetArray, 2, 3, 6), /* array[0] = 10 */ + OP3(OSetArray, 2, 4, 7), /* array[1] = 20 */ + OP3(OSetArray, 2, 5, 8), /* array[2] = 12 */ + OP3(OGetArray, 9, 2, 3), /* r9 = array[0] */ + OP3(OGetArray, 10, 2, 4), /* r10 = array[1] */ + OP3(OGetArray, 11, 2, 5), /* r11 = array[2] */ + OP3(OAdd, 12, 9, 10), /* r12 = r9 + r10 */ + OP3(OAdd, 12, 12, 11), /* r12 = r12 + r11 */ + OP1(ORet, 12), + }; + + test_alloc_function(c, 0, fn_type, 13, regs, 18, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + int expected = 10 + 20 + 12; + if (ret != expected) { + fprintf(stderr, " Expected %d, got %d\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OArraySize + * + * array = alloc_array(i32, 5) + * return array_size(array) ; should be 5 + */ +TEST(array_size) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 5 }; + test_init_ints(c, 1, ints); + + hl_type *array_i32 = create_array_type(c, &c->types[T_I32]); + + hl_type *alloc_args[] = { &c->types[T_TYPE], &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, array_i32, 2, alloc_args); + test_add_native(c, 1, "std", "alloc_array", alloc_fn_type, (void*)hl_alloc_array); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_TYPE], /* r0 = type pointer */ + &c->types[T_I32], /* r1 = size */ + array_i32, /* r2 = array */ + &c->types[T_I32], /* r3 = array_size */ + }; + + hl_opcode ops[] = { + OP2(OType, 0, T_I32), /* r0 = type for i32 */ + OP2(OInt, 1, 0), /* r1 = 5 (size) */ + OP4_CALL2(OCall2, 2, 1, 0, 1), /* r2 = alloc_array(r0, r1) */ + OP2(OArraySize, 3, 2), /* r3 = array_size(r2) */ + OP1(ORet, 3), + }; + + test_alloc_function(c, 0, fn_type, 4, regs, 5, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 5) { + fprintf(stderr, " Expected 5, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OSetArray and OGetArray with i64 elements + */ +TEST(array_i64_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 2, 0, 1, 1000, 2000 }; + test_init_ints(c, 5, ints); + + hl_type *array_i64 = create_array_type(c, &c->types[T_I64]); + + hl_type *alloc_args[] = { &c->types[T_TYPE], &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, array_i64, 2, alloc_args); + test_add_native(c, 1, "std", "alloc_array", alloc_fn_type, (void*)hl_alloc_array); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + + hl_type *regs[] = { + &c->types[T_TYPE], /* r0 = type pointer */ + &c->types[T_I32], /* r1 = size */ + array_i64, /* r2 = array */ + &c->types[T_I32], /* r3 = idx 0 */ + &c->types[T_I32], /* r4 = idx 1 */ + &c->types[T_I64], /* r5 = val 1000 */ + &c->types[T_I64], /* r6 = val 2000 */ + &c->types[T_I64], /* r7 = read[0] */ + &c->types[T_I64], /* r8 = read[1] */ + &c->types[T_I64], /* r9 = sum */ + }; + + hl_opcode ops[] = { + OP2(OType, 0, T_I64), /* r0 = type for i64 */ + OP2(OInt, 1, 0), /* r1 = 2 (size) */ + OP4_CALL2(OCall2, 2, 1, 0, 1), /* r2 = alloc_array(r0, r1) */ + OP2(OInt, 3, 1), /* r3 = 0 */ + OP2(OInt, 4, 2), /* r4 = 1 */ + OP2(OInt, 5, 3), /* r5 = 1000 */ + OP2(OInt, 6, 4), /* r6 = 2000 */ + OP3(OSetArray, 2, 3, 5), /* array[0] = 1000 */ + OP3(OSetArray, 2, 4, 6), /* array[1] = 2000 */ + OP3(OGetArray, 7, 2, 3), /* r7 = array[0] */ + OP3(OGetArray, 8, 2, 4), /* r8 = array[1] */ + OP3(OAdd, 9, 7, 8), /* r9 = r7 + r8 */ + OP1(ORet, 9), + }; + + test_alloc_function(c, 0, fn_type, 10, regs, 13, ops); + + int result; + int64_t (*fn)(void) = (int64_t(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int64_t ret = fn(); + int64_t expected = 1000 + 2000; + if (ret != expected) { + fprintf(stderr, " Expected %ld, got %ld\n", (long)expected, (long)ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OSetArray and OGetArray with f64 elements + */ +TEST(array_f64_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 2, 0, 1 }; + test_init_ints(c, 3, ints); + + double floats[] = { 1.5, 2.5 }; + test_init_floats(c, 2, floats); + + hl_type *array_f64 = create_array_type(c, &c->types[T_F64]); + + hl_type *alloc_args[] = { &c->types[T_TYPE], &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, array_f64, 2, alloc_args); + test_add_native(c, 1, "std", "alloc_array", alloc_fn_type, (void*)hl_alloc_array); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + + hl_type *regs[] = { + &c->types[T_TYPE], /* r0 = type pointer */ + &c->types[T_I32], /* r1 = size */ + array_f64, /* r2 = array */ + &c->types[T_I32], /* r3 = idx 0 */ + &c->types[T_I32], /* r4 = idx 1 */ + &c->types[T_F64], /* r5 = val 1.5 */ + &c->types[T_F64], /* r6 = val 2.5 */ + &c->types[T_F64], /* r7 = read[0] */ + &c->types[T_F64], /* r8 = read[1] */ + &c->types[T_F64], /* r9 = sum */ + }; + + hl_opcode ops[] = { + OP2(OType, 0, T_F64), /* r0 = type for f64 */ + OP2(OInt, 1, 0), /* r1 = 2 (size) */ + OP4_CALL2(OCall2, 2, 1, 0, 1), /* r2 = alloc_array(r0, r1) */ + OP2(OInt, 3, 1), /* r3 = 0 */ + OP2(OInt, 4, 2), /* r4 = 1 */ + OP2(OFloat, 5, 0), /* r5 = 1.5 */ + OP2(OFloat, 6, 1), /* r6 = 2.5 */ + OP3(OSetArray, 2, 3, 5), /* array[0] = 1.5 */ + OP3(OSetArray, 2, 4, 6), /* array[1] = 2.5 */ + OP3(OGetArray, 7, 2, 3), /* r7 = array[0] */ + OP3(OGetArray, 8, 2, 4), /* r8 = array[1] */ + OP3(OAdd, 9, 7, 8), /* r9 = r7 + r8 */ + OP1(ORet, 9), + }; + + test_alloc_function(c, 0, fn_type, 10, regs, 13, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + double expected = 1.5 + 2.5; + double diff = ret - expected; + if (diff < 0) diff = -diff; + if (diff > 0.0001) { + fprintf(stderr, " Expected %f, got %f\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Array with dynamic indices (not compile-time constants) + */ +TEST(array_dynamic_index) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 0, 42, 1 }; /* size, idx0, value, idx_offset */ + test_init_ints(c, 4, ints); + + hl_type *array_i32 = create_array_type(c, &c->types[T_I32]); + + hl_type *alloc_args[] = { &c->types[T_TYPE], &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, array_i32, 2, alloc_args); + test_add_native(c, 1, "std", "alloc_array", alloc_fn_type, (void*)hl_alloc_array); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_TYPE], /* r0 = type pointer */ + &c->types[T_I32], /* r1 = size */ + array_i32, /* r2 = array */ + &c->types[T_I32], /* r3 = idx (computed) */ + &c->types[T_I32], /* r4 = value */ + &c->types[T_I32], /* r5 = idx_offset */ + &c->types[T_I32], /* r6 = computed idx */ + &c->types[T_I32], /* r7 = read value */ + }; + + /* Store at index 0, then compute index 0+1-1=0 to read back */ + hl_opcode ops[] = { + OP2(OType, 0, T_I32), /* r0 = type */ + OP2(OInt, 1, 0), /* r1 = 10 */ + OP4_CALL2(OCall2, 2, 1, 0, 1), /* r2 = alloc_array(r0, r1) */ + OP2(OInt, 3, 1), /* r3 = 0 */ + OP2(OInt, 4, 2), /* r4 = 42 */ + OP3(OSetArray, 2, 3, 4), /* array[0] = 42 */ + OP2(OInt, 5, 3), /* r5 = 1 */ + OP3(OAdd, 6, 3, 5), /* r6 = r3 + r5 = 1 */ + OP3(OSub, 6, 6, 5), /* r6 = r6 - r5 = 0 */ + OP3(OGetArray, 7, 2, 6), /* r7 = array[r6] = array[0] */ + OP1(ORet, 7), + }; + + test_alloc_function(c, 0, fn_type, 8, regs, 11, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(array_i32_basic), + TEST_ENTRY(array_size), + TEST_ENTRY(array_i64_basic), + TEST_ENTRY(array_f64_basic), + TEST_ENTRY(array_dynamic_index), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Array Operation Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_binop_inplace.c b/other/tests/minimal/test_binop_inplace.c new file mode 100644 index 000000000..03fc559b8 --- /dev/null +++ b/other/tests/minimal/test_binop_inplace.c @@ -0,0 +1,670 @@ +/* + * Test in-place binary operations followed by spill + * + * Tests the bug where in-place binops like r0 = r0 << r1 don't properly + * update the register binding, causing the old (pre-operation) value to + * be spilled instead of the new value. + * + * Bug scenario: + * 1. r0 = 21 + * 2. r1 = 1 + * 3. r0 = r0 << r1 ; in-place shift, result should be 42 + * 4. call fn() ; triggers spill_regs - BUG: spills old r0 (21) instead of new (42) + * 5. return r0 ; BUG: returns 21 instead of 42 + */ +#include "test_harness.h" + +/* Helper to allocate multiple functions at once */ +static void test_alloc_functions(hl_code *c, int count) { + c->functions = (hl_function*)calloc(count, sizeof(hl_function)); + c->nfunctions = 0; +} + +static hl_function *test_add_function(hl_code *c, int findex, hl_type *type, + int nregs, hl_type **regs, + int nops, hl_opcode *ops) { + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = findex; + f->type = type; + f->nregs = nregs; + f->nops = nops; + + f->regs = (hl_type**)malloc(sizeof(hl_type*) * nregs); + memcpy(f->regs, regs, sizeof(hl_type*) * nregs); + + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * nops); + memcpy(f->ops, ops, sizeof(hl_opcode) * nops); + + f->debug = NULL; + f->obj = NULL; + f->field.ref = NULL; + f->ref = 0; + + return f; +} + +/* + * Test: In-place left shift followed by function call + * + * This is the minimal reproduction of the string concat bug where: + * OShl r5, r5, r6 ; in-place shift + * OCallN ... ; triggers spill, but spills the OLD r5 value + * + * fn0: () -> i32 { return 0; } ; dummy function to trigger spill + * fn1: () -> i32 { ; entry point + * r0 = 21 + * r1 = 1 + * r0 = r0 << r1 ; r0 should become 42 + * call fn0() ; triggers spill - bug causes old r0 (21) to be saved + * return r0 ; should return 42, but returns 21 if bug present + * } + */ +TEST(shl_inplace_then_call) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 21, 1, 0 }; + test_init_ints(c, 3, ints); + + /* Function types */ + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 2); + + /* fn0: findex=0, returns 0 (dummy to trigger spill) */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 2), /* r0 = 0 */ + OP1(ORet, 0), + }; + test_add_function(c, 0, fn_type_i32, 1, regs, 2, ops); + } + + /* fn1: findex=1, does in-place shift then calls fn0 (entry point) */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 21 */ + OP2(OInt, 1, 1), /* r1 = 1 */ + OP3(OShl, 0, 0, 1), /* r0 = r0 << r1 (in-place! dst == src) */ + OP2(OCall0, 2, 0), /* r2 = call fn0() - triggers spill */ + OP1(ORet, 0), /* return r0 - should be 42 */ + }; + test_add_function(c, 1, fn_type_i32, 3, regs, 5, ops); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + fprintf(stderr, " (Bug: in-place shift value not properly spilled)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: In-place add followed by function call + * Same bug pattern but with OAdd instead of OShl + */ +TEST(add_inplace_then_call) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 21, 0 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 2); + + /* fn0: returns 0 */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 1), + OP1(ORet, 0), + }; + test_add_function(c, 0, fn_type_i32, 1, regs, 2, ops); + } + + /* fn1: r0 = r0 + r0 (21 + 21 = 42), then call, then return r0 */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 21 */ + OP3(OAdd, 0, 0, 0), /* r0 = r0 + r0 (in-place!) */ + OP2(OCall0, 1, 0), /* r1 = call fn0() */ + OP1(ORet, 0), /* return r0 - should be 42 */ + }; + test_add_function(c, 1, fn_type_i32, 2, regs, 4, ops); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + fprintf(stderr, " (Bug: in-place add value not properly spilled)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: In-place multiply followed by function call + */ +TEST(mul_inplace_then_call) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 6, 7, 0 }; + test_init_ints(c, 3, ints); + + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 2); + + /* fn0: returns 0 */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 2), + OP1(ORet, 0), + }; + test_add_function(c, 0, fn_type_i32, 1, regs, 2, ops); + } + + /* fn1: r0 = 6, r1 = 7, r0 = r0 * r1, call, return r0 */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 6 */ + OP2(OInt, 1, 1), /* r1 = 7 */ + OP3(OMul, 0, 0, 1), /* r0 = r0 * r1 (in-place!) */ + OP2(OCall0, 2, 0), /* r2 = call fn0() */ + OP1(ORet, 0), /* return r0 - should be 42 */ + }; + test_add_function(c, 1, fn_type_i32, 3, regs, 5, ops); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + fprintf(stderr, " (Bug: in-place mul value not properly spilled)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Chain of in-place operations then call + * This is closer to the real-world string concat scenario + */ +TEST(chain_inplace_then_call) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 1, 0 }; + test_init_ints(c, 3, ints); + + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 2); + + /* fn0: returns 0 */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 2), + OP1(ORet, 0), + }; + test_add_function(c, 0, fn_type_i32, 1, regs, 2, ops); + } + + /* fn1: r0 = 10, r1 = 1, r0 = r0 << r1, r0 = r0 + r0, r0 = r0 + r1 + r1, call, return r0 + * 10 << 1 = 20, 20 + 20 = 40, 40 + 1 + 1 = 42 + */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 10 */ + OP2(OInt, 1, 1), /* r1 = 1 */ + OP3(OShl, 0, 0, 1), /* r0 = r0 << r1 = 20 */ + OP3(OAdd, 0, 0, 0), /* r0 = r0 + r0 = 40 */ + OP3(OAdd, 0, 0, 1), /* r0 = r0 + r1 = 41 */ + OP3(OAdd, 0, 0, 1), /* r0 = r0 + r1 = 42 */ + OP2(OCall0, 2, 0), /* r2 = call fn0() */ + OP1(ORet, 0), /* return r0 - should be 42 */ + }; + test_add_function(c, 1, fn_type_i32, 3, regs, 8, ops); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + fprintf(stderr, " (Bug: chain of in-place ops not properly spilled)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Simulates the actual string concat bug pattern more closely + * + * The real bug occurs in this sequence (from function 20): + * OField r5, r1, 1 ; r5 = load from object field + * OInt r6, 1 ; r6 = 1 + * OShl r5, r5, r6 ; r5 = r5 << r6 (in-place) + * ... more ops ... + * OCallN ; triggers spill - BUG: spills old r5 + * + * The key is that r5 comes from OField (not OInt), so fetch() loads it + * into a register. Then OShl does in-place shift, allocating a NEW + * register for the result. But the old register still thinks it holds r5. + * + * We simulate this with OCall1 to pass a value, then shift it in-place. + */ +TEST(shl_inplace_arg_then_call) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 1, 0 }; + test_init_ints(c, 2, ints); + + /* Function types */ + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 3); + + /* fn0: returns 21 (simulates loading a value like OField does) */ + { + int fn0_ints[] = { 21 }; + /* We need to add these ints - but we'll use the global pool */ + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = ints[0] = 1... wait we need 21 */ + OP1(ORet, 0), + }; + test_add_function(c, 0, fn_type_i32, 1, regs, 2, ops); + } + + /* fn1: returns 0 (dummy to trigger second spill) */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 1), /* r0 = 0 */ + OP1(ORet, 0), + }; + test_add_function(c, 1, fn_type_i32, 1, regs, 2, ops); + } + + /* fn2: entry point + * r0 = call fn0() ; r0 = 21 (value comes from call, like OField) + * r1 = 1 + * r0 = r0 << r1 ; in-place shift, r0 should be 42 + * r2 = call fn1() ; triggers spill + * return r0 ; should be 42 + */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OCall0, 0, 0), /* r0 = call fn0() = 21 */ + OP2(OInt, 1, 0), /* r1 = 1 */ + OP3(OShl, 0, 0, 1), /* r0 = r0 << r1 = 42 (in-place!) */ + OP2(OCall0, 2, 1), /* r2 = call fn1() - triggers spill */ + OP1(ORet, 0), /* return r0 - should be 42 */ + }; + test_add_function(c, 2, fn_type_i32, 3, regs, 5, ops); + } + + c->entrypoint = 2; + + /* Fix: fn0 needs to return 21. Let's update the ints pool */ + c->ints[0] = 21; /* fn0 uses ints[0] */ + c->ints[1] = 1; /* fn2 uses ints[1] for the shift amount... wait no */ + + /* Actually let's redo the ints pool properly */ + free(c->ints); + int new_ints[] = { 21, 1, 0 }; /* 21 for fn0, 1 for shift, 0 for fn1 */ + test_init_ints(c, 3, new_ints); + + /* Update fn0 to use ints[0]=21, fn1 to use ints[2]=0, fn2 r1 to use ints[1]=1 */ + c->functions[0].ops[0].p2 = 0; /* fn0: OInt r0, ints[0]=21 */ + c->functions[1].ops[0].p2 = 2; /* fn1: OInt r0, ints[2]=0 */ + c->functions[2].ops[1].p2 = 1; /* fn2: OInt r1, ints[1]=1 */ + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + fprintf(stderr, " (Bug: value from call not properly spilled after in-place shift)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Multiple registers active, simulating the string concat pattern + * + * This more closely matches the real bug: + * r4 = get length1 + * r5 = 1 + * r4 = r4 << r5 ; first shift + * r5 = get length2 ; r5 REUSED for different value + * r6 = 1 + * r5 = r5 << r6 ; second shift (in-place) - THIS IS WHERE BUG OCCURS + * r6 = r4 + r5 ; need both shifted values + * call(...) ; spill - r5 gets wrong value + */ +TEST(string_concat_pattern) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* ints: 13, 1, 1 (two lengths, and 1 for shift) */ + int ints[] = { 13, 1, 1, 0 }; + test_init_ints(c, 4, ints); + + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 2); + + /* fn0: returns 0 */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 3), /* r0 = 0 */ + OP1(ORet, 0), + }; + test_add_function(c, 0, fn_type_i32, 1, regs, 2, ops); + } + + /* fn1: entry - simulates string concat length calculation + * r0 = 13 ; length1 (chars) + * r1 = 1 ; shift amount + * r0 = r0 << r1 ; length1 in bytes = 26 + * r2 = 1 ; length2 (chars) - reusing pattern + * r3 = 1 ; shift amount + * r2 = r2 << r3 ; length2 in bytes = 2 (in-place!) + * r4 = r0 + r2 ; total = 28 + * call fn0() ; triggers spill + * return r4 ; should be 28 + */ + { + hl_type *regs[] = { + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] + }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 13 */ + OP2(OInt, 1, 1), /* r1 = 1 */ + OP3(OShl, 0, 0, 1), /* r0 = r0 << r1 = 26 */ + OP2(OInt, 2, 1), /* r2 = 1 */ + OP2(OInt, 3, 2), /* r3 = 1 */ + OP3(OShl, 2, 2, 3), /* r2 = r2 << r3 = 2 (in-place!) */ + OP3(OAdd, 4, 0, 2), /* r4 = r0 + r2 = 28 */ + OP2(OCall0, 5, 0), /* r5 = call fn0() - triggers spill */ + OP1(ORet, 4), /* return r4 = 28 */ + }; + test_add_function(c, 1, fn_type_i32, 6, regs, 9, ops); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 28) { + fprintf(stderr, " Expected 28, got %d\n", ret); + fprintf(stderr, " (Bug: in-place shift in multi-register scenario failed)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Force pd < pa scenario + * + * THE REAL BUG: The bug only manifests when the RESULT register (pd) has a + * LOWER index than the SOURCE register (pa). In spill_regs(), registers are + * processed from X0 to X17. If pd < pa: + * 1. pd is spilled first (correct value stored) + * 2. pa is spilled later (OLD value overwrites correct value!) + * + * To trigger this, we need: + * 1. Allocate several low-numbered registers (X0, X1, X2, ...) + * 2. Free a low register (X0) + * 3. Load a value into a high register (X5 say) + * 4. Do in-place operation - result goes to freed X0, source stays in X5 + * 5. Call function - spill_regs processes X0 first, then X5 overwrites! + */ +TEST(force_pd_less_than_pa) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 0, 1, 2, 3, 4, 21, 1 }; + test_init_ints(c, 7, ints); + + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 2); + + /* fn0: returns 0 */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 0 */ + OP1(ORet, 0), + }; + test_add_function(c, 0, fn_type_i32, 1, regs, 2, ops); + } + + /* fn1: Entry point + * Strategy: Allocate registers 0-4, then use r5 for the actual value. + * When we do the in-place op on r5, the result register will be + * allocated to a lower number (after we stop using r0-r4). + * + * r0 = 0 (uses X0) + * r1 = 1 (uses X1) + * r2 = 2 (uses X2) + * r3 = 3 (uses X3) + * r4 = 4 (uses X4) + * r5 = 21 (uses X5) + * r6 = 1 (uses X6) + * ; Now we "forget" r0-r4 by doing operations that don't involve them + * ; (The registers will be evicted when new ones are needed) + * r5 = r5 << r6 ; In-place shift. pd might be X0-X4 if they get freed + * ; Actually, let's force it by having the MOV instruction cause eviction + * r0 = r5 ; This forces r5's value to r0 + * call fn0() ; Spill + * return r0 ; Should be 42 + */ + { + hl_type *regs[] = { + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32] + }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 0 - allocates X0 */ + OP2(OInt, 1, 1), /* r1 = 1 - allocates X1 */ + OP2(OInt, 2, 2), /* r2 = 2 - allocates X2 */ + OP2(OInt, 3, 3), /* r3 = 3 - allocates X3 */ + OP2(OInt, 4, 4), /* r4 = 4 - allocates X4 */ + OP2(OInt, 5, 5), /* r5 = 21 - allocates X5 */ + OP2(OInt, 6, 6), /* r6 = 1 - allocates X6 */ + OP3(OShl, 5, 5, 6), /* r5 = r5 << r6 = 42 (in-place!) */ + OP2(OMov, 0, 5), /* r0 = r5 (moves 42 to r0) */ + OP2(OCall0, 7, 0), /* r7 = call fn0() - triggers spill */ + OP1(ORet, 0), /* return r0 - should be 42 */ + }; + test_add_function(c, 1, fn_type_i32, 8, regs, 11, ops); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + fprintf(stderr, " (Bug: pd < pa causes old value to overwrite new in spill_regs)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Direct reproduction of the hello.hl bug scenario + * + * In hello.hl, the bug occurred in function 20 (String.__add): + * - OField loads string length into r5 (ends up in X5) + * - OShl shifts r5 by 1 (to convert chars to bytes) + * - Result goes into a LOWER register (X2) + * - OCallN triggers spill + * - X2 is spilled first (correct value) + * - X5 is spilled later (OLD value overwrites!) + * + * We can't easily reproduce OField in our minimal test, but we CAN + * reproduce the scenario by: + * 1. Getting a value via function call (forces it into return register X0) + * 2. Moving it to a higher-numbered vreg + * 3. Doing in-place shift + */ +TEST(hello_hl_scenario) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 13, 1, 0 }; + test_init_ints(c, 3, ints); + + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 3); + + /* fn0: returns 13 (simulates OField loading string length) */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 13 */ + OP1(ORet, 0), + }; + test_add_function(c, 0, fn_type_i32, 1, regs, 2, ops); + } + + /* fn1: returns 0 (dummy to trigger second spill) */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 2), /* r0 = 0 */ + OP1(ORet, 0), + }; + test_add_function(c, 1, fn_type_i32, 1, regs, 2, ops); + } + + /* fn2: Entry point - simulates String.__add length calculation + * r0 = call fn0() ; Get length (13), result in X0, then stored to r0 + * r1 = 1 ; Shift amount + * r0 = r0 << r1 ; r0 = 13 << 1 = 26 (in-place!) + * r2 = call fn1() ; Triggers spill - BUG: old r0 may overwrite new + * return r0 ; Should be 26 + */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OCall0, 0, 0), /* r0 = call fn0() = 13 */ + OP2(OInt, 1, 1), /* r1 = 1 */ + OP3(OShl, 0, 0, 1), /* r0 = r0 << r1 = 26 (in-place!) */ + OP2(OCall0, 2, 1), /* r2 = call fn1() - triggers spill */ + OP1(ORet, 0), /* return r0 - should be 26 */ + }; + test_add_function(c, 2, fn_type_i32, 3, regs, 5, ops); + } + + c->entrypoint = 2; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 26) { + fprintf(stderr, " Expected 26, got %d\n", ret); + fprintf(stderr, " (Bug: String.__add pattern - in-place shift corrupted by spill)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(shl_inplace_then_call), + TEST_ENTRY(add_inplace_then_call), + TEST_ENTRY(mul_inplace_then_call), + TEST_ENTRY(chain_inplace_then_call), + TEST_ENTRY(shl_inplace_arg_then_call), + TEST_ENTRY(string_concat_pattern), + TEST_ENTRY(force_pd_less_than_pa), + TEST_ENTRY(hello_hl_scenario), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - In-place Binary Op + Spill Tests\n"); + printf("(Tests for register binding bug in op_binop)\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_bool_ops.c b/other/tests/minimal/test_bool_ops.c new file mode 100644 index 000000000..a8f0425bf --- /dev/null +++ b/other/tests/minimal/test_bool_ops.c @@ -0,0 +1,288 @@ +/* + * Test boolean operations for HashLink AArch64 JIT + * + * Tests: OBool, ONot + */ +#include "test_harness.h" + +/* + * Test: Return true + */ +TEST(return_true) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BOOL], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 1), /* r0 = true (p2=1 means true) */ + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 2, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 1) { + fprintf(stderr, " Expected 1 (true), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Return false + */ +TEST(return_false) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BOOL], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 0), /* r0 = false (p2=0 means false) */ + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 2, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 0) { + fprintf(stderr, " Expected 0 (false), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: NOT true = false + */ +TEST(not_true) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BOOL], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL], &c->types[T_BOOL] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 1), /* r0 = true */ + OP2(ONot, 1, 0), /* r1 = !r0 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 0) { + fprintf(stderr, " Expected 0 (false), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: NOT false = true + */ +TEST(not_false) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BOOL], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL], &c->types[T_BOOL] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 0), /* r0 = false */ + OP2(ONot, 1, 0), /* r1 = !r0 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 1) { + fprintf(stderr, " Expected 1 (true), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Double NOT: !!true = true + */ +TEST(double_not_true) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BOOL], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL], &c->types[T_BOOL], &c->types[T_BOOL] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 1), /* r0 = true */ + OP2(ONot, 1, 0), /* r1 = !r0 = false */ + OP2(ONot, 2, 1), /* r2 = !r1 = true */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 1) { + fprintf(stderr, " Expected 1 (true), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: NOT on bool (false) -> true + * Note: ONot is only valid for boolean operands (0 or 1). + * Using OInt with 0 works because bool false is represented as 0. + */ +TEST(not_bool_false) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BOOL], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL], &c->types[T_BOOL] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 0), /* r0 = false (0) */ + OP2(ONot, 1, 0), /* r1 = !r0 = true */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 1) { + fprintf(stderr, " Expected 1 (true), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: NOT on bool (true) -> false + */ +TEST(not_bool_true_explicit) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BOOL], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL], &c->types[T_BOOL] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 1), /* r0 = true (1) */ + OP2(ONot, 1, 0), /* r1 = !r0 = false */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 0) { + fprintf(stderr, " Expected 0 (false), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Move bool register + */ +TEST(mov_bool) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BOOL], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL], &c->types[T_BOOL] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 1), /* r0 = true */ + OP2(OMov, 1, 0), /* r1 = r0 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 1) { + fprintf(stderr, " Expected 1 (true), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(return_true), + TEST_ENTRY(return_false), + TEST_ENTRY(not_true), + TEST_ENTRY(not_false), + TEST_ENTRY(double_not_true), + TEST_ENTRY(not_bool_false), + TEST_ENTRY(not_bool_true_explicit), + TEST_ENTRY(mov_bool), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Boolean Operations Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_callbacks.c b/other/tests/minimal/test_callbacks.c new file mode 100644 index 000000000..6f1e23a48 --- /dev/null +++ b/other/tests/minimal/test_callbacks.c @@ -0,0 +1,518 @@ +/* + * Test C-to-HL callback mechanism for HashLink AArch64 JIT + * + * Tests the callback_c2hl and jit_c2hl trampoline by: + * 1. JIT compiling a function with arguments + * 2. Calling it through hl_dyn_call (which uses callback_c2hl) + * + * This exercises the path: hl_dyn_call -> hl_call_method -> callback_c2hl -> jit_c2hl -> JIT code + */ +#include "test_harness.h" + +/* hl_dyn_call declaration from hl.h */ +extern vdynamic *hl_dyn_call(vclosure *c, vdynamic **args, int nargs); +extern vdynamic *hl_alloc_dynamic(hl_type *t); + +/* + * Test: Simple function call through callback (no arguments) + * + * JIT function: () -> i32 { return 42 } + * Call through hl_dyn_call and verify result + */ +TEST(callback_no_args) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Function type: () -> i32 */ + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 */ + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 2, ops); + + int result; + void *fn_ptr = test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* Create a closure for the function */ + vclosure cl; + memset(&cl, 0, sizeof(cl)); + cl.t = fn_type; + cl.fun = fn_ptr; + cl.hasValue = 0; + + /* Call through hl_dyn_call */ + vdynamic *ret = hl_dyn_call(&cl, NULL, 0); + + if (ret == NULL) { + fprintf(stderr, " hl_dyn_call returned NULL\n"); + return TEST_FAIL; + } + + if (ret->v.i != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret->v.i); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Function with one i32 argument + * + * JIT function: (i32 x) -> i32 { return x + 10 } + */ +TEST(callback_one_int_arg) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10 }; + test_init_ints(c, 1, ints); + + /* Function type: (i32) -> i32 */ + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + + /* r0 = arg (i32), r1 = result (i32), r2 = const 10 */ + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 2, 0), /* r2 = 10 */ + OP3(OAdd, 1, 0, 2), /* r1 = r0 + r2 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 3, ops); + + int result; + void *fn_ptr = test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* Create a closure */ + vclosure cl; + memset(&cl, 0, sizeof(cl)); + cl.t = fn_type; + cl.fun = fn_ptr; + cl.hasValue = 0; + + /* Create argument: i32 value = 32 */ + vdynamic arg_val; + arg_val.t = &c->types[T_I32]; + arg_val.v.i = 32; + vdynamic *args[] = { &arg_val }; + + /* Call through hl_dyn_call */ + vdynamic *ret = hl_dyn_call(&cl, args, 1); + + if (ret == NULL) { + fprintf(stderr, " hl_dyn_call returned NULL\n"); + return TEST_FAIL; + } + + /* Expected: 32 + 10 = 42 */ + if (ret->v.i != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret->v.i); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Function with two i32 arguments + * + * JIT function: (i32 a, i32 b) -> i32 { return a + b } + */ +TEST(callback_two_int_args) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Function type: (i32, i32) -> i32 */ + hl_type *arg_types[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 2, arg_types); + + /* r0 = arg0, r1 = arg1, r2 = result */ + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP3(OAdd, 2, 0, 1), /* r2 = r0 + r1 */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 2, ops); + + int result; + void *fn_ptr = test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* Create a closure */ + vclosure cl; + memset(&cl, 0, sizeof(cl)); + cl.t = fn_type; + cl.fun = fn_ptr; + cl.hasValue = 0; + + /* Create arguments: 10 + 32 = 42 */ + vdynamic arg0, arg1; + arg0.t = &c->types[T_I32]; + arg0.v.i = 10; + arg1.t = &c->types[T_I32]; + arg1.v.i = 32; + vdynamic *args[] = { &arg0, &arg1 }; + + /* Call through hl_dyn_call */ + vdynamic *ret = hl_dyn_call(&cl, args, 2); + + if (ret == NULL) { + fprintf(stderr, " hl_dyn_call returned NULL\n"); + return TEST_FAIL; + } + + if (ret->v.i != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret->v.i); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Function with i64 argument + * + * JIT function: (i64 x) -> i64 { return x } + */ +TEST(callback_i64_arg) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Function type: (i64) -> i64 */ + hl_type *arg_types[] = { &c->types[T_I64] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 1, arg_types); + + /* r0 = arg (i64) */ + hl_type *regs[] = { &c->types[T_I64] }; + + hl_opcode ops[] = { + OP1(ORet, 0), /* return r0 */ + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 1, ops); + + int result; + void *fn_ptr = test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* Create a closure */ + vclosure cl; + memset(&cl, 0, sizeof(cl)); + cl.t = fn_type; + cl.fun = fn_ptr; + cl.hasValue = 0; + + /* Create argument: i64 value = 0x123456789ABCDEF0 */ + vdynamic arg_val; + arg_val.t = &c->types[T_I64]; + arg_val.v.i64 = 0x123456789ABCDEF0LL; + vdynamic *args[] = { &arg_val }; + + /* Call through hl_dyn_call */ + vdynamic *ret = hl_dyn_call(&cl, args, 1); + + if (ret == NULL) { + fprintf(stderr, " hl_dyn_call returned NULL\n"); + return TEST_FAIL; + } + + if (ret->v.i64 != 0x123456789ABCDEF0LL) { + fprintf(stderr, " Expected 0x123456789ABCDEF0, got 0x%llx\n", + (unsigned long long)ret->v.i64); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Function with f64 argument + * + * JIT function: (f64 x) -> f64 { return x } + */ +TEST(callback_f64_arg) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Function type: (f64) -> f64 */ + hl_type *arg_types[] = { &c->types[T_F64] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 1, arg_types); + + /* r0 = arg (f64) */ + hl_type *regs[] = { &c->types[T_F64] }; + + hl_opcode ops[] = { + OP1(ORet, 0), /* return r0 */ + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 1, ops); + + int result; + void *fn_ptr = test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* Create a closure */ + vclosure cl; + memset(&cl, 0, sizeof(cl)); + cl.t = fn_type; + cl.fun = fn_ptr; + cl.hasValue = 0; + + /* Create argument: f64 value = 3.14159 */ + vdynamic arg_val; + arg_val.t = &c->types[T_F64]; + arg_val.v.d = 3.14159; + vdynamic *args[] = { &arg_val }; + + /* Call through hl_dyn_call */ + vdynamic *ret = hl_dyn_call(&cl, args, 1); + + if (ret == NULL) { + fprintf(stderr, " hl_dyn_call returned NULL\n"); + return TEST_FAIL; + } + + double diff = ret->v.d - 3.14159; + if (diff < -0.00001 || diff > 0.00001) { + fprintf(stderr, " Expected 3.14159, got %f\n", ret->v.d); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Mixed int and float arguments + * + * JIT function: (i32 a, f64 b, i32 c) -> i32 { return a + c } + * Tests that arguments are marshaled to correct registers + */ +TEST(callback_mixed_args) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Function type: (i32, f64, i32) -> i32 */ + hl_type *arg_types[] = { &c->types[T_I32], &c->types[T_F64], &c->types[T_I32] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 3, arg_types); + + /* r0 = a (i32), r1 = b (f64), r2 = c (i32), r3 = result (i32) */ + hl_type *regs[] = { &c->types[T_I32], &c->types[T_F64], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP3(OAdd, 3, 0, 2), /* r3 = r0 + r2 */ + OP1(ORet, 3), + }; + + test_alloc_function(c, 0, fn_type, 4, regs, 2, ops); + + int result; + void *fn_ptr = test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* Create a closure */ + vclosure cl; + memset(&cl, 0, sizeof(cl)); + cl.t = fn_type; + cl.fun = fn_ptr; + cl.hasValue = 0; + + /* Create arguments: a=10, b=99.9, c=32 -> result = 42 */ + vdynamic arg0, arg1, arg2; + arg0.t = &c->types[T_I32]; + arg0.v.i = 10; + arg1.t = &c->types[T_F64]; + arg1.v.d = 99.9; + arg2.t = &c->types[T_I32]; + arg2.v.i = 32; + vdynamic *args[] = { &arg0, &arg1, &arg2 }; + + /* Call through hl_dyn_call */ + vdynamic *ret = hl_dyn_call(&cl, args, 3); + + if (ret == NULL) { + fprintf(stderr, " hl_dyn_call returned NULL\n"); + return TEST_FAIL; + } + + if (ret->v.i != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret->v.i); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Many arguments (stress test register allocation) + * + * JIT function: (i32 a, i32 b, i32 c, i32 d, i32 e, i32 f) -> i32 + * { return a + b + c + d + e + f } + */ +TEST(callback_many_int_args) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Function type: (i32, i32, i32, i32, i32, i32) -> i32 */ + hl_type *arg_types[] = { + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] + }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 6, arg_types); + + /* r0-r5 = args, r6 = temp, r7 = result */ + hl_type *regs[] = { + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32] + }; + + hl_opcode ops[] = { + OP3(OAdd, 6, 0, 1), /* r6 = r0 + r1 */ + OP3(OAdd, 6, 6, 2), /* r6 = r6 + r2 */ + OP3(OAdd, 6, 6, 3), /* r6 = r6 + r3 */ + OP3(OAdd, 6, 6, 4), /* r6 = r6 + r4 */ + OP3(OAdd, 7, 6, 5), /* r7 = r6 + r5 */ + OP1(ORet, 7), + }; + + test_alloc_function(c, 0, fn_type, 8, regs, 6, ops); + + int result; + void *fn_ptr = test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* Create a closure */ + vclosure cl; + memset(&cl, 0, sizeof(cl)); + cl.t = fn_type; + cl.fun = fn_ptr; + cl.hasValue = 0; + + /* Create arguments: 1 + 2 + 3 + 4 + 5 + 27 = 42 */ + vdynamic arg0, arg1, arg2, arg3, arg4, arg5; + arg0.t = &c->types[T_I32]; arg0.v.i = 1; + arg1.t = &c->types[T_I32]; arg1.v.i = 2; + arg2.t = &c->types[T_I32]; arg2.v.i = 3; + arg3.t = &c->types[T_I32]; arg3.v.i = 4; + arg4.t = &c->types[T_I32]; arg4.v.i = 5; + arg5.t = &c->types[T_I32]; arg5.v.i = 27; + vdynamic *args[] = { &arg0, &arg1, &arg2, &arg3, &arg4, &arg5 }; + + /* Call through hl_dyn_call */ + vdynamic *ret = hl_dyn_call(&cl, args, 6); + + if (ret == NULL) { + fprintf(stderr, " hl_dyn_call returned NULL\n"); + return TEST_FAIL; + } + + if (ret->v.i != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret->v.i); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Pointer argument (bytes) + * + * JIT function: (bytes ptr) -> bytes { return ptr } + */ +TEST(callback_ptr_arg) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Function type: (bytes) -> bytes */ + hl_type *arg_types[] = { &c->types[T_BYTES] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 1, arg_types); + + /* r0 = arg (bytes) */ + hl_type *regs[] = { &c->types[T_BYTES] }; + + hl_opcode ops[] = { + OP1(ORet, 0), /* return r0 */ + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 1, ops); + + int result; + void *fn_ptr = test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* Create a closure */ + vclosure cl; + memset(&cl, 0, sizeof(cl)); + cl.t = fn_type; + cl.fun = fn_ptr; + cl.hasValue = 0; + + /* Create argument: pointer to a test value */ + static char test_data[] = "hello"; + vdynamic arg_val; + arg_val.t = &c->types[T_BYTES]; + arg_val.v.ptr = test_data; + vdynamic *args[] = { &arg_val }; + + /* Call through hl_dyn_call */ + vdynamic *ret = hl_dyn_call(&cl, args, 1); + + if (ret == NULL) { + fprintf(stderr, " hl_dyn_call returned NULL\n"); + return TEST_FAIL; + } + + if (ret->v.ptr != test_data) { + fprintf(stderr, " Expected %p, got %p\n", (void*)test_data, ret->v.ptr); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(callback_no_args), + TEST_ENTRY(callback_one_int_arg), + TEST_ENTRY(callback_two_int_args), + TEST_ENTRY(callback_i64_arg), + TEST_ENTRY(callback_f64_arg), + TEST_ENTRY(callback_mixed_args), + TEST_ENTRY(callback_many_int_args), + TEST_ENTRY(callback_ptr_arg), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - C-to-HL Callback Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_calls.c b/other/tests/minimal/test_calls.c new file mode 100644 index 000000000..bb0171173 --- /dev/null +++ b/other/tests/minimal/test_calls.c @@ -0,0 +1,446 @@ +/* + * Test function call operations for HashLink AArch64 JIT + * + * Tests: OCall0, OCall1, OCall2, OCall3 + * + * These tests require multiple functions in the hl_code structure. + */ +#include "test_harness.h" +#include + +/* Helper to allocate multiple functions at once */ +static void test_alloc_functions(hl_code *c, int count) { + c->functions = (hl_function*)calloc(count, sizeof(hl_function)); + c->nfunctions = 0; /* Will be incremented as we add */ +} + +/* Add a function to existing array */ +static hl_function *test_add_function(hl_code *c, int findex, hl_type *type, + int nregs, hl_type **regs, + int nops, hl_opcode *ops) { + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = findex; + f->type = type; + f->nregs = nregs; + f->nops = nops; + + f->regs = (hl_type**)malloc(sizeof(hl_type*) * nregs); + memcpy(f->regs, regs, sizeof(hl_type*) * nregs); + + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * nops); + memcpy(f->ops, ops, sizeof(hl_opcode) * nops); + + f->debug = NULL; + f->obj = NULL; + f->field.ref = NULL; + f->ref = 0; + + return f; +} + +/* + * Test: Call function with 0 arguments + * + * fn0: () -> i32 { return 42; } + * fn1: () -> i32 { return call0(fn0); } <- entry point + */ +TEST(call0_simple) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Function types */ + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* Pre-allocate function array */ + test_alloc_functions(c, 2); + + /* fn0: findex=0, returns 42 */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 */ + OP1(ORet, 0), + }; + test_add_function(c, 0, fn_type_i32, 1, regs, 2, ops); + } + + /* fn1: findex=1, calls fn0 (entry point) */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OCall0, 0, 0), /* r0 = call fn0() */ + OP1(ORet, 0), + }; + test_add_function(c, 1, fn_type_i32, 1, regs, 2, ops); + } + + c->entrypoint = 1; /* fn1 is entry */ + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Call function with 1 argument + * + * fn0: (i32) -> i32 { return arg + 10; } + * fn1: () -> i32 { return call1(fn0, 32); } <- entry point + */ +TEST(call1_simple) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 32 }; + test_init_ints(c, 2, ints); + + /* Function types */ + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 2); + + /* fn0: findex=0, returns arg0 + 10 */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + /* r0 = first argument (passed in) */ + OP2(OInt, 1, 0), /* r1 = 10 */ + OP3(OAdd, 2, 0, 1), /* r2 = r0 + r1 */ + OP1(ORet, 2), + }; + test_add_function(c, 0, fn_type_i32_i32, 3, regs, 3, ops); + } + + /* fn1: findex=1, calls fn0(32) */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 1, 1), /* r1 = 32 */ + OP3(OCall1, 0, 0, 1), /* r0 = call fn0(r1) */ + OP1(ORet, 0), + }; + test_add_function(c, 1, fn_type_i32, 2, regs, 3, ops); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { /* 32 + 10 = 42 */ + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Call function with 2 arguments + * + * fn0: (i32, i32) -> i32 { return a + b; } + * fn1: () -> i32 { return call2(fn0, 10, 32); } + */ +TEST(call2_simple) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 32 }; + test_init_ints(c, 2, ints); + + /* Function types */ + hl_type *arg_types[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_type *fn_type_i32_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 2, arg_types); + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 2); + + /* fn0: findex=0, returns arg0 + arg1 */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + /* r0 = arg0, r1 = arg1 */ + OP3(OAdd, 2, 0, 1), /* r2 = r0 + r1 */ + OP1(ORet, 2), + }; + test_add_function(c, 0, fn_type_i32_i32_i32, 3, regs, 2, ops); + } + + /* fn1: findex=1, calls fn0(10, 32) */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 1, 0), /* r1 = 10 */ + OP2(OInt, 2, 1), /* r2 = 32 */ + OP4_CALL2(OCall2, 0, 0, 1, 2), /* r0 = call fn0(r1, r2) */ + OP1(ORet, 0), + }; + test_add_function(c, 1, fn_type_i32, 3, regs, 4, ops); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { /* 10 + 32 = 42 */ + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Nested calls + * + * fn0: (i32) -> i32 { return arg * 2; } + * fn1: (i32) -> i32 { return call1(fn0, arg) + 1; } + * fn2: () -> i32 { return call1(fn1, 20); } <- entry (20*2+1 = 41, need 42) + * + * Actually: 21 * 2 = 42 + */ +TEST(nested_calls) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 2, 21 }; + test_init_ints(c, 2, ints); + + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 3); + + /* fn0: findex=0, returns arg * 2 */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 1, 0), /* r1 = 2 */ + OP3(OMul, 2, 0, 1), /* r2 = r0 * 2 */ + OP1(ORet, 2), + }; + test_add_function(c, 0, fn_type_i32_i32, 3, regs, 3, ops); + } + + /* fn1: findex=1, returns call0(fn0, arg) */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP3(OCall1, 1, 0, 0), /* r1 = call fn0(r0) */ + OP1(ORet, 1), + }; + test_add_function(c, 1, fn_type_i32_i32, 2, regs, 2, ops); + } + + /* fn2: findex=2, entry point */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 1, 1), /* r1 = 21 */ + OP3(OCall1, 0, 1, 1), /* r0 = call fn1(r1) */ + OP1(ORet, 0), + }; + test_add_function(c, 2, fn_type_i32, 2, regs, 3, ops); + } + + c->entrypoint = 2; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { /* 21 * 2 = 42 */ + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Recursive call (factorial) + * + * fn0: (i32) -> i32 { + * if (n <= 1) return 1; + * return n * call1(fn0, n-1); + * } + * fn1: () -> i32 { return call1(fn0, 5); } <- 5! = 120 + * + * Note: We want result 42, so let's compute something else + * Let's do: sum from 1 to n recursively + * sum(n) = n + sum(n-1), sum(0) = 0 + * sum(8) = 8+7+6+5+4+3+2+1 = 36... not 42 + * sum(9) = 45... + * + * Let's just verify 5! = 120 works, and accept that as the test value + */ +TEST(recursive_factorial) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 1, 5 }; + test_init_ints(c, 2, ints); + + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + hl_type *fn_type_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + test_alloc_functions(c, 2); + + /* fn0: findex=0, factorial(n) + * r0 = n + * r1 = 1 (constant) + * r2 = temp + * r3 = n-1 + * r4 = result of recursive call + */ + { + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + }; + hl_opcode ops[] = { + OP2(OInt, 1, 0), /* op0: r1 = 1 */ + OP3(OJSLte, 0, 1, 2), /* op1: if n <= 1 goto op3 */ + OP1(OJAlways, 3), /* op2: else goto op6 (skip return 1) */ + /* return 1 path */ + OP0(OLabel), /* op3: label */ + OP1(ORet, 1), /* op4: return 1 */ + /* recursive path */ + OP0(OLabel), /* op5: label */ + OP3(OSub, 3, 0, 1), /* op6: r3 = n - 1 */ + OP3(OCall1, 4, 0, 3), /* op7: r4 = factorial(n-1) */ + OP3(OMul, 2, 0, 4), /* op8: r2 = n * r4 */ + OP1(ORet, 2), /* op9: return r2 */ + }; + test_add_function(c, 0, fn_type_i32_i32, 5, regs, 10, ops); + } + + /* fn1: findex=1, entry point */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 1, 1), /* r1 = 5 */ + OP3(OCall1, 0, 0, 1), /* r0 = factorial(5) */ + OP1(ORet, 0), + }; + test_add_function(c, 1, fn_type_i32, 2, regs, 3, ops); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 120) { /* 5! = 120 */ + fprintf(stderr, " Expected 120, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Call with float argument + */ +TEST(call1_float) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { 10.5, 31.5 }; + test_init_floats(c, 2, floats); + + hl_type *arg_types[] = { &c->types[T_F64] }; + hl_type *fn_type_f64_f64 = test_alloc_fun_type(c, &c->types[T_F64], 1, arg_types); + hl_type *fn_type_f64 = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + + test_alloc_functions(c, 2); + + /* fn0: findex=0, returns arg + 10.5 */ + { + hl_type *regs[] = { &c->types[T_F64], &c->types[T_F64], &c->types[T_F64] }; + hl_opcode ops[] = { + OP2(OFloat, 1, 0), /* r1 = 10.5 */ + OP3(OAdd, 2, 0, 1), /* r2 = r0 + r1 */ + OP1(ORet, 2), + }; + test_add_function(c, 0, fn_type_f64_f64, 3, regs, 3, ops); + } + + /* fn1: findex=1, calls fn0(31.5) */ + { + hl_type *regs[] = { &c->types[T_F64], &c->types[T_F64] }; + hl_opcode ops[] = { + OP2(OFloat, 1, 1), /* r1 = 31.5 */ + OP3(OCall1, 0, 0, 1), /* r0 = call fn0(r1) */ + OP1(ORet, 0), + }; + test_add_function(c, 1, fn_type_f64, 2, regs, 3, ops); + } + + c->entrypoint = 1; + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + if (fabs(ret - 42.0) > 1e-9) { /* 31.5 + 10.5 = 42.0 */ + fprintf(stderr, " Expected 42.0, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(call0_simple), + TEST_ENTRY(call1_simple), + TEST_ENTRY(call2_simple), + TEST_ENTRY(nested_calls), + TEST_ENTRY(recursive_factorial), + TEST_ENTRY(call1_float), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Function Call Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_closures.c b/other/tests/minimal/test_closures.c new file mode 100644 index 000000000..921329c36 --- /dev/null +++ b/other/tests/minimal/test_closures.c @@ -0,0 +1,280 @@ +/* + * Test closure operations for HashLink AArch64 JIT + * + * Tests: OStaticClosure, OCallClosure + * + * These are key opcodes used in hello.hl's main function. + */ +#include "test_harness.h" + +/* + * Test: Create a static closure and call it with no args + * + * fn0: () -> i32 { return 42; } + * fn1: () -> i32 { + * r0 = static_closure(fn0) ; OStaticClosure + * r1 = call_closure(r0) ; OCallClosure with 0 args + * return r1 + * } + */ +TEST(static_closure_call0) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Function type: () -> i32 */ + hl_type *fn_type_void_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* We need a closure type for the register holding the closure */ + /* For now, use the function type directly */ + + /* Pre-allocate function array */ + c->functions = (hl_function*)calloc(MAX_FUNCTIONS, sizeof(hl_function)); + c->nfunctions = 0; + + /* fn0: findex=0, returns 42 */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 */ + OP1(ORet, 0), + }; + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 0; + f->type = fn_type_void_i32; + f->nregs = 1; + f->nops = 2; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 1); + f->regs[0] = &c->types[T_I32]; + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 2); + memcpy(f->ops, ops, sizeof(ops)); + } + + /* fn1: findex=1, creates closure and calls it */ + { + /* r0 = closure (pointer type), r1 = result */ + hl_type *regs[] = { fn_type_void_i32, &c->types[T_I32] }; + + /* OCallClosure: p1=dst, p2=closure_reg, p3=nargs, extra=args */ + hl_opcode ops[] = { + OP2(OStaticClosure, 0, 0), /* r0 = closure pointing to fn0 */ + {OCallClosure, 1, 0, 0, NULL}, /* r1 = call_closure(r0) with 0 args */ + OP1(ORet, 1), + }; + + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 1; + f->type = fn_type_void_i32; + f->nregs = 2; + f->nops = 3; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 2); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 3); + memcpy(f->ops, ops, sizeof(ops)); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Static closure with one argument + * + * fn0: (i32) -> i32 { return arg + 10; } + * fn1: () -> i32 { + * r0 = static_closure(fn0) + * r1 = 32 + * r2 = call_closure(r0, r1) ; 32 + 10 = 42 + * return r2 + * } + */ +TEST(static_closure_call1) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 32 }; + test_init_ints(c, 2, ints); + + /* Function types */ + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + hl_type *fn_type_void_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + c->functions = (hl_function*)calloc(MAX_FUNCTIONS, sizeof(hl_function)); + c->nfunctions = 0; + + /* fn0: findex=0, returns arg + 10 */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP2(OInt, 1, 0), /* r1 = 10 */ + OP3(OAdd, 2, 0, 1), /* r2 = r0 + r1 */ + OP1(ORet, 2), + }; + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 0; + f->type = fn_type_i32_i32; + f->nregs = 3; + f->nops = 3; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 3); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 3); + memcpy(f->ops, ops, sizeof(ops)); + } + + /* fn1: findex=1, creates closure and calls with arg */ + { + hl_type *regs[] = { fn_type_i32_i32, &c->types[T_I32], &c->types[T_I32] }; + + /* OCallClosure with 1 arg: extra[0] = arg register */ + static int extra[] = { 1 }; /* r1 is the argument */ + hl_opcode ops[] = { + OP2(OStaticClosure, 0, 0), /* r0 = closure pointing to fn0 */ + OP2(OInt, 1, 1), /* r1 = 32 */ + {OCallClosure, 2, 0, 1, extra}, /* r2 = call_closure(r0, r1) */ + OP1(ORet, 2), + }; + + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 1; + f->type = fn_type_void_i32; + f->nregs = 3; + f->nops = 4; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 3); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 4); + memcpy(f->ops, ops, sizeof(ops)); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Static closure with two arguments + * + * fn0: (i32, i32) -> i32 { return arg0 + arg1; } + * fn1: () -> i32 { + * r0 = static_closure(fn0) + * r1 = 10 + * r2 = 32 + * r3 = call_closure(r0, r1, r2) ; 10 + 32 = 42 + * return r3 + * } + * + * This matches the pattern used in hello.hl's F27. + */ +TEST(static_closure_call2) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 32 }; + test_init_ints(c, 2, ints); + + /* Function types */ + hl_type *arg_types[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_type *fn_type_i32_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 2, arg_types); + hl_type *fn_type_void_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + c->functions = (hl_function*)calloc(MAX_FUNCTIONS, sizeof(hl_function)); + c->nfunctions = 0; + + /* fn0: findex=0, returns arg0 + arg1 */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP3(OAdd, 2, 0, 1), /* r2 = r0 + r1 */ + OP1(ORet, 2), + }; + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 0; + f->type = fn_type_i32_i32_i32; + f->nregs = 3; + f->nops = 2; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 3); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 2); + memcpy(f->ops, ops, sizeof(ops)); + } + + /* fn1: findex=1, creates closure and calls with 2 args */ + { + hl_type *regs[] = { fn_type_i32_i32_i32, &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + /* OCallClosure with 2 args: extra[0] = arg0 reg, extra[1] = arg1 reg */ + static int extra[] = { 1, 2 }; /* r1 and r2 are the arguments */ + hl_opcode ops[] = { + OP2(OStaticClosure, 0, 0), /* r0 = closure pointing to fn0 */ + OP2(OInt, 1, 0), /* r1 = 10 */ + OP2(OInt, 2, 1), /* r2 = 32 */ + {OCallClosure, 3, 0, 2, extra}, /* r3 = call_closure(r0, r1, r2) */ + OP1(ORet, 3), + }; + + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 1; + f->type = fn_type_void_i32; + f->nregs = 4; + f->nops = 5; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 4); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 5); + memcpy(f->ops, ops, sizeof(ops)); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(static_closure_call0), + TEST_ENTRY(static_closure_call1), + TEST_ENTRY(static_closure_call2), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Closure Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_control_flow.c b/other/tests/minimal/test_control_flow.c new file mode 100644 index 000000000..e854eb37e --- /dev/null +++ b/other/tests/minimal/test_control_flow.c @@ -0,0 +1,560 @@ +/* + * Test control flow operations for HashLink AArch64 JIT + * + * Tests: OLabel, OJAlways, OJTrue, OJFalse, OJSLt, OJSGte, OJEq, OJNotEq + * + * Jump offset semantics: target = (currentOpIndex + 1) + offset + * Example: at op1 with offset=1 -> target = (1+1)+1 = 3 + */ +#include "test_harness.h" + +/* + * Test: Unconditional jump - skip one instruction + * + * op0: int r0, 0 ; r0 = 42 + * op1: jalways +1 ; jump to op3 (target = 2+1 = 3) + * op2: int r0, 1 ; r0 = 100 (SKIPPED) + * op3: ret r0 ; return 42 + */ +TEST(jump_always_skip) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42, 100 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 42 */ + OP1(OJAlways, 1), /* op1: jump to op3 (target = 2+1 = 3) */ + OP2(OInt, 0, 1), /* op2: r0 = 100 (skipped) */ + OP1(ORet, 0), /* op3: return r0 */ + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Jump if true (taken) + * + * op0: bool r0, 1 ; r0 = true + * op1: int r1, 0 ; r1 = 42 + * op2: jtrue r0, +1 ; if r0 goto op4 (target = 3+1 = 4) + * op3: int r1, 1 ; r1 = 100 (skipped) + * op4: ret r1 ; return 42 + */ +TEST(jump_true_taken) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42, 100 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 1), /* op0: r0 = true */ + OP2(OInt, 1, 0), /* op1: r1 = 42 */ + OP2(OJTrue, 0, 1), /* op2: if r0 goto op4 (target = 3+1 = 4) */ + OP2(OInt, 1, 1), /* op3: r1 = 100 (skipped) */ + OP1(ORet, 1), /* op4: return r1 */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 5, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Jump if true (not taken) + */ +TEST(jump_true_not_taken) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42, 100 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 0), /* op0: r0 = false */ + OP2(OInt, 1, 0), /* op1: r1 = 42 */ + OP2(OJTrue, 0, 1), /* op2: if r0 goto op4 (not taken) */ + OP2(OInt, 1, 1), /* op3: r1 = 100 */ + OP1(ORet, 1), /* op4: return r1 */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 5, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 100) { + fprintf(stderr, " Expected 100, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Jump if false (taken) + */ +TEST(jump_false_taken) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42, 100 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 0), /* op0: r0 = false */ + OP2(OInt, 1, 0), /* op1: r1 = 42 */ + OP2(OJFalse, 0, 1), /* op2: if !r0 goto op4 (target = 3+1 = 4) */ + OP2(OInt, 1, 1), /* op3: r1 = 100 (skipped) */ + OP1(ORet, 1), /* op4: return r1 */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 5, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Jump if false (not taken) + */ +TEST(jump_false_not_taken) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42, 100 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_BOOL], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OBool, 0, 1), /* op0: r0 = true */ + OP2(OInt, 1, 0), /* op1: r1 = 42 */ + OP2(OJFalse, 0, 1), /* op2: if !r0 goto op4 (not taken) */ + OP2(OInt, 1, 1), /* op3: r1 = 100 */ + OP1(ORet, 1), /* op4: return r1 */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 5, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 100) { + fprintf(stderr, " Expected 100, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Jump if signed less than (taken): 5 < 10 + */ +TEST(jump_slt_taken) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 5, 10, 42, 100 }; + test_init_ints(c, 4, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 5 */ + OP2(OInt, 1, 1), /* op1: r1 = 10 */ + OP2(OInt, 2, 2), /* op2: r2 = 42 */ + OP3(OJSLt, 0, 1, 1), /* op3: if r0 < r1 goto op5 (target = 4+1 = 5) */ + OP2(OInt, 2, 3), /* op4: r2 = 100 (skipped) */ + OP1(ORet, 2), /* op5: return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 6, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Jump if signed less than (not taken): 10 < 5 + */ +TEST(jump_slt_not_taken) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 5, 42, 100 }; + test_init_ints(c, 4, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 10 */ + OP2(OInt, 1, 1), /* op1: r1 = 5 */ + OP2(OInt, 2, 2), /* op2: r2 = 42 */ + OP3(OJSLt, 0, 1, 1), /* op3: if r0 < r1 goto op5 (not taken) */ + OP2(OInt, 2, 3), /* op4: r2 = 100 */ + OP1(ORet, 2), /* op5: return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 6, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 100) { + fprintf(stderr, " Expected 100, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Jump if signed greater-or-equal (taken): 10 >= 5 + */ +TEST(jump_sgte_taken) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 5, 42, 100 }; + test_init_ints(c, 4, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 10 */ + OP2(OInt, 1, 1), /* op1: r1 = 5 */ + OP2(OInt, 2, 2), /* op2: r2 = 42 */ + OP3(OJSGte, 0, 1, 1), /* op3: if r0 >= r1 goto op5 (target = 4+1 = 5) */ + OP2(OInt, 2, 3), /* op4: r2 = 100 (skipped) */ + OP1(ORet, 2), /* op5: return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 6, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Jump if equal (taken): 42 == 42 + */ +TEST(jump_eq_taken) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42, 100 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 42 */ + OP2(OInt, 1, 0), /* op1: r1 = 42 */ + OP2(OInt, 2, 0), /* op2: r2 = 42 */ + OP3(OJEq, 0, 1, 1), /* op3: if r0 == r1 goto op5 */ + OP2(OInt, 2, 1), /* op4: r2 = 100 (skipped) */ + OP1(ORet, 2), /* op5: return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 6, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Jump if equal (not taken): 42 == 100 + */ +TEST(jump_eq_not_taken) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42, 100 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 42 */ + OP2(OInt, 1, 1), /* op1: r1 = 100 */ + OP2(OInt, 2, 0), /* op2: r2 = 42 */ + OP3(OJEq, 0, 1, 1), /* op3: if r0 == r1 goto op5 (not taken) */ + OP2(OInt, 2, 1), /* op4: r2 = 100 */ + OP1(ORet, 2), /* op5: return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 6, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 100) { + fprintf(stderr, " Expected 100, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Jump if not equal (taken): 42 != 100 + */ +TEST(jump_neq_taken) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42, 100 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 42 */ + OP2(OInt, 1, 1), /* op1: r1 = 100 */ + OP2(OInt, 2, 0), /* op2: r2 = 42 */ + OP3(OJNotEq, 0, 1, 1), /* op3: if r0 != r1 goto op5 */ + OP2(OInt, 2, 1), /* op4: r2 = 100 (skipped) */ + OP1(ORet, 2), /* op5: return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 6, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Simple loop - sum 1 to 5 = 15 + * + * r0 = counter (starts at 1) + * r1 = sum (starts at 0) + * r2 = limit (5) + * + * loop: + * sum += counter + * counter++ + * if counter <= limit goto loop + * return sum + */ +TEST(simple_loop_sum) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 1, 0, 5 }; + test_init_ints(c, 3, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { + &c->types[T_I32], /* r0: counter */ + &c->types[T_I32], /* r1: sum */ + &c->types[T_I32], /* r2: limit */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 1 (counter) */ + OP2(OInt, 1, 1), /* op1: r1 = 0 (sum) */ + OP2(OInt, 2, 2), /* op2: r2 = 5 (limit) */ + /* loop body starts at op3 */ + OP0(OLabel), /* op3: loop target */ + OP3(OAdd, 1, 1, 0), /* op4: sum += counter */ + OP1(OIncr, 0), /* op5: counter++ */ + OP3(OJSLte, 0, 2, -4), /* op6: if counter <= limit goto op3 (target = 7-4 = 3) */ + OP1(ORet, 1), /* op7: return sum */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 8, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 15) { /* 1+2+3+4+5 = 15 */ + fprintf(stderr, " Expected 15, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Signed comparison with negative numbers: -5 < 5 + */ +TEST(jump_slt_negative) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { -5, 5, 42, 100 }; + test_init_ints(c, 4, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = -5 */ + OP2(OInt, 1, 1), /* op1: r1 = 5 */ + OP2(OInt, 2, 2), /* op2: r2 = 42 */ + OP3(OJSLt, 0, 1, 1), /* op3: if r0 < r1 goto op5 (target = 4+1 = 5) */ + OP2(OInt, 2, 3), /* op4: r2 = 100 (skipped) */ + OP1(ORet, 2), /* op5: return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 6, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(jump_always_skip), + TEST_ENTRY(jump_true_taken), + TEST_ENTRY(jump_true_not_taken), + TEST_ENTRY(jump_false_taken), + TEST_ENTRY(jump_false_not_taken), + TEST_ENTRY(jump_slt_taken), + TEST_ENTRY(jump_slt_not_taken), + TEST_ENTRY(jump_sgte_taken), + TEST_ENTRY(jump_eq_taken), + TEST_ENTRY(jump_eq_not_taken), + TEST_ENTRY(jump_neq_taken), + TEST_ENTRY(simple_loop_sum), + TEST_ENTRY(jump_slt_negative), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Control Flow Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_dynamic.c b/other/tests/minimal/test_dynamic.c new file mode 100644 index 000000000..4a2cb6281 --- /dev/null +++ b/other/tests/minimal/test_dynamic.c @@ -0,0 +1,294 @@ +/* + * Test dynamic object operations for HashLink AArch64 JIT + * + * Tests: ODynGet, ODynSet, OToVirtual, OToDyn + * + * These are key opcodes used in hello.hl for dynamic field access. + */ +#include "test_harness.h" + +/* Helper to create a HDYN type */ +static hl_type *get_dyn_type(hl_code *c) { + if (c->ntypes >= MAX_TYPES) return NULL; + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + t->kind = HDYN; + return t; +} + +/* Helper to create a virtual type with fields */ +static hl_type *create_virtual_type(hl_code *c, int nfields, const char **field_names, hl_type **field_types) { + if (c->ntypes >= MAX_TYPES) return NULL; + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HVIRTUAL; + t->virt = (hl_type_virtual*)calloc(1, sizeof(hl_type_virtual)); + t->virt->nfields = nfields; + + if (nfields > 0) { + t->virt->fields = (hl_obj_field*)calloc(nfields, sizeof(hl_obj_field)); + for (int i = 0; i < nfields; i++) { + t->virt->fields[i].name = (uchar*)field_names[i]; + t->virt->fields[i].t = field_types[i]; + t->virt->fields[i].hashed_name = hl_hash_gen(hl_get_ustring(c, 0), true); /* placeholder */ + } + } + + return t; +} + +/* + * Test: Convert i32 to dynamic with OToDyn + * + * r0 = 42 + * r1 = to_dyn(r0) + * return r0 ; just verify we don't crash + */ +TEST(to_dyn_i32) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + hl_type *dyn_type = get_dyn_type(c); + if (!dyn_type) return TEST_FAIL; + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], dyn_type }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 */ + OP2(OToDyn, 1, 0), /* r1 = to_dyn(r0) */ + OP1(ORet, 0), /* return r0 */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OMov with various types + * + * r0 = 42 + * r1 = mov r0 + * return r1 + */ +TEST(mov_i32) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 */ + OP2(OMov, 1, 0), /* r1 = r0 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: ONull - load null pointer + * + * r0 = null + * r1 = 42 + * return r1 ; just verify null doesn't crash us + */ +TEST(null_load) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + hl_type *dyn_type = get_dyn_type(c); + if (!dyn_type) return TEST_FAIL; + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { dyn_type, &c->types[T_I32] }; + + hl_opcode ops[] = { + OP1(ONull, 0), /* r0 = null */ + OP2(OInt, 1, 0), /* r1 = 42 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OJNull / OJNotNull - null check branches + * + * r0 = null + * if r0 == null goto L1 + * r1 = 0 ; should not reach here + * jmp L2 + * L1: + * r1 = 42 ; should reach here + * L2: + * return r1 + */ +TEST(jnull_branch) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 0, 42 }; + test_init_ints(c, 2, ints); + + hl_type *dyn_type = get_dyn_type(c); + if (!dyn_type) return TEST_FAIL; + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { dyn_type, &c->types[T_I32] }; + + hl_opcode ops[] = { + OP1(ONull, 0), /* op0: r0 = null */ + OP2(OJNull, 0, 2), /* op1: if r0 == null goto op4 (1+1+2=4) */ + OP2(OInt, 1, 0), /* op2: r1 = 0 (not reached) */ + OP1(OJAlways, 1), /* op3: goto op5 (3+1+1=5) */ + OP0(OLabel), /* op4: label */ + OP2(OInt, 1, 1), /* op5: r1 = 42 */ + OP0(OLabel), /* op6: label */ + OP1(ORet, 1), /* op7: return r1 */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 8, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OJNotNull branch + * + * r0 = 1 (non-null when treated as pointer) + * if r0 != null goto L1 + * r1 = 0 + * jmp L2 + * L1: + * r1 = 42 + * L2: + * return r1 + */ +TEST(jnotnull_branch) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 0, 42 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + /* Use BYTES type for the "pointer" register */ + hl_type *regs[] = { &c->types[T_BYTES], &c->types[T_I32] }; + + /* We'll use OString to get a non-null pointer */ + c->nstrings = 1; + c->strings = (char**)malloc(sizeof(char*)); + c->strings[0] = "x"; + c->strings_lens = (int*)malloc(sizeof(int)); + c->strings_lens[0] = 1; + c->ustrings = (uchar**)calloc(1, sizeof(uchar*)); + + hl_opcode ops[] = { + OP2(OString, 0, 0), /* op0: r0 = "x" (non-null) */ + OP2(OJNotNull, 0, 2), /* op1: if r0 != null goto op4 */ + OP2(OInt, 1, 0), /* op2: r1 = 0 (not reached) */ + OP1(OJAlways, 1), /* op3: goto op5 */ + OP0(OLabel), /* op4: label */ + OP2(OInt, 1, 1), /* op5: r1 = 42 */ + OP0(OLabel), /* op6: label */ + OP1(ORet, 1), /* op7: return r1 */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 8, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(to_dyn_i32), + TEST_ENTRY(mov_i32), + TEST_ENTRY(null_load), + TEST_ENTRY(jnull_branch), + TEST_ENTRY(jnotnull_branch), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Dynamic/Null Operations Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_enum.c b/other/tests/minimal/test_enum.c new file mode 100644 index 000000000..255e565b6 --- /dev/null +++ b/other/tests/minimal/test_enum.c @@ -0,0 +1,327 @@ +/* + * Test enum operations for HashLink AArch64 JIT + * + * Tests: OEnumAlloc, OEnumField, OSetEnumField, OEnumIndex, OMakeEnum + */ +#include "test_harness.h" + +/* + * Helper to create an enum type with a single construct that has pointer fields. + * This is similar to how Option or similar sum types work. + * + * Construct 0: has `nfields` pointer-sized fields at 8-byte offsets starting at offset 8 + * (offset 0 is typically the enum tag/index) + */ +static hl_type *create_enum_type(hl_code *c, const char *name, int nfields) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types\n"); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HENUM; + t->tenum = (hl_type_enum*)calloc(1, sizeof(hl_type_enum)); + t->tenum->name = (const uchar*)name; + t->tenum->nconstructs = 1; + t->tenum->constructs = (hl_enum_construct*)calloc(1, sizeof(hl_enum_construct)); + + hl_enum_construct *cons = &t->tenum->constructs[0]; + cons->name = (const uchar*)"Cons"; + cons->nparams = nfields; + cons->hasptr = true; + + if (nfields > 0) { + cons->params = (hl_type**)calloc(nfields, sizeof(hl_type*)); + cons->offsets = (int*)calloc(nfields, sizeof(int)); + for (int i = 0; i < nfields; i++) { + cons->params[i] = &c->types[T_I64]; /* Use i64/pointer type */ + cons->offsets[i] = 8 + i * 8; /* Fields start at offset 8 (after tag) */ + } + } + + /* Size = 8 (tag) + nfields * 8 */ + cons->size = 8 + nfields * 8; + + return t; +} + +/* + * Test: OEnumField - extract a field from an enum, then use it + * + * This test specifically targets the bug where OEnumField doesn't clear + * the destination register binding, causing stale values to be used. + * + * The pattern is: + * r1 = alloc_enum ; allocate enum + * set_enum_field r1, 0, r0 ; store a value (42) into field 0 + * r2 = enum_field r1, 0 ; extract field 0 -> should be 42 + * return r2 ; return extracted value + * + * If the register binding bug exists, r2 might return garbage instead of 42. + */ +TEST(enum_field_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Create enum type with 1 field */ + hl_type *enum_t = create_enum_type(c, "TestEnum", 1); + if (!enum_t) return TEST_FAIL; + + /* Function: () -> i64 */ + hl_type *ret_type = &c->types[T_I64]; + hl_type *fn_type = test_alloc_fun_type(c, ret_type, 0, NULL); + + /* Registers: + * r0: i64 (temp for value 42) + * r1: enum (the allocated enum) + * r2: i64 (extracted field value) + */ + hl_type *regs[] = { &c->types[T_I64], enum_t, &c->types[T_I64] }; + + /* Integer constants */ + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Opcodes */ + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 */ + OP2(OEnumAlloc, 1, 0), /* r1 = alloc enum (construct 0) */ + OP3(OSetEnumField, 1, 0, 0), /* r1.field[0] = r0 (42) */ + { OEnumField, 2, 1, 0, (int*)(intptr_t)0 }, /* r2 = r1.field[0] (extra=0) */ + OP1(ORet, 2), /* return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 5, ops); + + int result; + int64_t (*func)(void) = (int64_t (*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int64_t ret = func(); + if (ret != 42) { + printf("\n Expected 42, got %ld\n", (long)ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OEnumField with multiple fields and uses + * + * This test more closely matches the uvsample crash pattern: + * - Multiple OEnumField extractions + * - The extracted values are then used as function arguments + * + * Pattern: + * r0 = 100 + * r1 = 200 + * r2 = alloc_enum + * set_enum_field r2, 0, r0 ; field 0 = 100 + * set_enum_field r2, 1, r1 ; field 1 = 200 + * r3 = enum_field r2, 0 ; r3 = 100 + * r4 = enum_field r2, 1 ; r4 = 200 + * r5 = r3 + r4 ; r5 = 300 + * return r5 + */ +TEST(enum_field_multiple) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Create enum type with 2 fields */ + hl_type *enum_t = create_enum_type(c, "TestEnum2", 2); + if (!enum_t) return TEST_FAIL; + + /* Function: () -> i64 */ + hl_type *ret_type = &c->types[T_I64]; + hl_type *fn_type = test_alloc_fun_type(c, ret_type, 0, NULL); + + /* Registers: + * r0: i64 (value 100) + * r1: i64 (value 200) + * r2: enum + * r3: i64 (extracted field 0) + * r4: i64 (extracted field 1) + * r5: i64 (sum) + */ + hl_type *regs[] = { + &c->types[T_I64], &c->types[T_I64], enum_t, + &c->types[T_I64], &c->types[T_I64], &c->types[T_I64] + }; + + /* Integer constants */ + int ints[] = { 100, 200 }; + test_init_ints(c, 2, ints); + + /* Opcodes */ + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 100 */ + OP2(OInt, 1, 1), /* r1 = 200 */ + OP2(OEnumAlloc, 2, 0), /* r2 = alloc enum */ + OP3(OSetEnumField, 2, 0, 0), /* r2.field[0] = r0 */ + OP3(OSetEnumField, 2, 1, 1), /* r2.field[1] = r1 */ + { OEnumField, 3, 2, 0, (int*)(intptr_t)0 }, /* r3 = r2.field[0] */ + { OEnumField, 4, 2, 0, (int*)(intptr_t)1 }, /* r4 = r2.field[1] */ + OP3(OAdd, 5, 3, 4), /* r5 = r3 + r4 */ + OP1(ORet, 5), /* return r5 */ + }; + + test_alloc_function(c, 0, fn_type, 6, regs, 9, ops); + + int result; + int64_t (*func)(void) = (int64_t (*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int64_t ret = func(); + if (ret != 300) { + printf("\n Expected 300, got %ld\n", (long)ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OEnumField followed by function call + * + * This is the exact pattern that causes the uvsample crash: + * - Extract a field from enum + * - Pass it as argument to a function call + * + * If dst register binding isn't cleared, the call might use a stale value. + * + * Pattern: + * r0 = 42 + * r1 = alloc_enum + * set_enum_field r1, 0, r0 + * r2 = enum_field r1, 0 ; extract 42 + * r3 = call identity(r2) ; call function with extracted value + * return r3 + */ + +/* Native identity function for testing */ +static int64_t native_identity(int64_t x) { + return x; +} + +TEST(enum_field_then_call) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Create enum type with 1 field */ + hl_type *enum_t = create_enum_type(c, "TestEnum3", 1); + if (!enum_t) return TEST_FAIL; + + /* Native function type: (i64) -> i64 */ + hl_type *i64_t = &c->types[T_I64]; + hl_type *native_args[] = { i64_t }; + hl_type *native_fn_type = test_alloc_fun_type(c, i64_t, 1, native_args); + + /* Add native function at findex 1 */ + test_add_native(c, 1, "test", "identity", native_fn_type, (void*)native_identity); + + /* Main function type: () -> i64 */ + hl_type *fn_type = test_alloc_fun_type(c, i64_t, 0, NULL); + + /* Registers: + * r0: i64 (value 42) + * r1: enum + * r2: i64 (extracted field) + * r3: i64 (call result) + */ + hl_type *regs[] = { i64_t, enum_t, i64_t, i64_t }; + + /* Integer constants */ + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Opcodes */ + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 */ + OP2(OEnumAlloc, 1, 0), /* r1 = alloc enum */ + OP3(OSetEnumField, 1, 0, 0), /* r1.field[0] = r0 */ + { OEnumField, 2, 1, 0, (int*)(intptr_t)0 }, /* r2 = r1.field[0] */ + OP3(OCall1, 3, 1, 2), /* r3 = call F1(r2) - native identity */ + OP1(ORet, 3), /* return r3 */ + }; + + test_alloc_function(c, 0, fn_type, 4, regs, 6, ops); + + int result; + int64_t (*func)(void) = (int64_t (*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int64_t ret = func(); + if (ret != 42) { + printf("\n Expected 42, got %ld\n", (long)ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OEnumIndex - get the construct index of an enum value + */ +TEST(enum_index) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Create enum type */ + hl_type *enum_t = create_enum_type(c, "TestEnum4", 1); + if (!enum_t) return TEST_FAIL; + + /* Function: () -> i32 */ + hl_type *ret_type = &c->types[T_I32]; + hl_type *fn_type = test_alloc_fun_type(c, ret_type, 0, NULL); + + /* Registers: + * r0: enum + * r1: i32 (index result) + */ + hl_type *regs[] = { enum_t, &c->types[T_I32] }; + + /* Opcodes */ + hl_opcode ops[] = { + OP2(OEnumAlloc, 0, 0), /* r0 = alloc enum (construct 0) */ + OP2(OEnumIndex, 1, 0), /* r1 = index of r0 (should be 0) */ + OP1(ORet, 1), /* return r1 */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*func)(void) = (int (*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = func(); + if (ret != 0) { + printf("\n Expected 0, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test registry */ +int main(int argc, char **argv) { + test_entry_t tests[] = { + TEST_ENTRY(enum_field_basic), + TEST_ENTRY(enum_field_multiple), + TEST_ENTRY(enum_field_then_call), + TEST_ENTRY(enum_index), + }; + + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_exceptions.c b/other/tests/minimal/test_exceptions.c new file mode 100644 index 000000000..f0a18155b --- /dev/null +++ b/other/tests/minimal/test_exceptions.c @@ -0,0 +1,291 @@ +/* + * Test exception operations for HashLink AArch64 JIT + * + * Tests: OThrow, ORethrow, OTrap, OEndTrap, OCatch + * + * Exception handling in HashLink uses setjmp/longjmp. + * OTrap: set up exception handler (like try {) + * OEndTrap: tear down exception handler (end of try block) + * OThrow: throw an exception + * ORethrow: rethrow current exception + * OCatch: marks catch block (informational, no code generated) + */ +#include "test_harness.h" + +/* + * Test: OTrap and OEndTrap - basic try block without exception + * + * try { + * r0 = 42 + * } + * return r0 + * + * This tests that trap setup/teardown works without throwing. + */ +TEST(trap_no_exception) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = result */ + &c->types[T_VOID], /* r1 = exception (unused here) */ + }; + + /* + * Layout: + * 0: OTrap r1, 3 ; setup trap, if exception goto +3 (catch block) + * 1: OInt r0, $0 ; r0 = 42 (try body) + * 2: OEndTrap ; end try block + * 3: ORet r0 ; return r0 (after try or from catch) + * + * Catch block would be at opcode 4 (1+3), but we don't have one. + */ + hl_opcode ops[] = { + OP2(OTrap, 1, 3), /* trap -> catch at +3 */ + OP2(OInt, 0, 0), /* r0 = 42 */ + OP1(OEndTrap, 1), /* end trap */ + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OThrow - throw and catch exception + * + * try { + * throw 123 + * r0 = 10 ; should not execute + * } catch (e) { + * r0 = 42 ; should execute + * } + * return r0 + */ +TEST(throw_catch_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 42 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* For throwing, we need a dynamic value. + * We'll allocate a simple dynamic int and throw it. */ + hl_type *regs[] = { + &c->types[T_I32], /* r0 = result */ + &c->types[T_VOID], /* r1 = caught exception */ + &c->types[T_VOID], /* r2 = exception to throw */ + }; + + /* + * Layout: + * 0: OTrap r1, 4 ; setup trap, if exception goto +4 (opcode 5) + * 1: ONull r2 ; create null for throw (simplest throwable) + * 2: OThrow r2 ; throw + * 3: OInt r0, $0 ; r0 = 10 (should NOT execute) + * 4: OEndTrap ; end trap (won't reach if thrown) + * 5: OCatch ; catch marker + * 6: OInt r0, $1 ; r0 = 42 (catch body) + * 7: ORet r0 + */ + hl_opcode ops[] = { + OP2(OTrap, 1, 5), /* trap -> catch at op 5 (offset from next = 4) */ + OP1(ONull, 2), /* r2 = null */ + OP1(OThrow, 2), /* throw r2 */ + OP2(OInt, 0, 0), /* r0 = 10 (unreachable) */ + OP1(OEndTrap, 1), /* end trap (unreachable) */ + OP1(OCatch, 0), /* catch marker */ + OP2(OInt, 0, 1), /* r0 = 42 (catch body) */ + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 8, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42 (catch block), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Nested try blocks + * + * try { + * try { + * throw + * } catch { + * r0 = 10 + * } + * r0 = r0 + 32 ; 10 + 32 = 42 + * } catch { + * r0 = 99 ; should not reach + * } + * return r0 + */ +TEST(nested_trap) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 32, 99 }; + test_init_ints(c, 3, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = result */ + &c->types[T_VOID], /* r1 = outer exception */ + &c->types[T_VOID], /* r2 = inner exception */ + &c->types[T_VOID], /* r3 = throw value */ + &c->types[T_I32], /* r4 = temp */ + }; + + /* + * Outer try: 0-11 + * Inner try: 1-6 + * Inner catch: 7-8 + * Continue outer (merge point): 9-11 + * Outer catch: 13-15 + * + * Note: OLabel is required at merge points (op 9, op 16) because: + * - Op 9 is reached via OJAlways from op 6 AND via fallthrough from op 8 + * - Op 16 is reached via OJAlways from op 12 AND via fallthrough from op 15 + * At runtime, spill_regs() before jumps puts values on stack, + * but the generated code must use discard_regs() at labels to ensure + * subsequent ops load from stack rather than assuming register bindings. + */ + hl_opcode ops[] = { + OP2(OTrap, 1, 13), /* 0: outer trap -> catch at 14 (0+1+13) */ + OP2(OTrap, 2, 5), /* 1: inner trap -> catch at 7 (1+1+5) */ + OP1(ONull, 3), /* 2: r3 = null */ + OP1(OThrow, 3), /* 3: throw */ + OP2(OInt, 0, 2), /* 4: unreachable */ + OP1(OEndTrap, 2), /* 5: end inner trap (unreachable) */ + OP2(OJAlways, 2, 0), /* 6: skip catch -> goto 9 (6+1+2) */ + OP1(OCatch, 0), /* 7: inner catch marker */ + OP2(OInt, 0, 0), /* 8: r0 = 10 */ + OP0(OLabel), /* 9: merge point for op 6 jump and fallthrough */ + OP2(OInt, 4, 1), /* 10: r4 = 32 */ + OP3(OAdd, 0, 0, 4), /* 11: r0 = r0 + 32 = 42 */ + OP1(OEndTrap, 1), /* 12: end outer trap */ + OP2(OJAlways, 2, 0), /* 13: skip outer catch -> goto 16 (13+1+2) */ + OP1(OCatch, 0), /* 14: outer catch marker */ + OP2(OInt, 0, 2), /* 15: r0 = 99 */ + OP0(OLabel), /* 16: merge point for op 13 jump and fallthrough */ + OP1(ORet, 0), /* 17: return */ + }; + + test_alloc_function(c, 0, fn_type, 5, regs, 18, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OEndTrap without exception cleans up properly + * + * Multiple sequential try blocks that don't throw. + */ +TEST(multiple_traps_no_throw) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 20, 12 }; + test_init_ints(c, 3, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_VOID], + &c->types[T_I32], + &c->types[T_I32], + }; + + hl_opcode ops[] = { + /* First try block */ + OP2(OTrap, 1, 3), /* trap */ + OP2(OInt, 0, 0), /* r0 = 10 */ + OP1(OEndTrap, 1), /* end trap */ + /* Second try block */ + OP2(OTrap, 1, 3), /* trap */ + OP2(OInt, 2, 1), /* r2 = 20 */ + OP1(OEndTrap, 1), /* end trap */ + /* Third try block */ + OP2(OTrap, 1, 3), /* trap */ + OP2(OInt, 3, 2), /* r3 = 12 */ + OP1(OEndTrap, 1), /* end trap */ + /* Combine */ + OP3(OAdd, 0, 0, 2), /* r0 = r0 + r2 = 30 */ + OP3(OAdd, 0, 0, 3), /* r0 = r0 + r3 = 42 */ + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 4, regs, 12, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(trap_no_exception), + TEST_ENTRY(throw_catch_basic), + TEST_ENTRY(nested_trap), + TEST_ENTRY(multiple_traps_no_throw), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Exception Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_float_ops.c b/other/tests/minimal/test_float_ops.c new file mode 100644 index 000000000..6200f3352 --- /dev/null +++ b/other/tests/minimal/test_float_ops.c @@ -0,0 +1,511 @@ +/* + * Test floating-point operations for HashLink AArch64 JIT + * + * Tests: OFloat, OAdd/OSub/OMul/OSDiv (f64), ONeg, conversions + */ +#include "test_harness.h" +#include + +/* Helper to compare floats with epsilon */ +static int float_eq(double a, double b) { + double eps = 1e-9; + return fabs(a - b) < eps; +} + +/* + * Test: Return constant float 3.14159 + */ +TEST(return_float_constant) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Float pool */ + double floats[] = { 3.14159 }; + test_init_floats(c, 1, floats); + + /* Function type: () -> f64 */ + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + + /* Registers: r0:f64 */ + hl_type *regs[] = { &c->types[T_F64] }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), /* r0 = floats[0] */ + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 2, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + if (!float_eq(ret, 3.14159)) { + fprintf(stderr, " Expected 3.14159, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Add floats: 1.5 + 2.5 = 4.0 + */ +TEST(add_float_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { 1.5, 2.5 }; + test_init_floats(c, 2, floats); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + hl_type *regs[] = { &c->types[T_F64], &c->types[T_F64], &c->types[T_F64] }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), + OP2(OFloat, 1, 1), + OP3(OAdd, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + if (!float_eq(ret, 4.0)) { + fprintf(stderr, " Expected 4.0, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Subtract floats: 10.5 - 6.5 = 4.0 + */ +TEST(sub_float_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { 10.5, 6.5 }; + test_init_floats(c, 2, floats); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + hl_type *regs[] = { &c->types[T_F64], &c->types[T_F64], &c->types[T_F64] }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), + OP2(OFloat, 1, 1), + OP3(OSub, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + if (!float_eq(ret, 4.0)) { + fprintf(stderr, " Expected 4.0, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Multiply floats: 2.0 * 3.5 = 7.0 + */ +TEST(mul_float_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { 2.0, 3.5 }; + test_init_floats(c, 2, floats); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + hl_type *regs[] = { &c->types[T_F64], &c->types[T_F64], &c->types[T_F64] }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), + OP2(OFloat, 1, 1), + OP3(OMul, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + if (!float_eq(ret, 7.0)) { + fprintf(stderr, " Expected 7.0, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Divide floats: 15.0 / 3.0 = 5.0 + */ +TEST(div_float_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { 15.0, 3.0 }; + test_init_floats(c, 2, floats); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + hl_type *regs[] = { &c->types[T_F64], &c->types[T_F64], &c->types[T_F64] }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), + OP2(OFloat, 1, 1), + OP3(OSDiv, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + if (!float_eq(ret, 5.0)) { + fprintf(stderr, " Expected 5.0, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Negate float: -(-3.5) = 3.5 + */ +TEST(neg_float) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { -3.5 }; + test_init_floats(c, 1, floats); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + hl_type *regs[] = { &c->types[T_F64], &c->types[T_F64] }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), + OP2(ONeg, 1, 0), + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + if (!float_eq(ret, 3.5)) { + fprintf(stderr, " Expected 3.5, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Move float register + */ +TEST(mov_float_register) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { 2.718281828 }; + test_init_floats(c, 1, floats); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + hl_type *regs[] = { &c->types[T_F64], &c->types[T_F64] }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), + OP2(OMov, 1, 0), + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + if (!float_eq(ret, 2.718281828)) { + fprintf(stderr, " Expected 2.718281828, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Convert int to float (signed): 42 -> 42.0 + */ +TEST(int_to_float_signed) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_F64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0:i32 = 42 */ + OP2(OToSFloat, 1, 0), /* r1:f64 = (f64)r0 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + if (!float_eq(ret, 42.0)) { + fprintf(stderr, " Expected 42.0, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Convert negative int to float: -42 -> -42.0 + */ +TEST(neg_int_to_float) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { -42 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_F64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OToSFloat, 1, 0), + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + if (!float_eq(ret, -42.0)) { + fprintf(stderr, " Expected -42.0, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Convert float to int: 42.7 -> 42 + */ +TEST(float_to_int) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { 42.7 }; + test_init_floats(c, 1, floats); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_F64], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), /* r0:f64 = 42.7 */ + OP2(OToInt, 1, 0), /* r1:i32 = (i32)r0 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Convert negative float to int: -42.7 -> -42 + */ +TEST(neg_float_to_int) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { -42.7 }; + test_init_floats(c, 1, floats); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_F64], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), + OP2(OToInt, 1, 0), + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != -42) { + fprintf(stderr, " Expected -42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: f32 operations - load and return + */ +TEST(return_f32_constant) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* f32 is stored in floats pool as f64, converted on load */ + double floats[] = { 3.14159f }; + test_init_floats(c, 1, floats); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F32], 0, NULL); + hl_type *regs[] = { &c->types[T_F32] }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 2, ops); + + int result; + float (*fn)(void) = (float(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + float ret = fn(); + if (fabsf(ret - 3.14159f) > 1e-5f) { + fprintf(stderr, " Expected ~3.14159, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: f32 addition + */ +TEST(add_f32_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { 1.5f, 2.5f }; + test_init_floats(c, 2, floats); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F32], 0, NULL); + hl_type *regs[] = { &c->types[T_F32], &c->types[T_F32], &c->types[T_F32] }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), + OP2(OFloat, 1, 1), + OP3(OAdd, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + float (*fn)(void) = (float(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + float ret = fn(); + if (fabsf(ret - 4.0f) > 1e-5f) { + fprintf(stderr, " Expected 4.0, got %f\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(return_float_constant), + TEST_ENTRY(add_float_constants), + TEST_ENTRY(sub_float_constants), + TEST_ENTRY(mul_float_constants), + TEST_ENTRY(div_float_constants), + TEST_ENTRY(neg_float), + TEST_ENTRY(mov_float_register), + TEST_ENTRY(int_to_float_signed), + TEST_ENTRY(neg_int_to_float), + TEST_ENTRY(float_to_int), + TEST_ENTRY(neg_float_to_int), + TEST_ENTRY(return_f32_constant), + TEST_ENTRY(add_f32_constants), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Floating Point Operations Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_globals.c b/other/tests/minimal/test_globals.c new file mode 100644 index 000000000..7704aafc2 --- /dev/null +++ b/other/tests/minimal/test_globals.c @@ -0,0 +1,189 @@ +/* + * Test global variable operations for HashLink AArch64 JIT + * + * Tests: OGetGlobal, OSetGlobal + */ +#include "test_harness.h" + +/* + * Helper to setup globals in the code structure + */ +static void test_init_globals(hl_code *c, int count, hl_type **types) { + c->nglobals = count; + c->globals = (hl_type**)malloc(sizeof(hl_type*) * count); + memcpy(c->globals, types, sizeof(hl_type*) * count); +} + +/* + * Test: Set and get a global integer + * + * op0: int r0, 0 ; r0 = 42 + * op1: setglobal 0, r0 ; global[0] = r0 + * op2: getglobal r1, 0 ; r1 = global[0] + * op3: ret r1 ; return 42 + */ +TEST(global_int_set_get) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Setup one global of type i32 */ + hl_type *global_types[] = { &c->types[T_I32] }; + test_init_globals(c, 1, global_types); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 42 */ + OP2(OSetGlobal, 0, 0), /* op1: global[0] = r0 */ + OP2(OGetGlobal, 1, 0), /* op2: r1 = global[0] */ + OP1(ORet, 1), /* op3: return r1 */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Multiple globals + * + * op0: int r0, 0 ; r0 = 10 + * op1: int r1, 1 ; r1 = 20 + * op2: setglobal 0, r0 ; global[0] = 10 + * op3: setglobal 1, r1 ; global[1] = 20 + * op4: getglobal r2, 0 ; r2 = global[0] = 10 + * op5: getglobal r3, 1 ; r3 = global[1] = 20 + * op6: add r4, r2, r3 ; r4 = 10 + 20 = 30 + * op7: ret r4 ; return 30 + */ +TEST(global_multiple) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 20 }; + test_init_ints(c, 2, ints); + + /* Setup two globals of type i32 */ + hl_type *global_types[] = { &c->types[T_I32], &c->types[T_I32] }; + test_init_globals(c, 2, global_types); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32] + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 10 */ + OP2(OInt, 1, 1), /* op1: r1 = 20 */ + OP2(OSetGlobal, 0, 0), /* op2: global[0] = r0 */ + OP2(OSetGlobal, 1, 1), /* op3: global[1] = r1 */ + OP2(OGetGlobal, 2, 0), /* op4: r2 = global[0] */ + OP2(OGetGlobal, 3, 1), /* op5: r3 = global[1] */ + OP3(OAdd, 4, 2, 3), /* op6: r4 = r2 + r3 */ + OP1(ORet, 4), /* op7: return r4 */ + }; + + test_alloc_function(c, 0, fn_type, 5, regs, 8, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 30) { + fprintf(stderr, " Expected 30, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Global persists across calls + * Call function twice - first sets global, second reads it + */ +TEST(global_persists) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 0, 99 }; + test_init_ints(c, 2, ints); + + /* Setup one global of type i32 */ + hl_type *global_types[] = { &c->types[T_I32] }; + test_init_globals(c, 1, global_types); + + /* Function takes an int arg: if arg==0, set global to 99; else return global */ + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + hl_type *regs[] = { + &c->types[T_I32], /* r0: arg */ + &c->types[T_I32], /* r1: temp */ + }; + + hl_opcode ops[] = { + OP2(OInt, 1, 0), /* op0: r1 = 0 */ + OP3(OJEq, 0, 1, 2), /* op1: if r0 == 0 goto op4 */ + OP2(OGetGlobal, 1, 0), /* op2: r1 = global[0] */ + OP1(ORet, 1), /* op3: return r1 */ + /* setter path */ + OP2(OInt, 1, 1), /* op4: r1 = 99 */ + OP2(OSetGlobal, 0, 1), /* op5: global[0] = 99 */ + OP1(ORet, 1), /* op6: return 99 */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 7, ops); + + int result; + int (*fn)(int) = (int(*)(int))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* First call: set global to 99 */ + int ret1 = fn(0); + if (ret1 != 99) { + fprintf(stderr, " First call: expected 99, got %d\n", ret1); + return TEST_FAIL; + } + + /* Second call: read global */ + int ret2 = fn(1); + if (ret2 != 99) { + fprintf(stderr, " Second call: expected 99, got %d\n", ret2); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(global_int_set_get), + TEST_ENTRY(global_multiple), + TEST_ENTRY(global_persists), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Global Variable Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_harness.h b/other/tests/minimal/test_harness.h new file mode 100644 index 000000000..694c814ad --- /dev/null +++ b/other/tests/minimal/test_harness.h @@ -0,0 +1,389 @@ +/* + * Minimal JIT Test Harness for HashLink AArch64 JIT + * + * This provides helpers to construct hl_code structures directly in memory, + * bypassing the bytecode file format. This allows testing individual opcodes + * without pulling in the entire Haxe stdlib. + */ +#ifndef TEST_HARNESS_H +#define TEST_HARNESS_H + +#include +#include +#include +#include +#include + +/* Test result codes */ +#define TEST_PASS 0 +#define TEST_FAIL 1 +#define TEST_SKIP 2 + +/* Colors for output */ +#define GREEN "\033[32m" +#define RED "\033[31m" +#define YELLOW "\033[33m" +#define RESET "\033[0m" + +/* Helper to create a minimal hl_code structure */ +static hl_code *test_alloc_code(void) { + hl_code *c = (hl_code*)calloc(1, sizeof(hl_code)); + c->version = 5; + hl_alloc_init(&c->alloc); + hl_alloc_init(&c->falloc); + return c; +} + +/* Predefined types - indices into types array */ +#define T_VOID 0 +#define T_I32 1 +#define T_I64 2 +#define T_F32 3 +#define T_F64 4 +#define T_BOOL 5 +#define T_BYTES 6 +#define T_TYPE 7 /* HTYPE - for type pointers, size = pointer size */ + +/* Base types array - common types needed for most tests */ +#define BASE_TYPES_COUNT 8 +#define MAX_TYPES 32 /* Pre-allocate space for additional types */ + +static void test_init_base_types(hl_code *c) { + /* Pre-allocate space for base types + function types */ + c->types = (hl_type*)calloc(MAX_TYPES, sizeof(hl_type)); + c->ntypes = BASE_TYPES_COUNT; + c->types[T_VOID].kind = HVOID; + c->types[T_I32].kind = HI32; + c->types[T_I64].kind = HI64; + c->types[T_F32].kind = HF32; + c->types[T_F64].kind = HF64; + c->types[T_BOOL].kind = HBOOL; + c->types[T_BYTES].kind = HBYTES; + c->types[T_TYPE].kind = HTYPE; +} + +/* Allocate a function type: fun(args...) -> ret */ +static hl_type *test_alloc_fun_type(hl_code *c, hl_type *ret, int nargs, hl_type **args) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types (max %d)\n", MAX_TYPES); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HFUN; + t->fun = (hl_type_fun*)calloc(1, sizeof(hl_type_fun)); + t->fun->ret = ret; + t->fun->nargs = nargs; + if (nargs > 0) { + t->fun->args = (hl_type**)malloc(sizeof(hl_type*) * nargs); + memcpy(t->fun->args, args, sizeof(hl_type*) * nargs); + } + return t; +} + +/* Max functions for pre-allocation */ +#define MAX_FUNCTIONS 16 + +/* Allocate a function */ +static hl_function *test_alloc_function(hl_code *c, int findex, hl_type *type, + int nregs, hl_type **regs, + int nops, hl_opcode *ops) { + if (c->functions == NULL) { + c->functions = (hl_function*)calloc(MAX_FUNCTIONS, sizeof(hl_function)); + c->nfunctions = 0; + } + + if (c->nfunctions >= MAX_FUNCTIONS) { + fprintf(stderr, "Too many functions (max %d)\n", MAX_FUNCTIONS); + return NULL; + } + + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = findex; + f->type = type; + f->nregs = nregs; + f->nops = nops; + + f->regs = (hl_type**)malloc(sizeof(hl_type*) * nregs); + memcpy(f->regs, regs, sizeof(hl_type*) * nregs); + + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * nops); + memcpy(f->ops, ops, sizeof(hl_opcode) * nops); + + /* No debug info for minimal tests */ + f->debug = NULL; + f->obj = NULL; + f->field.ref = NULL; + f->ref = 0; + + return f; +} + +/* Helper macro for creating opcodes */ +#define OP0(opcode) {opcode, 0, 0, 0, NULL} +#define OP1(opcode, a) {opcode, a, 0, 0, NULL} +#define OP2(opcode, a, b) {opcode, a, b, 0, NULL} +#define OP3(opcode, a, b, c) {opcode, a, b, c, NULL} + +/* + * For OCall2, the extra field stores the 4th parameter as an int cast to pointer. + * Usage: OP4_CALL2(OCall2, dst, findex, arg1, arg2) + */ +#define OP4_CALL2(opcode, a, b, c, d) {opcode, a, b, c, (int*)(intptr_t)(d)} + +/* Initialize integers pool */ +static void test_init_ints(hl_code *c, int count, int *values) { + c->nints = count; + c->ints = (int*)malloc(sizeof(int) * count); + memcpy(c->ints, values, sizeof(int) * count); +} + +/* Initialize floats pool */ +static void test_init_floats(hl_code *c, int count, double *values) { + c->nfloats = count; + c->floats = (double*)malloc(sizeof(double) * count); + memcpy(c->floats, values, sizeof(double) * count); +} + +/* Native function pointer registry + * Since hl_native doesn't have a ptr field, we track them separately */ +#define MAX_NATIVE_PTRS 16 +static struct { + int findex; + void *ptr; +} g_native_ptrs[MAX_NATIVE_PTRS]; +static int g_native_ptr_count = 0; + +static void test_register_native_ptr(int findex, void *ptr) { + if (g_native_ptr_count >= MAX_NATIVE_PTRS) { + fprintf(stderr, "Too many native functions (max %d)\n", MAX_NATIVE_PTRS); + return; + } + g_native_ptrs[g_native_ptr_count].findex = findex; + g_native_ptrs[g_native_ptr_count].ptr = ptr; + g_native_ptr_count++; +} + +static void test_clear_native_ptrs(void) { + g_native_ptr_count = 0; +} + +/* Add a native function to the code structure */ +static void test_add_native(hl_code *c, int findex, const char *lib, const char *name, + hl_type *fn_type, void *func_ptr) { + if (c->natives == NULL) { + c->natives = (hl_native*)calloc(MAX_NATIVE_PTRS, sizeof(hl_native)); + c->nnatives = 0; + } + + hl_native *n = &c->natives[c->nnatives++]; + n->findex = findex; + n->lib = lib; + n->name = name; + n->t = fn_type; + + /* Register the function pointer separately */ + test_register_native_ptr(findex, func_ptr); +} + +/* Build and JIT compile the code, returns the function pointer */ +typedef void *(*jit_func_t)(void); + +static void *test_jit_compile(hl_code *c, int *out_result) { + /* Set entrypoint if not set */ + if (c->nfunctions > 0 && c->entrypoint == 0) { + c->entrypoint = c->functions[0].findex; + } + + /* Ensure we have globals array (can be empty) */ + if (c->globals == NULL) { + c->nglobals = 0; + c->globals = NULL; + } + + /* Natives are optional - keep if set */ + if (c->natives == NULL) { + c->nnatives = 0; + } + + /* No constants */ + c->nconstants = 0; + c->constants = NULL; + + /* No strings/bytes for now */ + if (c->strings == NULL) { + c->nstrings = 0; + c->strings = NULL; + c->strings_lens = NULL; + c->ustrings = NULL; + } + c->nbytes = 0; + c->bytes = NULL; + c->bytes_pos = NULL; + + /* No debug */ + c->hasdebug = false; + c->ndebugfiles = 0; + c->debugfiles = NULL; + c->debugfiles_lens = NULL; + + /* Allocate module */ + hl_module *m = hl_module_alloc(c); + if (m == NULL) { + fprintf(stderr, "Failed to allocate module\n"); + *out_result = TEST_FAIL; + return NULL; + } + + /* Setup module context for object types (needed for hl_get_obj_rt allocator) */ + for (int i = 0; i < c->ntypes; i++) { + if (c->types[i].kind == HOBJ && c->types[i].obj != NULL) { + c->types[i].obj->m = &m->ctx; + } + } + + /* Setup function indexes */ + for (int i = 0; i < c->nfunctions; i++) { + hl_function *f = c->functions + i; + m->functions_indexes[f->findex] = i; + m->ctx.functions_types[f->findex] = f->type; + } + + /* Setup native function indexes and pointers */ + for (int i = 0; i < c->nnatives; i++) { + hl_native *n = &c->natives[i]; + m->functions_indexes[n->findex] = i + c->nfunctions; /* natives come after functions */ + m->ctx.functions_types[n->findex] = n->t; + } + for (int i = 0; i < g_native_ptr_count; i++) { + m->functions_ptrs[g_native_ptrs[i].findex] = g_native_ptrs[i].ptr; + } + test_clear_native_ptrs(); /* Reset for next test */ + + /* JIT compile */ + jit_ctx *ctx = hl_jit_alloc(); + if (ctx == NULL) { + fprintf(stderr, "Failed to allocate JIT context\n"); + hl_module_free(m); + *out_result = TEST_FAIL; + return NULL; + } + + hl_jit_init(ctx, m); + + for (int i = 0; i < c->nfunctions; i++) { + hl_function *f = c->functions + i; + int fpos = hl_jit_function(ctx, m, f); + if (fpos < 0) { + fprintf(stderr, "Failed to JIT function %d\n", f->findex); + hl_jit_free(ctx, false); + hl_module_free(m); + *out_result = TEST_FAIL; + return NULL; + } + m->functions_ptrs[f->findex] = (void*)(intptr_t)fpos; + } + + int codesize; + hl_debug_infos *debug_info = NULL; + void *jit_code = hl_jit_code(ctx, m, &codesize, &debug_info, NULL); + + if (jit_code == NULL) { + fprintf(stderr, "Failed to finalize JIT code\n"); + hl_jit_free(ctx, false); + hl_module_free(m); + *out_result = TEST_FAIL; + return NULL; + } + + /* Fix up function pointers */ + for (int i = 0; i < c->nfunctions; i++) { + hl_function *f = c->functions + i; + m->functions_ptrs[f->findex] = (unsigned char*)jit_code + (intptr_t)m->functions_ptrs[f->findex]; + } + + m->jit_code = jit_code; + m->codesize = codesize; + + hl_jit_free(ctx, false); + + *out_result = TEST_PASS; + + /* Return pointer to entry function */ + return m->functions_ptrs[c->entrypoint]; +} + +/* Test runner infrastructure */ +typedef int (*test_func_t)(void); + +typedef struct { + const char *name; + test_func_t func; +} test_entry_t; + +static int run_tests(test_entry_t *tests, int count) { + int passed = 0, failed = 0, skipped = 0; + + printf("\n=== Running %d tests ===\n\n", count); + + for (int i = 0; i < count; i++) { + printf(" [%d/%d] %s ... ", i + 1, count, tests[i].name); + fflush(stdout); + + int result = tests[i].func(); + + switch (result) { + case TEST_PASS: + printf(GREEN "PASS" RESET "\n"); + passed++; + break; + case TEST_FAIL: + printf(RED "FAIL" RESET "\n"); + failed++; + break; + case TEST_SKIP: + printf(YELLOW "SKIP" RESET "\n"); + skipped++; + break; + } + } + + printf("\n=== Results: %d passed, %d failed, %d skipped ===\n\n", + passed, failed, skipped); + + return failed > 0 ? 1 : 0; +} + +/* Convenience macro to define a test */ +#define TEST(name) static int test_##name(void) +#define TEST_ENTRY(name) { #name, test_##name } + +/* Stub functions for exception handling */ +static uchar *test_resolve_symbol(void *addr, uchar *out, int *outSize) { + (void)addr; (void)out; (void)outSize; + return NULL; /* No symbol resolution in minimal tests */ +} + +static int test_capture_stack(void **stack, int size) { + (void)stack; (void)size; + return 0; /* No stack capture in minimal tests */ +} + +/* Initialize HL runtime - call once at start */ +static void test_init_runtime(void) { + static int initialized = 0; + if (!initialized) { + hl_global_init(); + static int ctx; + hl_register_thread(&ctx); + /* Set up exception handling - REQUIRED for hl_throw to work! */ + hl_setup.resolve_symbol = test_resolve_symbol; + hl_setup.capture_stack = test_capture_stack; + initialized = 1; + } +} + +#endif /* TEST_HARNESS_H */ diff --git a/other/tests/minimal/test_i64_ops.c b/other/tests/minimal/test_i64_ops.c new file mode 100644 index 000000000..31c6695bb --- /dev/null +++ b/other/tests/minimal/test_i64_ops.c @@ -0,0 +1,545 @@ +/* + * Test 64-bit integer operations for HashLink AArch64 JIT + * + * Tests: i64 arithmetic with OAdd, OSub, OMul, OSDiv + * + * Note: OInt loads 32-bit values. For i64 registers, the value is sign-extended. + */ +#include "test_harness.h" + +/* + * Test: Return 64-bit constant (sign-extended from i32) + */ +TEST(return_i64_constant) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0:i64 = 42 (sign-extended) */ + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 2, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Add 64-bit integers + */ +TEST(add_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 32 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64], &c->types[T_I64], &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OAdd, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Subtract 64-bit integers + */ +TEST(sub_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 100, 58 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64], &c->types[T_I64], &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OSub, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Multiply 64-bit integers + */ +TEST(mul_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 6, 7 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64], &c->types[T_I64], &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OMul, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Divide 64-bit integers + */ +TEST(sdiv_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 84, 2 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64], &c->types[T_I64], &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OSDiv, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Modulo 64-bit integers + */ +TEST(smod_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 142, 100 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64], &c->types[T_I64], &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OSMod, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Negate 64-bit integer + */ +TEST(neg_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { -42 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64], &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(ONeg, 1, 0), + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Bitwise AND 64-bit + */ +TEST(and_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 0xFF, 0x2A }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64], &c->types[T_I64], &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OAnd, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Bitwise OR 64-bit + */ +TEST(or_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 0x20, 0x0A }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64], &c->types[T_I64], &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OOr, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Left shift 64-bit: 21 << 1 = 42 + */ +TEST(shl_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 21, 1 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64], &c->types[T_I64], &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OShl, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Large shift - beyond 32 bits + * 1 << 40 = 0x10000000000 (1099511627776) + */ +TEST(shl_i64_large) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 1, 40 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64], &c->types[T_I64], &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OShl, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + long long expected = 1LL << 40; + if (ret != expected) { + fprintf(stderr, " Expected %lld, got %lld\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Move i64 register + */ +TEST(mov_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64], &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OMov, 1, 0), + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Increment i64 + */ +TEST(incr_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 41 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP1(OIncr, 0), + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 3, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Decrement i64 + */ +TEST(decr_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 43 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + hl_type *regs[] = { &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP1(ODecr, 0), + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 3, ops); + + int result; + long long (*fn)(void) = (long long(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + long long ret = fn(); + if (ret != 42LL) { + fprintf(stderr, " Expected 42, got %lld\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(return_i64_constant), + TEST_ENTRY(add_i64), + TEST_ENTRY(sub_i64), + TEST_ENTRY(mul_i64), + TEST_ENTRY(sdiv_i64), + TEST_ENTRY(smod_i64), + TEST_ENTRY(neg_i64), + TEST_ENTRY(and_i64), + TEST_ENTRY(or_i64), + TEST_ENTRY(shl_i64), + TEST_ENTRY(shl_i64_large), + TEST_ENTRY(mov_i64), + TEST_ENTRY(incr_i64), + TEST_ENTRY(decr_i64), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - 64-bit Integer Operations Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_instance_closure.c b/other/tests/minimal/test_instance_closure.c new file mode 100644 index 000000000..d9dd59bca --- /dev/null +++ b/other/tests/minimal/test_instance_closure.c @@ -0,0 +1,390 @@ +/* + * Test instance and virtual closure operations for HashLink AArch64 JIT + * + * Tests: OInstanceClosure, OVirtualClosure, OCallClosure with captured values + * + * OInstanceClosure creates a closure that captures a value (typically 'this'). + * OVirtualClosure creates a closure from a virtual method lookup. + */ +#include "test_harness.h" + +/* + * Test: OInstanceClosure with captured i32 value + * + * fn0: (i32) -> i32 { return arg; } // The captured value becomes the arg + * fn1: () -> i32 { + * r0 = 42 + * r1 = instance_closure(fn0, r0) ; OInstanceClosure with captured value + * r2 = call_closure(r1) ; OCallClosure with 0 explicit args + * return r2 + * } + * + * When called, the closure passes the captured value (42) as the first argument. + */ +TEST(instance_closure_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Function types */ + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + hl_type *fn_type_void_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + c->functions = (hl_function*)calloc(MAX_FUNCTIONS, sizeof(hl_function)); + c->nfunctions = 0; + + /* fn0: findex=0, returns its argument */ + { + hl_type *regs[] = { &c->types[T_I32] }; + hl_opcode ops[] = { + OP1(ORet, 0), /* return r0 (the captured value passed as arg) */ + }; + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 0; + f->type = fn_type_i32_i32; + f->nregs = 1; + f->nops = 1; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 1); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 1); + memcpy(f->ops, ops, sizeof(ops)); + } + + /* fn1: findex=1, creates instance closure and calls it */ + { + /* r0 = captured value, r1 = closure, r2 = result */ + hl_type *regs[] = { &c->types[T_I32], fn_type_i32_i32, &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 (captured value) */ + OP3(OInstanceClosure, 1, 0, 0), /* r1 = closure(fn0, r0) */ + {OCallClosure, 2, 1, 0, NULL}, /* r2 = call_closure(r1) with 0 explicit args */ + OP1(ORet, 2), + }; + + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 1; + f->type = fn_type_void_i32; + f->nregs = 3; + f->nops = 4; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 3); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 4); + memcpy(f->ops, ops, sizeof(ops)); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OInstanceClosure with captured value and additional arguments + * + * fn0: (i32, i32) -> i32 { return arg0 + arg1; } + * fn1: () -> i32 { + * r0 = 10 ; value to capture + * r1 = instance_closure(fn0, r0) ; closure captures 10 + * r2 = 32 + * r3 = call_closure(r1, r2) ; calls fn0(10, 32) = 42 + * return r3 + * } + */ +TEST(instance_closure_with_arg) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 32 }; + test_init_ints(c, 2, ints); + + /* Function types */ + hl_type *two_args[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_type *fn_type_i32_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 2, two_args); + hl_type *fn_type_void_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* For the closure type: when called with 1 arg, passes captured + arg */ + hl_type *one_arg[] = { &c->types[T_I32] }; + hl_type *fn_type_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 1, one_arg); + + c->functions = (hl_function*)calloc(MAX_FUNCTIONS, sizeof(hl_function)); + c->nfunctions = 0; + + /* fn0: findex=0, returns arg0 + arg1 */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP3(OAdd, 2, 0, 1), /* r2 = r0 + r1 */ + OP1(ORet, 2), + }; + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 0; + f->type = fn_type_i32_i32_i32; + f->nregs = 3; + f->nops = 2; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 3); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 2); + memcpy(f->ops, ops, sizeof(ops)); + } + + /* fn1: findex=1, creates instance closure and calls with additional arg */ + { + /* r0 = captured value, r1 = closure, r2 = additional arg, r3 = result */ + hl_type *regs[] = { &c->types[T_I32], fn_type_i32_i32, &c->types[T_I32], &c->types[T_I32] }; + + static int extra[] = { 2 }; /* r2 is the additional argument */ + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 10 (captured value) */ + OP3(OInstanceClosure, 1, 0, 0), /* r1 = closure(fn0, r0) */ + OP2(OInt, 2, 1), /* r2 = 32 */ + {OCallClosure, 3, 1, 1, extra}, /* r3 = call_closure(r1, r2) -> fn0(10, 32) */ + OP1(ORet, 3), + }; + + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 1; + f->type = fn_type_void_i32; + f->nregs = 4; + f->nops = 5; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 4); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 5); + memcpy(f->ops, ops, sizeof(ops)); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OInstanceClosure used in a loop pattern + * + * This tests that closures work correctly when called multiple times, + * similar to how they're used in event handlers. + * + * fn0: (i32, i32) -> i32 { return arg0 + arg1; } + * fn1: () -> i32 { + * r0 = 0 ; accumulator + * r1 = instance_closure(fn0, r0) ; closure captures accumulator reference + * // Call closure 3 times with different values + * r2 = 10 + * r3 = call_closure(r1, r2) ; 0 + 10 = 10 + * r4 = 20 + * r5 = call_closure(r1, r4) ; 0 + 20 = 20 + * r6 = r3 + r5 ; 10 + 20 = 30 + * return r6 + * } + */ +TEST(instance_closure_multiple_calls) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 0, 10, 20 }; + test_init_ints(c, 3, ints); + + /* Function types */ + hl_type *two_args[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_type *fn_type_i32_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 2, two_args); + hl_type *fn_type_void_i32 = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *one_arg[] = { &c->types[T_I32] }; + hl_type *fn_type_i32_i32 = test_alloc_fun_type(c, &c->types[T_I32], 1, one_arg); + + c->functions = (hl_function*)calloc(MAX_FUNCTIONS, sizeof(hl_function)); + c->nfunctions = 0; + + /* fn0: findex=0, returns arg0 + arg1 */ + { + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_opcode ops[] = { + OP3(OAdd, 2, 0, 1), + OP1(ORet, 2), + }; + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 0; + f->type = fn_type_i32_i32_i32; + f->nregs = 3; + f->nops = 2; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 3); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 2); + memcpy(f->ops, ops, sizeof(ops)); + } + + /* fn1: findex=1, creates closure and calls it multiple times */ + { + /* + * r0 = captured base value (0) + * r1 = closure + * r2 = first arg (10) + * r3 = first result + * r4 = second arg (20) + * r5 = second result + * r6 = final sum + */ + hl_type *regs[] = { + &c->types[T_I32], /* r0 */ + fn_type_i32_i32, /* r1 */ + &c->types[T_I32], /* r2 */ + &c->types[T_I32], /* r3 */ + &c->types[T_I32], /* r4 */ + &c->types[T_I32], /* r5 */ + &c->types[T_I32], /* r6 */ + }; + + static int extra1[] = { 2 }; + static int extra2[] = { 4 }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 0 */ + OP3(OInstanceClosure, 1, 0, 0), /* r1 = closure(fn0, r0) */ + OP2(OInt, 2, 1), /* r2 = 10 */ + {OCallClosure, 3, 1, 1, extra1},/* r3 = closure(10) = 0 + 10 = 10 */ + OP2(OInt, 4, 2), /* r4 = 20 */ + {OCallClosure, 5, 1, 1, extra2},/* r5 = closure(20) = 0 + 20 = 20 */ + OP3(OAdd, 6, 3, 5), /* r6 = r3 + r5 = 30 */ + OP1(ORet, 6), + }; + + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 1; + f->type = fn_type_void_i32; + f->nregs = 7; + f->nops = 8; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 7); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 8); + memcpy(f->ops, ops, sizeof(ops)); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 30) { + fprintf(stderr, " Expected 30, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OInstanceClosure with i64 captured value + * + * This tests that pointer-sized captured values work correctly. + */ +TEST(instance_closure_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Function types with i64 */ + hl_type *arg_types[] = { &c->types[T_I64] }; + hl_type *fn_type_i64_i64 = test_alloc_fun_type(c, &c->types[T_I64], 1, arg_types); + hl_type *fn_type_void_i64 = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + + c->functions = (hl_function*)calloc(MAX_FUNCTIONS, sizeof(hl_function)); + c->nfunctions = 0; + + /* fn0: findex=0, returns its argument */ + { + hl_type *regs[] = { &c->types[T_I64] }; + hl_opcode ops[] = { + OP1(ORet, 0), + }; + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 0; + f->type = fn_type_i64_i64; + f->nregs = 1; + f->nops = 1; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 1); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 1); + memcpy(f->ops, ops, sizeof(ops)); + } + + /* fn1: findex=1, creates instance closure with i64 */ + { + hl_type *regs[] = { &c->types[T_I64], fn_type_i64_i64, &c->types[T_I64] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 (will be i64) */ + OP3(OInstanceClosure, 1, 0, 0), /* r1 = closure(fn0, r0) */ + {OCallClosure, 2, 1, 0, NULL}, /* r2 = call_closure(r1) */ + OP1(ORet, 2), + }; + + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 1; + f->type = fn_type_void_i64; + f->nregs = 3; + f->nops = 4; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 3); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 4); + memcpy(f->ops, ops, sizeof(ops)); + } + + c->entrypoint = 1; + + int result; + int64_t (*fn)(void) = (int64_t(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int64_t ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %ld\n", (long)ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(instance_closure_basic), + TEST_ENTRY(instance_closure_with_arg), + TEST_ENTRY(instance_closure_multiple_calls), + TEST_ENTRY(instance_closure_i64), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Instance Closure Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_int_ops.c b/other/tests/minimal/test_int_ops.c new file mode 100644 index 000000000..51afbb186 --- /dev/null +++ b/other/tests/minimal/test_int_ops.c @@ -0,0 +1,622 @@ +/* + * Test integer operations for HashLink AArch64 JIT + * + * Tests: OInt, OMov, OAdd, OSub, OMul, ORet + */ +#include "test_harness.h" + +/* + * Test: Return constant integer 42 + * + * function test() -> i32: + * r0 = 42 + * ret r0 + */ +TEST(return_int_constant) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Integer pool: [42] */ + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Function type: () -> i32 */ + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* Registers: r0:i32 */ + hl_type *regs[] = { &c->types[T_I32] }; + + /* Opcodes: + * OInt r0, $0 ; r0 = ints[0] = 42 + * ORet r0 ; return r0 + */ + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = ints[0] */ + OP1(ORet, 0), /* return r0 */ + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 2, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Add two constants: 10 + 32 = 42 + * + * function test() -> i32: + * r0 = 10 + * r1 = 32 + * r2 = r0 + r1 + * ret r2 + */ +TEST(add_int_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Integer pool: [10, 32] */ + int ints[] = { 10, 32 }; + test_init_ints(c, 2, ints); + + /* Function type: () -> i32 */ + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* Registers: r0:i32, r1:i32, r2:i32 */ + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + /* Opcodes: + * OInt r0, $0 ; r0 = 10 + * OInt r1, $1 ; r1 = 32 + * OAdd r2, r0, r1 ; r2 = r0 + r1 + * ORet r2 ; return r2 + */ + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = ints[0] = 10 */ + OP2(OInt, 1, 1), /* r1 = ints[1] = 32 */ + OP3(OAdd, 2, 0, 1), /* r2 = r0 + r1 */ + OP1(ORet, 2), /* return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Subtract: 100 - 58 = 42 + */ +TEST(sub_int_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 100, 58 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 100 */ + OP2(OInt, 1, 1), /* r1 = 58 */ + OP3(OSub, 2, 0, 1), /* r2 = r0 - r1 */ + OP1(ORet, 2), /* return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Multiply: 6 * 7 = 42 + */ +TEST(mul_int_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 6, 7 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 6 */ + OP2(OInt, 1, 1), /* r1 = 7 */ + OP3(OMul, 2, 0, 1), /* r2 = r0 * r1 */ + OP1(ORet, 2), /* return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Move register: r1 = r0 + */ +TEST(mov_register) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 */ + OP2(OMov, 1, 0), /* r1 = r0 */ + OP1(ORet, 1), /* return r1 */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Signed division: 84 / 2 = 42 + */ +TEST(sdiv_int_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 84, 2 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 84 */ + OP2(OInt, 1, 1), /* r1 = 2 */ + OP3(OSDiv, 2, 0, 1), /* r2 = r0 / r1 */ + OP1(ORet, 2), /* return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Signed modulo: 142 % 100 = 42 + */ +TEST(smod_int_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 142, 100 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 142 */ + OP2(OInt, 1, 1), /* r1 = 100 */ + OP3(OSMod, 2, 0, 1), /* r2 = r0 % r1 */ + OP1(ORet, 2), /* return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Bitwise AND: 0xFF & 0x2A = 42 + */ +TEST(and_int_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 0xFF, 0x2A }; /* 255 & 42 = 42 */ + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 0xFF */ + OP2(OInt, 1, 1), /* r1 = 0x2A */ + OP3(OAnd, 2, 0, 1), /* r2 = r0 & r1 */ + OP1(ORet, 2), /* return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Bitwise OR: 0x20 | 0x0A = 42 + */ +TEST(or_int_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 0x20, 0x0A }; /* 32 | 10 = 42 */ + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OOr, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Bitwise XOR: 0x55 ^ 0x7F = 42 + */ +TEST(xor_int_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 0x55, 0x7F }; /* 85 ^ 127 = 42 */ + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OXor, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Left shift: 21 << 1 = 42 + */ +TEST(shl_int_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 21, 1 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OShl, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Signed right shift: 168 >> 2 = 42 + */ +TEST(sshr_int_constants) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 168, 2 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OSShr, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Negate: -(-42) = 42 + */ +TEST(neg_int) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { -42 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = -42 */ + OP2(ONeg, 1, 0), /* r1 = -r0 = 42 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Increment: 41 + 1 = 42 + */ +TEST(incr_int) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 41 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 41 */ + OP1(OIncr, 0), /* r0++ */ + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Decrement: 43 - 1 = 42 + */ +TEST(decr_int) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 43 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 43 */ + OP1(ODecr, 0), /* r0-- */ + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(return_int_constant), + TEST_ENTRY(add_int_constants), + TEST_ENTRY(sub_int_constants), + TEST_ENTRY(mul_int_constants), + TEST_ENTRY(mov_register), + TEST_ENTRY(sdiv_int_constants), + TEST_ENTRY(smod_int_constants), + TEST_ENTRY(and_int_constants), + TEST_ENTRY(or_int_constants), + TEST_ENTRY(xor_int_constants), + TEST_ENTRY(shl_int_constants), + TEST_ENTRY(sshr_int_constants), + TEST_ENTRY(neg_int), + TEST_ENTRY(incr_int), + TEST_ENTRY(decr_int), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Integer Operations Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_jumps_unsigned.c b/other/tests/minimal/test_jumps_unsigned.c new file mode 100644 index 000000000..53abc1525 --- /dev/null +++ b/other/tests/minimal/test_jumps_unsigned.c @@ -0,0 +1,422 @@ +/* + * Test unsigned jump operations for HashLink AArch64 JIT + * + * Tests: OJULt, OJUGte, OJNotLt, OJNotGte, OJSGt + * + * These opcodes perform unsigned comparisons and conditional jumps. + * OJNotLt and OJNotGte are for NaN-aware float comparisons. + */ +#include "test_harness.h" + +/* + * Test: OJULt - unsigned less than + * + * Tests that -1 (0xFFFFFFFF) is NOT less than 1 when compared as unsigned. + * With signed comparison, -1 < 1 would be true. + */ +TEST(jult_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { -1, 1, 10, 20 }; /* -1 as unsigned is 0xFFFFFFFF */ + test_init_ints(c, 4, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = large unsigned (0xFFFFFFFF) */ + &c->types[T_I32], /* r1 = small value (1) */ + &c->types[T_I32], /* r2 = result */ + }; + + /* + * if (0xFFFFFFFF types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 1, opcode 0 */ + OP2(OInt, 1, 1), /* r1 = 100, opcode 1 */ + OP3(OJULt, 0, 1, 3), /* if r0 =u 1 should be true (unsigned) + */ +TEST(jugte_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { -1, 1, 10, 20 }; + test_init_ints(c, 4, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = -1 (0xFFFFFFFF), opcode 0 */ + OP2(OInt, 1, 1), /* r1 = 1, opcode 1 */ + OP3(OJUGte, 0, 1, 3), /* if r0 >=u r1 goto opcode 6, opcode 2 */ + OP2(OInt, 2, 3), /* r2 = 20 (false branch), opcode 3 */ + OP2(OJAlways, 2, 0), /* goto opcode 7, opcode 4 */ + OP0(OLabel), /* true branch target, opcode 5 */ + OP2(OInt, 2, 2), /* r2 = 10 (true branch), opcode 6 */ + OP0(OLabel), /* end (merge point), opcode 7 */ + OP1(ORet, 2), /* opcode 8 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 9, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 10) { + fprintf(stderr, " Expected 10 (true branch), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OJSGt - signed greater than + * + * Tests signed comparison: 1 > -1 should be true + */ +TEST(jsgt_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 1, -1, 10, 20 }; + test_init_ints(c, 4, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 1, opcode 0 */ + OP2(OInt, 1, 1), /* r1 = -1, opcode 1 */ + OP3(OJSGt, 0, 1, 3), /* if r0 > r1 (signed) goto opcode 6, opcode 2 */ + OP2(OInt, 2, 3), /* r2 = 20 (false branch), opcode 3 */ + OP2(OJAlways, 2, 0), /* goto opcode 7, opcode 4 */ + OP0(OLabel), /* true branch target, opcode 5 */ + OP2(OInt, 2, 2), /* r2 = 10 (true branch), opcode 6 */ + OP0(OLabel), /* end (merge point), opcode 7 */ + OP1(ORet, 2), /* opcode 8 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 9, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 10) { + fprintf(stderr, " Expected 10 (true branch), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OJNotLt - "not less than" for NaN-aware float comparison + * + * For floats, NaN comparisons need special handling. + * OJNotLt: jumps if !(a < b), which includes NaN cases. + */ +TEST(jnotlt_float) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { 2.0, 1.0 }; /* 2.0 is not less than 1.0 */ + test_init_floats(c, 2, floats); + + int ints[] = { 10, 20 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_F64], + &c->types[T_F64], + &c->types[T_I32], + }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), /* r0 = 2.0, opcode 0 */ + OP2(OFloat, 1, 1), /* r1 = 1.0, opcode 1 */ + OP3(OJNotLt, 0, 1, 3), /* if !(r0 < r1) goto opcode 6, opcode 2 */ + OP2(OInt, 2, 1), /* r2 = 20 (false: r0 < r1), opcode 3 */ + OP2(OJAlways, 2, 0), /* goto opcode 7, opcode 4 */ + OP0(OLabel), /* true branch target, opcode 5 */ + OP2(OInt, 2, 0), /* r2 = 10 (true: r0 >= r1 or NaN), opcode 6 */ + OP0(OLabel), /* end (merge point), opcode 7 */ + OP1(ORet, 2), /* opcode 8 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 9, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + /* 2.0 is NOT less than 1.0, so we should take the true branch */ + if (ret != 10) { + fprintf(stderr, " Expected 10 (not-less-than branch), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OJNotGte - "not greater than or equal" for NaN-aware comparison + */ +TEST(jnotgte_float) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { 1.0, 2.0 }; /* 1.0 is not >= 2.0 */ + test_init_floats(c, 2, floats); + + int ints[] = { 10, 20 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_F64], + &c->types[T_F64], + &c->types[T_I32], + }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), /* r0 = 1.0, opcode 0 */ + OP2(OFloat, 1, 1), /* r1 = 2.0, opcode 1 */ + OP3(OJNotGte, 0, 1, 3), /* if !(r0 >= r1) goto opcode 6, opcode 2 */ + OP2(OInt, 2, 1), /* r2 = 20 (false: r0 >= r1), opcode 3 */ + OP2(OJAlways, 2, 0), /* goto opcode 7, opcode 4 */ + OP0(OLabel), /* true branch target, opcode 5 */ + OP2(OInt, 2, 0), /* r2 = 10 (true: r0 < r1 or NaN), opcode 6 */ + OP0(OLabel), /* end (merge point), opcode 7 */ + OP1(ORet, 2), /* opcode 8 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 9, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + /* 1.0 is NOT >= 2.0, so we should take the true branch */ + if (ret != 10) { + fprintf(stderr, " Expected 10 (not-gte branch), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Compare signed vs unsigned jump behavior + * + * -1 vs 1: + * Signed: -1 < 1 (true) + * Unsigned: 0xFFFFFFFF > 1 (true) + */ +TEST(signed_vs_unsigned) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { -1, 1, 0 }; + test_init_ints(c, 3, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = -1 */ + &c->types[T_I32], /* r1 = 1 */ + &c->types[T_I32], /* r2 = signed result */ + &c->types[T_I32], /* r3 = unsigned result */ + &c->types[T_I32], /* r4 = combined */ + }; + + /* + * Test signed: -1 < 1 (true) -> r2 = 1 + * Test unsigned: -1 r3 = 0 + * Return r2 * 10 + r3 = 10 + * + * Structure for each test: + * if (condition) goto set_value + * goto after_test + * OLabel (set_value target) + * set value = 1 + * OLabel (after_test / merge point) + */ + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = -1, opcode 0 */ + OP2(OInt, 1, 1), /* r1 = 1, opcode 1 */ + OP2(OInt, 2, 2), /* r2 = 0 (default), opcode 2 */ + OP2(OInt, 3, 2), /* r3 = 0 (default), opcode 3 */ + /* Signed test: if -1 < 1 (true), set r2 = 1 */ + OP3(OJSLt, 0, 1, 2), /* if r0 < r1 goto opcode 7 (set_r2), opcode 4 */ + OP2(OJAlways, 2, 0), /* goto opcode 8 (after_signed), opcode 5 */ + OP0(OLabel), /* set_r2 target, opcode 6 */ + OP2(OInt, 2, 1), /* r2 = 1 (signed true), opcode 7 */ + OP0(OLabel), /* after_signed, opcode 8 */ + /* Unsigned test: if -1 1), so r3 = 0 + * Result: 1 * 10 + 0 = 10 + */ + if (ret != 10) { + fprintf(stderr, " Expected 10, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(jult_basic), + TEST_ENTRY(jult_small_values), + TEST_ENTRY(jugte_basic), + TEST_ENTRY(jsgt_basic), + TEST_ENTRY(jnotlt_float), + TEST_ENTRY(jnotgte_float), + TEST_ENTRY(signed_vs_unsigned), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Unsigned Jump Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_memory_ops.c b/other/tests/minimal/test_memory_ops.c new file mode 100644 index 000000000..3975513f8 --- /dev/null +++ b/other/tests/minimal/test_memory_ops.c @@ -0,0 +1,448 @@ +/* + * Test memory operations for HashLink AArch64 JIT + * + * Tests: OGetI8, OGetI16, OGetMem, OSetI8, OSetI16, OSetMem + * + * These opcodes access memory at (base + offset) where offset is a register value. + * OGetI8/OGetI16/OGetMem: dst = *(type*)(base + offset) + * OSetI8/OSetI16/OSetMem: *(type*)(base + offset) = value + */ +#include "test_harness.h" + +/* Native function to allocate test buffer */ +static void *alloc_test_buffer(int size) { + void *buf = malloc(size); + memset(buf, 0, size); + return buf; +} + +/* Native function to free test buffer */ +static void free_test_buffer(void *buf) { + free(buf); +} + +/* + * Test: OSetI8 and OGetI8 - write and read byte values + * + * alloc buffer + * set_i8(buffer, 0, 0x42) + * set_i8(buffer, 1, 0x37) + * r0 = get_i8(buffer, 0) ; should be 0x42 = 66 + * r1 = get_i8(buffer, 1) ; should be 0x37 = 55 + * r2 = r0 + r1 ; 66 + 55 = 121 + * return r2 + */ +TEST(mem_i8_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 64, 0, 1, 0x42, 0x37 }; /* size, offset0, offset1, val0, val1 */ + test_init_ints(c, 5, ints); + + /* Native: alloc_test_buffer(size) -> bytes */ + hl_type *alloc_args[] = { &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 1, alloc_args); + test_add_native(c, 1, "test", "alloc_buffer", alloc_fn_type, (void*)alloc_test_buffer); + + /* Function type: () -> i32 */ + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* + * Registers: + * r0: size (64) + * r1: buffer (bytes) + * r2: offset0 (0) + * r3: offset1 (1) + * r4: val0 (0x42) + * r5: val1 (0x37) + * r6: read val0 + * r7: read val1 + * r8: result + */ + hl_type *regs[] = { + &c->types[T_I32], /* r0 = size */ + &c->types[T_BYTES], /* r1 = buffer */ + &c->types[T_I32], /* r2 = offset0 */ + &c->types[T_I32], /* r3 = offset1 */ + &c->types[T_I32], /* r4 = val0 */ + &c->types[T_I32], /* r5 = val1 */ + &c->types[T_I32], /* r6 = read val0 */ + &c->types[T_I32], /* r7 = read val1 */ + &c->types[T_I32], /* r8 = result */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 64 (size) */ + OP3(OCall1, 1, 1, 0), /* r1 = alloc_buffer(r0) */ + OP2(OInt, 2, 1), /* r2 = 0 (offset) */ + OP2(OInt, 3, 2), /* r3 = 1 (offset) */ + OP2(OInt, 4, 3), /* r4 = 0x42 */ + OP2(OInt, 5, 4), /* r5 = 0x37 */ + OP3(OSetI8, 1, 2, 4), /* *(i8*)(r1 + r2) = r4 */ + OP3(OSetI8, 1, 3, 5), /* *(i8*)(r1 + r3) = r5 */ + OP3(OGetI8, 6, 1, 2), /* r6 = *(i8*)(r1 + r2) */ + OP3(OGetI8, 7, 1, 3), /* r7 = *(i8*)(r1 + r3) */ + OP3(OAdd, 8, 6, 7), /* r8 = r6 + r7 */ + OP1(ORet, 8), + }; + + test_alloc_function(c, 0, fn_type, 9, regs, 12, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + int expected = 0x42 + 0x37; /* 66 + 55 = 121 */ + if (ret != expected) { + fprintf(stderr, " Expected %d, got %d\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OSetI16 and OGetI16 - write and read 16-bit values + */ +TEST(mem_i16_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 64, 0, 2, 0x1234, 0x5678 }; + test_init_ints(c, 5, ints); + + hl_type *alloc_args[] = { &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 1, alloc_args); + test_add_native(c, 1, "test", "alloc_buffer", alloc_fn_type, (void*)alloc_test_buffer); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = size */ + &c->types[T_BYTES], /* r1 = buffer */ + &c->types[T_I32], /* r2 = offset0 */ + &c->types[T_I32], /* r3 = offset1 */ + &c->types[T_I32], /* r4 = val0 */ + &c->types[T_I32], /* r5 = val1 */ + &c->types[T_I32], /* r6 = read val0 */ + &c->types[T_I32], /* r7 = read val1 */ + &c->types[T_I32], /* r8 = result */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 64 (size) */ + OP3(OCall1, 1, 1, 0), /* r1 = alloc_buffer(r0) */ + OP2(OInt, 2, 1), /* r2 = 0 (offset) */ + OP2(OInt, 3, 2), /* r3 = 2 (offset for second i16) */ + OP2(OInt, 4, 3), /* r4 = 0x1234 */ + OP2(OInt, 5, 4), /* r5 = 0x5678 */ + OP3(OSetI16, 1, 2, 4), /* *(i16*)(r1 + r2) = r4 */ + OP3(OSetI16, 1, 3, 5), /* *(i16*)(r1 + r3) = r5 */ + OP3(OGetI16, 6, 1, 2), /* r6 = *(i16*)(r1 + r2) */ + OP3(OGetI16, 7, 1, 3), /* r7 = *(i16*)(r1 + r3) */ + OP3(OAdd, 8, 6, 7), /* r8 = r6 + r7 */ + OP1(ORet, 8), + }; + + test_alloc_function(c, 0, fn_type, 9, regs, 12, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + int expected = 0x1234 + 0x5678; /* 4660 + 22136 = 26796 */ + if (ret != expected) { + fprintf(stderr, " Expected %d, got %d\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OSetMem and OGetMem - write and read 32-bit values (i32) + */ +TEST(mem_i32_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 64, 0, 4, 100, 200 }; + test_init_ints(c, 5, ints); + + hl_type *alloc_args[] = { &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 1, alloc_args); + test_add_native(c, 1, "test", "alloc_buffer", alloc_fn_type, (void*)alloc_test_buffer); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = size */ + &c->types[T_BYTES], /* r1 = buffer */ + &c->types[T_I32], /* r2 = offset0 */ + &c->types[T_I32], /* r3 = offset1 */ + &c->types[T_I32], /* r4 = val0 */ + &c->types[T_I32], /* r5 = val1 */ + &c->types[T_I32], /* r6 = read val0 */ + &c->types[T_I32], /* r7 = read val1 */ + &c->types[T_I32], /* r8 = result */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 64 (size) */ + OP3(OCall1, 1, 1, 0), /* r1 = alloc_buffer(r0) */ + OP2(OInt, 2, 1), /* r2 = 0 (offset) */ + OP2(OInt, 3, 2), /* r3 = 4 (offset for second i32) */ + OP2(OInt, 4, 3), /* r4 = 100 */ + OP2(OInt, 5, 4), /* r5 = 200 */ + OP3(OSetMem, 1, 2, 4), /* *(i32*)(r1 + r2) = r4 */ + OP3(OSetMem, 1, 3, 5), /* *(i32*)(r1 + r3) = r5 */ + OP3(OGetMem, 6, 1, 2), /* r6 = *(i32*)(r1 + r2) */ + OP3(OGetMem, 7, 1, 3), /* r7 = *(i32*)(r1 + r3) */ + OP3(OAdd, 8, 6, 7), /* r8 = r6 + r7 */ + OP1(ORet, 8), + }; + + test_alloc_function(c, 0, fn_type, 9, regs, 12, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + int expected = 100 + 200; + if (ret != expected) { + fprintf(stderr, " Expected %d, got %d\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OSetMem and OGetMem with i64 values + */ +TEST(mem_i64_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 64, 0, 8, 1000, 2000 }; + test_init_ints(c, 5, ints); + + hl_type *alloc_args[] = { &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 1, alloc_args); + test_add_native(c, 1, "test", "alloc_buffer", alloc_fn_type, (void*)alloc_test_buffer); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = size */ + &c->types[T_BYTES], /* r1 = buffer */ + &c->types[T_I32], /* r2 = offset0 */ + &c->types[T_I32], /* r3 = offset1 */ + &c->types[T_I64], /* r4 = val0 */ + &c->types[T_I64], /* r5 = val1 */ + &c->types[T_I64], /* r6 = read val0 */ + &c->types[T_I64], /* r7 = read val1 */ + &c->types[T_I64], /* r8 = result */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 64 (size) */ + OP3(OCall1, 1, 1, 0), /* r1 = alloc_buffer(r0) */ + OP2(OInt, 2, 1), /* r2 = 0 (offset) */ + OP2(OInt, 3, 2), /* r3 = 8 (offset for second i64) */ + OP2(OInt, 4, 3), /* r4 = 1000 (as i64) */ + OP2(OInt, 5, 4), /* r5 = 2000 (as i64) */ + OP3(OSetMem, 1, 2, 4), /* *(i64*)(r1 + r2) = r4 */ + OP3(OSetMem, 1, 3, 5), /* *(i64*)(r1 + r3) = r5 */ + OP3(OGetMem, 6, 1, 2), /* r6 = *(i64*)(r1 + r2) */ + OP3(OGetMem, 7, 1, 3), /* r7 = *(i64*)(r1 + r3) */ + OP3(OAdd, 8, 6, 7), /* r8 = r6 + r7 */ + OP1(ORet, 8), + }; + + test_alloc_function(c, 0, fn_type, 9, regs, 12, ops); + + int result; + int64_t (*fn)(void) = (int64_t(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int64_t ret = fn(); + int64_t expected = 1000 + 2000; + if (ret != expected) { + fprintf(stderr, " Expected %ld, got %ld\n", (long)expected, (long)ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OSetMem and OGetMem with f64 values + */ +TEST(mem_f64_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 64, 0, 8 }; + test_init_ints(c, 3, ints); + + double floats[] = { 1.5, 2.5 }; + test_init_floats(c, 2, floats); + + hl_type *alloc_args[] = { &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 1, alloc_args); + test_add_native(c, 1, "test", "alloc_buffer", alloc_fn_type, (void*)alloc_test_buffer); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = size */ + &c->types[T_BYTES], /* r1 = buffer */ + &c->types[T_I32], /* r2 = offset0 */ + &c->types[T_I32], /* r3 = offset1 */ + &c->types[T_F64], /* r4 = val0 */ + &c->types[T_F64], /* r5 = val1 */ + &c->types[T_F64], /* r6 = read val0 */ + &c->types[T_F64], /* r7 = read val1 */ + &c->types[T_F64], /* r8 = result */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 64 (size) */ + OP3(OCall1, 1, 1, 0), /* r1 = alloc_buffer(r0) */ + OP2(OInt, 2, 1), /* r2 = 0 (offset) */ + OP2(OInt, 3, 2), /* r3 = 8 (offset for second f64) */ + OP2(OFloat, 4, 0), /* r4 = 1.5 */ + OP2(OFloat, 5, 1), /* r5 = 2.5 */ + OP3(OSetMem, 1, 2, 4), /* *(f64*)(r1 + r2) = r4 */ + OP3(OSetMem, 1, 3, 5), /* *(f64*)(r1 + r3) = r5 */ + OP3(OGetMem, 6, 1, 2), /* r6 = *(f64*)(r1 + r2) */ + OP3(OGetMem, 7, 1, 3), /* r7 = *(f64*)(r1 + r3) */ + OP3(OAdd, 8, 6, 7), /* r8 = r6 + r7 */ + OP1(ORet, 8), + }; + + test_alloc_function(c, 0, fn_type, 9, regs, 12, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + double expected = 1.5 + 2.5; + double diff = ret - expected; + if (diff < 0) diff = -diff; + if (diff > 0.0001) { + fprintf(stderr, " Expected %f, got %f\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Non-zero base offset + * + * Tests accessing memory at non-aligned offsets + */ +TEST(mem_nonzero_offset) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 64, 10, 11, 12, 13, 1, 2, 3, 4 }; + test_init_ints(c, 9, ints); + + hl_type *alloc_args[] = { &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 1, alloc_args); + test_add_native(c, 1, "test", "alloc_buffer", alloc_fn_type, (void*)alloc_test_buffer); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = size */ + &c->types[T_BYTES], /* r1 = buffer */ + &c->types[T_I32], /* r2-r5 = offsets */ + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], /* r6-r9 = values */ + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], /* r10 = sum */ + &c->types[T_I32], /* r11-r14 = read values */ + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 64 (size) */ + OP3(OCall1, 1, 1, 0), /* r1 = alloc_buffer(r0) */ + OP2(OInt, 2, 1), /* r2 = 10 */ + OP2(OInt, 3, 2), /* r3 = 11 */ + OP2(OInt, 4, 3), /* r4 = 12 */ + OP2(OInt, 5, 4), /* r5 = 13 */ + OP2(OInt, 6, 5), /* r6 = 1 */ + OP2(OInt, 7, 6), /* r7 = 2 */ + OP2(OInt, 8, 7), /* r8 = 3 */ + OP2(OInt, 9, 8), /* r9 = 4 */ + OP3(OSetI8, 1, 2, 6), /* buf[10] = 1 */ + OP3(OSetI8, 1, 3, 7), /* buf[11] = 2 */ + OP3(OSetI8, 1, 4, 8), /* buf[12] = 3 */ + OP3(OSetI8, 1, 5, 9), /* buf[13] = 4 */ + OP3(OGetI8, 11, 1, 2), /* r11 = buf[10] */ + OP3(OGetI8, 12, 1, 3), /* r12 = buf[11] */ + OP3(OGetI8, 13, 1, 4), /* r13 = buf[12] */ + OP3(OGetI8, 14, 1, 5), /* r14 = buf[13] */ + OP3(OAdd, 10, 11, 12), /* r10 = r11 + r12 */ + OP3(OAdd, 10, 10, 13), /* r10 = r10 + r13 */ + OP3(OAdd, 10, 10, 14), /* r10 = r10 + r14 */ + OP1(ORet, 10), + }; + + test_alloc_function(c, 0, fn_type, 15, regs, 22, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + int expected = 1 + 2 + 3 + 4; /* 10 */ + if (ret != expected) { + fprintf(stderr, " Expected %d, got %d\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(mem_i8_basic), + TEST_ENTRY(mem_i16_basic), + TEST_ENTRY(mem_i32_basic), + TEST_ENTRY(mem_i64_basic), + TEST_ENTRY(mem_f64_basic), + TEST_ENTRY(mem_nonzero_offset), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Memory Operation Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_methods.c b/other/tests/minimal/test_methods.c new file mode 100644 index 000000000..b0bb5d144 --- /dev/null +++ b/other/tests/minimal/test_methods.c @@ -0,0 +1,330 @@ +/* + * Test method call operations for HashLink AArch64 JIT + * + * Tests: OCallMethod, OCallThis, OCall4 + * + * OCallMethod: call a method on an object via vtable + * OCallThis: call a method with implicit 'this' (R0) + * OCall4: call a function with 4 arguments + */ +#include "test_harness.h" + +/* Helper to create an object type with a method */ +static hl_type *create_obj_type_with_method(hl_code *c, const char *name, int method_findex) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types\n"); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HOBJ; + t->obj = (hl_type_obj*)calloc(1, sizeof(hl_type_obj)); + t->obj->name = (uchar*)name; + t->obj->nfields = 0; + t->obj->nproto = 1; + t->obj->nbindings = 0; + + t->obj->proto = (hl_obj_proto*)calloc(1, sizeof(hl_obj_proto)); + t->obj->proto[0].name = (uchar*)"testMethod"; + t->obj->proto[0].findex = method_findex; + t->obj->proto[0].pindex = 0; + + return t; +} + +/* + * Test: OCall4 - call function with 4 arguments + * + * fn0: (i32, i32, i32, i32) -> i32 { return a + b + c + d; } + * fn1: () -> i32 { return fn0(10, 20, 5, 7); } // 10+20+5+7 = 42 + */ +TEST(call4_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 20, 5, 7 }; + test_init_ints(c, 4, ints); + + /* fn0 type: (i32, i32, i32, i32) -> i32 */ + hl_type *fn0_args[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_type *fn0_type = test_alloc_fun_type(c, &c->types[T_I32], 4, fn0_args); + + /* fn1 type: () -> i32 */ + hl_type *fn1_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + c->functions = (hl_function*)calloc(MAX_FUNCTIONS, sizeof(hl_function)); + c->nfunctions = 0; + + /* fn0: sum of 4 args */ + { + hl_type *regs[] = { + &c->types[T_I32], /* r0 = a */ + &c->types[T_I32], /* r1 = b */ + &c->types[T_I32], /* r2 = c */ + &c->types[T_I32], /* r3 = d */ + &c->types[T_I32], /* r4 = result */ + }; + hl_opcode ops[] = { + OP3(OAdd, 4, 0, 1), /* r4 = a + b */ + OP3(OAdd, 4, 4, 2), /* r4 = r4 + c */ + OP3(OAdd, 4, 4, 3), /* r4 = r4 + d */ + OP1(ORet, 4), + }; + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 0; + f->type = fn0_type; + f->nregs = 5; + f->nops = 4; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 5); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 4); + memcpy(f->ops, ops, sizeof(ops)); + } + + /* fn1: calls fn0 with 4 args */ + { + hl_type *regs[] = { + &c->types[T_I32], /* r0 = arg 0 */ + &c->types[T_I32], /* r1 = arg 1 */ + &c->types[T_I32], /* r2 = arg 2 */ + &c->types[T_I32], /* r3 = arg 3 */ + &c->types[T_I32], /* r4 = result */ + }; + + /* OCall4: dst=p1, findex=p2, arg0=p3, extra=[arg1, arg2, arg3] */ + static int extra[] = { 1, 2, 3 }; /* registers for args 1, 2, 3 */ + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 10 */ + OP2(OInt, 1, 1), /* r1 = 20 */ + OP2(OInt, 2, 2), /* r2 = 5 */ + OP2(OInt, 3, 3), /* r3 = 7 */ + { OCall4, 4, 0, 0, extra }, /* r4 = fn0(r0, r1, r2, r3) */ + OP1(ORet, 4), + }; + + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 1; + f->type = fn1_type; + f->nregs = 5; + f->nops = 6; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 5); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 6); + memcpy(f->ops, ops, sizeof(ops)); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OCall4 with mixed types (some floats) + */ +TEST(call4_mixed_types) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 32 }; + test_init_ints(c, 2, ints); + + /* fn0: (i32, i32, i32, i32) -> i32 */ + hl_type *fn0_args[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_type *fn0_type = test_alloc_fun_type(c, &c->types[T_I32], 4, fn0_args); + + hl_type *fn1_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + c->functions = (hl_function*)calloc(MAX_FUNCTIONS, sizeof(hl_function)); + c->nfunctions = 0; + + /* fn0: return just first + second arg */ + { + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + }; + hl_opcode ops[] = { + OP3(OAdd, 4, 0, 1), + OP1(ORet, 4), + }; + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 0; + f->type = fn0_type; + f->nregs = 5; + f->nops = 2; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 5); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 2); + memcpy(f->ops, ops, sizeof(ops)); + } + + /* fn1: call fn0(10, 32, 0, 0) = 42 */ + { + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + }; + + static int extra[] = { 1, 2, 3 }; /* registers for args 1, 2, 3 */ + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 10 */ + OP2(OInt, 1, 1), /* r1 = 32 */ + OP1(ONull, 2), /* r2 = 0 (null as int) */ + OP1(ONull, 3), /* r3 = 0 */ + { OCall4, 4, 0, 0, extra }, /* r4 = fn0(10, 32, 0, 0) */ + OP1(ORet, 4), + }; + + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 1; + f->type = fn1_type; + f->nregs = 5; + f->nops = 6; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 5); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 6); + memcpy(f->ops, ops, sizeof(ops)); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Multiple OCall4 in sequence + * + * This tests that register allocation works correctly across multiple calls. + */ +TEST(call4_multiple) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 1, 2, 3, 4, 10 }; + test_init_ints(c, 5, ints); + + hl_type *fn0_args[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_type *fn0_type = test_alloc_fun_type(c, &c->types[T_I32], 4, fn0_args); + hl_type *fn1_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + c->functions = (hl_function*)calloc(MAX_FUNCTIONS, sizeof(hl_function)); + c->nfunctions = 0; + + /* fn0: sum of 4 args */ + { + hl_type *regs[] = { + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] + }; + hl_opcode ops[] = { + OP3(OAdd, 4, 0, 1), + OP3(OAdd, 4, 4, 2), + OP3(OAdd, 4, 4, 3), + OP1(ORet, 4), + }; + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 0; + f->type = fn0_type; + f->nregs = 5; + f->nops = 4; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 5); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 4); + memcpy(f->ops, ops, sizeof(ops)); + } + + /* fn1: call fn0 twice and sum results */ + { + hl_type *regs[] = { + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], + }; + + static int extra1[] = { 1, 2, 3 }; /* registers for args 1, 2, 3 */ + static int extra2[] = { 1, 2, 3 }; /* registers for args 1, 2, 3 */ + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 1 */ + OP2(OInt, 1, 1), /* r1 = 2 */ + OP2(OInt, 2, 2), /* r2 = 3 */ + OP2(OInt, 3, 3), /* r3 = 4 */ + { OCall4, 4, 0, 0, extra1 }, /* r4 = fn0(1,2,3,4) = 10 */ + OP2(OInt, 0, 4), /* r0 = 10 */ + OP2(OInt, 1, 4), /* r1 = 10 */ + OP2(OInt, 2, 4), /* r2 = 10 */ + OP2(OInt, 3, 1), /* r3 = 2 */ + { OCall4, 5, 0, 0, extra2 }, /* r5 = fn0(10,10,10,2) = 32 */ + OP3(OAdd, 6, 4, 5), /* r6 = 10 + 32 = 42 */ + OP1(ORet, 6), + }; + + hl_function *f = &c->functions[c->nfunctions++]; + f->findex = 1; + f->type = fn1_type; + f->nregs = 7; + f->nops = 12; + f->regs = (hl_type**)malloc(sizeof(hl_type*) * 7); + memcpy(f->regs, regs, sizeof(regs)); + f->ops = (hl_opcode*)malloc(sizeof(hl_opcode) * 12); + memcpy(f->ops, ops, sizeof(ops)); + } + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(call4_basic), + TEST_ENTRY(call4_mixed_types), + TEST_ENTRY(call4_multiple), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Method Call Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_native_field.c b/other/tests/minimal/test_native_field.c new file mode 100644 index 000000000..f64de7a4f --- /dev/null +++ b/other/tests/minimal/test_native_field.c @@ -0,0 +1,490 @@ +/* + * Test native call result stored in object field + * + * This mimics the pattern in hello.hl that crashes: + * 1. Call native function that returns a value + * 2. Store result in object field + * 3. Return object + * 4. Read field from returned object + * 5. Use the value + */ +#include "test_harness.h" + +/* Native function that returns an integer */ +static int native_get_value(void) { + return 42; +} + +/* Native function that returns a pointer */ +static void *native_get_ptr(void) { + static int data = 123; + return &data; +} + +/* Helper to create an object type with fields */ +static hl_type *create_obj_type(hl_code *c, const char *name, int nfields, hl_type **field_types) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types\n"); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HOBJ; + t->obj = (hl_type_obj*)calloc(1, sizeof(hl_type_obj)); + t->obj->name = (uchar*)name; + t->obj->nfields = nfields; + t->obj->nproto = 0; + t->obj->nbindings = 0; + + if (nfields > 0) { + t->obj->fields = (hl_obj_field*)calloc(nfields, sizeof(hl_obj_field)); + for (int i = 0; i < nfields; i++) { + t->obj->fields[i].name = (uchar*)"field"; + t->obj->fields[i].t = field_types[i]; + t->obj->fields[i].hashed_name = i; + } + } + + return t; +} + +/* + * Test: Call native, store in field, return object, read field + * + * This is a two-function test to match hello.hl's pattern: + * + * F0 (inner): + * r0 = new Obj + * r1 = call native_get_value() + * set_field r0.field[0] = r1 + * return r0 + * + * F1 (outer, entrypoint): + * r0 = call F0() + * r1 = get_field r0.field[0] + * return r1 + * + * Expected: 42 + */ +TEST(native_to_field_to_return) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Create object type with one i32 field */ + hl_type *field_types[] = { &c->types[T_I32] }; + hl_type *obj_type = create_obj_type(c, "TestObj", 1, field_types); + if (!obj_type) return TEST_FAIL; + + /* Native function type: () -> i32 */ + hl_type *native_fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* Add native function at findex 2 */ + test_add_native(c, 2, "test", "native_get_value", native_fn_type, native_get_value); + + /* F0 (inner function): () -> obj + * r0 = new Obj + * r1 = call native (findex 2) + * set_field r0.field[0] = r1 + * return r0 + */ + hl_type *inner_fn_type = test_alloc_fun_type(c, obj_type, 0, NULL); + hl_type *inner_regs[] = { obj_type, &c->types[T_I32] }; + hl_opcode inner_ops[] = { + OP1(ONew, 0), /* r0 = new Obj */ + OP2(OCall0, 1, 2), /* r1 = call native F2 */ + OP3(OSetField, 0, 0, 1), /* r0.field[0] = r1 */ + OP1(ORet, 0), /* return r0 */ + }; + test_alloc_function(c, 0, inner_fn_type, 2, inner_regs, 4, inner_ops); + + /* F1 (outer function, entrypoint): () -> i32 + * r0 = call F0() + * r1 = get_field r0.field[0] + * return r1 + */ + hl_type *outer_fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *outer_regs[] = { obj_type, &c->types[T_I32] }; + hl_opcode outer_ops[] = { + OP2(OCall0, 0, 0), /* r0 = call F0 */ + OP3(OField, 1, 0, 0), /* r1 = r0.field[0] */ + OP1(ORet, 1), /* return r1 */ + }; + test_alloc_function(c, 1, outer_fn_type, 2, outer_regs, 3, outer_ops); + + /* Set entrypoint to F1 */ + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Same pattern but with pointer type (like array) + * + * F0 (inner): + * r0 = new Obj + * r1 = call native_get_ptr() + * set_field r0.field[0] = r1 + * return r0 + * + * F1 (outer): + * r0 = call F0() + * r1 = get_field r0.field[0] + * return r1 + */ +TEST(native_ptr_to_field_to_return) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Create object type with one bytes (pointer) field */ + hl_type *field_types[] = { &c->types[T_BYTES] }; + hl_type *obj_type = create_obj_type(c, "TestObjPtr", 1, field_types); + if (!obj_type) return TEST_FAIL; + + /* Native function type: () -> bytes */ + hl_type *native_fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 0, NULL); + + /* Add native function at findex 2 */ + test_add_native(c, 2, "test", "native_get_ptr", native_fn_type, native_get_ptr); + + /* F0 (inner function): () -> obj */ + hl_type *inner_fn_type = test_alloc_fun_type(c, obj_type, 0, NULL); + hl_type *inner_regs[] = { obj_type, &c->types[T_BYTES] }; + hl_opcode inner_ops[] = { + OP1(ONew, 0), /* r0 = new Obj */ + OP2(OCall0, 1, 2), /* r1 = call native F2 */ + OP3(OSetField, 0, 0, 1), /* r0.field[0] = r1 */ + OP1(ORet, 0), /* return r0 */ + }; + test_alloc_function(c, 0, inner_fn_type, 2, inner_regs, 4, inner_ops); + + /* F1 (outer function): () -> bytes */ + hl_type *outer_fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 0, NULL); + hl_type *outer_regs[] = { obj_type, &c->types[T_BYTES] }; + hl_opcode outer_ops[] = { + OP2(OCall0, 0, 0), /* r0 = call F0 */ + OP3(OField, 1, 0, 0), /* r1 = r0.field[0] */ + OP1(ORet, 1), /* return r1 */ + }; + test_alloc_function(c, 1, outer_fn_type, 2, outer_regs, 3, outer_ops); + + c->entrypoint = 1; + + int result; + void *(*fn)(void) = (void*(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + void *ret = fn(); + static int expected_data = 123; + if (ret != &expected_data) { + /* The native returns a pointer to its static - compare values */ + if (ret == NULL) { + fprintf(stderr, " Got NULL pointer\n"); + return TEST_FAIL; + } + int got = *(int*)ret; + if (got != 123) { + fprintf(stderr, " Expected ptr to 123, got ptr to %d\n", got); + return TEST_FAIL; + } + } + + return TEST_PASS; +} + +/* + * Test: Multiple fields set from native calls + * + * This more closely matches F295 which sets multiple fields + */ +TEST(native_multiple_fields) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 100 }; + test_init_ints(c, 1, ints); + + /* Create object type with 3 fields */ + hl_type *field_types[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_BYTES] }; + hl_type *obj_type = create_obj_type(c, "TestObj3", 3, field_types); + if (!obj_type) return TEST_FAIL; + + /* Native function types */ + hl_type *native_int_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *native_ptr_type = test_alloc_fun_type(c, &c->types[T_BYTES], 0, NULL); + + /* Add native functions at findex 2 and 3 */ + test_add_native(c, 2, "test", "native_get_value", native_int_type, native_get_value); + test_add_native(c, 3, "test", "native_get_ptr", native_ptr_type, native_get_ptr); + + /* F0 (inner): () -> obj + * r0 = new Obj + * r1 = 100 + * set_field r0.field[0] = r1 + * r2 = call native_get_value() + * set_field r0.field[1] = r2 + * r3 = call native_get_ptr() + * set_field r0.field[2] = r3 + * return r0 + */ + hl_type *inner_fn_type = test_alloc_fun_type(c, obj_type, 0, NULL); + hl_type *inner_regs[] = { obj_type, &c->types[T_I32], &c->types[T_I32], &c->types[T_BYTES] }; + hl_opcode inner_ops[] = { + OP1(ONew, 0), /* r0 = new Obj */ + OP2(OInt, 1, 0), /* r1 = 100 */ + OP3(OSetField, 0, 0, 1), /* r0.field[0] = r1 */ + OP2(OCall0, 2, 2), /* r2 = call native F2 (returns 42) */ + OP3(OSetField, 0, 1, 2), /* r0.field[1] = r2 */ + OP2(OCall0, 3, 3), /* r3 = call native F3 (returns ptr) */ + OP3(OSetField, 0, 2, 3), /* r0.field[2] = r3 */ + OP1(ORet, 0), /* return r0 */ + }; + test_alloc_function(c, 0, inner_fn_type, 4, inner_regs, 8, inner_ops); + + /* F1 (outer): () -> i32 + * r0 = call F0() + * r1 = get_field r0.field[0] ; should be 100 + * r2 = get_field r0.field[1] ; should be 42 + * r3 = r1 + r2 ; should be 142 + * r4 = get_field r0.field[2] ; should be ptr + * null_check r4 ; ptr should not be null + * return r3 + */ + hl_type *outer_fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *outer_regs[] = { obj_type, &c->types[T_I32], &c->types[T_I32], &c->types[T_I32], &c->types[T_BYTES] }; + hl_opcode outer_ops[] = { + OP2(OCall0, 0, 0), /* r0 = call F0 */ + OP3(OField, 1, 0, 0), /* r1 = r0.field[0] */ + OP3(OField, 2, 0, 1), /* r2 = r0.field[1] */ + OP3(OAdd, 3, 1, 2), /* r3 = r1 + r2 */ + OP3(OField, 4, 0, 2), /* r4 = r0.field[2] */ + OP1(ONullCheck, 4), /* null_check r4 */ + OP1(ORet, 3), /* return r3 */ + }; + test_alloc_function(c, 1, outer_fn_type, 5, outer_regs, 7, outer_ops); + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 142) { + fprintf(stderr, " Expected 142 (100+42), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OCall2 with arguments passed to inner function + * + * This matches hello.hl's pattern more closely: + * - Entrypoint uses OCall2 to call F295 with 2 type args + * - F295 uses OCall1 to call native with one of those args + * + * F0 (inner): (i32 a, i32 b) -> obj + * r2 = new Obj + * r3 = call native_get_value() ; returns 42 + * r4 = a + b + r3 + * set_field r2.field[0] = r4 + * return r2 + * + * F1 (outer): () -> i32 + * r0 = 10 + * r1 = 20 + * r2 = call F0(r0, r1) ; OCall2 + * r3 = get_field r2.field[0] ; should be 10+20+42=72 + * return r3 + */ +TEST(ocall2_with_native_in_callee) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 20 }; + test_init_ints(c, 2, ints); + + /* Create object type with one i32 field */ + hl_type *field_types[] = { &c->types[T_I32] }; + hl_type *obj_type = create_obj_type(c, "TestObj", 1, field_types); + if (!obj_type) return TEST_FAIL; + + /* Native function type: () -> i32 */ + hl_type *native_fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* Add native function at findex 2 */ + test_add_native(c, 2, "test", "native_get_value", native_fn_type, native_get_value); + + /* F0 (inner): (i32, i32) -> obj + * r0 = arg a (i32) + * r1 = arg b (i32) + * r2 = new Obj + * r3 = call native F2 (returns 42) + * r4 = a + b + * r5 = r4 + r3 + * set_field r2.field[0] = r5 + * return r2 + */ + hl_type *inner_arg_types[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_type *inner_fn_type = test_alloc_fun_type(c, obj_type, 2, inner_arg_types); + hl_type *inner_regs[] = { + &c->types[T_I32], &c->types[T_I32], /* r0, r1 = args */ + obj_type, &c->types[T_I32], /* r2 = obj, r3 = native result */ + &c->types[T_I32], &c->types[T_I32] /* r4, r5 = temps */ + }; + hl_opcode inner_ops[] = { + OP1(ONew, 2), /* r2 = new Obj */ + OP2(OCall0, 3, 2), /* r3 = call native F2 (returns 42) */ + OP3(OAdd, 4, 0, 1), /* r4 = r0 + r1 */ + OP3(OAdd, 5, 4, 3), /* r5 = r4 + r3 */ + OP3(OSetField, 2, 0, 5), /* r2.field[0] = r5 */ + OP1(ORet, 2), /* return r2 */ + }; + test_alloc_function(c, 0, inner_fn_type, 6, inner_regs, 6, inner_ops); + + /* F1 (outer): () -> i32 + * r0 = 10 + * r1 = 20 + * r2 = call F0(r0, r1) ; OCall2 + * r3 = get_field r2.field[0] + * return r3 + */ + hl_type *outer_fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *outer_regs[] = { + &c->types[T_I32], &c->types[T_I32], /* r0, r1 = args to pass */ + obj_type, &c->types[T_I32] /* r2 = result obj, r3 = field value */ + }; + hl_opcode outer_ops[] = { + OP2(OInt, 0, 0), /* r0 = 10 */ + OP2(OInt, 1, 1), /* r1 = 20 */ + OP4_CALL2(OCall2, 2, 0, 0, 1), /* r2 = call F0(r0, r1) */ + OP3(OField, 3, 2, 0), /* r3 = r2.field[0] */ + OP1(ORet, 3), /* return r3 */ + }; + test_alloc_function(c, 1, outer_fn_type, 4, outer_regs, 5, outer_ops); + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 72) { /* 10 + 20 + 42 = 72 */ + fprintf(stderr, " Expected 72 (10+20+42), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OCall1 passing argument to native + * + * F0 (inner): (i32 x) -> i32 + * r1 = call native_add_ten(r0) + * return r1 + * + * F1 (outer): () -> i32 + * r0 = 32 + * r1 = call F0(r0) ; OCall1 + * return r1 ; should be 42 + */ +static int native_add_ten(int x) { + return x + 10; +} + +TEST(ocall1_arg_to_native) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 32 }; + test_init_ints(c, 1, ints); + + /* Native function type: (i32) -> i32 */ + hl_type *native_arg_types[] = { &c->types[T_I32] }; + hl_type *native_fn_type = test_alloc_fun_type(c, &c->types[T_I32], 1, native_arg_types); + + /* Add native function at findex 2 */ + test_add_native(c, 2, "test", "native_add_ten", native_fn_type, native_add_ten); + + /* F0 (inner): (i32) -> i32 + * r0 = arg x + * r1 = call native F2(r0) + * return r1 + */ + hl_type *inner_arg_types[] = { &c->types[T_I32] }; + hl_type *inner_fn_type = test_alloc_fun_type(c, &c->types[T_I32], 1, inner_arg_types); + hl_type *inner_regs[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_opcode inner_ops[] = { + OP3(OCall1, 1, 2, 0), /* r1 = call F2(r0) */ + OP1(ORet, 1), /* return r1 */ + }; + test_alloc_function(c, 0, inner_fn_type, 2, inner_regs, 2, inner_ops); + + /* F1 (outer): () -> i32 + * r0 = 32 + * r1 = call F0(r0) ; OCall1 + * return r1 + */ + hl_type *outer_fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *outer_regs[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_opcode outer_ops[] = { + OP2(OInt, 0, 0), /* r0 = 32 */ + OP3(OCall1, 1, 0, 0), /* r1 = call F0(r0) */ + OP1(ORet, 1), /* return r1 */ + }; + test_alloc_function(c, 1, outer_fn_type, 2, outer_regs, 3, outer_ops); + + c->entrypoint = 1; + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42 (32+10), got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(native_to_field_to_return), + TEST_ENTRY(native_ptr_to_field_to_return), + TEST_ENTRY(native_multiple_fields), + TEST_ENTRY(ocall2_with_native_in_callee), + TEST_ENTRY(ocall1_arg_to_native), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Native->Field Pattern Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_natives.c b/other/tests/minimal/test_natives.c new file mode 100644 index 000000000..65868a339 --- /dev/null +++ b/other/tests/minimal/test_natives.c @@ -0,0 +1,234 @@ +/* + * Test native function calls for HashLink AArch64 JIT + * + * Tests calling C functions from JIT code + */ +#include "test_harness.h" + +/* Simple native functions for testing */ +static int native_return_42(void) { + return 42; +} + +static int native_add(int a, int b) { + return a + b; +} + +static int native_add3(int a, int b, int c) { + return a + b + c; +} + +static int g_side_effect = 0; + +static void native_set_global(int val) { + g_side_effect = val; +} + +static int native_get_global(void) { + return g_side_effect; +} + +/* + * Test: Call native function with no args + * + * op0: call0 r0, native_return_42 + * op1: ret r0 + */ +TEST(native_call0) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Native at findex 1, our function at findex 0 */ + hl_type *native_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + test_add_native(c, 1, "test", "return_42", native_type, native_return_42); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OCall0, 0, 1), /* op0: r0 = call native findex=1 */ + OP1(ORet, 0), /* op1: return r0 */ + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 2, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Call native function with 2 args + * + * op0: int r0, 0 ; r0 = 10 + * op1: int r1, 1 ; r1 = 32 + * op2: call2 r2, native_add, r0, r1 + * op3: ret r2 ; return 42 + */ +TEST(native_call2) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 32 }; + test_init_ints(c, 2, ints); + + /* Native at findex 1 */ + hl_type *arg_types[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_type *native_type = test_alloc_fun_type(c, &c->types[T_I32], 2, arg_types); + test_add_native(c, 1, "test", "add", native_type, native_add); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 10 */ + OP2(OInt, 1, 1), /* op1: r1 = 32 */ + OP4_CALL2(OCall2, 2, 1, 0, 1), /* op2: r2 = call native(r0, r1) */ + OP1(ORet, 2), /* op3: return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Call native function with 3 args (uses OCall3) + * + * op0: int r0, 0 ; r0 = 10 + * op1: int r1, 1 ; r1 = 20 + * op2: int r2, 2 ; r2 = 12 + * op3: call3 r3, native_add3, r0, r1, r2 + * op4: ret r3 ; return 42 + */ +TEST(native_call3) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 20, 12 }; + test_init_ints(c, 3, ints); + + /* Native at findex 1 */ + hl_type *arg_types[] = { &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + hl_type *native_type = test_alloc_fun_type(c, &c->types[T_I32], 3, arg_types); + test_add_native(c, 1, "test", "add3", native_type, native_add3); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { + &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32] + }; + + /* OCall3: p1=dst, p2=findex, p3=arg0, extra[0]=arg1, extra[1]=arg2 */ + int extra[] = { 1, 2 }; + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 10 */ + OP2(OInt, 1, 1), /* op1: r1 = 20 */ + OP2(OInt, 2, 2), /* op2: r2 = 12 */ + {OCall3, 3, 1, 0, extra}, /* op3: r3 = call native(r0, r1, r2) */ + OP1(ORet, 3), /* op4: return r3 */ + }; + + test_alloc_function(c, 0, fn_type, 4, regs, 5, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Call void native function (side effect) + * + * op0: int r0, 0 ; r0 = 99 + * op1: call1 r1, native_set_global, r0 + * op2: call0 r2, native_get_global + * op3: ret r2 ; return 99 + */ +TEST(native_void_call) { + test_init_runtime(); + + g_side_effect = 0; /* Reset */ + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 99 }; + test_init_ints(c, 1, ints); + + /* Two natives */ + hl_type *set_args[] = { &c->types[T_I32] }; + hl_type *set_type = test_alloc_fun_type(c, &c->types[T_VOID], 1, set_args); + test_add_native(c, 1, "test", "set_global", set_type, native_set_global); + + hl_type *get_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + test_add_native(c, 2, "test", "get_global", get_type, native_get_global); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32], &c->types[T_VOID], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* op0: r0 = 99 */ + OP3(OCall1, 1, 1, 0), /* op1: call set_global(r0) */ + OP2(OCall0, 2, 2), /* op2: r2 = call get_global() */ + OP1(ORet, 2), /* op3: return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 99) { + fprintf(stderr, " Expected 99, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(native_call0), + TEST_ENTRY(native_call2), + TEST_ENTRY(native_call3), + TEST_ENTRY(native_void_call), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Native Function Call Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_objects.c b/other/tests/minimal/test_objects.c new file mode 100644 index 000000000..0254e3666 --- /dev/null +++ b/other/tests/minimal/test_objects.c @@ -0,0 +1,410 @@ +/* + * Test object operations for HashLink AArch64 JIT + * + * Tests: ONew, OField, OSetField, ONullCheck, OGetThis, OSetThis + * + * These are key opcodes used in hello.hl + */ +#include "test_harness.h" + +/* We need to create object types for these tests */ + +/* Helper to create an HDYNOBJ type (dynamic object) */ +static hl_type *create_dynobj_type(hl_code *c) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types\n"); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HDYNOBJ; + /* HDYNOBJ has no obj pointer - it's dynamically allocated */ + return t; +} + +/* Helper to create an HVIRTUAL type */ +static hl_type *create_virtual_type(hl_code *c, int nfields, hl_type **field_types) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types\n"); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HVIRTUAL; + t->virt = (hl_type_virtual*)calloc(1, sizeof(hl_type_virtual)); + t->virt->nfields = nfields; + + if (nfields > 0) { + t->virt->fields = (hl_obj_field*)calloc(nfields, sizeof(hl_obj_field)); + for (int i = 0; i < nfields; i++) { + t->virt->fields[i].name = (uchar*)"field"; + t->virt->fields[i].t = field_types[i]; + t->virt->fields[i].hashed_name = i; + } + } + + return t; +} + +/* Helper to create an object type with fields */ +static hl_type *create_obj_type(hl_code *c, const char *name, int nfields, hl_type **field_types) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types\n"); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HOBJ; + t->obj = (hl_type_obj*)calloc(1, sizeof(hl_type_obj)); + t->obj->name = (uchar*)name; + t->obj->nfields = nfields; + t->obj->nproto = 0; + t->obj->nbindings = 0; + + if (nfields > 0) { + t->obj->fields = (hl_obj_field*)calloc(nfields, sizeof(hl_obj_field)); + for (int i = 0; i < nfields; i++) { + t->obj->fields[i].name = (uchar*)"field"; + t->obj->fields[i].t = field_types[i]; + t->obj->fields[i].hashed_name = i; /* Simple hash for testing */ + } + } + + /* Don't call hl_get_obj_rt here - it needs a module allocator. + * The JIT will call it when needed, after the module is set up. */ + + return t; +} + +/* + * Test: ONullCheck on non-null value (should not throw) + * + * r0 = 42 + * null_check r0 ; should pass (non-zero) + * return r0 + */ +TEST(null_check_nonnull) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { &c->types[T_I32] }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 */ + OP1(ONullCheck, 0), /* null_check r0 - should pass */ + OP1(ORet, 0), + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Create object with ONew and access field with OField/OSetField + * + * Object type: { i32 value } + * + * r0 = new Obj + * r1 = 42 + * set_field r0.field[0] = r1 + * r2 = get_field r0.field[0] + * return r2 + */ +TEST(object_field_access) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Create object type with one i32 field */ + hl_type *field_types[] = { &c->types[T_I32] }; + hl_type *obj_type = create_obj_type(c, "TestObj", 1, field_types); + if (!obj_type) return TEST_FAIL; + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { obj_type, &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP1(ONew, 0), /* r0 = new Obj */ + OP2(OInt, 1, 0), /* r1 = 42 */ + OP3(OSetField, 0, 0, 1), /* r0.field[0] = r1 */ + OP3(OField, 2, 0, 0), /* r2 = r0.field[0] */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 5, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Object with multiple fields + * + * Object type: { i32 a, i32 b } + * + * r0 = new Obj + * r1 = 10 + * r2 = 32 + * set_field r0.field[0] = r1 ; a = 10 + * set_field r0.field[1] = r2 ; b = 32 + * r3 = get_field r0.field[0] ; r3 = 10 + * r4 = get_field r0.field[1] ; r4 = 32 + * r5 = r3 + r4 ; r5 = 42 + * return r5 + */ +TEST(object_multiple_fields) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 32 }; + test_init_ints(c, 2, ints); + + /* Create object type with two i32 fields */ + hl_type *field_types[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_type *obj_type = create_obj_type(c, "TestObj2", 2, field_types); + if (!obj_type) return TEST_FAIL; + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { + obj_type, + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32] + }; + + hl_opcode ops[] = { + OP1(ONew, 0), /* r0 = new Obj */ + OP2(OInt, 1, 0), /* r1 = 10 */ + OP2(OInt, 2, 1), /* r2 = 32 */ + OP3(OSetField, 0, 0, 1), /* r0.field[0] = r1 */ + OP3(OSetField, 0, 1, 2), /* r0.field[1] = r2 */ + OP3(OField, 3, 0, 0), /* r3 = r0.field[0] */ + OP3(OField, 4, 0, 1), /* r4 = r0.field[1] */ + OP3(OAdd, 5, 3, 4), /* r5 = r3 + r4 */ + OP1(ORet, 5), + }; + + test_alloc_function(c, 0, fn_type, 6, regs, 9, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Object with pointer field + * + * Object type: { bytes ptr } + */ +TEST(object_pointer_field) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Create object type with one pointer field */ + hl_type *field_types[] = { &c->types[T_BYTES] }; + hl_type *obj_type = create_obj_type(c, "TestObjPtr", 1, field_types); + if (!obj_type) return TEST_FAIL; + + /* Setup a string to store */ + c->nstrings = 1; + c->strings = (char**)malloc(sizeof(char*)); + c->strings[0] = "test"; + c->strings_lens = (int*)malloc(sizeof(int)); + c->strings_lens[0] = 4; + c->ustrings = (uchar**)calloc(1, sizeof(uchar*)); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 0, NULL); + hl_type *regs[] = { obj_type, &c->types[T_BYTES], &c->types[T_BYTES] }; + + hl_opcode ops[] = { + OP1(ONew, 0), /* r0 = new Obj */ + OP2(OString, 1, 0), /* r1 = "test" */ + OP3(OSetField, 0, 0, 1), /* r0.field[0] = r1 */ + OP3(OField, 2, 0, 0), /* r2 = r0.field[0] */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 5, ops); + + int result; + uchar* (*fn)(void) = (uchar*(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + uchar *ret = fn(); + if (ret == NULL) { + fprintf(stderr, " Got NULL pointer\n"); + return TEST_FAIL; + } + + /* Check first char is 't' (UTF-16) */ + if (ret[0] != 't') { + fprintf(stderr, " Expected 't', got 0x%04x\n", ret[0]); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: ONew with HDYNOBJ type + * + * This tests that dynamic objects (HDYNOBJ) are allocated correctly. + * The JIT must call hl_alloc_dynobj() (no args) instead of hl_alloc_obj(type). + * + * r0 = new DynObj ; allocate dynamic object + * r1 = 42 + * return r1 ; just verify allocation doesn't crash + */ +TEST(new_dynobj) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Create HDYNOBJ type */ + hl_type *dynobj_type = create_dynobj_type(c); + if (!dynobj_type) return TEST_FAIL; + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { dynobj_type, &c->types[T_I32] }; + + hl_opcode ops[] = { + OP1(ONew, 0), /* r0 = new DynObj - must call hl_alloc_dynobj() */ + OP2(OInt, 1, 0), /* r1 = 42 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: ONew with HVIRTUAL type + * + * This tests that virtual objects (HVIRTUAL) are allocated correctly. + * The JIT must call hl_alloc_virtual(type) instead of hl_alloc_obj(type). + * + * r0 = new Virtual ; allocate virtual object + * r1 = 42 + * return r1 ; just verify allocation doesn't crash + */ +TEST(new_virtual) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Create HVIRTUAL type with one i32 field */ + hl_type *field_types[] = { &c->types[T_I32] }; + hl_type *virt_type = create_virtual_type(c, 1, field_types); + if (!virt_type) return TEST_FAIL; + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + hl_type *regs[] = { virt_type, &c->types[T_I32] }; + + hl_opcode ops[] = { + OP1(ONew, 0), /* r0 = new Virtual - must call hl_alloc_virtual() */ + OP2(OInt, 1, 0), /* r1 = 42 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(null_check_nonnull), + TEST_ENTRY(object_field_access), + TEST_ENTRY(object_multiple_fields), + TEST_ENTRY(object_pointer_field), + TEST_ENTRY(new_dynobj), + /* new_virtual requires complex type setup (virt->indexes) that our minimal + * test harness doesn't support. HVIRTUAL allocation is tested via hello.hl. */ +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Object Operations Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_ref_ops.c b/other/tests/minimal/test_ref_ops.c new file mode 100644 index 000000000..c1e3ad5bb --- /dev/null +++ b/other/tests/minimal/test_ref_ops.c @@ -0,0 +1,474 @@ +/* + * Test reference operations for HashLink AArch64 JIT + * + * Tests: ORef, OUnref, OSetref, ORefData, ORefOffset + * + * ORef: creates a reference (pointer) to a stack variable + * OUnref: dereferences a reference + * OSetref: assigns through a reference + * ORefData: gets pointer to array/bytes data + * ORefOffset: offsets a reference by index * element_size + */ +#include "test_harness.h" + +/* Helper to create a reference type */ +static hl_type *create_ref_type(hl_code *c, hl_type *elem_type) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types\n"); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HREF; + t->tparam = elem_type; + + return t; +} + +/* Helper to create an array type */ +static hl_type *create_array_type(hl_code *c, hl_type *elem_type) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types\n"); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HARRAY; + t->tparam = elem_type; + + return t; +} + +/* + * Test: ORef and OUnref basic - create reference and dereference + * + * r0 = 42 + * r1 = ref(r0) ; r1 = &r0 + * r2 = unref(r1) ; r2 = *r1 = 42 + * return r2 + */ +TEST(ref_unref_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + hl_type *ref_i32 = create_ref_type(c, &c->types[T_I32]); + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = value */ + ref_i32, /* r1 = reference */ + &c->types[T_I32], /* r2 = dereferenced value */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 42 */ + OP2(ORef, 1, 0), /* r1 = &r0 */ + OP2(OUnref, 2, 1), /* r2 = *r1 */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OSetref - modify value through reference + * + * r0 = 10 + * r1 = ref(r0) ; r1 = &r0 + * r2 = 42 + * setref(r1, r2) ; *r1 = 42, so r0 = 42 + * return r0 ; should be 42 + */ +TEST(setref_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 42 }; + test_init_ints(c, 2, ints); + + hl_type *ref_i32 = create_ref_type(c, &c->types[T_I32]); + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = value */ + ref_i32, /* r1 = reference */ + &c->types[T_I32], /* r2 = new value */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 10 */ + OP2(ORef, 1, 0), /* r1 = &r0 */ + OP2(OInt, 2, 1), /* r2 = 42 */ + OP2(OSetref, 1, 2), /* *r1 = r2 */ + OP1(ORet, 0), /* return r0 (should be 42 now) */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 5, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 42) { + fprintf(stderr, " Expected 42, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: ORef/OUnref with i64 + */ +TEST(ref_unref_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 12345 }; + test_init_ints(c, 1, ints); + + hl_type *ref_i64 = create_ref_type(c, &c->types[T_I64]); + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I64], /* r0 = value */ + ref_i64, /* r1 = reference */ + &c->types[T_I64], /* r2 = dereferenced value */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 12345 */ + OP2(ORef, 1, 0), /* r1 = &r0 */ + OP2(OUnref, 2, 1), /* r2 = *r1 */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int64_t (*fn)(void) = (int64_t(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int64_t ret = fn(); + if (ret != 12345) { + fprintf(stderr, " Expected 12345, got %ld\n", (long)ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: ORef/OUnref with f64 + */ +TEST(ref_unref_f64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + double floats[] = { 3.14159 }; + test_init_floats(c, 1, floats); + + hl_type *ref_f64 = create_ref_type(c, &c->types[T_F64]); + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + + hl_type *regs[] = { + &c->types[T_F64], /* r0 = value */ + ref_f64, /* r1 = reference */ + &c->types[T_F64], /* r2 = dereferenced value */ + }; + + hl_opcode ops[] = { + OP2(OFloat, 0, 0), /* r0 = 3.14159 */ + OP2(ORef, 1, 0), /* r1 = &r0 */ + OP2(OUnref, 2, 1), /* r2 = *r1 */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + double expected = 3.14159; + double diff = ret - expected; + if (diff < 0) diff = -diff; + if (diff > 0.00001) { + fprintf(stderr, " Expected %f, got %f\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: ORefData with array - get pointer to array data + * + * array = alloc_array(i32, 3) + * array[0] = 10 + * array[1] = 20 + * array[2] = 12 + * ptr = ref_data(array) ; get pointer to element data + * val = *ptr ; read first element via pointer + * return val ; should be 10 + */ +TEST(ref_data_array) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 3, 0, 1, 2, 10, 20, 12 }; + test_init_ints(c, 7, ints); + + hl_type *array_i32 = create_array_type(c, &c->types[T_I32]); + hl_type *ref_i32 = create_ref_type(c, &c->types[T_I32]); + + hl_type *alloc_args[] = { &c->types[T_TYPE], &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, array_i32, 2, alloc_args); + test_add_native(c, 1, "std", "alloc_array", alloc_fn_type, (void*)hl_alloc_array); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_TYPE], /* r0 = type pointer */ + &c->types[T_I32], /* r1 = size */ + array_i32, /* r2 = array */ + &c->types[T_I32], /* r3 = idx 0 */ + &c->types[T_I32], /* r4 = idx 1 */ + &c->types[T_I32], /* r5 = idx 2 */ + &c->types[T_I32], /* r6 = val 10 */ + &c->types[T_I32], /* r7 = val 20 */ + &c->types[T_I32], /* r8 = val 12 */ + ref_i32, /* r9 = ptr to data */ + &c->types[T_I32], /* r10 = read value */ + }; + + hl_opcode ops[] = { + OP2(OType, 0, T_I32), /* r0 = type for i32 */ + OP2(OInt, 1, 0), /* r1 = 3 (size) */ + OP4_CALL2(OCall2, 2, 1, 0, 1), /* r2 = alloc_array(r0, r1) */ + OP2(OInt, 3, 1), /* r3 = 0 */ + OP2(OInt, 4, 2), /* r4 = 1 */ + OP2(OInt, 5, 3), /* r5 = 2 */ + OP2(OInt, 6, 4), /* r6 = 10 */ + OP2(OInt, 7, 5), /* r7 = 20 */ + OP2(OInt, 8, 6), /* r8 = 12 */ + OP3(OSetArray, 2, 3, 6), /* array[0] = 10 */ + OP3(OSetArray, 2, 4, 7), /* array[1] = 20 */ + OP3(OSetArray, 2, 5, 8), /* array[2] = 12 */ + OP2(ORefData, 9, 2), /* r9 = ptr to array data */ + OP2(OUnref, 10, 9), /* r10 = *r9 = first element */ + OP1(ORet, 10), + }; + + test_alloc_function(c, 0, fn_type, 11, regs, 15, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 10) { + fprintf(stderr, " Expected 10, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: ORefOffset - offset a pointer to access array elements + * + * array = alloc_array(i32, 3) + * array[0] = 10 + * array[1] = 20 + * array[2] = 12 + * ptr = ref_data(array) ; get pointer to element data + * ptr2 = ref_offset(ptr, 2) ; ptr to array[2] + * val = *ptr2 ; read third element + * return val ; should be 12 + */ +TEST(ref_offset_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 3, 0, 1, 2, 10, 20, 12 }; + test_init_ints(c, 7, ints); + + hl_type *array_i32 = create_array_type(c, &c->types[T_I32]); + hl_type *ref_i32 = create_ref_type(c, &c->types[T_I32]); + + hl_type *alloc_args[] = { &c->types[T_TYPE], &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, array_i32, 2, alloc_args); + test_add_native(c, 1, "std", "alloc_array", alloc_fn_type, (void*)hl_alloc_array); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_TYPE], /* r0 = type pointer */ + &c->types[T_I32], /* r1 = size */ + array_i32, /* r2 = array */ + &c->types[T_I32], /* r3-r5 = indices */ + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], /* r6-r8 = values */ + &c->types[T_I32], + &c->types[T_I32], + ref_i32, /* r9 = ptr to data */ + ref_i32, /* r10 = offset ptr */ + &c->types[T_I32], /* r11 = read value */ + }; + + hl_opcode ops[] = { + OP2(OType, 0, T_I32), /* r0 = type for i32 */ + OP2(OInt, 1, 0), /* r1 = 3 (size) */ + OP4_CALL2(OCall2, 2, 1, 0, 1), /* r2 = alloc_array(r0, r1) */ + OP2(OInt, 3, 1), /* r3 = 0 */ + OP2(OInt, 4, 2), /* r4 = 1 */ + OP2(OInt, 5, 3), /* r5 = 2 */ + OP2(OInt, 6, 4), /* r6 = 10 */ + OP2(OInt, 7, 5), /* r7 = 20 */ + OP2(OInt, 8, 6), /* r8 = 12 */ + OP3(OSetArray, 2, 3, 6), /* array[0] = 10 */ + OP3(OSetArray, 2, 4, 7), /* array[1] = 20 */ + OP3(OSetArray, 2, 5, 8), /* array[2] = 12 */ + OP2(ORefData, 9, 2), /* r9 = ptr to array data */ + OP3(ORefOffset, 10, 9, 5), /* r10 = r9 + 2 * sizeof(i32) */ + OP2(OUnref, 11, 10), /* r11 = *r10 = array[2] */ + OP1(ORet, 11), + }; + + test_alloc_function(c, 0, fn_type, 12, regs, 16, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 12) { + fprintf(stderr, " Expected 12, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: ORefOffset with i64 elements - larger element size + */ +TEST(ref_offset_i64) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 3, 0, 1, 2, 100, 200, 300 }; + test_init_ints(c, 7, ints); + + hl_type *array_i64 = create_array_type(c, &c->types[T_I64]); + hl_type *ref_i64 = create_ref_type(c, &c->types[T_I64]); + + hl_type *alloc_args[] = { &c->types[T_TYPE], &c->types[T_I32] }; + hl_type *alloc_fn_type = test_alloc_fun_type(c, array_i64, 2, alloc_args); + test_add_native(c, 1, "std", "alloc_array", alloc_fn_type, (void*)hl_alloc_array); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + + hl_type *regs[] = { + &c->types[T_TYPE], /* r0 = type pointer */ + &c->types[T_I32], /* r1 = size */ + array_i64, /* r2 = array */ + &c->types[T_I32], /* r3-r5 = indices */ + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I64], /* r6-r8 = values */ + &c->types[T_I64], + &c->types[T_I64], + ref_i64, /* r9 = ptr to data */ + ref_i64, /* r10 = offset ptr */ + &c->types[T_I64], /* r11 = read value */ + }; + + hl_opcode ops[] = { + OP2(OType, 0, T_I64), /* r0 = type for i64 */ + OP2(OInt, 1, 0), /* r1 = 3 (size) */ + OP4_CALL2(OCall2, 2, 1, 0, 1), /* r2 = alloc_array(r0, r1) */ + OP2(OInt, 3, 1), /* r3 = 0 */ + OP2(OInt, 4, 2), /* r4 = 1 */ + OP2(OInt, 5, 3), /* r5 = 2 */ + OP2(OInt, 6, 4), /* r6 = 100 */ + OP2(OInt, 7, 5), /* r7 = 200 */ + OP2(OInt, 8, 6), /* r8 = 300 */ + OP3(OSetArray, 2, 3, 6), /* array[0] = 100 */ + OP3(OSetArray, 2, 4, 7), /* array[1] = 200 */ + OP3(OSetArray, 2, 5, 8), /* array[2] = 300 */ + OP2(ORefData, 9, 2), /* r9 = ptr to array data */ + OP3(ORefOffset, 10, 9, 4), /* r10 = r9 + 1 * sizeof(i64) */ + OP2(OUnref, 11, 10), /* r11 = *r10 = array[1] */ + OP1(ORet, 11), + }; + + test_alloc_function(c, 0, fn_type, 12, regs, 16, ops); + + int result; + int64_t (*fn)(void) = (int64_t(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int64_t ret = fn(); + if (ret != 200) { + fprintf(stderr, " Expected 200, got %ld\n", (long)ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(ref_unref_basic), + TEST_ENTRY(setref_basic), + TEST_ENTRY(ref_unref_i64), + TEST_ENTRY(ref_unref_f64), + TEST_ENTRY(ref_data_array), + TEST_ENTRY(ref_offset_basic), + TEST_ENTRY(ref_offset_i64), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Reference Operation Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_strings.c b/other/tests/minimal/test_strings.c new file mode 100644 index 000000000..ea718f5cf --- /dev/null +++ b/other/tests/minimal/test_strings.c @@ -0,0 +1,205 @@ +/* + * Test string operations for HashLink AArch64 JIT + * + * Tests: OString, OBytes, string handling + */ +#include "test_harness.h" + +/* + * Test: Load a string constant and return its pointer + * + * op0: string r0, 0 ; r0 = "hello" + * op1: ret r0 ; return pointer + */ +TEST(load_string) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Setup string pool */ + c->nstrings = 1; + c->strings = (char**)malloc(sizeof(char*)); + c->strings[0] = "hello"; + c->strings_lens = (int*)malloc(sizeof(int)); + c->strings_lens[0] = 5; + c->ustrings = (uchar**)calloc(1, sizeof(uchar*)); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 0, NULL); + hl_type *regs[] = { &c->types[T_BYTES] }; + + hl_opcode ops[] = { + OP2(OString, 0, 0), /* op0: r0 = string[0] = "hello" */ + OP1(ORet, 0), /* op1: return r0 */ + }; + + test_alloc_function(c, 0, fn_type, 1, regs, 2, ops); + + int result; + uchar* (*fn)(void) = (uchar*(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + uchar *ret = fn(); + if (ret == NULL) { + fprintf(stderr, " Got NULL pointer\n"); + return TEST_FAIL; + } + + /* Check the string content - uchar is UTF-16, so each element is a 16-bit char */ + /* Note: uchar is 16-bit, so ret[0]='h', ret[1]='e', etc. for ASCII */ + if (ret[0] != 'h' || ret[1] != 'e' || ret[2] != 'l' || ret[3] != 'l' || ret[4] != 'o') { + fprintf(stderr, " String content mismatch: got 0x%04x 0x%04x 0x%04x 0x%04x 0x%04x\n", + ret[0], ret[1], ret[2], ret[3], ret[4]); + fprintf(stderr, " As chars: '%c' '%c' '%c' '%c' '%c'\n", + (char)(ret[0] & 0xFF), (char)(ret[1] & 0xFF), + (char)(ret[2] & 0xFF), (char)(ret[3] & 0xFF), (char)(ret[4] & 0xFF)); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Load two different strings + */ +TEST(load_two_strings) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Setup string pool */ + c->nstrings = 2; + c->strings = (char**)malloc(sizeof(char*) * 2); + c->strings[0] = "first"; + c->strings[1] = "second"; + c->strings_lens = (int*)malloc(sizeof(int) * 2); + c->strings_lens[0] = 5; + c->strings_lens[1] = 6; + c->ustrings = (uchar**)calloc(2, sizeof(uchar*)); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_BYTES], 0, NULL); + hl_type *regs[] = { &c->types[T_BYTES], &c->types[T_BYTES] }; + + hl_opcode ops[] = { + OP2(OString, 0, 0), /* op0: r0 = "first" */ + OP2(OString, 1, 1), /* op1: r1 = "second" */ + OP1(ORet, 1), /* op2: return r1 ("second") */ + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + uchar* (*fn)(void) = (uchar*(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + uchar *ret = fn(); + if (ret == NULL) { + fprintf(stderr, " Got NULL pointer\n"); + return TEST_FAIL; + } + + /* Should be "second" */ + if (ret[0] != 's' || ret[1] != 'e' || ret[2] != 'c') { + fprintf(stderr, " Expected 'second', got 0x%04x 0x%04x 0x%04x...\n", + ret[0], ret[1], ret[2]); + fprintf(stderr, " As chars: '%c' '%c' '%c'...\n", + (char)(ret[0] & 0xFF), (char)(ret[1] & 0xFF), (char)(ret[2] & 0xFF)); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Store string in dynobj and retrieve it + * This mimics what trace() does - store strings in dynamic object fields + * + * r0 = new DynObj + * r1 = "hello" + * dynset r0, fieldHash, r1 ; store string + * r2 = dynget r0, fieldHash ; retrieve string + * return r2 + */ +TEST(dynobj_string_roundtrip) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + /* Setup string pool - index 0 = field name "msg", index 1 = value "hello" */ + c->nstrings = 2; + c->strings = (char**)malloc(sizeof(char*) * 2); + c->strings[0] = "msg"; /* field name */ + c->strings[1] = "hello"; /* field value */ + c->strings_lens = (int*)malloc(sizeof(int) * 2); + c->strings_lens[0] = 3; + c->strings_lens[1] = 5; + c->ustrings = (uchar**)calloc(2, sizeof(uchar*)); + + /* Create HDYNOBJ type */ + if (c->ntypes >= MAX_TYPES) return TEST_FAIL; + int dynobj_idx = c->ntypes++; + c->types[dynobj_idx].kind = HDYNOBJ; + + /* Create HDYN type for the result */ + if (c->ntypes >= MAX_TYPES) return TEST_FAIL; + int dyn_idx = c->ntypes++; + c->types[dyn_idx].kind = HDYN; + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[dyn_idx], 0, NULL); + hl_type *regs[] = { + &c->types[dynobj_idx], /* r0: dynobj */ + &c->types[T_BYTES], /* r1: string "hello" */ + &c->types[dyn_idx], /* r2: retrieved value */ + }; + + hl_opcode ops[] = { + OP1(ONew, 0), /* r0 = new DynObj */ + OP2(OString, 1, 1), /* r1 = "hello" (string index 1) */ + OP3(ODynSet, 0, 0, 1), /* dynset r0, field[0]="msg", r1 */ + OP3(ODynGet, 2, 0, 0), /* r2 = dynget r0, field[0]="msg" */ + OP1(ORet, 2), /* return r2 */ + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 5, ops); + + int result; + vdynamic* (*fn)(void) = (vdynamic*(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + vdynamic *ret = fn(); + if (ret == NULL) { + fprintf(stderr, " Got NULL vdynamic\n"); + return TEST_FAIL; + } + + /* The returned value should be a string wrapped in vdynamic */ + /* For HBYTES, v.ptr points to the UTF-16 string */ + uchar *str = (uchar*)ret->v.ptr; + if (str == NULL) { + fprintf(stderr, " Got NULL string pointer in vdynamic\n"); + return TEST_FAIL; + } + + if (str[0] != 'h' || str[1] != 'e' || str[2] != 'l' || str[3] != 'l' || str[4] != 'o') { + fprintf(stderr, " String mismatch: got '%c%c%c%c%c'\n", + (char)(str[0] & 0xFF), (char)(str[1] & 0xFF), + (char)(str[2] & 0xFF), (char)(str[3] & 0xFF), (char)(str[4] & 0xFF)); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(load_string), + TEST_ENTRY(load_two_strings), + TEST_ENTRY(dynobj_string_roundtrip), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - String Operations Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_switch.c b/other/tests/minimal/test_switch.c new file mode 100644 index 000000000..317b28828 --- /dev/null +++ b/other/tests/minimal/test_switch.c @@ -0,0 +1,367 @@ +/* + * Test switch operations for HashLink AArch64 JIT + * + * Tests: OSwitch + * + * OSwitch: switch(value) { case 0: ..., case 1: ..., ... } + * Parameters: + * p1 = register containing value to switch on + * p2 = number of cases + * extra[i] = jump offset for case i (relative to opcode after switch) + */ +#include "test_harness.h" + +/* + * Test: OSwitch with 3 cases + * + * switch(value) { + * case 0: return 10; + * case 1: return 20; + * case 2: return 30; + * default: return 0; + * } + */ +TEST(switch_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 20, 30, 0 }; /* return values for each case */ + test_init_ints(c, 4, ints); + + /* Function type: (i32) -> i32 */ + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = input value */ + &c->types[T_I32], /* r1 = return value */ + }; + + /* + * Opcodes layout (default case must be immediately after switch): + * 0: OSwitch r0, 3, [2, 4, 6] ; switch on r0 with 3 cases + * 1: OInt r1, $3 ; default: r1 = 0 (fall through lands here) + * 2: ORet r1 + * 3: OInt r1, $0 ; case 0: r1 = 10 (offset 2 -> opcode 3) + * 4: ORet r1 + * 5: OInt r1, $1 ; case 1: r1 = 20 (offset 4 -> opcode 5) + * 6: ORet r1 + * 7: OInt r1, $2 ; case 2: r1 = 30 (offset 6 -> opcode 7) + * 8: ORet r1 + * + * Jump offsets from opcode 1 (after switch): + * case 0: offset 2 -> opcode 3 + * case 1: offset 4 -> opcode 5 + * case 2: offset 6 -> opcode 7 + */ + static int switch_offsets[] = { 2, 4, 6 }; + hl_opcode ops[] = { + { OSwitch, 0, 3, 0, switch_offsets }, /* switch r0, 3 cases */ + OP2(OInt, 1, 3), /* default: r1 = 0 */ + OP1(ORet, 1), + OP2(OInt, 1, 0), /* case 0: r1 = 10 */ + OP1(ORet, 1), + OP2(OInt, 1, 1), /* case 1: r1 = 20 */ + OP1(ORet, 1), + OP2(OInt, 1, 2), /* case 2: r1 = 30 */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 9, ops); + + int result; + int (*fn)(int) = (int(*)(int))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* Test all cases */ + int test_cases[][2] = { + { 0, 10 }, + { 1, 20 }, + { 2, 30 }, + { 3, 0 }, /* default */ + { 100, 0 }, /* default */ + { -1, 0 }, /* default */ + }; + + for (int i = 0; i < 6; i++) { + int input = test_cases[i][0]; + int expected = test_cases[i][1]; + int got = fn(input); + if (got != expected) { + fprintf(stderr, " switch(%d): expected %d, got %d\n", input, expected, got); + return TEST_FAIL; + } + } + + return TEST_PASS; +} + +/* + * Test: OSwitch with single case + */ +TEST(switch_single_case) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42, 0 }; + test_init_ints(c, 2, ints); + + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + }; + + /* Default immediately after switch, case 0 at offset 2 */ + static int switch_offsets[] = { 2 }; + hl_opcode ops[] = { + { OSwitch, 0, 1, 0, switch_offsets }, /* switch r0, 1 case */ + OP2(OInt, 1, 1), /* default: r1 = 0 (fall through) */ + OP1(ORet, 1), + OP2(OInt, 1, 0), /* case 0: r1 = 42 (offset 2) */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 5, ops); + + int result; + int (*fn)(int) = (int(*)(int))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + if (fn(0) != 42) { + fprintf(stderr, " switch(0): expected 42, got %d\n", fn(0)); + return TEST_FAIL; + } + + if (fn(1) != 0) { + fprintf(stderr, " switch(1): expected 0 (default), got %d\n", fn(1)); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OSwitch where all cases jump to same target + */ +TEST(switch_same_target) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 42, 0 }; + test_init_ints(c, 2, ints); + + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + }; + + /* Default at opcode 1, all 3 cases jump to offset 2 (opcode 3) */ + static int switch_offsets[] = { 2, 2, 2 }; + hl_opcode ops[] = { + { OSwitch, 0, 3, 0, switch_offsets }, /* switch r0, 3 cases all going to same place */ + OP2(OInt, 1, 1), /* default: r1 = 0 (fall through) */ + OP1(ORet, 1), + OP2(OInt, 1, 0), /* case 0,1,2: r1 = 42 (offset 2) */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 5, ops); + + int result; + int (*fn)(int) = (int(*)(int))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* Cases 0, 1, 2 should all return 42 */ + for (int i = 0; i < 3; i++) { + if (fn(i) != 42) { + fprintf(stderr, " switch(%d): expected 42, got %d\n", i, fn(i)); + return TEST_FAIL; + } + } + + /* Anything else is default (0) */ + if (fn(5) != 0) { + fprintf(stderr, " switch(5): expected 0, got %d\n", fn(5)); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OSwitch with fallthrough pattern (consecutive cases) + * + * switch(value) { + * case 0: + * case 1: + * return 10; // cases 0 and 1 both return 10 + * case 2: + * return 20; + * default: + * return 0; + * } + */ +TEST(switch_fallthrough) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 20, 0 }; + test_init_ints(c, 3, ints); + + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + }; + + /* Default at opcode 1, case 0,1 at offset 2, case 2 at offset 4 */ + static int switch_offsets[] = { 2, 2, 4 }; + hl_opcode ops[] = { + { OSwitch, 0, 3, 0, switch_offsets }, + OP2(OInt, 1, 2), /* default: r1 = 0 (fall through) */ + OP1(ORet, 1), + OP2(OInt, 1, 0), /* case 0,1: r1 = 10 (offset 2) */ + OP1(ORet, 1), + OP2(OInt, 1, 1), /* case 2: r1 = 20 (offset 4) */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 7, ops); + + int result; + int (*fn)(int) = (int(*)(int))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + if (fn(0) != 10 || fn(1) != 10) { + fprintf(stderr, " Cases 0,1 should return 10\n"); + return TEST_FAIL; + } + + if (fn(2) != 20) { + fprintf(stderr, " Case 2 should return 20, got %d\n", fn(2)); + return TEST_FAIL; + } + + if (fn(3) != 0) { + fprintf(stderr, " Default should return 0, got %d\n", fn(3)); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OSwitch with computation after switch + * + * This tests that control flow properly resumes after switch. + * OLabel opcodes are required at jump targets to discard register bindings. + */ +TEST(switch_with_computation) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 10, 20, 100 }; + test_init_ints(c, 3, ints); + + hl_type *arg_types[] = { &c->types[T_I32] }; + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 1, arg_types); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = input */ + &c->types[T_I32], /* r1 = multiplier from switch */ + &c->types[T_I32], /* r2 = base value */ + &c->types[T_I32], /* r3 = result */ + }; + + /* + * Default after switch, cases jump past default to their handlers. + * Then all paths converge to common continuation. + * OLabel is required at each jump target. + * + * 0: OSwitch r0, 2, [2, 5] ; case 0->op3, case 1->op6 + * 1: OInt r1, 100 ; default: r1 = 100 + * 2: OJAlways 5 ; default jumps to op 8 (continuation) + * 3: OLabel ; case 0 target + * 4: OInt r1, 10 ; case 0: r1 = 10 + * 5: OJAlways 2 ; case 0 jumps to op 8 + * 6: OLabel ; case 1 target + * 7: OInt r1, 20 ; case 1: r1 = 20, falls through + * 8: OLabel ; continuation (merge point) + * 9: OInt r2, 100 ; r2 = 100 + * 10: OAdd r3, r1, r2 + * 11: ORet r3 + */ + static int switch_offsets[] = { 2, 5 }; + hl_opcode ops[] = { + { OSwitch, 0, 2, 0, switch_offsets }, + OP2(OInt, 1, 2), /* default: r1 = 100 */ + OP2(OJAlways, 5, 0), /* default jumps to continuation (op 8) */ + OP0(OLabel), /* case 0 target */ + OP2(OInt, 1, 0), /* case 0: r1 = 10 */ + OP2(OJAlways, 2, 0), /* case 0 jumps to continuation (op 8) */ + OP0(OLabel), /* case 1 target */ + OP2(OInt, 1, 1), /* case 1: r1 = 20, falls through */ + OP0(OLabel), /* continuation (merge point) */ + OP2(OInt, 2, 2), /* r2 = 100 */ + OP3(OAdd, 3, 1, 2), /* r3 = r1 + r2 */ + OP1(ORet, 3), + }; + + test_alloc_function(c, 0, fn_type, 4, regs, 12, ops); + + int result; + int (*fn)(int) = (int(*)(int))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + /* case 0: 10 + 100 = 110 */ + if (fn(0) != 110) { + fprintf(stderr, " switch(0): expected 110, got %d\n", fn(0)); + return TEST_FAIL; + } + + /* case 1: 20 + 100 = 120 */ + if (fn(1) != 120) { + fprintf(stderr, " switch(1): expected 120, got %d\n", fn(1)); + return TEST_FAIL; + } + + /* default: 100 + 100 = 200 */ + if (fn(5) != 200) { + fprintf(stderr, " switch(5) default: expected 200, got %d\n", fn(5)); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(switch_basic), + TEST_ENTRY(switch_single_case), + TEST_ENTRY(switch_same_target), + TEST_ENTRY(switch_fallthrough), + TEST_ENTRY(switch_with_computation), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Switch Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_type_ops.c b/other/tests/minimal/test_type_ops.c new file mode 100644 index 000000000..6a26b8245 --- /dev/null +++ b/other/tests/minimal/test_type_ops.c @@ -0,0 +1,290 @@ +/* + * Test type operations for HashLink AArch64 JIT + * + * Tests: OType, OGetType, OGetTID, OSafeCast, OUnsafeCast, OToUFloat + * + * OType: load a type pointer from the types array + * OGetType: get the runtime type of an object + * OGetTID: get the type ID (first 4 bytes) of an object + * OSafeCast: safe dynamic cast with runtime check + * OUnsafeCast: unchecked type cast + * OToUFloat: convert unsigned int to float + */ +#include "test_harness.h" + +/* Helper to create an object type */ +static hl_type *create_obj_type(hl_code *c, const char *name) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types\n"); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HOBJ; + t->obj = (hl_type_obj*)calloc(1, sizeof(hl_type_obj)); + t->obj->name = (uchar*)name; + t->obj->nfields = 0; + t->obj->nproto = 0; + t->obj->nbindings = 0; + + return t; +} + +/* + * Test: OType - load type pointer + * + * The type pointer should be non-null and have the correct kind. + * We use a native to verify the type kind. + */ +static int verify_type_kind(hl_type *t, int expected_kind) { + if (t == NULL) return 0; + return (t->kind == expected_kind) ? 1 : 0; +} + +TEST(type_load) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { HI32 }; /* expected kind */ + test_init_ints(c, 1, ints); + + /* Native: verify_type_kind(type, kind) -> i32 */ + hl_type *verify_args[] = { &c->types[T_TYPE], &c->types[T_I32] }; + hl_type *verify_fn_type = test_alloc_fun_type(c, &c->types[T_I32], 2, verify_args); + test_add_native(c, 1, "test", "verify_type_kind", verify_fn_type, (void*)verify_type_kind); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_TYPE], /* r0 = type pointer */ + &c->types[T_I32], /* r1 = expected kind */ + &c->types[T_I32], /* r2 = result */ + }; + + hl_opcode ops[] = { + OP2(OType, 0, T_I32), /* r0 = &types[T_I32] */ + OP2(OInt, 1, 0), /* r1 = HI32 */ + OP4_CALL2(OCall2, 2, 1, 0, 1), /* r2 = verify_type_kind(r0, r1) */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 1) { + fprintf(stderr, " Type verification failed\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OToUFloat - convert unsigned int to float + * + * This is important for correctly converting large unsigned values. + * 0xFFFFFFFF as unsigned is 4294967295, not -1. + */ +TEST(to_ufloat_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 1000000 }; /* 1 million */ + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = unsigned value */ + &c->types[T_F64], /* r1 = float result */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 1000000 */ + OP2(OToUFloat, 1, 0), /* r1 = (float)r0 unsigned */ + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + double expected = 1000000.0; + double diff = ret - expected; + if (diff < 0) diff = -diff; + if (diff > 0.1) { + fprintf(stderr, " Expected %f, got %f\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OToUFloat with large unsigned value + * + * 0x80000000 (2147483648) - would be negative if signed + */ +TEST(to_ufloat_large) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { (int)0x80000000 }; /* 2^31 as unsigned */ + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_F64], + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OToUFloat, 1, 0), + OP1(ORet, 1), + }; + + test_alloc_function(c, 0, fn_type, 2, regs, 3, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + double expected = 2147483648.0; /* 2^31 */ + double diff = ret - expected; + if (diff < 0) diff = -diff; + if (diff > 1.0) { + fprintf(stderr, " Expected %f, got %f\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OUnsafeCast - reinterpret type without checks + * + * Cast an i64 to bytes (pointer type) and back. + */ +TEST(unsafe_cast_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 12345 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I64], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I64], /* r0 = original value */ + &c->types[T_BYTES], /* r1 = cast to bytes (pointer) */ + &c->types[T_I64], /* r2 = cast back to i64 */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 12345 */ + OP2(OUnsafeCast, 1, 0), /* r1 = (bytes)r0 */ + OP2(OUnsafeCast, 2, 1), /* r2 = (i64)r1 */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int64_t (*fn)(void) = (int64_t(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int64_t ret = fn(); + if (ret != 12345) { + fprintf(stderr, " Expected 12345, got %ld\n", (long)ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OToSFloat vs OToUFloat comparison + * + * -1 converted: + * ToSFloat: -1.0 + * ToUFloat: 4294967295.0 + */ +TEST(tofloat_signed_vs_unsigned) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { -1 }; + test_init_ints(c, 1, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_F64], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = -1 */ + &c->types[T_F64], /* r1 = signed float */ + &c->types[T_F64], /* r2 = unsigned float */ + &c->types[T_F64], /* r3 = difference */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = -1 */ + OP2(OToSFloat, 1, 0), /* r1 = (float)r0 signed = -1.0 */ + OP2(OToUFloat, 2, 0), /* r2 = (float)r0 unsigned = 4294967295.0 */ + OP3(OSub, 3, 2, 1), /* r3 = r2 - r1 */ + OP1(ORet, 3), + }; + + test_alloc_function(c, 0, fn_type, 4, regs, 5, ops); + + int result; + double (*fn)(void) = (double(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + double ret = fn(); + /* unsigned(-1) - signed(-1) = 4294967295.0 - (-1.0) = 4294967296.0 */ + double expected = 4294967296.0; + double diff = ret - expected; + if (diff < 0) diff = -diff; + if (diff > 1.0) { + fprintf(stderr, " Expected %f, got %f\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(type_load), + TEST_ENTRY(to_ufloat_basic), + TEST_ENTRY(to_ufloat_large), + TEST_ENTRY(unsafe_cast_basic), + TEST_ENTRY(tofloat_signed_vs_unsigned), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Type Operation Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_unsigned_ops.c b/other/tests/minimal/test_unsigned_ops.c new file mode 100644 index 000000000..ef44e4d6a --- /dev/null +++ b/other/tests/minimal/test_unsigned_ops.c @@ -0,0 +1,359 @@ +/* + * Test unsigned operations for HashLink AArch64 JIT + * + * Tests: OUDiv, OUMod, OUShr + * + * These opcodes perform unsigned arithmetic: + * OUDiv: unsigned division + * OUMod: unsigned modulo + * OUShr: unsigned (logical) right shift + */ +#include "test_harness.h" + +/* + * Test: OUDiv - unsigned division + * + * 100 / 3 = 33 (unsigned) + */ +TEST(udiv_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 100, 3 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = dividend */ + &c->types[T_I32], /* r1 = divisor */ + &c->types[T_I32], /* r2 = result */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 100 */ + OP2(OInt, 1, 1), /* r1 = 3 */ + OP3(OUDiv, 2, 0, 1), /* r2 = r0 / r1 (unsigned) */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 33) { + fprintf(stderr, " Expected 33, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OUDiv with large unsigned values + * + * When treating -1 as unsigned int32, it's 0xFFFFFFFF = 4294967295 + * 4294967295 / 2 = 2147483647 (unsigned division) + * + * With signed division, -1 / 2 = 0 + */ +TEST(udiv_large_unsigned) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { -1, 2 }; /* -1 as unsigned is 0xFFFFFFFF */ + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = -1 (0xFFFFFFFF as unsigned) */ + OP2(OInt, 1, 1), /* r1 = 2 */ + OP3(OUDiv, 2, 0, 1), /* r2 = 0xFFFFFFFF / 2 = 0x7FFFFFFF */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + unsigned int (*fn)(void) = (unsigned int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + unsigned int ret = fn(); + unsigned int expected = 0x7FFFFFFF; /* 2147483647 */ + if (ret != expected) { + fprintf(stderr, " Expected %u, got %u\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OUMod - unsigned modulo + * + * 100 % 3 = 1 + */ +TEST(umod_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 100, 3 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 100 */ + OP2(OInt, 1, 1), /* r1 = 3 */ + OP3(OUMod, 2, 0, 1), /* r2 = r0 % r1 (unsigned) */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 1) { + fprintf(stderr, " Expected 1, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OUMod with large unsigned values + * + * 0xFFFFFFFF % 7 = 4294967295 % 7 = 3 + */ +TEST(umod_large_unsigned) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { -1, 7 }; /* -1 as unsigned is 0xFFFFFFFF */ + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), + OP2(OInt, 1, 1), + OP3(OUMod, 2, 0, 1), + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + unsigned int (*fn)(void) = (unsigned int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + unsigned int ret = fn(); + unsigned int expected = 0xFFFFFFFF % 7; /* 4294967295 % 7 = 3 */ + if (ret != expected) { + fprintf(stderr, " Expected %u, got %u\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OUShr - unsigned (logical) right shift + * + * 0xFF000000 >> 8 (logical) = 0x00FF0000 + * + * Signed shift would sign-extend: 0xFFFF0000 + */ +TEST(ushr_basic) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { (int)0xFF000000, 8 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], + &c->types[T_I32], + &c->types[T_I32], + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 0xFF000000 */ + OP2(OInt, 1, 1), /* r1 = 8 */ + OP3(OUShr, 2, 0, 1), /* r2 = r0 >>> r1 (logical shift) */ + OP1(ORet, 2), + }; + + test_alloc_function(c, 0, fn_type, 3, regs, 4, ops); + + int result; + unsigned int (*fn)(void) = (unsigned int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + unsigned int ret = fn(); + unsigned int expected = 0x00FF0000; + if (ret != expected) { + fprintf(stderr, " Expected 0x%08X, got 0x%08X\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OUShr vs OSShr - compare unsigned vs signed shift + * + * -1 (0xFFFFFFFF) >> 16: + * - Unsigned: 0x0000FFFF + * - Signed: 0xFFFFFFFF (sign-extended) + */ +TEST(ushr_vs_sshr) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { -1, 16 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = value */ + &c->types[T_I32], /* r1 = shift amount */ + &c->types[T_I32], /* r2 = unsigned result */ + &c->types[T_I32], /* r3 = signed result */ + &c->types[T_I32], /* r4 = difference */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = -1 */ + OP2(OInt, 1, 1), /* r1 = 16 */ + OP3(OUShr, 2, 0, 1), /* r2 = unsigned shift */ + OP3(OSShr, 3, 0, 1), /* r3 = signed shift */ + OP3(OSub, 4, 2, 3), /* r4 = r2 - r3 */ + OP1(ORet, 4), /* return difference */ + }; + + test_alloc_function(c, 0, fn_type, 5, regs, 6, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + /* + * UShr: 0xFFFFFFFF >>> 16 = 0x0000FFFF = 65535 + * SShr: 0xFFFFFFFF >> 16 = 0xFFFFFFFF = -1 + * Difference: 65535 - (-1) = 65536 + */ + int expected = 65536; + if (ret != expected) { + fprintf(stderr, " Expected %d, got %d\n", expected, ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: OUDiv and OUMod together - verify quotient * divisor + remainder = dividend + */ +TEST(udiv_umod_combined) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + test_init_base_types(c); + + int ints[] = { 12345, 67 }; + test_init_ints(c, 2, ints); + + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + hl_type *regs[] = { + &c->types[T_I32], /* r0 = dividend */ + &c->types[T_I32], /* r1 = divisor */ + &c->types[T_I32], /* r2 = quotient */ + &c->types[T_I32], /* r3 = remainder */ + &c->types[T_I32], /* r4 = quotient * divisor */ + &c->types[T_I32], /* r5 = reconstructed dividend */ + }; + + hl_opcode ops[] = { + OP2(OInt, 0, 0), /* r0 = 12345 */ + OP2(OInt, 1, 1), /* r1 = 67 */ + OP3(OUDiv, 2, 0, 1), /* r2 = 12345 / 67 = 184 */ + OP3(OUMod, 3, 0, 1), /* r3 = 12345 % 67 = 17 */ + OP3(OMul, 4, 2, 1), /* r4 = 184 * 67 = 12328 */ + OP3(OAdd, 5, 4, 3), /* r5 = 12328 + 17 = 12345 */ + OP1(ORet, 5), + }; + + test_alloc_function(c, 0, fn_type, 6, regs, 7, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 12345) { + fprintf(stderr, " Expected 12345, got %d\n", ret); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test list */ +static test_entry_t tests[] = { + TEST_ENTRY(udiv_basic), + TEST_ENTRY(udiv_large_unsigned), + TEST_ENTRY(umod_basic), + TEST_ENTRY(umod_large_unsigned), + TEST_ENTRY(ushr_basic), + TEST_ENTRY(ushr_vs_sshr), + TEST_ENTRY(udiv_umod_combined), +}; + +int main(int argc, char **argv) { + printf("HashLink AArch64 JIT - Unsigned Operation Tests\n"); + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/other/tests/minimal/test_virtual_fields.c b/other/tests/minimal/test_virtual_fields.c new file mode 100644 index 000000000..593c9e332 --- /dev/null +++ b/other/tests/minimal/test_virtual_fields.c @@ -0,0 +1,357 @@ +/* + * Test HVIRTUAL field access with different sizes for HashLink AArch64 JIT + * + * This tests the fix for a bug where OSetField/OField for HVIRTUAL objects + * would always use 64-bit load/store instructions regardless of the actual + * field size. This caused adjacent fields to be corrupted. + * + * The bug manifested when storing a 32-bit integer to a vfield - the 64-bit + * store would zero out the adjacent field's memory. + */ +#include "test_harness.h" + +/* Extended type indices for this test */ +#define T_UI8 8 +#define T_UI16 9 + +/* Initialize types including smaller integer types */ +static void init_extended_types(hl_code *c) { + test_init_base_types(c); + + /* Add HUI8 */ + c->types[T_UI8].kind = HUI8; + + /* Add HUI16 */ + c->types[T_UI16].kind = HUI16; + + c->ntypes = 10; +} + +/* Helper to get type size (simplified version of hl_type_size) */ +static int get_type_size(hl_type *t) { + switch (t->kind) { + case HUI8: case HBOOL: return 1; + case HUI16: return 2; + case HI32: case HF32: return 4; + case HI64: case HF64: return 8; + default: return sizeof(void*); /* Pointers */ + } +} + +/* Helper to calculate alignment padding */ +static int pad_struct(int size, hl_type *t) { + int align; + switch (t->kind) { + case HVOID: return 0; + case HUI8: case HBOOL: align = 1; break; + case HUI16: align = 2; break; + case HI32: case HF32: align = 4; break; + case HI64: case HF64: align = 8; break; + default: align = sizeof(void*); break; /* Pointers */ + } + return (-size) & (align - 1); +} + +/* Helper to create an HVIRTUAL type */ +static hl_type *create_virtual_type(hl_code *c, int nfields, hl_type **field_types) { + if (c->ntypes >= MAX_TYPES) { + fprintf(stderr, "Too many types\n"); + return NULL; + } + + int idx = c->ntypes++; + hl_type *t = &c->types[idx]; + memset(t, 0, sizeof(hl_type)); + + t->kind = HVIRTUAL; + t->virt = (hl_type_virtual*)calloc(1, sizeof(hl_type_virtual)); + t->virt->nfields = nfields; + + if (nfields > 0) { + t->virt->fields = (hl_obj_field*)calloc(nfields, sizeof(hl_obj_field)); + t->virt->indexes = (int*)calloc(nfields, sizeof(int)); + + /* Calculate field layout (matching hl_init_virtual logic) */ + int vsize = sizeof(vvirtual) + sizeof(void*) * nfields; + int size = vsize; + + for (int i = 0; i < nfields; i++) { + char *name = (char*)malloc(16); + sprintf(name, "field%d", i); + t->virt->fields[i].name = (uchar*)name; + t->virt->fields[i].t = field_types[i]; + t->virt->fields[i].hashed_name = i + 1000; /* Unique hash */ + + /* Add alignment padding */ + size += pad_struct(size, field_types[i]); + t->virt->indexes[i] = size; + size += get_type_size(field_types[i]); + } + + t->virt->dataSize = size - vsize; + } + + return t; +} + +/* + * Test: HVIRTUAL with adjacent i32 fields + * + * This tests the core bug: storing to one i32 field should not corrupt + * the adjacent i32 field. + * + * struct { i32 a; i32 b; } + * + * r0 = new Virtual + * r1 = 0xDEADBEEF + * r2 = 0xCAFEBABE + * set_field r0.field[0] = r1 (a = 0xDEADBEEF) + * set_field r0.field[1] = r2 (b = 0xCAFEBABE) + * r3 = get_field r0.field[0] (read a - should still be 0xDEADBEEF) + * return r3 + */ +TEST(virtual_adjacent_i32_fields) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + init_extended_types(c); + + int ints[] = { (int)0xDEADBEEF, (int)0xCAFEBABE }; + test_init_ints(c, 2, ints); + + /* Create HVIRTUAL type with two i32 fields */ + hl_type *field_types[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_type *virt_type = create_virtual_type(c, 2, field_types); + + /* Function: () -> i32 */ + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* Registers: r0=virtual, r1=i32, r2=i32, r3=i32 */ + hl_type *regs[] = { virt_type, &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP1(ONew, 0), /* r0 = new Virtual */ + OP2(OInt, 1, 0), /* r1 = 0xDEADBEEF */ + OP2(OInt, 2, 1), /* r2 = 0xCAFEBABE */ + OP3(OSetField, 0, 0, 1), /* r0.field[0] = r1 */ + OP3(OSetField, 0, 1, 2), /* r0.field[1] = r2 */ + OP3(OField, 3, 0, 0), /* r3 = r0.field[0] */ + OP1(ORet, 3), + }; + + test_alloc_function(c, 0, fn_type, 4, regs, 7, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != (int)0xDEADBEEF) { + fprintf(stderr, " Expected 0xDEADBEEF, got 0x%X\n", ret); + fprintf(stderr, " (Adjacent field store corrupted first field)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: HVIRTUAL with mixed size fields (i32 followed by pointer) + * + * This is the exact scenario from the bug report: an i32 field followed + * by a pointer field. The i32 store with 64-bit instruction would zero + * out the adjacent pointer. + * + * struct { i32 a; ptr b; } + * + * r0 = new Virtual (the struct) + * r1 = 42 + * r2 = new Virtual (a non-null pointer to use as field value) + * set_field r0.field[1] = r2 (b = pointer) - SET SECOND FIELD FIRST + * set_field r0.field[0] = r1 (a = 42) - BUG: 64-bit store would zero b! + * r3 = get_field r0.field[1] (read b - should still be the pointer) + * return r3 + */ +TEST(virtual_i32_then_pointer) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + init_extended_types(c); + + int ints[] = { 42 }; + test_init_ints(c, 1, ints); + + /* Create a simple virtual type to use as a pointer value */ + hl_type *empty_field_types[] = { &c->types[T_I32] }; + hl_type *ptr_virt_type = create_virtual_type(c, 1, empty_field_types); + + /* Create HVIRTUAL type: { i32, virtual (pointer) } */ + hl_type *field_types[] = { &c->types[T_I32], ptr_virt_type }; + hl_type *virt_type = create_virtual_type(c, 2, field_types); + + /* Function: () -> virtual (pointer) */ + hl_type *fn_type = test_alloc_fun_type(c, ptr_virt_type, 0, NULL); + + /* Registers: r0=struct virtual, r1=i32, r2=ptr virtual, r3=ptr virtual */ + hl_type *regs[] = { virt_type, &c->types[T_I32], ptr_virt_type, ptr_virt_type }; + + /* + * We use ONew on r2 which has type ptr_virt_type (HVIRTUAL), + * which can be new'd, giving us a non-null pointer. + */ + hl_opcode ops[] = { + OP1(ONew, 0), /* r0 = new Virtual (the struct) */ + OP1(ONew, 2), /* r2 = new Virtual (a non-null pointer value) */ + OP2(OInt, 1, 0), /* r1 = 42 */ + OP3(OSetField, 0, 1, 2), /* r0.field[1] = r2 (set pointer FIRST) */ + OP3(OSetField, 0, 0, 1), /* r0.field[0] = r1 (BUG: would corrupt field[1]) */ + OP3(OField, 3, 0, 1), /* r3 = r0.field[1] (read back pointer) */ + OP1(ORet, 3), /* return r3 */ + }; + + test_alloc_function(c, 0, fn_type, 4, regs, 7, ops); + + int result; + void *(*fn)(void) = (void*(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + void *ret = fn(); + if (ret == NULL) { + fprintf(stderr, " Expected non-null pointer, got NULL\n"); + fprintf(stderr, " (i32 store corrupted adjacent pointer field)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: HVIRTUAL with multiple i32 fields - verify no corruption + * + * struct { i32 a; i32 b; i32 c; i32 d; } + * + * Set all fields to different values, then read them all back. + * Any corruption will show up as wrong values. + */ +TEST(virtual_multiple_i32_fields) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + init_extended_types(c); + + int ints[] = { 111, 222, 333, 444 }; + test_init_ints(c, 4, ints); + + /* Create HVIRTUAL type with four i32 fields */ + hl_type *field_types[] = { &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32] }; + hl_type *virt_type = create_virtual_type(c, 4, field_types); + + /* Function: () -> i32 */ + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* Registers: r0=virtual, r1-r4=i32, r5=i32(result), r6=i32(temp) */ + hl_type *regs[] = { virt_type, + &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32], + &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP1(ONew, 0), /* r0 = new Virtual */ + OP2(OInt, 1, 0), /* r1 = 111 */ + OP2(OInt, 2, 1), /* r2 = 222 */ + OP2(OInt, 3, 2), /* r3 = 333 */ + OP2(OInt, 4, 3), /* r4 = 444 */ + OP3(OSetField, 0, 0, 1), /* r0.field[0] = 111 */ + OP3(OSetField, 0, 1, 2), /* r0.field[1] = 222 */ + OP3(OSetField, 0, 2, 3), /* r0.field[2] = 333 */ + OP3(OSetField, 0, 3, 4), /* r0.field[3] = 444 */ + /* Read back field[0] - should be 111 */ + OP3(OField, 5, 0, 0), /* r5 = r0.field[0] */ + OP1(ORet, 5), + }; + + test_alloc_function(c, 0, fn_type, 7, regs, 11, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 111) { + fprintf(stderr, " Expected 111, got %d\n", ret); + fprintf(stderr, " (Field corruption detected)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* + * Test: Read back second field after setting first + * + * Same as above but read field[1] to verify it wasn't corrupted + * by the field[0] store. + */ +TEST(virtual_read_second_field) { + test_init_runtime(); + + hl_code *c = test_alloc_code(); + init_extended_types(c); + + int ints[] = { 111, 222 }; + test_init_ints(c, 2, ints); + + /* Create HVIRTUAL type with two i32 fields */ + hl_type *field_types[] = { &c->types[T_I32], &c->types[T_I32] }; + hl_type *virt_type = create_virtual_type(c, 2, field_types); + + /* Function: () -> i32 */ + hl_type *fn_type = test_alloc_fun_type(c, &c->types[T_I32], 0, NULL); + + /* Registers: r0=virtual, r1=i32, r2=i32, r3=i32 */ + hl_type *regs[] = { virt_type, &c->types[T_I32], &c->types[T_I32], &c->types[T_I32] }; + + hl_opcode ops[] = { + OP1(ONew, 0), /* r0 = new Virtual */ + OP2(OInt, 1, 0), /* r1 = 111 */ + OP2(OInt, 2, 1), /* r2 = 222 */ + OP3(OSetField, 0, 1, 2), /* r0.field[1] = 222 (SET SECOND FIRST) */ + OP3(OSetField, 0, 0, 1), /* r0.field[0] = 111 (this would corrupt field[1]) */ + OP3(OField, 3, 0, 1), /* r3 = r0.field[1] (read back second field) */ + OP1(ORet, 3), + }; + + test_alloc_function(c, 0, fn_type, 4, regs, 7, ops); + + int result; + int (*fn)(void) = (int(*)(void))test_jit_compile(c, &result); + if (result != TEST_PASS) return result; + + int ret = fn(); + if (ret != 222) { + fprintf(stderr, " Expected 222, got %d\n", ret); + fprintf(stderr, " (field[0] store corrupted field[1] - the bug!)\n"); + return TEST_FAIL; + } + + return TEST_PASS; +} + +/* Test registry */ +static test_entry_t tests[] = { + TEST_ENTRY(virtual_adjacent_i32_fields), + TEST_ENTRY(virtual_i32_then_pointer), + TEST_ENTRY(virtual_multiple_i32_fields), + TEST_ENTRY(virtual_read_second_field), +}; + +/* Main test runner */ +int main(int argc, char **argv) { + (void)argc; (void)argv; + + printf("HashLink AArch64 JIT - HVIRTUAL Field Size Tests\n"); + printf("Testing fix for 64-bit store corrupting adjacent fields\n\n"); + + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/src/hl.h b/src/hl.h index 30bcdf59c..d76d8281c 100644 --- a/src/hl.h +++ b/src/hl.h @@ -93,10 +93,14 @@ # define HL_BSD #endif -#if defined(_64BITS) || defined(__x86_64__) || defined(_M_X64) || defined(__LP64__) || defined(__wasm64__) +#if defined(_64BITS) || defined(__x86_64__) || defined(_M_X64) || defined(__LP64__) || defined(__wasm64__) || defined(__aarch64__) # define HL_64 #endif +#if defined(__aarch64__) || defined(_M_ARM64) +# define HL_ARM64 +#endif + #if defined(__GNUC__) # define HL_GCC #endif @@ -882,6 +886,8 @@ typedef struct { int length; } vstring; +HL_API int hl_str_cmp( vstring *a, vstring *b ); + #define DEFINE_PRIM(t,name,args) DEFINE_PRIM_WITH_NAME(t,name,args,name) #define _DEFINE_PRIM_WITH_NAME(t,name,args,realName) C_FUNCTION_BEGIN EXPORT void *hlp_##realName( const char **sign ) { *sign = _FUN(t,args); return (void*)(&HL_NAME(name)); } C_FUNCTION_END diff --git a/src/jit_aarch64.c b/src/jit_aarch64.c new file mode 100644 index 000000000..69e1aac99 --- /dev/null +++ b/src/jit_aarch64.c @@ -0,0 +1,6807 @@ +/* + * Copyright (C)2015-2016 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/* + * AArch64 JIT Backend + * + * This file contains the AArch64-specific implementation of the HashLink JIT compiler. + * It translates HashLink bytecode into native AArch64 machine code. + * + * Key differences from x86: + * - Fixed 32-bit instruction encoding (vs variable-length on x86) + * - Load/store architecture (no direct memory operands in ALU ops) + * - AAPCS64 calling convention (8 register args vs 4/6 on x86-64) + * - Condition codes with branch instructions (no conditional moves in base ISA) + * - PC-relative addressing with ADRP/ADD for globals + */ + +#if !defined(__aarch64__) && !defined(_M_ARM64) +# error "This file is for AArch64 architecture only. Use jit_x86.c for x86/x64." +#endif + +#include +#include +#include +#include "jit_common.h" +#include "jit_aarch64_emit.h" +#include "hlsystem.h" + +// Helper for LDR/STR scaled offset from struct field +#define FIELD_OFFSET_SCALED(type, field) (offsetof(type, field) / 8) + +// ============================================================================ +// AArch64 Register Configuration (AAPCS64) +// ============================================================================ + +/* + * AAPCS64 (ARM Architecture Procedure Call Standard for ARM64) + * + * Register Usage: + * - X0-X7: Argument/result registers (caller-saved) + * - X8: Indirect result location register (caller-saved) + * - X9-X15: Temporary registers (caller-saved) + * - X16-X17: Intra-procedure-call temporary registers (caller-saved) + * - X18: Platform register (avoid use - may be reserved by OS) + * - X19-X28: Callee-saved registers + * - X29: Frame pointer (FP) + * - X30: Link register (LR) + * - SP: Stack pointer (must be 16-byte aligned) + * + * FP/SIMD Registers: + * - V0-V7: Argument/result registers (caller-saved) + * - V8-V15: Callee-saved (only lower 64 bits, D8-D15) + * - V16-V31: Temporary registers (caller-saved) + */ + +#define RCPU_COUNT 31 // X0-X30 (SP is not a general register) +#define RFPU_COUNT 32 // V0-V31 + +// Calling convention: first 8 args in X0-X7 +#define CALL_NREGS 8 +static const Arm64Reg CALL_REGS[] = { X0, X1, X2, X3, X4, X5, X6, X7 }; +static const Arm64FpReg FP_CALL_REGS[] = { V0, V1, V2, V3, V4, V5, V6, V7 }; + +// Caller-saved (scratch) registers: X0-X17 (avoid X18) +// Note: We use X0-X17 as scratch, but X0-X7 are also argument registers +#define RCPU_SCRATCH_COUNT 18 + +// vdynamic structure: type (8 bytes) + value (8 bytes) +#define HDYN_VALUE 8 +static const Arm64Reg RCPU_SCRATCH_REGS[] = { + X0, X1, X2, X3, X4, X5, X6, X7, + X8, X9, X10, X11, X12, X13, X14, X15, + X16, X17 +}; + +// All FP registers are caller-saved in AAPCS64 +#define RFPU_SCRATCH_COUNT 32 + +// Callee-saved registers: X19-X28 +// X29 (FP) and X30 (LR) are also callee-saved but handled specially +#define RCPU_CALLEE_SAVED_COUNT 10 +static const Arm64Reg RCPU_CALLEE_SAVED[] = { + X19, X20, X21, X22, X23, X24, X25, X26, X27, X28 +}; + +// Callee-saved registers available for allocation (excludes RTMP/RTMP2) +// These survive function calls, so we don't need to spill them before BLR +#define RCPU_CALLEE_ALLOC_COUNT 8 +static const Arm64Reg RCPU_CALLEE_ALLOC[] = { + X19, X20, X21, X22, X23, X24, X25, X26 +}; + +// FP callee-saved: V8-V15 (only lower 64 bits) +#define RFPU_CALLEE_SAVED_COUNT 8 +static const Arm64FpReg RFPU_CALLEE_SAVED[] = { + V8, V9, V10, V11, V12, V13, V14, V15 +}; + +// Helper macros for accessing registers +#define REG_COUNT (RCPU_COUNT + RFPU_COUNT) +#define VFPR(i) ((i) + RCPU_COUNT) // FP register index +#define PVFPR(i) REG_AT(VFPR(i)) // Pointer to FP register + +// Reserved registers for JIT internal use +#define RTMP X27 // Temporary register for multi-instruction sequences +#define RTMP2 X28 // Second temporary register + +// Special purpose registers +#define RFP X29 // Frame pointer +#define RLR X30 // Link register + +// Stack alignment requirement +#define STACK_ALIGN 16 + +// EMIT32 is defined in jit_common.h - use EMIT32(ctx,ctx, val) + +// ============================================================================ +// Error Handling +// ============================================================================ + +void _jit_error(jit_ctx *ctx, const char *msg, int line) { + printf("JIT ERROR: %s (jit_aarch64.c:%d)\n", msg, line); + if (ctx && ctx->f) { + // hl_function doesn't have a 'name' field directly + // The function object info would be in the module + int func_index = (int)(ctx->f - ctx->m->code->functions); + printf("In function at index %d\n", func_index); + } + jit_exit(); +} + +void on_jit_error(const char *msg, int_val line) { + printf("JIT Runtime Error: %s (line %d)\n", msg, (int)line); + jit_exit(); +} + +#define JIT_ASSERT(cond) do { if (!(cond)) { \ + printf("JIT ASSERTION FAILED: %s (jit_aarch64.c:%d)\n", #cond, __LINE__); \ + jit_exit(); \ +} } while(0) + +// ============================================================================ +// Register Allocation Helpers +// ============================================================================ + +/** + * Check if a CPU register is a call (argument) register + */ +static bool is_call_reg(Arm64Reg r) { + for (int i = 0; i < CALL_NREGS; i++) { + if (CALL_REGS[i] == r) + return true; + } + return false; +} + +/** + * Get the index of a register in the call register array + * Returns -1 if not a call register + */ +static int call_reg_index(Arm64Reg r) { + for (int i = 0; i < CALL_NREGS; i++) { + if (CALL_REGS[i] == r) + return i; + } + return -1; +} + +/** + * Check if a register is callee-saved (must be preserved across calls) + */ +static bool is_callee_saved_cpu(Arm64Reg r) { + for (int i = 0; i < RCPU_CALLEE_SAVED_COUNT; i++) { + if (RCPU_CALLEE_SAVED[i] == r) + return true; + } + return r == RFP || r == RLR; +} + +static bool is_callee_saved_fpu(Arm64FpReg r) { + for (int i = 0; i < RFPU_CALLEE_SAVED_COUNT; i++) { + if (RFPU_CALLEE_SAVED[i] == r) + return true; + } + return false; +} + +/** + * Check if type is String (HOBJ with bytes:HBYTES + length:HI32) + * Used for value-based string comparison per Haxe spec. + */ +static bool is_string_type(hl_type *t) { + if (t->kind != HOBJ || !t->obj) return false; + if (t->obj->nfields != 2) return false; + return t->obj->fields[0].t->kind == HBYTES && + t->obj->fields[1].t->kind == HI32; +} + +// ============================================================================ +// Register Allocation +// ============================================================================ + +// Forward declarations +static void free_reg(jit_ctx *ctx, preg *p); +static void patch_jump(jit_ctx *ctx, int pos, int target_pos); + +/** + * Find a free CPU register, evicting if necessary + * @param k Register kind (RCPU, RCPU_CALL, etc.) + * @return Pointer to allocated physical register + */ +static preg *alloc_cpu(jit_ctx *ctx, preg_kind k) { + preg *p; + int i; + int start_idx = 0; + int count = RCPU_SCRATCH_COUNT; + const Arm64Reg *regs = RCPU_SCRATCH_REGS; + + // For RCPU_CALL, only use non-argument registers + if (k == RCPU_CALL) { + // Use registers that are NOT in CALL_REGS + // For now, use X8-X17 (scratch registers that aren't args) + start_idx = 8; // Start from X8 + } + + // First pass: find a free scratch register (not holding anything and not locked) + // Lock check: p->lock >= ctx->currentPos means locked at current operation + for (i = start_idx; i < count; i++) { + p = REG_AT(regs[i]); + if (p->holds == NULL && p->lock < ctx->currentPos) + return p; + } + + // Second pass: try callee-saved registers (X19-X26) before evicting scratch + // These survive function calls, so values don't need to be spilled before BLR + for (i = 0; i < RCPU_CALLEE_ALLOC_COUNT; i++) { + p = REG_AT(RCPU_CALLEE_ALLOC[i]); + if (p->holds == NULL && p->lock < ctx->currentPos) { + ctx->callee_saved_used |= (1 << i); // Mark register as used for Phase 2 NOP patching + return p; + } + } + + // Third pass: evict a callee-saved register if one is unlocked + for (i = 0; i < RCPU_CALLEE_ALLOC_COUNT; i++) { + p = REG_AT(RCPU_CALLEE_ALLOC[i]); + if (p->lock < ctx->currentPos) { + ctx->callee_saved_used |= (1 << i); // Mark register as used for Phase 2 NOP patching + free_reg(ctx, p); // Spill to stack before reusing + return p; + } + } + + // Fourth pass: evict a scratch register + for (i = start_idx; i < count; i++) { + p = REG_AT(regs[i]); + if (p->lock < ctx->currentPos) { + free_reg(ctx, p); // Spill to stack before reusing + return p; + } + } + + // All registers are locked - this is an error + JIT_ASSERT(0); + return NULL; +} + +/** + * Allocate a floating-point register + */ +static preg *alloc_fpu(jit_ctx *ctx) { + preg *p; + int i; + + // First pass: find a free register + // Prefer caller-saved registers (V0-V7, V16-V31) + // Lock check: p->lock >= ctx->currentPos means locked at current operation + for (i = 0; i < RFPU_COUNT; i++) { + if (i >= 8 && i < 16) + continue; // Skip callee-saved V8-V15 for now + p = PVFPR(i); + if (p->holds == NULL && p->lock < ctx->currentPos) + return p; + } + + // Second pass: try callee-saved if needed + for (i = 8; i < 16; i++) { + p = PVFPR(i); + if (p->holds == NULL && p->lock < ctx->currentPos) + return p; + } + + // Third pass: evict an unlocked register + for (i = 0; i < RFPU_COUNT; i++) { + p = PVFPR(i); + if (p->lock < ctx->currentPos) { + free_reg(ctx, p); // Spill to stack before reusing + return p; + } + } + + JIT_ASSERT(0); + return NULL; +} + +/** + * Allocate a register of the appropriate type based on the virtual register's type + */ +static preg *alloc_reg(jit_ctx *ctx, vreg *r, preg_kind k) { + if (IS_FLOAT(r)) + return alloc_fpu(ctx); + else + return alloc_cpu(ctx, k); +} + +// ============================================================================ +// Register State Management +// ============================================================================ + +/** + * Store a virtual register to its stack location + */ +static void store(jit_ctx *ctx, vreg *r, preg *p); // Forward declaration +static void mov_reg_reg(jit_ctx *ctx, Arm64Reg dst, Arm64Reg src, bool is_64bit); // Forward declaration +static void ldr_stack(jit_ctx *ctx, Arm64Reg dst, int stack_offset, int size); // Forward declaration +static void emit_call_findex(jit_ctx *ctx, int findex, int stack_space); // Forward declaration + +/** + * Free a physical register by storing its content to stack if needed + */ +static void free_reg(jit_ctx *ctx, preg *p) { + vreg *r = p->holds; + if (r != NULL) { + store(ctx, r, p); + r->current = NULL; + p->holds = NULL; + } + // Unlock the register so it can be reused + RUNLOCK(p); +} + +/** + * Discard the content of a physical register, storing if dirty. + * Used when we're done using a register but the vreg might still be live. + * If the vreg is dirty (modified but not yet on stack), we store it first. + */ +static void discard(jit_ctx *ctx, preg *p) { + vreg *r = p->holds; + if (r != NULL) { + // If dirty, store to stack before clearing the binding + if (r->dirty) { + store(ctx, r, p); + } + r->current = NULL; + p->holds = NULL; + } + // Unlock the register so it can be reused + RUNLOCK(p); +} + +/** + * Spill all caller-saved registers to stack before a function call. + * In AAPCS64: X0-X17 and V0-V7 are caller-saved and may be clobbered. + * + * This function: + * 1. Stores each bound register's value to its vreg's stack slot + * 2. Clears the register↔vreg bindings + * + * IMPORTANT: Must be called BEFORE the BLR instruction, not after! + * At that point register values are still valid and can be spilled to stack. + * After the call, caller-saved registers contain garbage from the callee. + * + * ARCHITECTURAL NOTE - Why AArch64 differs from x86: + * + * The x86 JIT's discard_regs() just clears register bindings without spilling. + * This works because x86 (CISC) can use memory operands directly in ALU + * instructions: + * + * x86: ADD [rbp-8], rax ; Operate directly on stack slot + * + * So x86 treats stack slots as the "source of truth" - values are written + * to stack as part of normal operations, and registers are just caches. + * Clearing bindings is safe because the value is already on the stack. + * + * AArch64 (RISC load/store architecture) cannot do this: + * + * AArch64: LDR x1, [fp, #-8] ; Must load to register first + * ADD x0, x0, x1 ; Operate on registers only + * STR x0, [fp, #-8] ; Separate store instruction + * + * Adding a store after every operation would cost ~1 extra instruction per op. + * Instead, we keep values in registers (registers are "source of truth") and + * only spill when necessary - specifically, before function calls that will + * clobber caller-saved registers. + * + * This is not a workaround but the natural design for load/store architectures. + */ +static void spill_regs(jit_ctx *ctx) { + int i; + // Spill and discard CPU scratch registers (X0-X17) - these get clobbered by calls + for (i = 0; i < 18; i++) { + preg *r = &ctx->pregs[i]; + if (r->holds) { + if (r->holds->dirty) { + free_reg(ctx, r); // Dirty: store to stack, then clear binding + } else { + discard(ctx, r); // Clean: just clear binding (value already on stack) + } + } + } + // NOTE: Do NOT spill callee-saved registers (X19-X26)! + // They survive function calls, so their values remain valid after BLR. + // This is the key optimization - values in callee-saved don't need spilling. + + // Spill and discard FPU scratch registers (V0-V7) - these get clobbered by calls + for (i = 0; i < 8; i++) { + preg *r = &ctx->pregs[RCPU_COUNT + i]; + if (r->holds) { + if (r->holds->dirty) { + free_reg(ctx, r); // Dirty: store to stack, then clear binding + } else { + discard(ctx, r); // Clean: just clear binding (value already on stack) + } + } + } +} + +/** + * Spill callee-saved registers to stack. + * Called before jumps to labels - callee-saved must be on stack at merge points. + * NOTE: This is NOT called before function calls (callee-saved survive calls). + */ +static void spill_callee_saved(jit_ctx *ctx) { + int i; + // Spill callee-saved CPU registers (X19-X26) that are in use + for (i = 0; i < RCPU_CALLEE_ALLOC_COUNT; i++) { + preg *r = REG_AT(RCPU_CALLEE_ALLOC[i]); + if (r->holds) { + if (r->holds->dirty) { + free_reg(ctx, r); // Dirty: store to stack, then clear binding + } else { + discard(ctx, r); // Clean: just clear binding + } + } + } +} + +/** + * Ensure a virtual register is in a physical register + * Loads from stack if necessary + */ +static preg *fetch(jit_ctx *ctx, vreg *r); // Forward declaration + +/** + * Bind a vreg to a physical register (bidirectional association) + * This is essential for proper spilling when the register is evicted + */ +static void reg_bind(jit_ctx *ctx, vreg *r, preg *p) { + // If vreg was dirty in another register, store to stack first + // This prevents losing values when rebinding (e.g., dst = dst op src) + if (r->current && r->current != p) { + if (r->dirty) { + store(ctx, r, r->current); + } + r->current->holds = NULL; + } + // Set new binding + r->current = p; + p->holds = r; +} + +/** + * Allocate a destination register for a vreg + * Helper function used by many operations + * Binds the vreg to the allocated register for proper spilling + */ +static preg *alloc_dst(jit_ctx *ctx, vreg *r) { + preg *p; + if (IS_FLOAT(r)) { + p = alloc_fpu(ctx); + } else { + p = alloc_cpu(ctx, RCPU); + } + // Bind the vreg to this register so we can spill it later if needed + reg_bind(ctx, r, p); + // Mark dirty: a new value is about to be written to this register, + // and it's not on the stack yet. This ensures spill_regs() will + // store it before the next call/jump. + r->dirty = 1; + return p; +} + +// ============================================================================ +// Basic Data Movement - Encoding Helpers +// ============================================================================ + +/** + * Generate MOV instruction (register to register) + * For integer: MOV Xd, Xn (using ORR Xd, XZR, Xn) + * For float: FMOV Vd, Vn + */ +static void mov_reg_reg(jit_ctx *ctx, Arm64Reg dst, Arm64Reg src, bool is_64bit) { + // SP (register 31) can't be used with ORR - must use ADD instead + if (src == SP_REG || dst == SP_REG) { + // MOV Xd, SP or MOV SP, Xn => ADD Xd, Xn, #0 + encode_add_sub_imm(ctx, is_64bit ? 1 : 0, 0, 0, 0, 0, src, dst); + } else if (is_64bit) { + // MOV Xd, Xn => ORR Xd, XZR, Xn + encode_logical_reg(ctx, 1, 0x01, 0, 0, src, 0, XZR, dst); + } else { + // MOV Wd, Wn => ORR Wd, WZR, Wn + encode_logical_reg(ctx, 0, 0x01, 0, 0, src, 0, XZR, dst); + } +} + +static void fmov_reg_reg(jit_ctx *ctx, Arm64FpReg dst, Arm64FpReg src, bool is_double) { + // FMOV Vd, Vn (using FP 1-source with opcode 0) + int type = is_double ? 0x01 : 0x00; // 01=double, 00=single + encode_fp_1src(ctx, 0, 0, type, 0, src, dst); +} + +/** + * Load from stack to register + * Format: LDR/LDUR Xt, [FP, #offset] + * + * Uses LDUR for signed offsets in range [-256, +255] (single instruction) + * Uses LDR with scaled unsigned offset for aligned positive offsets + * Falls back to computing address in register for large offsets + */ +static void ldr_stack(jit_ctx *ctx, Arm64Reg dst, int stack_offset, int size) { + int size_enc = (size == 8) ? 3 : ((size == 4) ? 2 : ((size == 2) ? 1 : 0)); + + // Priority 1: Use LDUR for small signed offsets (-256 to +255) + // This handles most negative stack offsets in a single instruction + if (stack_offset >= -256 && stack_offset <= 255) { + encode_ldur_stur(ctx, size_enc, 0, 0x01, stack_offset, RFP, dst); + return; + } + + // Priority 2: Use LDR with scaled unsigned offset for larger positive aligned offsets + if (stack_offset >= 0 && (stack_offset % size == 0) && stack_offset < 4096 * size) { + int scaled_offset = stack_offset / size; + encode_ldr_str_imm(ctx, size_enc, 0, 0x01, scaled_offset, RFP, dst); + return; + } + + // Fallback: Compute address in register for large/unaligned offsets + load_immediate(ctx, stack_offset, RTMP, true); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP, 0, RFP, RTMP); + encode_ldr_str_imm(ctx, size_enc, 0, 0x01, 0, RTMP, dst); +} + +/** + * Load from stack to FP register + * Format: LDR/LDUR Dt/St, [FP, #offset] + */ +static void ldr_stack_fp(jit_ctx *ctx, Arm64FpReg dst, int stack_offset, int size) { + int size_enc = (size == 8) ? 3 : ((size == 4) ? 2 : 1); + + // Priority 1: Use LDUR for small signed offsets (-256 to +255) + if (stack_offset >= -256 && stack_offset <= 255) { + encode_ldur_stur(ctx, size_enc, 1, 0x01, stack_offset, RFP, dst); + return; + } + + // Priority 2: Use LDR with scaled unsigned offset for larger positive aligned offsets + if (stack_offset >= 0 && (stack_offset % size == 0) && stack_offset < 4096 * size) { + int scaled_offset = stack_offset / size; + encode_ldr_str_imm(ctx, size_enc, 1, 0x01, scaled_offset, RFP, dst); + return; + } + + // Fallback: Compute address in register + load_immediate(ctx, stack_offset, RTMP, true); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP, 0, RFP, RTMP); + encode_ldr_str_imm(ctx, size_enc, 1, 0x01, 0, RTMP, dst); +} + +/** + * Store register to stack + * Format: STR/STUR Xt, [FP, #offset] + * + * Uses STUR for signed offsets in range [-256, +255] (single instruction) + * Uses STR with scaled unsigned offset for aligned positive offsets + * Falls back to computing address in register for large offsets + */ +static void str_stack(jit_ctx *ctx, Arm64Reg src, int stack_offset, int size) { + int size_enc = (size == 8) ? 3 : ((size == 4) ? 2 : ((size == 2) ? 1 : 0)); + + // Priority 1: Use STUR for small signed offsets (-256 to +255) + if (stack_offset >= -256 && stack_offset <= 255) { + encode_ldur_stur(ctx, size_enc, 0, 0x00, stack_offset, RFP, src); + return; + } + + // Priority 2: Use STR with scaled unsigned offset for larger positive aligned offsets + if (stack_offset >= 0 && (stack_offset % size == 0) && stack_offset < 4096 * size) { + int scaled_offset = stack_offset / size; + encode_ldr_str_imm(ctx, size_enc, 0, 0x00, scaled_offset, RFP, src); + return; + } + + // Fallback: Compute address in register + load_immediate(ctx, stack_offset, RTMP, true); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP, 0, RFP, RTMP); + encode_ldr_str_imm(ctx, size_enc, 0, 0x00, 0, RTMP, src); +} + +/** + * Store FP register to stack + * Format: STR/STUR Dt/St, [FP, #offset] + */ +static void str_stack_fp(jit_ctx *ctx, Arm64FpReg src, int stack_offset, int size) { + int size_enc = (size == 8) ? 3 : ((size == 4) ? 2 : 1); + + // Priority 1: Use STUR for small signed offsets (-256 to +255) + if (stack_offset >= -256 && stack_offset <= 255) { + encode_ldur_stur(ctx, size_enc, 1, 0x00, stack_offset, RFP, src); + return; + } + + // Priority 2: Use STR with scaled unsigned offset for larger positive aligned offsets + if (stack_offset >= 0 && (stack_offset % size == 0) && stack_offset < 4096 * size) { + int scaled_offset = stack_offset / size; + encode_ldr_str_imm(ctx, size_enc, 1, 0x00, scaled_offset, RFP, src); + return; + } + + // Fallback: Compute address in register + load_immediate(ctx, stack_offset, RTMP, true); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP, 0, RFP, RTMP); + encode_ldr_str_imm(ctx, size_enc, 1, 0x00, 0, RTMP, src); +} + +/** + * STP with signed offset (no writeback) - for NOPpable callee-saved saves. + * Format: STP Xt1, Xt2, [Xn, #imm] + * This allows individual STPs to be patched to NOPs without affecting SP. + */ +static void stp_offset(jit_ctx *ctx, Arm64Reg rt, Arm64Reg rt2, Arm64Reg rn, int offset) { + int imm7 = offset / 8; + // opc=10 (64-bit), 101, addr_mode=10 (signed offset), L=0 (store), imm7, Rt2, Rn, Rt + unsigned int insn = (2u << 30) | (5u << 27) | (2u << 23) | (0u << 22) | + ((imm7 & 0x7F) << 15) | (rt2 << 10) | (rn << 5) | rt; + EMIT32(ctx, insn); +} + +/** + * LDP with signed offset (no writeback) - for NOPpable callee-saved restores. + * Format: LDP Xt1, Xt2, [Xn, #imm] + */ +static void ldp_offset(jit_ctx *ctx, Arm64Reg rt, Arm64Reg rt2, Arm64Reg rn, int offset) { + int imm7 = offset / 8; + // opc=10 (64-bit), 101, addr_mode=10 (signed offset), L=1 (load), imm7, Rt2, Rn, Rt + unsigned int insn = (2u << 30) | (5u << 27) | (2u << 23) | (1u << 22) | + ((imm7 & 0x7F) << 15) | (rt2 << 10) | (rn << 5) | rt; + EMIT32(ctx, insn); +} + +// ============================================================================ +// Data Movement Operations +// ============================================================================ + +/** + * Store a virtual register to its stack location + */ +static void store(jit_ctx *ctx, vreg *r, preg *p) { + if (r == NULL || p == NULL || r->size == 0) + return; + + int size = r->size; + int offset = r->stackPos; + + if (p->kind == RCPU) { + str_stack(ctx, p->id, offset, size); + } else if (p->kind == RFPU) { + str_stack_fp(ctx, p->id, offset, size); + } + + r->dirty = 0; // Stack is now up-to-date +} + +/** + * Mark a virtual register as dirty (register value differs from stack). + * The value will be spilled at the next basic block boundary (jump, call, label). + * This defers stores to reduce instruction count within basic blocks. + */ +static void mark_dirty(jit_ctx *ctx, vreg *r) { + (void)ctx; // unused, kept for consistency with other functions + if (r != NULL && r->current != NULL && r->size > 0) { + r->dirty = 1; + } +} + +/** + * Store to a vreg's stack slot and clear any stale register binding. + * Use this when storing directly (e.g., from X0 after a call) without + * going through the normal register allocation path. + * + * This prevents spill_regs from later overwriting the correct stack + * value with a stale register value. + */ +static void store_result(jit_ctx *ctx, vreg *dst) { + // Clear any stale binding - the correct value is now on stack + if (dst->current != NULL) { + dst->current->holds = NULL; + dst->current = NULL; + } +} + +/** + * Load a virtual register from stack to a physical register + */ +static preg *fetch(jit_ctx *ctx, vreg *r) { + preg *p; + + // HVOID registers have size 0 and no value to load + if (r->size == 0) + return UNUSED; + + // Check if already in a register + if (r->current != NULL && r->current->kind != RSTACK) { + // Lock the register to prevent eviction during subsequent allocations + RLOCK(r->current); + return r->current; + } + + // Allocate a register + p = alloc_reg(ctx, r, RCPU); + + // If the register we got already holds something, evict it + if (p->holds != NULL) + free_reg(ctx, p); + + // Load from stack + int size = r->size; + int offset = r->stackPos; + + if (IS_FLOAT(r)) { + ldr_stack_fp(ctx, p->id, offset, size); + } else { + ldr_stack(ctx, p->id, offset, size); + } + + // Bind vreg to register and lock it to prevent eviction by subsequent allocs + reg_bind(ctx, r, p); + RLOCK(p); + + return p; +} + +/** + * Copy data between locations (register, stack, immediate) + * This is the main data movement workhorse function + */ +static void copy(jit_ctx *ctx, vreg *dst, preg *dst_p, vreg *src, preg *src_p) { + if (src_p->kind == RCONST) { + // Load immediate into destination + int64_t val = src_p->id; + + if (IS_FLOAT(dst)) { + // Load float constant: load bits as integer, then move to FP register + preg *d = (dst_p && dst_p->kind == RFPU) ? dst_p : alloc_fpu(ctx); + + if (val == 0) { + // FMOV Dd, XZR - zero the FP register + EMIT32(ctx, (1 << 31) | (0 << 29) | (0x1E << 24) | (1 << 22) | (1 << 21) | (7 << 16) | (31 << 5) | d->id); + } else { + // Load bits to GPR, then FMOV to FPR + load_immediate(ctx, val, RTMP, true); + // FMOV Dd, Xn: sf=1, S=0, type=01, rmode=00, opcode=00111, Rn, Rd + EMIT32(ctx, (0x9E670000) | (RTMP << 5) | d->id); + } + + if (dst_p == NULL || dst_p != d) { + reg_bind(ctx, dst, d); + } + } else { + // Load integer immediate + preg *d = (dst_p && dst_p->kind == RCPU) ? dst_p : fetch(ctx, dst); + load_immediate(ctx, val, d->id, dst->size == 8); + if (dst_p == NULL || dst_p != d) { + reg_bind(ctx, dst, d); + } + } + } else if (src_p->kind == RCPU && dst_p && dst_p->kind == RCPU) { + // Register to register + mov_reg_reg(ctx, dst_p->id, src_p->id, dst->size == 8); + } else if (src_p->kind == RFPU && dst_p && dst_p->kind == RFPU) { + // FP register to FP register + fmov_reg_reg(ctx, dst_p->id, src_p->id, dst->size == 8); + } else { + // Generic case: fetch src, store to dst + preg *s = (src_p && (src_p->kind == RCPU || src_p->kind == RFPU)) ? src_p : fetch(ctx, src); + preg *d = (dst_p && (dst_p->kind == RCPU || dst_p->kind == RFPU)) ? dst_p : fetch(ctx, dst); + + if (IS_FLOAT(dst)) { + fmov_reg_reg(ctx, d->id, s->id, dst->size == 8); + } else { + mov_reg_reg(ctx, d->id, s->id, dst->size == 8); + } + + reg_bind(ctx, dst, d); + } +} + +// ============================================================================ +// Opcode Handlers +// ============================================================================ + +/** + * OMov - Move/copy a value from one register to another + */ +static void op_mov(jit_ctx *ctx, vreg *dst, vreg *src) { + preg *r = fetch(ctx, src); + + // Handle special case for HF32 (32-bit float) + // Ensure it's in an FP register + if (src->t->kind == HF32 && r->kind != RFPU) { + r = alloc_fpu(ctx); + // Load from stack to FP register + ldr_stack_fp(ctx, r->id, src->stackPos, src->size); + reg_bind(ctx, src, r); + } + + // Store to destination stack slot + store(ctx, dst, r); + + // Clear dst's old register binding to prevent stale value from being spilled + // The correct value is now on the stack from store() above + if (dst->current != NULL) { + dst->current->holds = NULL; + dst->current = NULL; + } +} + +/** + * Store a constant value to a virtual register + */ +static void store_const(jit_ctx *ctx, vreg *dst, int64_t val) { + preg *p; + + if (IS_FLOAT(dst)) { + // Allocate FPU register for float constants + p = alloc_fpu(ctx); + if (p->holds != NULL) + free_reg(ctx, p); + + if (val == 0) { + // FMOV Dd, XZR - zero the FP register + EMIT32(ctx, (1 << 31) | (0 << 29) | (0x1E << 24) | (1 << 22) | (1 << 21) | (7 << 16) | (31 << 5) | p->id); + } else { + // Load bits to GPR, then FMOV to FPR + load_immediate(ctx, val, RTMP, true); + // FMOV Dd, Xn: sf=1, S=0, type=01, rmode=00, opcode=00111, Rn, Rd + EMIT32(ctx, (0x9E670000) | (RTMP << 5) | p->id); + } + } else { + p = alloc_reg(ctx, dst, RCPU); + if (p->holds != NULL) + free_reg(ctx, p); + load_immediate(ctx, val, p->id, dst->size == 8); + } + + reg_bind(ctx, dst, p); + store(ctx, dst, p); // Constants must be stored immediately for correct loop initialization +} + +// ============================================================================ +// Arithmetic Operations +// ============================================================================ + +// Forward declaration for op_call_native (used by floating-point modulo) +static void op_call_native(jit_ctx *ctx, vreg *dst, hl_type *ftype, void *func_ptr, vreg **args, int nargs); +// Forward declaration for prepare_call_args (used by op_jump for dynamic comparisons) +static int prepare_call_args(jit_ctx *ctx, hl_type **arg_types, vreg **args, int nargs, bool is_native); + +/** + * Binary arithmetic/logic operations handler + * Handles: OAdd, OSub, OMul, OSDiv, OUDiv, OSMod, OUMod, OAnd, OOr, OXor, shifts + */ +static void op_binop(jit_ctx *ctx, vreg *dst, vreg *a, vreg *b, hl_op op) { + bool is_64bit = dst->size == 8; + int sf = is_64bit ? 1 : 0; + + // Handle floating-point operations + if (IS_FLOAT(dst)) { + preg *pa = fetch(ctx, a); + preg *pb = fetch(ctx, b); + preg *pd; + + // If dst == a, reuse pa as destination to avoid clobbering issues + // when reg_bind tries to store the old (now stale) value + if (dst == a) { + pd = pa; + } else { + pd = alloc_fpu(ctx); + if (pd->holds != NULL) + free_reg(ctx, pd); + } + + int type = (dst->t->kind == HF64) ? 0x01 : 0x00; // 01=double, 00=single + + switch (op) { + case OAdd: + // FADD Vd, Vn, Vm + encode_fp_arith(ctx, 0, 0, type, pb->id, 0x02, pa->id, pd->id); + break; + case OSub: + // FSUB Vd, Vn, Vm + encode_fp_arith(ctx, 0, 0, type, pb->id, 0x03, pa->id, pd->id); + break; + case OMul: + // FMUL Vd, Vn, Vm + encode_fp_arith(ctx, 0, 0, type, pb->id, 0x00, pa->id, pd->id); + break; + case OSDiv: + case OUDiv: // Same as OSDiv for floats + // FDIV Vd, Vn, Vm + encode_fp_arith(ctx, 0, 0, type, pb->id, 0x01, pa->id, pd->id); + break; + case OSMod: + case OUMod: { + // Floating-point modulo: call fmod/fmodf from C library + // Need to discard pa/pb since op_call_native will spill + discard(ctx, pa); + discard(ctx, pb); + void *mod_func = (dst->t->kind == HF64) ? (void*)fmod : (void*)fmodf; + vreg *args[2] = { a, b }; + op_call_native(ctx, dst, NULL, mod_func, args, 2); + return; // op_call_native handles result storage + } + default: + JIT_ASSERT(0); // Invalid FP operation + } + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); + return; + } + + // Integer operations + preg *pa = fetch(ctx, a); + preg *pb = fetch(ctx, b); + preg *pd; + + // If dst == a, reuse pa as destination to avoid clobbering issues + // when reg_bind tries to store the old (now stale) value + if (dst == a) { + pd = pa; + } else { + pd = alloc_cpu(ctx, RCPU); + if (pd->holds != NULL) + free_reg(ctx, pd); + } + + switch (op) { + case OAdd: + // ADD Xd, Xn, Xm + encode_add_sub_reg(ctx, sf, 0, 0, 0, pb->id, 0, pa->id, pd->id); + break; + + case OSub: + // SUB Xd, Xn, Xm + encode_add_sub_reg(ctx, sf, 1, 0, 0, pb->id, 0, pa->id, pd->id); + break; + + case OMul: + // MUL Xd, Xn, Xm (using MADD with XZR as addend) + encode_madd_msub(ctx, sf, 0, pb->id, XZR, pa->id, pd->id); + break; + + case OSDiv: + // SDIV Xd, Xn, Xm (signed division) + // Note: encode_div U=1 means SDIV, U=0 means UDIV (per ARM ISA) + encode_div(ctx, sf, 1, pb->id, pa->id, pd->id); + break; + + case OUDiv: + // UDIV Xd, Xn, Xm (unsigned division) + encode_div(ctx, sf, 0, pb->id, pa->id, pd->id); + break; + + case OSMod: { + // Signed modulo with special case handling: + // - divisor == 0: return 0 (avoid returning dividend) + // - divisor == -1: return 0 (avoid MIN % -1 overflow) + // CBZ divisor, zero_case + int jz = BUF_POS(); + encode_cbz_cbnz(ctx, sf, 0, 0, pb->id); // CBZ + + // CMP divisor, #-1; B.EQ zero_case + // CMN is ADD setting flags, so CMN Xn, #1 checks if Xn == -1 + encode_add_sub_imm(ctx, sf, 1, 1, 0, 1, pb->id, XZR); // CMN divisor, #1 + int jneg1 = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); // B.EQ + + // Normal path: remainder = dividend - (quotient * divisor) + encode_div(ctx, sf, 1, pb->id, pa->id, RTMP); // RTMP = a / b (signed) + encode_madd_msub(ctx, sf, 1, pb->id, pa->id, RTMP, pd->id); // pd = a - (RTMP * b) + int jend = BUF_POS(); + encode_branch_uncond(ctx, 0); // B end + + // Zero case: return 0 + int zero_pos = BUF_POS(); + // MOV pd, #0 (using ORR with XZR) + encode_logical_reg(ctx, sf, 0x01, 0, 0, XZR, 0, XZR, pd->id); // ORR pd, XZR, XZR + + patch_jump(ctx, jz, zero_pos); + patch_jump(ctx, jneg1, zero_pos); + patch_jump(ctx, jend, BUF_POS()); + break; + } + + case OUMod: { + // Unsigned modulo with special case: + // - divisor == 0: return 0 + // CBZ divisor, zero_case + int jz = BUF_POS(); + encode_cbz_cbnz(ctx, sf, 0, 0, pb->id); // CBZ + + // Normal path + encode_div(ctx, sf, 0, pb->id, pa->id, RTMP); // RTMP = a / b (unsigned) + encode_madd_msub(ctx, sf, 1, pb->id, pa->id, RTMP, pd->id); // pd = a - (RTMP * b) + int jend = BUF_POS(); + encode_branch_uncond(ctx, 0); // B end + + // Zero case: return 0 + int zero_pos = BUF_POS(); + encode_logical_reg(ctx, sf, 0x01, 0, 0, XZR, 0, XZR, pd->id); // ORR pd, XZR, XZR + + patch_jump(ctx, jz, zero_pos); + patch_jump(ctx, jend, BUF_POS()); + break; + } + + case OAnd: + // AND Xd, Xn, Xm + encode_logical_reg(ctx, sf, 0x00, 0, 0, pb->id, 0, pa->id, pd->id); + break; + + case OOr: + // ORR Xd, Xn, Xm + encode_logical_reg(ctx, sf, 0x01, 0, 0, pb->id, 0, pa->id, pd->id); + break; + + case OXor: + // EOR Xd, Xn, Xm + encode_logical_reg(ctx, sf, 0x02, 0, 0, pb->id, 0, pa->id, pd->id); + break; + + case OShl: + // LSL Xd, Xn, Xm (logical shift left) + encode_shift_reg(ctx, sf, 0x00, pb->id, pa->id, pd->id); + break; + + case OUShr: + // LSR Xd, Xn, Xm (logical shift right - unsigned) + encode_shift_reg(ctx, sf, 0x01, pb->id, pa->id, pd->id); + break; + + case OSShr: + // ASR Xd, Xn, Xm (arithmetic shift right - signed) + encode_shift_reg(ctx, sf, 0x02, pb->id, pa->id, pd->id); + break; + + default: + JIT_ASSERT(0); // Unknown operation + } + + // Mask result for sub-32-bit integer types (UI8, UI16) + // AArch64 doesn't have 8/16-bit registers like x86, so we need explicit masking + if (dst->t->kind == HUI8 || dst->t->kind == HBOOL) { + // AND Wd, Wd, #0xFF (sf=0, opc=0, N=0, immr=0, imms=7) + encode_logical_imm(ctx, 0, 0x00, 0, 0, 7, pd->id, pd->id); + } else if (dst->t->kind == HUI16) { + // AND Wd, Wd, #0xFFFF (sf=0, opc=0, N=0, immr=0, imms=15) + encode_logical_imm(ctx, 0, 0x00, 0, 0, 15, pd->id, pd->id); + } + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); +} + +/** + * Unary negation (ONeg) + */ +static void op_neg(jit_ctx *ctx, vreg *dst, vreg *a) { + if (IS_FLOAT(a)) { + // FNEG Vd, Vn + preg *pa = fetch(ctx, a); + preg *pd = alloc_fpu(ctx); + + if (pd->holds != NULL) + free_reg(ctx, pd); + + int type = (dst->t->kind == HF64) ? 0x01 : 0x00; + encode_fp_1src(ctx, 0, 0, type, 0x02, pa->id, pd->id); + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); + } else { + // NEG Xd, Xn (implemented as SUB Xd, XZR, Xn) + preg *pa = fetch(ctx, a); + preg *pd = alloc_cpu(ctx, RCPU); + + if (pd->holds != NULL) + free_reg(ctx, pd); + + int sf = (dst->size == 8) ? 1 : 0; + encode_add_sub_reg(ctx, sf, 1, 0, 0, pa->id, 0, XZR, pd->id); + + // Mask result for sub-32-bit integer types + if (dst->t->kind == HUI8 || dst->t->kind == HBOOL) { + encode_logical_imm(ctx, 0, 0x00, 0, 0, 7, pd->id, pd->id); + } else if (dst->t->kind == HUI16) { + encode_logical_imm(ctx, 0, 0x00, 0, 0, 15, pd->id, pd->id); + } + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); + } +} + +/** + * Logical NOT (ONot) - boolean negation + */ +static void op_not(jit_ctx *ctx, vreg *dst, vreg *a) { + // XOR with 1 (boolean NOT) + preg *pa = fetch(ctx, a); + preg *pd = alloc_cpu(ctx, RCPU); + + if (pd->holds != NULL) + free_reg(ctx, pd); + + // Load immediate 1 + load_immediate(ctx, 1, RTMP, false); + + // EOR Wd, Wn, Wtmp (32-bit XOR with 1) + encode_logical_reg(ctx, 0, 0x02, 0, 0, RTMP, 0, pa->id, pd->id); + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); +} + +/** + * Increment (OIncr) + */ +static void op_incr(jit_ctx *ctx, vreg *dst) { + // ADD Xd, Xd, #1 with memory writeback + preg *pd = fetch(ctx, dst); + int sf = (dst->size == 8) ? 1 : 0; + + // ADD Xd, Xn, #1 + encode_add_sub_imm(ctx, sf, 0, 0, 0, 1, pd->id, pd->id); + + // Mask result for sub-32-bit integer types + if (dst->t->kind == HUI8 || dst->t->kind == HBOOL) { + encode_logical_imm(ctx, 0, 0x00, 0, 0, 7, pd->id, pd->id); + } else if (dst->t->kind == HUI16) { + encode_logical_imm(ctx, 0, 0x00, 0, 0, 15, pd->id, pd->id); + } + + mark_dirty(ctx, dst); +} + +/** + * Decrement (ODecr) + */ +static void op_decr(jit_ctx *ctx, vreg *dst) { + // SUB Xd, Xd, #1 with memory writeback + preg *pd = fetch(ctx, dst); + int sf = (dst->size == 8) ? 1 : 0; + + // SUB Xd, Xn, #1 + encode_add_sub_imm(ctx, sf, 1, 0, 0, 1, pd->id, pd->id); + + // Mask result for sub-32-bit integer types + if (dst->t->kind == HUI8 || dst->t->kind == HBOOL) { + encode_logical_imm(ctx, 0, 0x00, 0, 0, 7, pd->id, pd->id); + } else if (dst->t->kind == HUI16) { + encode_logical_imm(ctx, 0, 0x00, 0, 0, 15, pd->id, pd->id); + } + + mark_dirty(ctx, dst); +} + +// ============================================================================ +// Type Conversion Operations +// ============================================================================ + +/** + * Convert to integer (OToInt) + * Handles: float->int, i32->i64 sign extension, and int->int copy + */ +static void op_toint(jit_ctx *ctx, vreg *dst, vreg *src) { + // Same register optimization + if (dst == src) return; + + // Case 1: Float to integer conversion + if (IS_FLOAT(src)) { + preg *ps = fetch(ctx, src); + preg *pd = alloc_cpu(ctx, RCPU); + + if (pd->holds != NULL) + free_reg(ctx, pd); + + int sf = (dst->size == 8) ? 1 : 0; + int type = (src->t->kind == HF64) ? 0x01 : 0x00; + + // FCVTZS Xd, Vn (float to signed int, round toward zero) + encode_fcvt_int(ctx, sf, 0, type, 0x03, 0x00, ps->id, pd->id); + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); + return; + } + + // Case 2: i32 to i64 sign extension + if (dst->size == 8 && src->size == 4) { + preg *ps = fetch(ctx, src); + preg *pd = alloc_cpu(ctx, RCPU); + + if (pd->holds != NULL) + free_reg(ctx, pd); + + Arm64Reg src_r = (ps->kind == RCPU) ? (Arm64Reg)ps->id : RTMP; + if (ps->kind == RCONST) { + load_immediate(ctx, ps->id, src_r, false); + } else if (ps->kind != RCPU) { + ldr_stack(ctx, src_r, src->stackPos, src->size); + } + + // SXTW Xd, Wn (sign extend word to doubleword) + // Encoding: 0x93407c00 | (Rn << 5) | Rd + EMIT32(ctx, 0x93407c00 | (src_r << 5) | pd->id); + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); + return; + } + + // Case 3: Integer to integer copy (same size or truncation) + preg *ps = fetch(ctx, src); + preg *pd = alloc_cpu(ctx, RCPU); + + if (pd->holds != NULL) + free_reg(ctx, pd); + + Arm64Reg src_r = (ps->kind == RCPU) ? (Arm64Reg)ps->id : RTMP; + if (ps->kind == RCONST) { + load_immediate(ctx, ps->id, src_r, src->size == 8); + } else if (ps->kind != RCPU) { + ldr_stack(ctx, src_r, src->stackPos, src->size); + } + + // MOV Xd, Xn (or MOV Wd, Wn for 32-bit) + int sf = (dst->size == 8) ? 1 : 0; + mov_reg_reg(ctx, pd->id, src_r, sf); + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); +} + +/** + * Convert signed integer to float, or convert between float precisions (OToSFloat) + * Handles: integer -> float (SCVTF), F64 -> F32, F32 -> F64 (FCVT) + */ +static void op_tosfloat(jit_ctx *ctx, vreg *dst, vreg *src) { + // Handle float-to-float precision conversions + if (src->t->kind == HF64 && dst->t->kind == HF32) { + // F64 -> F32: FCVT Sd, Dn + preg *ps = fetch(ctx, src); + preg *pd = alloc_fpu(ctx); + if (pd->holds != NULL) + free_reg(ctx, pd); + + Arm64FpReg src_r = (ps->kind == RFPU) ? (Arm64FpReg)ps->id : V16; + if (ps->kind != RFPU) { + ldr_stack_fp(ctx, src_r, src->stackPos, src->size); + } + + // FCVT Sd, Dn: type=1 (double source), opcode=4 (convert to single) + encode_fp_1src(ctx, 0, 0, 1, 4, src_r, pd->id); + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); + return; + } + + if (src->t->kind == HF32 && dst->t->kind == HF64) { + // F32 -> F64: FCVT Dd, Sn + preg *ps = fetch(ctx, src); + preg *pd = alloc_fpu(ctx); + if (pd->holds != NULL) + free_reg(ctx, pd); + + Arm64FpReg src_r = (ps->kind == RFPU) ? (Arm64FpReg)ps->id : V16; + if (ps->kind != RFPU) { + ldr_stack_fp(ctx, src_r, src->stackPos, src->size); + } + + // FCVT Dd, Sn: type=0 (single source), opcode=5 (convert to double) + encode_fp_1src(ctx, 0, 0, 0, 5, src_r, pd->id); + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); + return; + } + + // Integer to float conversion (original behavior) + preg *ps = fetch(ctx, src); + preg *pd = alloc_fpu(ctx); + + if (pd->holds != NULL) + free_reg(ctx, pd); + + int sf = (src->size == 8) ? 1 : 0; + int type = (dst->t->kind == HF64) ? 0x01 : 0x00; + + // SCVTF Vd, Xn (signed int to float) + encode_int_fcvt(ctx, sf, 0, type, 0x00, 0x02, ps->id, pd->id); + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); +} + +/** + * Convert unsigned integer to float (OToUFloat) + */ +static void op_toufloat(jit_ctx *ctx, vreg *dst, vreg *src) { + preg *ps = fetch(ctx, src); + preg *pd = alloc_fpu(ctx); + + if (pd->holds != NULL) + free_reg(ctx, pd); + + int sf = (src->size == 8) ? 1 : 0; + int type = (dst->t->kind == HF64) ? 0x01 : 0x00; + + // UCVTF Vd, Xn (unsigned int to float) + encode_int_fcvt(ctx, sf, 0, type, 0x00, 0x03, ps->id, pd->id); + + reg_bind(ctx, dst, pd); + mark_dirty(ctx, dst); +} + +// ============================================================================ +// Jump Patching +// ============================================================================ + +/** + * Add a jump to the patch list + * Also mark the target opcode so we know to discard registers when we reach it + */ +static void register_jump(jit_ctx *ctx, int pos, int target) { + jlist *j = (jlist*)malloc(sizeof(jlist)); + j->pos = pos; + j->target = target; + j->next = ctx->jumps; + ctx->jumps = j; + + // Mark target as a jump destination (like x86 does) + // This tells us to discard register bindings when we reach this opcode + if (target > 0 && target < ctx->maxOps && ctx->opsPos[target] == 0) + ctx->opsPos[target] = -1; +} + +/** + * Patch a jump instruction with the correct offset + * AArch64 branches use instruction offsets (divide byte offset by 4) + */ +static void patch_jump(jit_ctx *ctx, int pos, int target_pos) { + unsigned int *code = (unsigned int*)(ctx->startBuf + pos); + int offset = target_pos - pos; // Byte offset + int insn_offset = offset / 4; // Instruction offset + + // Check if this is a conditional branch (B.cond) or unconditional (B) + unsigned int insn = *code; + unsigned int opcode = (insn >> 24) & 0xFF; + + if (opcode == 0x54) { + // B.cond - 19-bit signed offset + // Range: ±1MB (±0x40000 instructions, ±0x100000 bytes) + if (insn_offset < -0x40000 || insn_offset >= 0x40000) { + printf("JIT Error: Conditional branch offset too large: %d\n", insn_offset); + JIT_ASSERT(0); + } + // Clear old offset, set new offset (bits 5-23) + *code = (insn & 0xFF00001F) | ((insn_offset & 0x7FFFF) << 5); + } else if ((opcode & 0xFC) == 0x14) { + // B or BL - 26-bit signed offset + // Range: ±128MB (±0x2000000 instructions, ±0x8000000 bytes) + if (insn_offset < -0x2000000 || insn_offset >= 0x2000000) { + printf("JIT Error: Branch offset too large: %d\n", insn_offset); + JIT_ASSERT(0); + } + // Clear old offset, set new offset (bits 0-25) + *code = (insn & 0xFC000000) | (insn_offset & 0x3FFFFFF); + } else if ((opcode & 0x7E) == 0x34) { + // CBZ/CBNZ - 19-bit signed offset + if (insn_offset < -0x40000 || insn_offset >= 0x40000) { + printf("JIT Error: CBZ/CBNZ offset too large: %d\n", insn_offset); + JIT_ASSERT(0); + } + *code = (insn & 0xFF00001F) | ((insn_offset & 0x7FFFF) << 5); + } else { + printf("JIT Error: Unknown branch instruction at %d: 0x%08X\n", pos, insn); + JIT_ASSERT(0); + } +} + +// ============================================================================ +// Control Flow & Comparisons +// ============================================================================ + +/** + * Map HashLink condition to AArch64 condition code + */ +static ArmCondition hl_cond_to_arm(hl_op op, bool is_float) { + switch (op) { + case OJEq: return COND_EQ; // Equal + case OJNotEq: return COND_NE; // Not equal + case OJSLt: return is_float ? COND_MI : COND_LT; // Signed less than + case OJSGte: return is_float ? COND_PL : COND_GE; // Signed greater or equal + case OJSGt: return COND_GT; // Signed greater than + case OJSLte: return COND_LE; // Signed less or equal + case OJULt: return COND_LO; // Unsigned less than (carry clear) + case OJUGte: return COND_HS; // Unsigned greater or equal (carry set) + // Float NaN-aware comparisons (includes unordered case) + case OJNotLt: return COND_HS; // Not less than (C=1: >=, or unordered) + case OJNotGte: return COND_LT; // Not greater/equal (N!=V: <, or unordered) + default: + JIT_ASSERT(0); + return COND_AL; + } +} + +/** + * Conditional and comparison jumps + * + * Handles special cases for dynamic types: + * - HDYN/HFUN: Call hl_dyn_compare() to compare dynamic values + * - HTYPE: Call hl_same_type() to compare type objects + * - HNULL: Compare boxed values (Null) + * - HVIRTUAL: Compare virtual objects with underlying values + */ +static void op_jump(jit_ctx *ctx, vreg *a, vreg *b, hl_op op, int target_opcode) { + // Spill all registers to stack BEFORE the branch. + // Target label will use discard_regs() and expect values on stack. + spill_regs(ctx); + spill_callee_saved(ctx); // Callee-saved must also be spilled at control flow merge + + // Handle dynamic and function type comparisons + if (a->t->kind == HDYN || b->t->kind == HDYN || a->t->kind == HFUN || b->t->kind == HFUN) { + // Call hl_dyn_compare(a, b) which returns: + // 0 if equal + // negative if a < b + // positive if a > b + // hl_invalid_comparison (0xAABBCCDD) for incomparable types + vreg *args[2] = { a, b }; + int stack_space = prepare_call_args(ctx, NULL, args, 2, true); + + // Load function pointer and call + load_immediate(ctx, (int64_t)hl_dyn_compare, RTMP, true); + EMIT32(ctx, 0xD63F0000 | (RTMP << 5)); // BLR RTMP + + // Clean up stack + if (stack_space > 0) { + encode_add_sub_imm(ctx, 1, 0, 0, 0, stack_space, SP_REG, SP_REG); + } + + // Handle ordered comparisons (OJSLt/OJSGt/OJSLte/OJSGte) - need to check for hl_invalid_comparison + if (op == OJSLt || op == OJSGt || op == OJSLte || op == OJSGte) { + // Compare result with hl_invalid_comparison (0xAABBCCDD) + // If equal, don't take the branch (skip the jump) + load_immediate(ctx, hl_invalid_comparison, RTMP, false); + encode_add_sub_reg(ctx, 0, 1, 1, 0, RTMP, 0, X0, XZR); // CMP W0, WTMP + int skip_pos = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); // B.EQ skip (if invalid comparison) + + // Valid comparison - compare result with 0 for sign flags + encode_add_sub_imm(ctx, 0, 1, 1, 0, 0, X0, XZR); // CMP W0, #0 + ArmCondition cond = hl_cond_to_arm(op, false); + int jump_pos = BUF_POS(); + encode_branch_cond(ctx, 0, cond); + register_jump(ctx, jump_pos, target_opcode); + + // Patch the skip branch to here + int skip_offset = (BUF_POS() - skip_pos) / 4; + *(int*)(ctx->startBuf + skip_pos) = (*(int*)(ctx->startBuf + skip_pos) & 0xFF00001F) | ((skip_offset & 0x7FFFF) << 5); + return; + } + + // For OJEq/OJNotEq: result == 0 means equal + // TST W0, W0 (sets flags based on W0 & W0) + encode_logical_reg(ctx, 0, 0x3, 0, 0, X0, 0, X0, XZR); // ANDS WZR, W0, W0 + + // Branch based on zero flag (only equality ops should reach here) + ArmCondition cond = (op == OJEq) ? COND_EQ : COND_NE; + int jump_pos = BUF_POS(); + encode_branch_cond(ctx, 0, cond); + register_jump(ctx, jump_pos, target_opcode); + return; + } + + // Handle type comparisons + if (a->t->kind == HTYPE) { + // Call hl_same_type(a, b) which returns bool + vreg *args[2] = { a, b }; + int stack_space = prepare_call_args(ctx, NULL, args, 2, true); + + load_immediate(ctx, (int64_t)hl_same_type, RTMP, true); + EMIT32(ctx, 0xD63F0000 | (RTMP << 5)); // BLR RTMP + + if (stack_space > 0) { + encode_add_sub_imm(ctx, 1, 0, 0, 0, stack_space, SP_REG, SP_REG); + } + + // Compare result with 1 (true): CMP W0, #1 = SUBS WZR, W0, #1 + // Note: S=1 is required both to set flags AND to make Rd=31 mean XZR (not SP) + encode_add_sub_imm(ctx, 0, 1, 1, 0, 1, X0, XZR); // CMP W0, #1 + + ArmCondition cond = (op == OJEq) ? COND_EQ : COND_NE; + int jump_pos = BUF_POS(); + encode_branch_cond(ctx, 0, cond); + register_jump(ctx, jump_pos, target_opcode); + return; + } + + // Handle HNULL (Null) comparisons + // HNULL values have their inner value at offset HDYN_VALUE (8) + if (a->t->kind == HNULL) { + preg *pa = fetch(ctx, a); + preg *pb = fetch(ctx, b); + Arm64Reg ra = (pa->kind == RCPU) ? (Arm64Reg)pa->id : RTMP; + Arm64Reg rb = (pb->kind == RCPU) ? (Arm64Reg)pb->id : RTMP2; + if (pa->kind != RCPU) ldr_stack(ctx, ra, a->stackPos, 8); + if (pb->kind != RCPU) ldr_stack(ctx, rb, b->stackPos, 8); + + if (op == OJEq) { + // if (a == b || (a && b && a->v == b->v)) goto target + // First: CMP a, b - if equal, jump to target + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); + int jump_pos1 = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); + register_jump(ctx, jump_pos1, target_opcode); + + // If a == NULL, skip (don't jump) + int skip_a = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, ra); // CBZ ra, skip + + // If b == NULL, skip (don't jump) + int skip_b = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, rb); // CBZ rb, skip + + // Load inner values: a->v and b->v (at offset HDYN_VALUE) + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, HDYN_VALUE / 8, ra, ra); // LDR ra, [ra, #8] + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, HDYN_VALUE / 8, rb, rb); // LDR rb, [rb, #8] + + // Compare inner values + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); + int jump_pos2 = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); + register_jump(ctx, jump_pos2, target_opcode); + + // Patch skip branches to here + int here = BUF_POS(); + int off_a = (here - skip_a) / 4; + int off_b = (here - skip_b) / 4; + *(int*)(ctx->startBuf + skip_a) = (*(int*)(ctx->startBuf + skip_a) & 0xFF00001F) | ((off_a & 0x7FFFF) << 5); + *(int*)(ctx->startBuf + skip_b) = (*(int*)(ctx->startBuf + skip_b) & 0xFF00001F) | ((off_b & 0x7FFFF) << 5); + } else if (op == OJNotEq) { + // if (a != b && (!a || !b || a->v != b->v)) goto target + // First: CMP a, b - if equal, skip entirely + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); + int skip_eq = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); // B.EQ skip (a == b means not-not-equal) + + // If a == NULL, goto target (NULL != non-NULL) + int jump_a = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, ra); // CBZ ra, target + register_jump(ctx, jump_a, target_opcode); + + // If b == NULL, goto target (non-NULL != NULL) + int jump_b = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, rb); // CBZ rb, target + register_jump(ctx, jump_b, target_opcode); + + // Load inner values + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, HDYN_VALUE / 8, ra, ra); + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, HDYN_VALUE / 8, rb, rb); + + // Compare inner values - if not equal, goto target + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); + int skip_cmp = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); // B.EQ skip (values equal, don't jump) + + // Values not equal - jump to target + int jump_ne = BUF_POS(); + encode_branch_uncond(ctx, 0); + register_jump(ctx, jump_ne, target_opcode); + + // Patch skip branches + int here = BUF_POS(); + int off_eq = (here - skip_eq) / 4; + int off_cmp = (here - skip_cmp) / 4; + *(int*)(ctx->startBuf + skip_eq) = (*(int*)(ctx->startBuf + skip_eq) & 0xFF00001F) | ((off_eq & 0x7FFFF) << 5); + *(int*)(ctx->startBuf + skip_cmp) = (*(int*)(ctx->startBuf + skip_cmp) & 0xFF00001F) | ((off_cmp & 0x7FFFF) << 5); + } else { + jit_error("Unsupported comparison op for HNULL"); + } + return; + } + + // Handle HVIRTUAL comparisons + // Virtual objects have a 'value' pointer at offset HL_WSIZE (8) + if (a->t->kind == HVIRTUAL) { + preg *pa = fetch(ctx, a); + preg *pb = fetch(ctx, b); + Arm64Reg ra = (pa->kind == RCPU) ? (Arm64Reg)pa->id : RTMP; + Arm64Reg rb = (pb->kind == RCPU) ? (Arm64Reg)pb->id : RTMP2; + if (pa->kind != RCPU) ldr_stack(ctx, ra, a->stackPos, 8); + if (pb->kind != RCPU) ldr_stack(ctx, rb, b->stackPos, 8); + + if (b->t->kind == HOBJ) { + // Comparing virtual to object: compare a->value with b + if (op == OJEq) { + // if (a ? (b && a->value == b) : (b == NULL)) goto target + int ja = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, ra); // CBZ ra, check_b_null + + // a != NULL: check if b != NULL and a->value == b + int jb = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, rb); // CBZ rb, skip (a!=NULL, b==NULL: not equal) + + // Load a->value and compare with b + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, HL_WSIZE / 8, ra, ra); // LDR ra, [ra, #8] + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); + int jvalue = BUF_POS(); + encode_branch_uncond(ctx, 0); // B to_cmp + + // a == NULL: check if b == NULL + int here_ja = BUF_POS(); + int off_ja = (here_ja - ja) / 4; + *(int*)(ctx->startBuf + ja) = (*(int*)(ctx->startBuf + ja) & 0xFF00001F) | ((off_ja & 0x7FFFF) << 5); + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, XZR, XZR); // CMP rb, #0 (TST rb) + + // Patch jvalue to here (to_cmp) + int here_jv = BUF_POS(); + int off_jv = (here_jv - jvalue) / 4; + *(int*)(ctx->startBuf + jvalue) = 0x14000000 | (off_jv & 0x3FFFFFF); + + // Now flags are set - branch if equal + int jump_pos = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); + register_jump(ctx, jump_pos, target_opcode); + + // Patch jb to skip + int here_jb = BUF_POS(); + int off_jb = (here_jb - jb) / 4; + *(int*)(ctx->startBuf + jb) = (*(int*)(ctx->startBuf + jb) & 0xFF00001F) | ((off_jb & 0x7FFFF) << 5); + } else if (op == OJNotEq) { + // if (a ? (b == NULL || a->value != b) : (b != NULL)) goto target + int ja = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, ra); // CBZ ra, check_b_notnull + + // a != NULL: jump if b == NULL + int jump_b_null = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, rb); // CBZ rb, target + register_jump(ctx, jump_b_null, target_opcode); + + // Load a->value and compare with b + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, HL_WSIZE / 8, ra, ra); + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); + int jvalue = BUF_POS(); + encode_branch_uncond(ctx, 0); // B to_cmp + + // a == NULL: check if b != NULL + int here_ja = BUF_POS(); + int off_ja = (here_ja - ja) / 4; + *(int*)(ctx->startBuf + ja) = (*(int*)(ctx->startBuf + ja) & 0xFF00001F) | ((off_ja & 0x7FFFF) << 5); + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, XZR, XZR); // CMP rb, #0 + + // Patch jvalue + int here_jv = BUF_POS(); + int off_jv = (here_jv - jvalue) / 4; + *(int*)(ctx->startBuf + jvalue) = 0x14000000 | (off_jv & 0x3FFFFFF); + + // Branch if not equal + int jump_pos = BUF_POS(); + encode_branch_cond(ctx, 0, COND_NE); + register_jump(ctx, jump_pos, target_opcode); + } else { + jit_error("Unsupported comparison op for HVIRTUAL vs HOBJ"); + } + return; + } + + // Both are HVIRTUAL - compare underlying values + if (op == OJEq) { + // if (a == b || (a && b && a->value && b->value && a->value == b->value)) goto + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); + int jump_eq = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); + register_jump(ctx, jump_eq, target_opcode); + + // Check a != NULL + int skip_a = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, ra); + // Check b != NULL + int skip_b = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, rb); + + // Load a->value + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, HL_WSIZE / 8, ra, ra); + int skip_av = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, ra); // CBZ if a->value == NULL + + // Load b->value + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, HL_WSIZE / 8, rb, rb); + int skip_bv = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, rb); // CBZ if b->value == NULL + + // Compare values + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); + int jump_val = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); + register_jump(ctx, jump_val, target_opcode); + + // Patch all skips to here + int here = BUF_POS(); + int patches[] = { skip_a, skip_b, skip_av, skip_bv }; + for (int i = 0; i < 4; i++) { + int off = (here - patches[i]) / 4; + *(int*)(ctx->startBuf + patches[i]) = (*(int*)(ctx->startBuf + patches[i]) & 0xFF00001F) | ((off & 0x7FFFF) << 5); + } + } else if (op == OJNotEq) { + // if (a != b && (!a || !b || !a->value || !b->value || a->value != b->value)) goto + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); + int skip_eq = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); // Skip if a == b + + // If a == NULL, jump + int jump_a = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, ra); + register_jump(ctx, jump_a, target_opcode); + + // If b == NULL, jump + int jump_b = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, rb); + register_jump(ctx, jump_b, target_opcode); + + // Load a->value + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, HL_WSIZE / 8, ra, ra); + int jump_av = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, ra); + register_jump(ctx, jump_av, target_opcode); + + // Load b->value + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, HL_WSIZE / 8, rb, rb); + int jump_bv = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, rb); + register_jump(ctx, jump_bv, target_opcode); + + // Compare - if not equal, jump + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); + int skip_val = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); + + // Not equal - jump to target + int jump_ne = BUF_POS(); + encode_branch_uncond(ctx, 0); + register_jump(ctx, jump_ne, target_opcode); + + // Patch skips + int here = BUF_POS(); + int off_eq = (here - skip_eq) / 4; + int off_val = (here - skip_val) / 4; + *(int*)(ctx->startBuf + skip_eq) = (*(int*)(ctx->startBuf + skip_eq) & 0xFF00001F) | ((off_eq & 0x7FFFF) << 5); + *(int*)(ctx->startBuf + skip_val) = (*(int*)(ctx->startBuf + skip_val) & 0xFF00001F) | ((off_val & 0x7FFFF) << 5); + } else { + jit_error("Unsupported comparison op for HVIRTUAL"); + } + return; + } + + // Handle HOBJ/HSTRUCT vs HVIRTUAL (swap operands) + if ((a->t->kind == HOBJ || a->t->kind == HSTRUCT) && b->t->kind == HVIRTUAL) { + // Swap and recurse - the HVIRTUAL case handles HOBJ on the right + op_jump(ctx, b, a, op, target_opcode); + return; + } + + // Handle String EQUALITY comparison (value-based per Haxe spec) + // hl_str_cmp only returns 0 (equal) or 1 (not equal), so it can only be used + // for OJEq/OJNotEq. For ordered comparisons, fall through to compareFun path. + if ((op == OJEq || op == OJNotEq) && is_string_type(a->t) && is_string_type(b->t)) { + // Spill before call + spill_regs(ctx); + spill_callee_saved(ctx); + + // Call hl_str_cmp(a, b) - returns 0 if equal, non-zero if not equal + vreg *args[2] = { a, b }; + int stack_space = prepare_call_args(ctx, NULL, args, 2, true); + load_immediate(ctx, (int64_t)hl_str_cmp, RTMP, true); + EMIT32(ctx, 0xD63F0000 | (RTMP << 5)); // BLR RTMP + if (stack_space > 0) { + encode_add_sub_imm(ctx, 1, 0, 0, 0, stack_space, SP_REG, SP_REG); + } + + // Result in X0: 0 = equal, non-zero = not equal + // TST X0, X0 sets Z flag (Z=1 if X0==0) + encode_logical_reg(ctx, 1, 0x3, 0, 0, X0, 0, X0, XZR); // TST X0, X0 + + // Branch based on op (only EQ or NE) + ArmCondition cond = (op == OJEq) ? COND_EQ : COND_NE; + int jump_pos = BUF_POS(); + encode_branch_cond(ctx, 0, cond); + register_jump(ctx, jump_pos, target_opcode); + return; + } + + // Handle HOBJ/HSTRUCT with compareFun (e.g., String) + // Use hl_get_obj_rt() to ensure runtime object is initialized (like x86 does) + // NOTE: compareFun is a FUNCTION INDEX, not a function pointer! + if ((a->t->kind == HOBJ || a->t->kind == HSTRUCT) && hl_get_obj_rt(a->t)->compareFun) { + int compareFunIndex = (int)(int_val)hl_get_obj_rt(a->t)->compareFun; + preg *pa = fetch(ctx, a); + preg *pb = fetch(ctx, b); + Arm64Reg ra = (pa->kind == RCPU) ? (Arm64Reg)pa->id : RTMP; + Arm64Reg rb = (pb->kind == RCPU) ? (Arm64Reg)pb->id : RTMP2; + if (pa->kind != RCPU) ldr_stack(ctx, ra, a->stackPos, 8); + if (pb->kind != RCPU) ldr_stack(ctx, rb, b->stackPos, 8); + + if (op == OJEq) { + // if (a == b || (a && b && cmp(a,b) == 0)) goto target + // First check pointer equality + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); // CMP ra, rb + int jump_eq = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); + register_jump(ctx, jump_eq, target_opcode); + + // If a == NULL, skip + int skip_a = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, ra); + + // If b == NULL, skip + int skip_b = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, rb); + + // Call compareFun(a, b) - compareFunIndex is a function index, not a pointer! + vreg *args[2] = { a, b }; + int stack_space = prepare_call_args(ctx, NULL, args, 2, true); + emit_call_findex(ctx, compareFunIndex, stack_space); + + // If result == 0, goto target + encode_logical_reg(ctx, 0, 0x3, 0, 0, X0, 0, X0, XZR); // TST W0, W0 + int skip_cmp = BUF_POS(); + encode_branch_cond(ctx, 0, COND_NE); // Skip if result != 0 + + // Jump to target + int jump_target = BUF_POS(); + encode_branch_uncond(ctx, 0); + register_jump(ctx, jump_target, target_opcode); + + // Patch all skips to here + int here = BUF_POS(); + int patches[] = { skip_a, skip_b, skip_cmp }; + for (int i = 0; i < 3; i++) { + int off = (here - patches[i]) / 4; + *(int*)(ctx->startBuf + patches[i]) = (*(int*)(ctx->startBuf + patches[i]) & 0xFF00001F) | ((off & 0x7FFFF) << 5); + } + } else if (op == OJNotEq) { + // if (a != b && (!a || !b || cmp(a,b) != 0)) goto target + // First check pointer equality - if equal, skip entirely + encode_add_sub_reg(ctx, 1, 1, 1, 0, rb, 0, ra, XZR); // CMP ra, rb + int skip_eq = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); + + // If a == NULL, goto target + int jump_a = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, ra); + register_jump(ctx, jump_a, target_opcode); + + // If b == NULL, goto target + int jump_b = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, rb); + register_jump(ctx, jump_b, target_opcode); + + // Call compareFun(a, b) - compareFunIndex is a function index, not a pointer! + vreg *args[2] = { a, b }; + int stack_space = prepare_call_args(ctx, NULL, args, 2, true); + emit_call_findex(ctx, compareFunIndex, stack_space); + + // If result != 0, goto target + encode_logical_reg(ctx, 0, 0x3, 0, 0, X0, 0, X0, XZR); // TST W0, W0 + int skip_cmp = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); // Skip if result == 0 + + // Jump to target + int jump_target = BUF_POS(); + encode_branch_uncond(ctx, 0); + register_jump(ctx, jump_target, target_opcode); + + // Patch skips to here + int here = BUF_POS(); + int off_eq = (here - skip_eq) / 4; + int off_cmp = (here - skip_cmp) / 4; + *(int*)(ctx->startBuf + skip_eq) = (*(int*)(ctx->startBuf + skip_eq) & 0xFF00001F) | ((off_eq & 0x7FFFF) << 5); + *(int*)(ctx->startBuf + skip_cmp) = (*(int*)(ctx->startBuf + skip_cmp) & 0xFF00001F) | ((off_cmp & 0x7FFFF) << 5); + } else { + // For OJSGt, OJSGte, OJSLt, OJSLte: if (a && b && cmp(a,b) ?? 0) goto + int skip_a = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, ra); + + int skip_b = BUF_POS(); + encode_cbz_cbnz(ctx, 1, 0, 0, rb); + + // Call compareFun(a, b) - compareFunIndex is a function index, not a pointer! + vreg *args[2] = { a, b }; + int stack_space = prepare_call_args(ctx, NULL, args, 2, true); + emit_call_findex(ctx, compareFunIndex, stack_space); + + // Compare result with 0: CMP W0, #0 + encode_add_sub_imm(ctx, 0, 1, 1, 0, 0, X0, XZR); // CMP W0, #0 + + // Branch based on condition + ArmCondition cond = hl_cond_to_arm(op, false); + int jump_pos = BUF_POS(); + encode_branch_cond(ctx, 0, cond); + register_jump(ctx, jump_pos, target_opcode); + + // Patch skips to here + int here = BUF_POS(); + int off_a = (here - skip_a) / 4; + int off_b = (here - skip_b) / 4; + *(int*)(ctx->startBuf + skip_a) = (*(int*)(ctx->startBuf + skip_a) & 0xFF00001F) | ((off_a & 0x7FFFF) << 5); + *(int*)(ctx->startBuf + skip_b) = (*(int*)(ctx->startBuf + skip_b) & 0xFF00001F) | ((off_b & 0x7FFFF) << 5); + } + return; + } + + // Standard comparison for other types + bool is_float = IS_FLOAT(a); + preg *pa = fetch(ctx, a); + preg *pb = fetch(ctx, b); + + if (is_float) { + // Floating-point comparison: FCMP Vn, Vm + int type = (a->t->kind == HF64) ? 0x01 : 0x00; + encode_fp_compare(ctx, 0, 0, type, pb->id, 0, pa->id); + } else { + // Integer comparison: CMP Xn, Xm (implemented as SUBS XZR, Xn, Xm) + int sf = (a->size == 8) ? 1 : 0; + encode_add_sub_reg(ctx, sf, 1, 1, 0, pb->id, 0, pa->id, XZR); + } + + // Emit conditional branch + ArmCondition cond = hl_cond_to_arm(op, is_float); + int jump_pos = BUF_POS(); + encode_branch_cond(ctx, 0, cond); // Offset will be patched later + + // Register for patching + register_jump(ctx, jump_pos, target_opcode); +} + +/** + * Simple conditional jumps (OJTrue, OJFalse, OJNull, OJNotNull) + */ +static void op_jcond(jit_ctx *ctx, vreg *a, hl_op op, int target_opcode) { + // Spill all registers to stack BEFORE the branch. + // Target label will use discard_regs() and expect values on stack. + spill_regs(ctx); + spill_callee_saved(ctx); // Callee-saved must also be spilled at control flow merge + + preg *pa = fetch(ctx, a); + int jump_pos = BUF_POS(); + + // Determine which condition to test + bool test_zero = (op == OJFalse || op == OJNull); + + // Use CBZ (compare and branch if zero) or CBNZ (compare and branch if non-zero) + int sf = (a->size == 8) ? 1 : 0; + int op_bit = test_zero ? 0 : 1; // 0=CBZ, 1=CBNZ + + encode_cbz_cbnz(ctx, sf, op_bit, 0, pa->id); // Offset will be patched + + // Register for patching + register_jump(ctx, jump_pos, target_opcode); +} + +/** + * Unconditional jump (OJAlways) + */ +static void op_jalways(jit_ctx *ctx, int target_opcode) { + // Spill all registers to stack BEFORE the branch. + // Target label will use discard_regs() and expect values on stack. + spill_regs(ctx); + spill_callee_saved(ctx); // Callee-saved must also be spilled at control flow merge + + int jump_pos = BUF_POS(); + encode_branch_uncond(ctx, 0); // Offset will be patched + + // Register for patching + register_jump(ctx, jump_pos, target_opcode); +} + +/** + * Discard all register bindings at merge points (labels). + * + * Used at labels where control flow can come from multiple paths. + * Clears register↔vreg bindings so subsequent operations load from stack. + * + * With dirty tracking: If reached via fallthrough (not a jump), registers + * might still be dirty and need to be spilled first. Registers reached via + * jump are already clean because spill_regs() is called before all jumps. + */ +static void discard_regs(jit_ctx *ctx) { + int i; + // Handle CPU scratch registers (X0-X17) + // NOTE: This function must NOT emit any code! + // At labels, spill_regs() + spill_callee_saved() is called BEFORE this (for fallthrough). + // We just clear bindings here - values are already on stack. + for (i = 0; i < 18; i++) { + preg *r = &ctx->pregs[i]; + if (r->holds) { + r->holds->dirty = 0; + r->holds->current = NULL; + r->holds = NULL; + } + } + // Handle callee-saved CPU registers (X19-X26) + // At merge points, callee-saved must also be discarded for consistent state + for (i = 0; i < RCPU_CALLEE_ALLOC_COUNT; i++) { + preg *r = REG_AT(RCPU_CALLEE_ALLOC[i]); + if (r->holds) { + r->holds->dirty = 0; + r->holds->current = NULL; + r->holds = NULL; + } + } + // Handle FPU scratch registers (V0-V7) + for (i = 0; i < 8; i++) { + preg *r = &ctx->pregs[RCPU_COUNT + i]; + if (r->holds) { + r->holds->dirty = 0; + r->holds->current = NULL; + r->holds = NULL; + } + } +} + +/** + * Label marker (OLabel) - just records position for jump targets + * At a label, control flow could come from multiple places, + * so we must invalidate all register associations. + * + * IMPORTANT: No code is emitted here! The main loop calls spill_regs() + * BEFORE this function for the fallthrough path. Jump paths have already + * spilled before jumping. We just clear bindings so subsequent ops + * load from stack. + */ +static void op_label(jit_ctx *ctx) { + // Just clear bindings - spill_regs() was already called in main loop + discard_regs(ctx); +} + +// ============================================================================ +// Memory Operations +// ============================================================================ + +/* + * Load byte/halfword/word from memory + * OGetI8/OGetI16/OGetI32: dst = *(type*)(base + offset) + */ +static void op_get_mem(jit_ctx *ctx, vreg *dst, vreg *base, int offset, int size) { + preg *base_reg = fetch(ctx, base); + preg *dst_reg = alloc_dst(ctx, dst); + + Arm64Reg base_r = (base_reg->kind == RCPU) ? (Arm64Reg)base_reg->id : RTMP; + if (base_reg->kind != RCPU) { + ldr_stack(ctx, base_r, base->stackPos, base->size); + } + + // Handle float and integer cases separately + if (IS_FLOAT(dst)) { + // Float: load into FPU register + Arm64FpReg dst_r = (dst_reg->kind == RFPU) ? (Arm64FpReg)dst_reg->id : V16; + int size_bits = (size == 8) ? 0x03 : 0x02; // D or S + + if (offset >= 0 && offset < (1 << 12) * size) { + int imm12 = offset / size; + encode_ldr_str_imm(ctx, size_bits, 1, 0x01, imm12, base_r, dst_r); // V=1 for FP + } else { + load_immediate(ctx, offset, RTMP2, false); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP2, 0, base_r, RTMP); + encode_ldr_str_imm(ctx, size_bits, 1, 0x01, 0, RTMP, dst_r); // V=1 for FP + } + + str_stack_fp(ctx, dst_r, dst->stackPos, dst->size); + } else { + // Integer/pointer: load into CPU register + // Use RTMP2 as temp (not RTMP) because str_stack's fallback uses RTMP internally. + // If we loaded into RTMP, str_stack would clobber the value. + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : RTMP2; + + // Load with offset + // LDR Xd, [Xn, #offset] or LDRB/LDRH for smaller sizes + if (offset >= 0 && offset < (1 << 12) * size) { + // Fits in immediate offset + int imm12 = offset / size; + // size: 1=LDRB, 2=LDRH, 4=LDR(W), 8=LDR(X) + int size_bits = (size == 1) ? 0x00 : (size == 2) ? 0x01 : (size == 4) ? 0x02 : 0x03; + encode_ldr_str_imm(ctx, size_bits, 0, 0x01, imm12, base_r, dst_r); + } else { + // Offset too large - compute effective address in RTMP, then load into dst_r + load_immediate(ctx, offset, RTMP, false); // Use RTMP for address computation + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP, 0, base_r, RTMP); + // LDR dst_r, [RTMP] + encode_ldr_str_imm(ctx, (size == 8) ? 0x03 : 0x02, 0, 0x01, 0, RTMP, dst_r); + } + + // Always store to stack - it's the source of truth for later loads + // (registers may be clobbered by subsequent calls) + str_stack(ctx, dst_r, dst->stackPos, dst->size); + } + + // Release the base register - discard() will store if dirty + discard(ctx, base_reg); +} + +/* + * Store byte/halfword/word to memory + * OSetI8/OSetI16/OSetI32: *(type*)(base + offset) = value + */ +static void op_set_mem(jit_ctx *ctx, vreg *base, int offset, vreg *value, int size) { + preg *base_reg = fetch(ctx, base); + preg *value_reg = fetch(ctx, value); + + /* + * IMPORTANT: Load value FIRST, then base. + * ldr_stack's fallback path uses RTMP internally, so if we load base into RTMP + * first, then load value from stack, RTMP would get clobbered. + * By loading value first (into RTMP2 or FPU reg), any RTMP usage is harmless. + * Then we load base into RTMP, which is safe since value is already loaded. + */ + + // Handle float and integer cases separately + if (IS_FLOAT(value)) { + // Float: load value first into FPU register + Arm64FpReg value_r = (value_reg->kind == RFPU) ? (Arm64FpReg)value_reg->id : V16; + if (value_reg->kind != RFPU) { + ldr_stack_fp(ctx, value_r, value->stackPos, value->size); + } + + // Now load base (safe - value is already in FPU reg) + Arm64Reg base_r = (base_reg->kind == RCPU) ? (Arm64Reg)base_reg->id : RTMP; + if (base_reg->kind != RCPU) { + ldr_stack(ctx, base_r, base->stackPos, base->size); + } + + int size_bits = (size == 8) ? 0x03 : 0x02; // D or S + + if (offset >= 0 && offset < (1 << 12) * size) { + int imm12 = offset / size; + encode_ldr_str_imm(ctx, size_bits, 1, 0x00, imm12, base_r, value_r); // V=1 for FP + } else { + load_immediate(ctx, offset, RTMP2, false); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP2, 0, base_r, RTMP); + encode_ldr_str_imm(ctx, size_bits, 1, 0x00, 0, RTMP, value_r); // V=1 for FP + } + } else { + // Integer/pointer: load value first into CPU register + Arm64Reg value_r = (value_reg->kind == RCPU) ? (Arm64Reg)value_reg->id : RTMP2; + if (value_reg->kind == RCONST) { + load_immediate(ctx, value_reg->id, value_r, value->size == 8); + } else if (value_reg->kind != RCPU) { + ldr_stack(ctx, value_r, value->stackPos, value->size); + } + + // Now load base (safe - value is already in RTMP2 or CPU reg) + Arm64Reg base_r = (base_reg->kind == RCPU) ? (Arm64Reg)base_reg->id : RTMP; + if (base_reg->kind != RCPU) { + ldr_stack(ctx, base_r, base->stackPos, base->size); + } + + // Store with offset + // STR Xd, [Xn, #offset] or STRB/STRH for smaller sizes + if (offset >= 0 && offset < (1 << 12) * size) { + // Fits in immediate offset + int imm12 = offset / size; + int size_bits = (size == 1) ? 0x00 : (size == 2) ? 0x01 : (size == 4) ? 0x02 : 0x03; + encode_ldr_str_imm(ctx, size_bits, 0, 0x00, imm12, base_r, value_r); + } else { + // Offset too large - load offset to temp register + if (value_r == RTMP2) { + // Value is already in RTMP2, use a different temp + load_immediate(ctx, offset, X9, false); + encode_add_sub_reg(ctx, 1, 0, 0, 0, X9, 0, base_r, RTMP); + } else { + load_immediate(ctx, offset, RTMP2, false); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP2, 0, base_r, RTMP); + } + // STR value_r, [RTMP] + encode_ldr_str_imm(ctx, (size == 8) ? 0x03 : 0x02, 0, 0x00, 0, RTMP, value_r); + } + } + + discard(ctx, base_reg); + discard(ctx, value_reg); +} + +/* + * Load byte/halfword/word from memory with register offset + * OGetI8/OGetI16/OGetMem: dst = *(type*)(base + offset_reg) + * Unlike op_get_mem which takes an immediate offset, this takes an offset vreg + */ +/* + * IMPORTANT: We must load offset BEFORE base when base uses RTMP, + * because ldr_stack's fallback path uses RTMP as a temporary. + * Order: offset -> base -> compute address -> load + */ +static void op_get_mem_reg(jit_ctx *ctx, vreg *dst, vreg *base, vreg *offset, int size) { + preg *base_reg = fetch(ctx, base); + preg *offset_reg = fetch(ctx, offset); + preg *dst_reg = alloc_dst(ctx, dst); + + // Step 1: Load offset FIRST (may clobber RTMP in fallback, but we haven't used it yet) + Arm64Reg offset_r = (offset_reg->kind == RCPU) ? (Arm64Reg)offset_reg->id : RTMP2; + if (offset_reg->kind == RCONST) { + load_immediate(ctx, offset_reg->id, offset_r, false); + } else if (offset_reg->kind != RCPU) { + ldr_stack(ctx, offset_r, offset->stackPos, offset->size); + } + + // Step 2: Load base (if it needs RTMP, the value will stay in RTMP) + Arm64Reg base_r = (base_reg->kind == RCPU) ? (Arm64Reg)base_reg->id : RTMP; + if (base_reg->kind != RCPU) { + ldr_stack(ctx, base_r, base->stackPos, base->size); + } + + // Step 3: Compute effective address: RTMP = base + offset + encode_add_sub_reg(ctx, 1, 0, 0, 0, offset_r, 0, base_r, RTMP); + + // Load from [RTMP] - handle float vs integer types + int size_bits = (size == 1) ? 0x00 : (size == 2) ? 0x01 : (size == 4) ? 0x02 : 0x03; + + if (IS_FLOAT(dst)) { + // Float load: use FPU register and V=1 + Arm64FpReg dst_fp = (dst_reg->kind == RFPU) ? (Arm64FpReg)dst_reg->id : V16; + encode_ldr_str_imm(ctx, size_bits, 1, 0x01, 0, RTMP, dst_fp); // V=1 for FP + str_stack_fp(ctx, dst_fp, dst->stackPos, dst->size); + } else { + // Integer load + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : X9; + encode_ldr_str_imm(ctx, size_bits, 0, 0x01, 0, RTMP, dst_r); + // For byte/halfword loads, the result is zero-extended automatically by LDRB/LDRH + str_stack(ctx, dst_r, dst->stackPos, dst->size); + } + + discard(ctx, base_reg); + discard(ctx, offset_reg); +} + +/* + * Store byte/halfword/word to memory with register offset + * OSetI8/OSetI16/OSetMem: *(type*)(base + offset_reg) = value + * Unlike op_set_mem which takes an immediate offset, this takes an offset vreg + * + * IMPORTANT: We must load the value BEFORE computing the address in RTMP, + * because ldr_stack's fallback path for large/unaligned offsets uses RTMP + * as a temporary register. + */ +static void op_set_mem_reg(jit_ctx *ctx, vreg *base, vreg *offset, vreg *value, int size) { + preg *base_reg = fetch(ctx, base); + preg *offset_reg = fetch(ctx, offset); + preg *value_reg = fetch(ctx, value); + + int size_bits = (size == 1) ? 0x00 : (size == 2) ? 0x01 : (size == 4) ? 0x02 : 0x03; + + // Step 1: Load value FIRST (before using RTMP for address computation) + // ldr_stack's fallback path uses RTMP, so we must do this before RTMP holds the address + Arm64FpReg value_fp = V16; + Arm64Reg value_r = X9; + + if (IS_FLOAT(value)) { + value_fp = (value_reg->kind == RFPU) ? (Arm64FpReg)value_reg->id : V16; + if (value_reg->kind != RFPU) { + ldr_stack_fp(ctx, value_fp, value->stackPos, value->size); + } + } else { + value_r = (value_reg->kind == RCPU) ? (Arm64Reg)value_reg->id : X9; + if (value_reg->kind == RCONST) { + load_immediate(ctx, value_reg->id, value_r, value->size == 8); + } else if (value_reg->kind != RCPU) { + ldr_stack(ctx, value_r, value->stackPos, value->size); + } + } + + // Step 2: Load base and offset (these may also use RTMP in fallback, but that's ok + // since we compute the final address in RTMP at the end) + Arm64Reg base_r = (base_reg->kind == RCPU) ? (Arm64Reg)base_reg->id : RTMP; + if (base_reg->kind != RCPU) { + ldr_stack(ctx, base_r, base->stackPos, base->size); + } + + Arm64Reg offset_r = (offset_reg->kind == RCPU) ? (Arm64Reg)offset_reg->id : RTMP2; + if (offset_reg->kind == RCONST) { + load_immediate(ctx, offset_reg->id, offset_r, false); + } else if (offset_reg->kind != RCPU) { + ldr_stack(ctx, offset_r, offset->stackPos, offset->size); + } + + // Step 3: Compute effective address: RTMP = base + offset + encode_add_sub_reg(ctx, 1, 0, 0, 0, offset_r, 0, base_r, RTMP); + + // Step 4: Store to [RTMP] + if (IS_FLOAT(value)) { + encode_ldr_str_imm(ctx, size_bits, 1, 0x00, 0, RTMP, value_fp); // V=1 for FP + } else { + encode_ldr_str_imm(ctx, size_bits, 0, 0x00, 0, RTMP, value_r); + } + + discard(ctx, base_reg); + discard(ctx, offset_reg); + discard(ctx, value_reg); +} + +/* + * Field access: dst = obj->field + * OField: dst = *(obj + field_offset) + * + * Special handling for HPACKED -> HSTRUCT: return address of inline storage + * instead of loading a value (LEA semantics). + */ +static void op_field(jit_ctx *ctx, vreg *dst, vreg *obj, int field_index) { + hl_runtime_obj *rt = hl_get_obj_rt(obj->t); + int offset = rt->fields_indexes[field_index]; + + // Check for packed field -> struct destination (LEA semantics) + if (dst->t->kind == HSTRUCT) { + hl_type *ft = hl_obj_field_fetch(obj->t, field_index)->t; + if (ft->kind == HPACKED) { + // Return address of inline storage: dst = &obj->field + preg *p_obj = fetch(ctx, obj); + preg *p_dst = alloc_dst(ctx, dst); // Allocates register, binds to dst, marks dirty + + Arm64Reg obj_r = (p_obj->kind == RCPU) ? (Arm64Reg)p_obj->id : RTMP; + if (p_obj->kind != RCPU) { + ldr_stack(ctx, obj_r, obj->stackPos, obj->size); + } + + Arm64Reg dst_r = (Arm64Reg)p_dst->id; // alloc_dst always returns RCPU for non-float + + // ADD dst, obj, #offset (equivalent to LEA) + if (offset >= 0 && offset < 4096) { + encode_add_sub_imm(ctx, 1, 0, 0, 0, offset, obj_r, dst_r); + } else { + load_immediate(ctx, offset, RTMP2, false); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP2, 0, obj_r, dst_r); + } + + // Don't call store_result - alloc_dst already set up the binding + // The value will be spilled when needed + discard(ctx, p_obj); + return; + } + } + + op_get_mem(ctx, dst, obj, offset, dst->size); +} + +/* + * Field assignment: obj->field = value + * OSetField: *(obj + field_offset) = value + * + * Special handling for HSTRUCT -> HPACKED: must copy struct byte-by-byte + * because HPACKED means the struct is stored inline, not as a pointer. + */ +static void op_set_field(jit_ctx *ctx, vreg *obj, int field_index, vreg *value) { + hl_runtime_obj *rt = hl_get_obj_rt(obj->t); + int field_offset = rt->fields_indexes[field_index]; + + // Check for struct-to-packed-field assignment + if (value->t->kind == HSTRUCT) { + hl_type *ft = hl_obj_field_fetch(obj->t, field_index)->t; + if (ft->kind == HPACKED) { + // Copy struct byte-by-byte + hl_runtime_obj *frt = hl_get_obj_rt(ft->tparam); + + // Load obj pointer into RTMP and value pointer into RTMP2. + // This is simpler than trying to manage register allocation for the copy. + preg *p_obj = fetch(ctx, obj); + preg *p_val = fetch(ctx, value); + + // Always load to scratch registers to avoid conflicts with copy temp + Arm64Reg obj_r = RTMP; + Arm64Reg val_r = RTMP2; + + if (p_obj->kind == RCPU) { + // Move from allocated register to RTMP: ORR RTMP, XZR, Rm + encode_logical_reg(ctx, 1, 0x01, 0, 0, (Arm64Reg)p_obj->id, 0, XZR, obj_r); + } else { + ldr_stack(ctx, obj_r, obj->stackPos, obj->size); + } + + if (p_val->kind == RCPU) { + // Move from allocated register to RTMP2: ORR RTMP2, XZR, Rm + encode_logical_reg(ctx, 1, 0x01, 0, 0, (Arm64Reg)p_val->id, 0, XZR, val_r); + } else { + ldr_stack(ctx, val_r, value->stackPos, value->size); + } + + // Use X9 for data copy, X10 for large offset computation + // Evict both if they're holding values + preg *p_x9 = &ctx->pregs[X9]; + preg *p_x10 = &ctx->pregs[X10]; + if (p_x9->holds != NULL) { + free_reg(ctx, p_x9); + } + if (p_x10->holds != NULL) { + free_reg(ctx, p_x10); + } + + Arm64Reg tmp = X9; + int offset = 0; + while (offset < frt->size) { + int remain = frt->size - offset; + int copy_size = remain >= HL_WSIZE ? HL_WSIZE : (remain >= 4 ? 4 : (remain >= 2 ? 2 : 1)); + int size_bits = (copy_size == 8) ? 0x03 : (copy_size == 4) ? 0x02 : (copy_size == 2) ? 0x01 : 0x00; + + // Load from source: LDR tmp, [val_r, #offset] + // Source offset starts at 0 and increments by copy_size, so always aligned + encode_ldr_str_imm(ctx, size_bits, 0, 0x01, offset / copy_size, val_r, tmp); + + // Store to dest: STR tmp, [obj_r + field_offset + offset] + // Dest offset may not be aligned to copy_size, so compute address explicitly + int dest_offset = field_offset + offset; + if ((dest_offset % copy_size) == 0 && dest_offset >= 0 && dest_offset < (1 << 12) * copy_size) { + // Aligned and fits in immediate - use scaled offset + encode_ldr_str_imm(ctx, size_bits, 0, 0x00, dest_offset / copy_size, obj_r, tmp); + } else { + // Misaligned or large offset - compute address in X10 + load_immediate(ctx, dest_offset, X10, false); + encode_add_sub_reg(ctx, 1, 0, 0, 0, X10, 0, obj_r, X10); + encode_ldr_str_imm(ctx, size_bits, 0, 0x00, 0, X10, tmp); + } + + offset += copy_size; + } + + discard(ctx, p_obj); + discard(ctx, p_val); + return; + } + } + + op_set_mem(ctx, obj, field_offset, value, value->size); +} + +/* + * Array element access: dst = array[index] + * OGetArray: dst = hl_aptr(array)[index] + * + * varray layout: { hl_type *t, hl_type *at, int size, int __pad } = 24 bytes + * Data is INLINE immediately after the header (not via a pointer!) + * hl_aptr(a,t) = (t*)(((varray*)(a))+1) = array + sizeof(varray) + * + * CArray (HABSTRACT) layout: raw memory, no header + * For HOBJ/HSTRUCT: return address of element (LEA) + * For other types: load value (LDR) + */ +/* + * IMPORTANT: We must load index BEFORE array when array uses RTMP, + * because ldr_stack's fallback path uses RTMP as a temporary. + * Order: index -> array -> compute address -> load + */ +static void op_get_array(jit_ctx *ctx, vreg *dst, vreg *array, vreg *index) { + preg *array_reg = fetch(ctx, array); + preg *index_reg = fetch(ctx, index); + preg *dst_reg = alloc_dst(ctx, dst); + + // CArrays (HABSTRACT) have different layout - no header, and for HOBJ/HSTRUCT + // we return the address (LEA) rather than loading the value + bool is_carray = (array->t->kind == HABSTRACT); + bool is_lea = is_carray && (dst->t->kind == HOBJ || dst->t->kind == HSTRUCT); + + int elem_size; + if (is_carray) { + if (is_lea) { + // For HOBJ/HSTRUCT in CArray, element size is the runtime object size + hl_runtime_obj *rt = hl_get_obj_rt(dst->t); + elem_size = rt->size; + } else { + // For other types in CArray, element size is pointer size + elem_size = sizeof(void*); + } + } else { + elem_size = hl_type_size(dst->t); + } + + // Step 1: Load index FIRST (may clobber RTMP in fallback, but we haven't used it yet) + Arm64Reg index_r = (index_reg->kind == RCPU) ? (Arm64Reg)index_reg->id : RTMP2; + if (index_reg->kind == RCONST) { + load_immediate(ctx, index_reg->id, index_r, false); + } else if (index_reg->kind != RCPU) { + ldr_stack(ctx, index_r, index->stackPos, index->size); + } + + // Step 2: Load array (if it needs RTMP, the value will stay in RTMP) + Arm64Reg array_r = (array_reg->kind == RCPU) ? (Arm64Reg)array_reg->id : RTMP; + if (array_reg->kind != RCPU) { + ldr_stack(ctx, array_r, array->stackPos, array->size); + } + + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : X9; + + // Step 3: Calculate element address + // For varray: array + sizeof(varray) + index * elem_size + // For CArray: array + index * elem_size (no header) + + if (is_carray) { + // CArray: no header offset, start from array_r directly + // Scale index by elem_size + if (elem_size == 1) { + encode_add_sub_reg(ctx, 1, 0, 0, SHIFT_LSL, index_r, 0, array_r, RTMP); + } else if (elem_size == 2 || elem_size == 4 || elem_size == 8) { + int shift = (elem_size == 2) ? 1 : (elem_size == 4) ? 2 : 3; + encode_add_sub_reg(ctx, 1, 0, 0, SHIFT_LSL, index_r, shift, array_r, RTMP); + } else { + // Non-power-of-2: compute index * elem_size in RTMP2, then add + load_immediate(ctx, elem_size, RTMP2, false); + encode_madd_msub(ctx, 1, 0, RTMP2, XZR, index_r, RTMP2); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP2, 0, array_r, RTMP); + } + } else { + // varray: add sizeof(varray) header offset first + encode_add_sub_imm(ctx, 1, 0, 0, 0, sizeof(varray), array_r, RTMP); + + // Add scaled index offset: RTMP = RTMP + (index_r << shift) + if (elem_size == 1) { + encode_add_sub_reg(ctx, 1, 0, 0, SHIFT_LSL, index_r, 0, RTMP, RTMP); + } else if (elem_size == 2 || elem_size == 4 || elem_size == 8) { + int shift = (elem_size == 2) ? 1 : (elem_size == 4) ? 2 : 3; + encode_add_sub_reg(ctx, 1, 0, 0, SHIFT_LSL, index_r, shift, RTMP, RTMP); + } else { + // Non-power-of-2: scale index into RTMP2, then add + load_immediate(ctx, elem_size, RTMP2, false); + encode_madd_msub(ctx, 1, 0, RTMP2, XZR, index_r, RTMP2); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP2, 0, RTMP, RTMP); + } + } + + if (is_lea) { + // LEA: just move the computed address to dst + mov_reg_reg(ctx, dst_r, RTMP, true); + str_stack(ctx, dst_r, dst->stackPos, dst->size); + } else if (IS_FLOAT(dst)) { + // Float load: use FP register with V=1 + preg *pv0 = PVFPR(0); + if (pv0->holds != NULL && pv0->holds != dst) { + free_reg(ctx, pv0); + } + int size_bits = (dst->size == 8) ? 0x03 : 0x02; // F64 or F32 + encode_ldr_str_imm(ctx, size_bits, 1, 0x01, 0, RTMP, V0); // V=1 for FP + str_stack_fp(ctx, V0, dst->stackPos, dst->size); + // Clear dst's old binding - value is now on stack, not in a register + if (dst->current != NULL) { + dst->current->holds = NULL; + dst->current = NULL; + } + } else { + // Integer load + int size_bits = (elem_size == 1) ? 0x00 : (elem_size == 2) ? 0x01 : (elem_size == 4) ? 0x02 : 0x03; + encode_ldr_str_imm(ctx, size_bits, 0, 0x01, 0, RTMP, dst_r); + str_stack(ctx, dst_r, dst->stackPos, dst->size); + } + + discard(ctx, array_reg); + discard(ctx, index_reg); +} + +/* + * Array element assignment: array[index] = value + * OSetArray: hl_aptr(array)[index] = value + * + * varray layout: { hl_type *t, hl_type *at, int size, int __pad } = 24 bytes + * Data is INLINE immediately after the header (not via a pointer!) + * + * CArray (HABSTRACT) layout: raw memory, no header + * For HOBJ/HSTRUCT: copy entire struct from value (which is address from LEA) + * For other types: store value directly + */ +/* + * IMPORTANT: We must load value and index BEFORE array when array uses RTMP, + * because ldr_stack's fallback path uses RTMP as a temporary. + * Order: value -> index -> array -> compute address -> store + */ +static void op_set_array(jit_ctx *ctx, vreg *array, vreg *index, vreg *value) { + preg *array_reg = fetch(ctx, array); + preg *index_reg = fetch(ctx, index); + preg *value_reg = fetch(ctx, value); + + // CArrays (HABSTRACT) have different semantics + bool is_carray = (array->t->kind == HABSTRACT); + bool is_struct_copy = is_carray && (value->t->kind == HOBJ || value->t->kind == HSTRUCT); + + int elem_size; + if (is_carray) { + if (is_struct_copy) { + // For HOBJ/HSTRUCT in CArray, element size is the runtime object size + hl_runtime_obj *rt = hl_get_obj_rt(value->t); + elem_size = rt->size; + } else { + // For other types in CArray, element size is pointer size + elem_size = sizeof(void*); + } + } else { + elem_size = hl_type_size(value->t); + } + + // Step 1: Load value FIRST (before using RTMP for address computation) + // For struct copy, value is a pointer to the source struct + // For floats, use FP register; for integers, use CPU register + Arm64Reg value_r = X9; + Arm64FpReg value_fp = V0; + bool is_float_value = IS_FLOAT(value); + + if (is_float_value) { + if (value_reg->kind == RFPU) { + value_fp = (Arm64FpReg)value_reg->id; + } else { + ldr_stack_fp(ctx, value_fp, value->stackPos, value->size); + } + } else { + value_r = (value_reg->kind == RCPU) ? (Arm64Reg)value_reg->id : X9; + if (value_reg->kind == RCONST) { + load_immediate(ctx, value_reg->id, value_r, value->size == 8); + } else if (value_reg->kind != RCPU) { + ldr_stack(ctx, value_r, value->stackPos, value->size); + } + } + + // Step 2: Load index (may clobber RTMP in fallback, but we haven't used it yet) + Arm64Reg index_r = (index_reg->kind == RCPU) ? (Arm64Reg)index_reg->id : RTMP2; + if (index_reg->kind == RCONST) { + load_immediate(ctx, index_reg->id, index_r, false); + } else if (index_reg->kind != RCPU) { + ldr_stack(ctx, index_r, index->stackPos, index->size); + } + + // Step 3: Load array (if it needs RTMP, the value will stay in RTMP) + Arm64Reg array_r = (array_reg->kind == RCPU) ? (Arm64Reg)array_reg->id : RTMP; + if (array_reg->kind != RCPU) { + ldr_stack(ctx, array_r, array->stackPos, array->size); + } + + // Step 4: Calculate element address + // For varray: array + sizeof(varray) + index * elem_size + // For CArray: array + index * elem_size (no header) + + if (is_carray) { + // CArray: no header offset + if (elem_size == 1) { + encode_add_sub_reg(ctx, 1, 0, 0, SHIFT_LSL, index_r, 0, array_r, RTMP); + } else if (elem_size == 2 || elem_size == 4 || elem_size == 8) { + int shift = (elem_size == 2) ? 1 : (elem_size == 4) ? 2 : 3; + encode_add_sub_reg(ctx, 1, 0, 0, SHIFT_LSL, index_r, shift, array_r, RTMP); + } else { + // Non-power-of-2: compute index * elem_size + load_immediate(ctx, elem_size, RTMP2, false); + encode_madd_msub(ctx, 1, 0, RTMP2, XZR, index_r, RTMP2); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP2, 0, array_r, RTMP); + } + } else { + // varray: add sizeof(varray) header offset first + encode_add_sub_imm(ctx, 1, 0, 0, 0, sizeof(varray), array_r, RTMP); + + // Add scaled index offset + if (elem_size == 1) { + encode_add_sub_reg(ctx, 1, 0, 0, SHIFT_LSL, index_r, 0, RTMP, RTMP); + } else if (elem_size == 2 || elem_size == 4 || elem_size == 8) { + int shift = (elem_size == 2) ? 1 : (elem_size == 4) ? 2 : 3; + encode_add_sub_reg(ctx, 1, 0, 0, SHIFT_LSL, index_r, shift, RTMP, RTMP); + } else { + load_immediate(ctx, elem_size, RTMP2, false); + encode_madd_msub(ctx, 1, 0, RTMP2, XZR, index_r, RTMP2); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP2, 0, RTMP, RTMP); + } + } + + if (is_struct_copy) { + // Copy struct from value (pointer) to RTMP (destination) + // value_r points to source struct, RTMP points to destination + // Use X10 as temporary for copy (not value_r which we need as source base) + int offset = 0; + while (offset < elem_size) { + int remain = elem_size - offset; + int copy_size, size_bits; + if (remain >= 8) { + copy_size = 8; + size_bits = 0x03; + } else if (remain >= 4) { + copy_size = 4; + size_bits = 0x02; + } else if (remain >= 2) { + copy_size = 2; + size_bits = 0x01; + } else { + copy_size = 1; + size_bits = 0x00; + } + // Load from source: X10 = [value_r + offset] + encode_ldur_stur(ctx, size_bits, 0, 0x01, offset, value_r, X10); + // Store to dest: [RTMP + offset] = X10 + encode_ldur_stur(ctx, size_bits, 0, 0x00, offset, RTMP, X10); + offset += copy_size; + } + } else if (is_float_value) { + // Float store: STR Vn, [RTMP] with V=1 + int size_bits = (value->size == 8) ? 0x03 : 0x02; // F64 or F32 + encode_ldr_str_imm(ctx, size_bits, 1, 0x00, 0, RTMP, value_fp); // V=1 for FP + } else { + // Integer store: STR Xn, [RTMP] + int size_bits = (elem_size == 1) ? 0x00 : (elem_size == 2) ? 0x01 : (elem_size == 4) ? 0x02 : 0x03; + encode_ldr_str_imm(ctx, size_bits, 0, 0x00, 0, RTMP, value_r); + } + + discard(ctx, array_reg); + discard(ctx, index_reg); + discard(ctx, value_reg); +} + +/* + * Global variable access: dst = globals[index] + * OGetGlobal: Use PC-relative addressing with ADRP + LDR + */ +static void op_get_global(jit_ctx *ctx, vreg *dst, int global_index) { + preg *dst_reg = alloc_dst(ctx, dst); + + // Get global address from module + void **globals = (void**)ctx->m->globals_data; + void *global_addr = &globals[global_index]; + + // Load global address to RTMP2 + load_immediate(ctx, (int64_t)global_addr, RTMP2, true); + + if (IS_FLOAT(dst)) { + // Float: load into FPU register + Arm64FpReg dst_r = (dst_reg->kind == RFPU) ? (Arm64FpReg)dst_reg->id : V16; + // LDR Vn, [RTMP2] - floating point load + // size: 0x02=32-bit (S), 0x03=64-bit (D) + encode_ldr_str_imm(ctx, dst->size == 8 ? 0x03 : 0x02, 1, 0x01, 0, RTMP2, dst_r); + // Store to stack + str_stack_fp(ctx, dst_r, dst->stackPos, dst->size); + } else { + // Integer/pointer: load into CPU register + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : RTMP; + // LDR Xn, [RTMP2] + encode_ldr_str_imm(ctx, dst->size == 8 ? 0x03 : 0x02, 0, 0x01, 0, RTMP2, dst_r); + // Store to stack + str_stack(ctx, dst_r, dst->stackPos, dst->size); + } +} + +/* + * Global variable assignment: globals[index] = value + * OSetGlobal + */ +static void op_set_global(jit_ctx *ctx, int global_index, vreg *value) { + preg *value_reg = fetch(ctx, value); + + // Get global address from module + void **globals = (void**)ctx->m->globals_data; + void *global_addr = &globals[global_index]; + + // Load global address to RTMP2 + load_immediate(ctx, (int64_t)global_addr, RTMP2, true); + + if (IS_FLOAT(value)) { + // Float: store from FPU register + Arm64FpReg value_r = (value_reg->kind == RFPU) ? (Arm64FpReg)value_reg->id : V16; + if (value_reg->kind != RFPU) { + // Load from stack into temp FPU register + ldr_stack_fp(ctx, value_r, value->stackPos, value->size); + } + // STR Vn, [RTMP2] - floating point store + encode_ldr_str_imm(ctx, value->size == 8 ? 0x03 : 0x02, 1, 0x00, 0, RTMP2, value_r); + } else { + // Integer/pointer: store from CPU register + Arm64Reg value_r = (value_reg->kind == RCPU) ? (Arm64Reg)value_reg->id : RTMP; + if (value_reg->kind == RCONST) { + load_immediate(ctx, value_reg->id, value_r, value->size == 8); + } else if (value_reg->kind != RCPU) { + ldr_stack(ctx, value_r, value->stackPos, value->size); + } + // STR Xn, [RTMP2] + encode_ldr_str_imm(ctx, value->size == 8 ? 0x03 : 0x02, 0, 0x00, 0, RTMP2, value_r); + } + + discard(ctx, value_reg); +} + +// ============================================================================ +// Reference Operations +// ============================================================================ + +/* + * Create reference: dst = &src + * ORef: dst = address of vreg + * + * IMPORTANT: After taking a reference to a vreg, that vreg may be modified + * through the reference (via OSetref). We must: + * 1. Ensure src is spilled to stack (in case it's only in a register) + * 2. Invalidate src's register binding so future reads go to stack + */ +static void op_ref(jit_ctx *ctx, vreg *dst, vreg *src) { + // First, ensure src is on stack and invalidate its register binding + // (like x86's scratch(ra->current)) + if (src->current != NULL) { + // Spill to stack if in a register + store(ctx, src, src->current); + // Invalidate the binding so future reads go to stack + src->current->holds = NULL; + src->current = NULL; + } + + preg *dst_reg = alloc_dst(ctx, dst); + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : RTMP; + + // Calculate stack address: FP + src->stackPos + if (src->stackPos >= 0) { + // ADD dst_r, FP, #stackPos + encode_add_sub_imm(ctx, 1, 0, 0, 0, src->stackPos, FP, dst_r); + } else { + // SUB dst_r, FP, #(-stackPos) + encode_add_sub_imm(ctx, 1, 1, 0, 0, -src->stackPos, FP, dst_r); + } + + // Always store to stack - source of truth for later loads + str_stack(ctx, dst_r, dst->stackPos, dst->size); +} + +/* + * Dereference: dst = *src + * OUnref: Load value from pointer + */ +static void op_unref(jit_ctx *ctx, vreg *dst, vreg *src) { + preg *src_reg = fetch(ctx, src); + + // Load the pointer (always integer register since it's an address) + Arm64Reg src_r = (src_reg->kind == RCPU) ? (Arm64Reg)src_reg->id : RTMP; + if (src_reg->kind != RCPU) { + ldr_stack(ctx, src_r, src->stackPos, src->size); + } + + int size_bits = (dst->size == 1) ? 0x00 : (dst->size == 2) ? 0x01 : (dst->size == 4) ? 0x02 : 0x03; + + if (IS_FLOAT(dst)) { + // Float dereference: LDR Vd, [src_r] + preg *dst_reg = alloc_dst(ctx, dst); + Arm64FpReg dst_r = (dst_reg->kind == RFPU) ? (Arm64FpReg)dst_reg->id : V16; + encode_ldr_str_imm(ctx, size_bits, 1, 0x01, 0, src_r, dst_r); + str_stack_fp(ctx, dst_r, dst->stackPos, dst->size); + } else { + // Integer dereference: LDR Xd, [src_r] + preg *dst_reg = alloc_dst(ctx, dst); + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : RTMP2; + encode_ldr_str_imm(ctx, size_bits, 0, 0x01, 0, src_r, dst_r); + str_stack(ctx, dst_r, dst->stackPos, dst->size); + } + + discard(ctx, src_reg); +} + +/* + * Set reference: *dst = src + * OSetref: Store value to pointer + */ +static void op_setref(jit_ctx *ctx, vreg *dst, vreg *src) { + preg *dst_reg = fetch(ctx, dst); + preg *src_reg = fetch(ctx, src); + + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : RTMP; + if (dst_reg->kind != RCPU) { + ldr_stack(ctx, dst_r, dst->stackPos, dst->size); + } + + Arm64Reg src_r = (src_reg->kind == RCPU) ? (Arm64Reg)src_reg->id : RTMP2; + if (src_reg->kind == RCONST) { + load_immediate(ctx, src_reg->id, src_r, src->size == 8); + } else if (src_reg->kind != RCPU) { + ldr_stack(ctx, src_r, src->stackPos, src->size); + } + + // Store to pointer: STR src_r, [dst_r] + int size_bits = (src->size == 1) ? 0x00 : (src->size == 2) ? 0x01 : (src->size == 4) ? 0x02 : 0x03; + encode_ldr_str_imm(ctx, size_bits, 0, 0x00, 0, dst_r, src_r); + + discard(ctx, dst_reg); + discard(ctx, src_reg); +} + +// ============================================================================ +// Comparison Operations (result stored, not branching) +// ============================================================================ + +/* + * Equality comparison: dst = (a == b) + * OEq/ONeq/OLt/OGte/etc: Store comparison result as boolean + */ +static void op_compare(jit_ctx *ctx, vreg *dst, vreg *a, vreg *b, hl_op op) { + preg *a_reg = fetch(ctx, a); + preg *b_reg = fetch(ctx, b); + preg *dst_reg = alloc_dst(ctx, dst); + + Arm64Reg a_r = (a_reg->kind == RCPU) ? (Arm64Reg)a_reg->id : RTMP; + if (a_reg->kind == RCONST) { + load_immediate(ctx, a_reg->id, a_r, a->size == 8); + } else if (a_reg->kind != RCPU) { + ldr_stack(ctx, a_r, a->stackPos, a->size); + } + + Arm64Reg b_r = (b_reg->kind == RCPU) ? (Arm64Reg)b_reg->id : RTMP2; + if (b_reg->kind == RCONST) { + load_immediate(ctx, b_reg->id, b_r, b->size == 8); + } else if (b_reg->kind != RCPU) { + ldr_stack(ctx, b_r, b->stackPos, b->size); + } + + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : X9; + + bool is_float = IS_FLOAT(a); + + if (is_float) { + // Floating-point comparison + Arm64FpReg fa_r = (a_reg->kind == RFPU) ? (Arm64FpReg)a_reg->id : V16; + Arm64FpReg fb_r = (b_reg->kind == RFPU) ? (Arm64FpReg)b_reg->id : V17; + + if (a_reg->kind != RFPU) { + // Load from stack to FP register + ldr_stack_fp(ctx, fa_r, a->stackPos, a->size); + } + if (b_reg->kind != RFPU) { + ldr_stack_fp(ctx, fb_r, b->stackPos, b->size); + } + + // FCMP fa_r, fb_r + int is_double = a->size == 8 ? 1 : 0; + encode_fp_compare(ctx, 0, is_double, is_double, fb_r, 0, fa_r); + } else { + // Integer comparison: CMP a_r, b_r + encode_add_sub_reg(ctx, a->size == 8 ? 1 : 0, 1, 1, 0, b_r, 0, a_r, XZR); + } + + // Get condition code for this operation + ArmCondition cond = hl_cond_to_arm(op, is_float); + + // CSET dst_r, cond (Set register to 1 if condition true, 0 otherwise) + // Encoding: CSINC dst, XZR, XZR, !cond + // This sets dst = (cond) ? 1 : 0 + int inv_cond = cond ^ 1; // Invert condition + // CSINC: sf=0, op=0, S=0, Rm=XZR, cond=inv_cond, o2=1, Rn=XZR, Rd=dst_r + EMIT32(ctx,(0 << 31) | (0 << 30) | (0xD4 << 21) | (XZR << 16) | (inv_cond << 12) | (1 << 10) | (XZR << 5) | dst_r); + + // Always store to stack - source of truth for later loads + str_stack(ctx, dst_r, dst->stackPos, dst->size); + + discard(ctx, a_reg); + discard(ctx, b_reg); +} + +// ============================================================================ +// Type and Object Operations +// ============================================================================ + +/* + * Get object type: dst = obj->type + * OType: Load type pointer from object + */ +static void op_type(jit_ctx *ctx, vreg *dst, vreg *obj) { + preg *obj_reg = fetch(ctx, obj); + preg *dst_reg = alloc_dst(ctx, dst); + + Arm64Reg obj_r = (obj_reg->kind == RCPU) ? (Arm64Reg)obj_reg->id : RTMP; + if (obj_reg->kind != RCPU) { + ldr_stack(ctx, obj_r, obj->stackPos, obj->size); + } + + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : RTMP2; + + // Load type pointer from object header (first field at offset 0) + // LDR dst_r, [obj_r] + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, 0, obj_r, dst_r); + + // Always store to stack - source of truth for later loads + str_stack(ctx, dst_r, dst->stackPos, dst->size); + + discard(ctx, obj_reg); +} + +/* + * OGetThis: Load a field from the "this" object (R(0)) + * Equivalent to OField but implicitly uses R(0) as the object + */ +static void op_get_this(jit_ctx *ctx, vreg *dst, int field_idx) { + vreg *this_vreg = R(0); + op_field(ctx, dst, this_vreg, field_idx); +} + +/* + * Get the dynamic cast function for a given type + */ +static void *get_dyncast(hl_type *t) { + switch (t->kind) { + case HF32: + return hl_dyn_castf; + case HF64: + return hl_dyn_castd; + case HI64: + case HGUID: + return hl_dyn_casti64; + case HI32: + case HUI16: + case HUI8: + case HBOOL: + return hl_dyn_casti; + default: + return hl_dyn_castp; + } +} + +/* + * Cast operation (safe cast with runtime check) + * OSafeCast: dst = (target_type)obj or NULL if cast fails + */ +static void op_safe_cast(jit_ctx *ctx, vreg *dst, vreg *obj, hl_type *target_type) { + // Special case: Null to T - unbox with null check + if (obj->t->kind == HNULL && obj->t->tparam->kind == dst->t->kind) { + int jnull, jend; + + switch (dst->t->kind) { + case HUI8: + case HUI16: + case HI32: + case HBOOL: + case HI64: + case HGUID: + { + preg *tmp = fetch(ctx, obj); + Arm64Reg r = (tmp->kind == RCPU) ? tmp->id : RTMP; + if (tmp->kind != RCPU) { + ldr_stack(ctx, r, obj->stackPos, obj->size); + } + // Test for null + encode_add_sub_imm(ctx, 1, 1, 1, 0, 0, r, XZR); // CMP r, #0 + jnull = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); // B.EQ null_path + + // Non-null: load value from offset 8 with correct size + // Size determines scale: 0x00=1, 0x01=2, 0x02=4, 0x03=8 + // So offset = 8 / scale to get byte offset 8 + int size_code; + int scaled_offset; + switch (dst->size) { + case 1: size_code = 0x00; scaled_offset = 8; break; // LDRB [r, #8] + case 2: size_code = 0x01; scaled_offset = 4; break; // LDRH [r, #8] + case 4: size_code = 0x02; scaled_offset = 2; break; // LDR W [r, #8] + default: size_code = 0x03; scaled_offset = 1; break; // LDR X [r, #8] + } + // The LDR below clobbers r. If obj is dirty in r, save it to stack first. + // This preserves obj's value (the dynamic pointer) for later use. + if (obj->dirty && obj->current == tmp) { + str_stack(ctx, r, obj->stackPos, obj->size); + obj->dirty = 0; + } + encode_ldr_str_imm(ctx, size_code, 0, 0x01, scaled_offset, r, r); + jend = BUF_POS(); + encode_branch_uncond(ctx, 0); // B end + + // Null path: set to zero + patch_jump(ctx, jnull, BUF_POS()); + load_immediate(ctx, 0, r, dst->size == 8); + + // End + patch_jump(ctx, jend, BUF_POS()); + str_stack(ctx, r, dst->stackPos, dst->size); + // Clear binding - register no longer holds obj's original value + discard(ctx, tmp); + // Invalidate dst's old binding since we wrote directly to stack + if (dst->current) { + dst->current->holds = NULL; + dst->current = NULL; + } + } + return; + + case HF32: + case HF64: + { + preg *tmp = fetch(ctx, obj); + Arm64Reg r = (tmp->kind == RCPU) ? tmp->id : RTMP; + if (tmp->kind != RCPU) { + ldr_stack(ctx, r, obj->stackPos, obj->size); + } + // Evict any vreg currently bound to V0 before using it + preg *pv0 = PVFPR(0); + if (pv0->holds != NULL && pv0->holds != dst) { + free_reg(ctx, pv0); + } + // Test for null + encode_add_sub_imm(ctx, 1, 1, 1, 0, 0, r, XZR); // CMP r, #0 + jnull = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); // B.EQ null_path + + // Non-null: load float from offset 8 + encode_ldr_str_imm(ctx, (dst->size == 8) ? 0x03 : 0x02, 1, 0x01, 8 / dst->size, r, V0); + jend = BUF_POS(); + encode_branch_uncond(ctx, 0); // B end + + // Null path: set to zero + patch_jump(ctx, jnull, BUF_POS()); + // FMOV Vd, XZR + EMIT32(ctx, (1 << 31) | (0 << 29) | (0x1E << 24) | (1 << 22) | (1 << 21) | (7 << 16) | (31 << 5) | V0); + + // End + patch_jump(ctx, jend, BUF_POS()); + str_stack_fp(ctx, V0, dst->stackPos, dst->size); + // Clear binding - register no longer holds obj's original value + discard(ctx, tmp); + // Invalidate dst's old binding since we wrote directly to stack + if (dst->current) { + dst->current->holds = NULL; + dst->current = NULL; + } + } + return; + + default: + break; + } + } + + // General case: call runtime cast function + spill_regs(ctx); + + // Get stack address of obj + // LEA X0, [FP, #obj->stackPos] or similar + if (obj->stackPos >= 0) { + encode_add_sub_imm(ctx, 1, 0, 0, 0, obj->stackPos, FP, X0); + } else { + encode_add_sub_imm(ctx, 1, 1, 0, 0, -obj->stackPos, FP, X0); + } + + // Set up arguments based on destination type + void *cast_func = get_dyncast(dst->t); + switch (dst->t->kind) { + case HF32: + case HF64: + case HI64: + // 2 args: ptr, src_type + load_immediate(ctx, (int64_t)obj->t, X1, true); + break; + default: + // 3 args: ptr, src_type, dst_type + load_immediate(ctx, (int64_t)obj->t, X1, true); + load_immediate(ctx, (int64_t)dst->t, X2, true); + break; + } + + // Call cast function + load_immediate(ctx, (int64_t)cast_func, RTMP, true); + EMIT32(ctx, 0xD63F0000 | (RTMP << 5)); // BLR RTMP + + // Store result and clear stale binding + if (IS_FLOAT(dst)) { + str_stack_fp(ctx, V0, dst->stackPos, dst->size); + } else { + str_stack(ctx, X0, dst->stackPos, dst->size); + } + store_result(ctx, dst); +} + +/* + * Null coalescing: dst = (a != null) ? a : b + * OCoalesce/ONullCheck + */ +static void op_null_check(jit_ctx *ctx, vreg *dst) { + // Check if dst is null and call hl_null_access if so + preg *dst_reg = fetch(ctx, dst); + + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : RTMP; + if (dst_reg->kind != RCPU) { + ldr_stack(ctx, dst_r, dst->stackPos, dst->size); + } + + // Compare with zero: CMP dst_r, #0 (actually SUBS XZR, dst_r, #0) + encode_add_sub_imm(ctx, 1, 1, 1, 0, 0, dst_r, XZR); + + // If not zero (not null), skip error handling: B.NE skip + int bne_pos = BUF_POS(); + encode_branch_cond(ctx, 0, COND_NE); // B.NE (will patch offset) + + // Null path: call hl_null_access(field, hashed_name) + // For simplicity, pass 0 for both (basic null access error) + // NOTE: Do NOT call spill_regs() here! hl_null_access never returns (it throws), + // and spill_regs() would corrupt compile-time register bindings for the non-null path. + load_immediate(ctx, 0, X0, true); // field = NULL + load_immediate(ctx, 0, X1, true); // hashed_name = 0 + load_immediate(ctx, (int64_t)hl_null_access, RTMP, true); + EMIT32(ctx, 0xD63F0000 | (RTMP << 5)); // BLR RTMP + // hl_null_access doesn't return (it throws), but we don't emit anything after + + // Patch the B.NE to skip here + int skip_pos = BUF_POS(); + int bne_offset = (skip_pos - bne_pos) / 4; + ctx->buf.b = ctx->startBuf + bne_pos; + encode_branch_cond(ctx, bne_offset, COND_NE); + ctx->buf.b = ctx->startBuf + skip_pos; + + discard(ctx, dst_reg); +} + +/* + * Object/memory allocation operations + * These typically call into the runtime allocator + */ +static void op_new(jit_ctx *ctx, vreg *dst, hl_type *type) { + // Call runtime allocator based on type kind + // Different type kinds require different allocation functions: + // - HOBJ/HSTRUCT: hl_alloc_obj(type) + // - HDYNOBJ: hl_alloc_dynobj() - no arguments! + // - HVIRTUAL: hl_alloc_virtual(type) + + // Spill all caller-saved registers BEFORE the call + spill_regs(ctx); + + void *alloc_func; + int has_type_arg = 1; + + switch (type->kind) { + case HOBJ: + case HSTRUCT: + alloc_func = (void*)hl_alloc_obj; + break; + case HDYNOBJ: + alloc_func = (void*)hl_alloc_dynobj; + has_type_arg = 0; // hl_alloc_dynobj takes no arguments + break; + case HVIRTUAL: + alloc_func = (void*)hl_alloc_virtual; + break; + default: + // Unsupported type for ONew + printf("op_new: unsupported type kind %d\n", type->kind); + return; + } + + // Load type address to X0 (first argument) if needed + if (has_type_arg) { + load_immediate(ctx, (int64_t)type, X0, true); + } + + // Load function pointer and call + load_immediate(ctx, (int64_t)alloc_func, RTMP, true); + + // Call allocator: BLR RTMP + EMIT32(ctx, (0xD63F0000) | (RTMP << 5)); + + // Result is in X0 - always store to stack first (source of truth for later loads) + str_stack(ctx, X0, dst->stackPos, dst->size); + + // Also keep in a register if allocated + preg *dst_reg = alloc_dst(ctx, dst); + if (dst_reg->kind == RCPU && (Arm64Reg)dst_reg->id != X0) { + mov_reg_reg(ctx, (Arm64Reg)dst_reg->id, X0, 8); + } +} + +/* + * String/bytes operations + */ +static void op_string(jit_ctx *ctx, vreg *dst, int string_index) { + // Load UTF-16 string from module string table + preg *dst_reg = alloc_dst(ctx, dst); + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : RTMP; + + // Get UTF-16 string pointer (hl_get_ustring converts from UTF-8 and caches) + const uchar *string_ptr = hl_get_ustring(ctx->m->code, string_index); + + // Load string address + load_immediate(ctx, (int64_t)string_ptr, dst_r, true); + + // Always store to stack - source of truth for later loads + str_stack(ctx, dst_r, dst->stackPos, dst->size); +} + +static void op_bytes(jit_ctx *ctx, vreg *dst, int bytes_index) { + // Load bytes from module bytes table + preg *dst_reg = alloc_dst(ctx, dst); + Arm64Reg dst_r = (dst_reg->kind == RCPU) ? (Arm64Reg)dst_reg->id : RTMP; + + // Get bytes pointer from module - use bytes_pos lookup for version >= 5 + char *bytes_ptr; + if (ctx->m->code->version >= 5) + bytes_ptr = ctx->m->code->bytes + ctx->m->code->bytes_pos[bytes_index]; + else + bytes_ptr = ctx->m->code->strings[bytes_index]; + + // Load bytes address + load_immediate(ctx, (int64_t)bytes_ptr, dst_r, true); + + // Always store to stack - source of truth for later loads + str_stack(ctx, dst_r, dst->stackPos, dst->size); +} + +// Forward declaration for prepare_call_args (defined later) +static int prepare_call_args(jit_ctx *ctx, hl_type **arg_types, vreg **args, int nargs, bool is_native); + +/* + * Virtual/method calls + * OCallMethod/OCallThis/OCallClosure + */ +// ============================================================================ +// Dynamic Object Helpers +// ============================================================================ + +/** + * Get the appropriate dynamic set function for a type + */ +static void *get_dynset(hl_type *t) { + switch (t->kind) { + case HF32: + return hl_dyn_setf; + case HF64: + return hl_dyn_setd; + case HI64: + case HGUID: + return hl_dyn_seti64; + case HI32: + case HUI16: + case HUI8: + case HBOOL: + return hl_dyn_seti; + default: + return hl_dyn_setp; + } +} + +/** + * Get the appropriate dynamic get function for a type + */ +static void *get_dynget(hl_type *t) { + switch (t->kind) { + case HF32: + return hl_dyn_getf; + case HF64: + return hl_dyn_getd; + case HI64: + case HGUID: + return hl_dyn_geti64; + case HI32: + case HUI16: + case HUI8: + case HBOOL: + return hl_dyn_geti; + default: + return hl_dyn_getp; + } +} + +// ============================================================================ +// Method and Function Calls +// ============================================================================ + +static void op_call_method_obj(jit_ctx *ctx, vreg *dst, vreg *obj, int method_index, vreg **args, int nargs) { + // HOBJ method call: obj->type->vobj_proto[method_index](obj, args...) + + // Spill all caller-saved registers BEFORE the call + spill_regs(ctx); + + // Now fetch obj (will load from stack since we just spilled) + preg *obj_reg = fetch(ctx, obj); + + Arm64Reg obj_r = (obj_reg->kind == RCPU) ? (Arm64Reg)obj_reg->id : RTMP; + if (obj_reg->kind != RCPU) { + ldr_stack(ctx, obj_r, obj->stackPos, obj->size); + } + + // Load type from obj[0] + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, 0, obj_r, RTMP); // RTMP = obj->type + // Load vobj_proto from type[16] (HL_WSIZE*2 = offset index 2) + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, 2, RTMP, RTMP); // RTMP = type->vobj_proto + // Load method pointer from proto[method_index] into RTMP2 + // NOTE: We use RTMP2 here because prepare_call_args uses RTMP for stack calculations + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, method_index, RTMP, RTMP2); + + discard(ctx, obj_reg); + + // Prepare call with obj as first argument + vreg **full_args = (vreg**)malloc(sizeof(vreg*) * (nargs + 1)); + full_args[0] = obj; + for (int i = 0; i < nargs; i++) { + full_args[i + 1] = args[i]; + } + + // Prepare arguments (this uses RTMP, but method pointer is safe in RTMP2) + int stack_space = prepare_call_args(ctx, NULL, full_args, nargs + 1, false); + free(full_args); + + // Call method: BLR RTMP2 + EMIT32(ctx,(0xD63F0000) | (RTMP2 << 5)); + + // Clean up stack + if (stack_space > 0) { + encode_add_sub_imm(ctx, 1, 0, 0, 0, stack_space, SP_REG, SP_REG); + } + + // Store return value + if (dst && dst->t->kind != HVOID) { + preg *p = alloc_dst(ctx, dst); + if (IS_FLOAT(dst)) { + if (p->kind == RFPU && (Arm64FpReg)p->id != V0) { + fmov_reg_reg(ctx, (Arm64FpReg)p->id, V0, dst->size); + } else if (p->kind == RSTACK) { + str_stack_fp(ctx, V0, dst->stackPos, dst->size); + } + } else { + if (p->kind == RCPU && (Arm64Reg)p->id != X0) { + mov_reg_reg(ctx, (Arm64Reg)p->id, X0, dst->size); + } else if (p->kind == RSTACK) { + str_stack(ctx, X0, dst->stackPos, dst->size); + } + } + } +} + +// ============================================================================ +// Function Calls +// ============================================================================ + +/* + * Prepare arguments for a function call according to AAPCS64: + * - First 8 integer/pointer args in X0-X7 + * - First 8 floating-point args in V0-V7 + * - Additional args on stack (16-byte aligned) + * - Returns the total stack space needed for overflow args + */ +static int prepare_call_args(jit_ctx *ctx, hl_type **arg_types, vreg **args, int nargs, bool is_native) { + int int_reg_count = 0; + int fp_reg_count = 0; + int stack_offset = 0; + + // First pass: count args and calculate stack space needed + for (int i = 0; i < nargs; i++) { + bool is_fp = IS_FLOAT(args[i]); + int *reg_count = is_fp ? &fp_reg_count : &int_reg_count; + + if (*reg_count >= CALL_NREGS) { + // Arg goes on stack + stack_offset += 8; // Each stack arg takes 8 bytes (aligned) + } + (*reg_count)++; + } + + // Align stack to 16 bytes + if (stack_offset & 15) + stack_offset = (stack_offset + 15) & ~15; + + // Allocate stack space for overflow args if needed + if (stack_offset > 0) { + // SUB SP, SP, #stack_offset + encode_add_sub_imm(ctx, 1, 1, 0, 0, stack_offset, SP_REG, SP_REG); + } + + // Second pass: move arguments to their locations + int_reg_count = 0; + fp_reg_count = 0; + int current_stack_offset = 0; + + // After spill_regs(), all values are on stack. + // Load arguments directly to their destination registers to avoid + // the register allocation problem where fetch() reuses registers. + for (int i = 0; i < nargs; i++) { + vreg *arg = args[i]; + bool is_fp = IS_FLOAT(arg); + + if (is_fp) { + if (fp_reg_count < CALL_NREGS) { + // Load directly to FP argument register + Arm64FpReg dest_reg = FP_CALL_REGS[fp_reg_count]; + ldr_stack_fp(ctx, dest_reg, arg->stackPos, arg->size); + fp_reg_count++; + } else { + // Overflow: load to temp, then store to stack + ldr_stack_fp(ctx, V16, arg->stackPos, arg->size); + encode_ldr_str_imm(ctx, arg->size == 4 ? 0x02 : 0x03, 1, 0x00, + current_stack_offset / (arg->size == 4 ? 4 : 8), + SP_REG, V16); + current_stack_offset += 8; + } + } else { + // Integer/pointer argument + if (int_reg_count < CALL_NREGS) { + // Load directly to integer argument register + Arm64Reg dest_reg = CALL_REGS[int_reg_count]; + ldr_stack(ctx, dest_reg, arg->stackPos, arg->size); + int_reg_count++; + } else { + // Overflow: load to temp, then store to stack + ldr_stack(ctx, RTMP, arg->stackPos, arg->size); + encode_ldr_str_imm(ctx, arg->size == 8 ? 0x03 : 0x02, 0, 0x00, + current_stack_offset / (arg->size == 8 ? 8 : 4), + SP_REG, RTMP); + current_stack_offset += 8; + } + } + } + + return stack_offset; +} + +/* + * Call a native C function + */ +static void op_call_native(jit_ctx *ctx, vreg *dst, hl_type *ftype, void *func_ptr, vreg **args, int nargs) { + // Spill all caller-saved registers BEFORE the call + spill_regs(ctx); + + // Prepare arguments (arg_types not actually used by prepare_call_args) + int stack_space = prepare_call_args(ctx, NULL, args, nargs, true); + + // Load function pointer to RTMP + load_immediate(ctx, (int64_t)func_ptr, RTMP, true); + + // BLR RTMP (Branch with Link to Register) + // Encoding: 1101 0110 0011 1111 0000 00rr rrr0 0000 + // where rrrrr = RTMP register number + EMIT32(ctx,(0xD63F0000) | (RTMP << 5)); + + // Clean up stack if we allocated space for args + if (stack_space > 0) { + // ADD SP, SP, #stack_space + encode_add_sub_imm(ctx, 1, 0, 0, 0, stack_space, SP_REG, SP_REG); + } + + // Store return value if needed + if (dst && dst->t->kind != HVOID) { + // Always store to stack first (source of truth for later loads) + if (IS_FLOAT(dst)) { + str_stack_fp(ctx, V0, dst->stackPos, dst->size); + } else { + str_stack(ctx, X0, dst->stackPos, dst->size); + } + + // Also keep in a register if allocated to a different one + preg *p = alloc_dst(ctx, dst); + if (IS_FLOAT(dst)) { + if (p->kind == RFPU && (Arm64FpReg)p->id != V0) { + fmov_reg_reg(ctx, (Arm64FpReg)p->id, V0, dst->size); + } + } else { + if (p->kind == RCPU && (Arm64Reg)p->id != X0) { + mov_reg_reg(ctx, (Arm64Reg)p->id, X0, dst->size); + } + } + } +} + +/* + * Call a native function with a known absolute address + * The address is embedded directly in the instruction stream (no patching needed) + */ +static void call_native(jit_ctx *ctx, void *nativeFun, int stack_space) { + // Emit indirect call sequence with the address embedded inline: + // LDR X17, #12 ; load target address from PC+12 + // BLR X17 ; call + // B #12 ; skip over the literal + // .quad addr ; 8-byte absolute address (embedded now, not patched later) + + EMIT32(ctx, 0x58000071); // LDR X17, #12 + EMIT32(ctx, 0xD63F0220); // BLR X17 + EMIT32(ctx, 0x14000003); // B #12 (skip 3 instructions = 12 bytes) + + // Embed the native function address directly + uint64_t addr = (uint64_t)nativeFun; + EMIT32(ctx, (uint32_t)(addr & 0xFFFFFFFF)); // Low 32 bits + EMIT32(ctx, (uint32_t)((addr >> 32) & 0xFFFFFFFF)); // High 32 bits + + // Clean up stack if we allocated space for args + if (stack_space > 0) { + encode_add_sub_imm(ctx, 1, 0, 0, 0, stack_space, SP_REG, SP_REG); + } +} + +/* + * Emit a call to a function by its index (without spill/prepare - for use when those are already done) + * Used by compareFun and other places that set up args manually + */ +static void emit_call_findex(jit_ctx *ctx, int findex, int stack_space) { + int fid = findex < 0 ? -1 : ctx->m->functions_indexes[findex]; + bool isNative = fid >= ctx->m->code->nfunctions; + + if (fid < 0) { + jit_error("Invalid function index"); + } else if (isNative) { + // Native function - address is already resolved + call_native(ctx, ctx->m->functions_ptrs[findex], stack_space); + } else { + // JIT function - use indirect call via literal pool (patched later) + EMIT32(ctx, 0x58000071); // LDR X17, #12 + EMIT32(ctx, 0xD63F0220); // BLR X17 + + // Register literal position for patching + jlist *j = (jlist*)hl_malloc(&ctx->galloc, sizeof(jlist)); + j->pos = BUF_POS() + 4; // Position of the 8-byte literal (after B instruction) + j->target = findex; + j->next = ctx->calls; + ctx->calls = j; + + EMIT32(ctx, 0x14000003); // B #12 (skip 3 instructions = 12 bytes) + EMIT32(ctx, 0); // Low 32 bits placeholder + EMIT32(ctx, 0); // High 32 bits placeholder + + // Clean up stack if we allocated space for args + if (stack_space > 0) { + encode_add_sub_imm(ctx, 1, 0, 0, 0, stack_space, SP_REG, SP_REG); + } + } +} + +/* + * Call a HashLink function (native or JIT-compiled) + * For OCall0-OCall4, OCallN + */ +static void op_call_hl(jit_ctx *ctx, vreg *dst, int findex, vreg **args, int nargs) { + // Spill all caller-saved registers BEFORE the call + // This must happen before prepare_call_args to save values that might be clobbered + spill_regs(ctx); + + // Prepare arguments + int stack_space = prepare_call_args(ctx, NULL, args, nargs, false); + + // Check if this is a native function or JIT function + int fid = findex < 0 ? -1 : ctx->m->functions_indexes[findex]; + bool isNative = fid >= ctx->m->code->nfunctions; + + if (fid < 0) { + // Invalid function index + jit_error("Invalid function index"); + } else if (isNative) { + // Native function - address is already resolved, call directly + call_native(ctx, ctx->m->functions_ptrs[findex], stack_space); + } else { + // JIT function - use indirect call via literal pool (patched later) + // During JIT compilation, functions_ptrs contains CODE OFFSETS. + // The conversion to absolute addresses happens in hl_jit_code. + // + // Sequence: + // LDR X17, #12 ; load target address from PC+12 + // BLR X17 ; call + // B #12 ; skip over the literal + // .quad addr ; 8-byte address placeholder (patched later) + + EMIT32(ctx, 0x58000071); // LDR X17, #12 + EMIT32(ctx, 0xD63F0220); // BLR X17 + + // Register literal position for patching + jlist *j = (jlist*)hl_malloc(&ctx->galloc, sizeof(jlist)); + j->pos = BUF_POS() + 4; // Position of the 8-byte literal (after B instruction) + j->target = findex; + j->next = ctx->calls; + ctx->calls = j; + + EMIT32(ctx, 0x14000003); // B #12 (skip 3 instructions = 12 bytes) + EMIT32(ctx, 0); // Low 32 bits placeholder + EMIT32(ctx, 0); // High 32 bits placeholder + + // Clean up stack if we allocated space for args + if (stack_space > 0) { + encode_add_sub_imm(ctx, 1, 0, 0, 0, stack_space, SP_REG, SP_REG); + } + } + + // Note: spill_regs was already called before prepare_call_args + + // Store return value if needed + if (dst && dst->t->kind != HVOID) { + // Always store to stack first (source of truth for later loads) + if (IS_FLOAT(dst)) { + str_stack_fp(ctx, V0, dst->stackPos, dst->size); + } else { + str_stack(ctx, X0, dst->stackPos, dst->size); + } + + // Also keep in a register if allocated to a different one + preg *p = alloc_dst(ctx, dst); + if (IS_FLOAT(dst)) { + if (p->kind == RFPU && (Arm64FpReg)p->id != V0) { + fmov_reg_reg(ctx, (Arm64FpReg)p->id, V0, dst->size); + } + } else { + if (p->kind == RCPU && (Arm64Reg)p->id != X0) { + mov_reg_reg(ctx, (Arm64Reg)p->id, X0, dst->size); + } + } + } +} + +// ============================================================================ +// C↔HL Trampolines +// ============================================================================ + +static void *call_jit_c2hl = NULL; +static void *call_jit_hl2c = NULL; + +// Maximum args for dynamic calls +#define MAX_ARGS 64 + +/** + * Wrapper function for HL->C calls - unpacks arguments and calls the wrapped function. + * Called from jit_hl2c trampoline. + */ +static vdynamic *jit_wrapper_call(vclosure_wrapper *c, char *stack_args, void **regs) { + vdynamic *args[MAX_ARGS]; + int i; + int nargs = c->cl.t->fun->nargs; + int nextCpu = 1; // Skip X0 which holds the closure pointer + int nextFpu = 0; + + if (nargs > MAX_ARGS) + hl_error("Too many arguments for wrapped call"); + + for (i = 0; i < nargs; i++) { + hl_type *t = c->cl.t->fun->args[i]; + + if (t->kind == HF32 || t->kind == HF64) { + // Float argument + if (nextFpu < CALL_NREGS) { + // In FP register - regs[CALL_NREGS + fpu_index] + args[i] = hl_make_dyn(regs + CALL_NREGS + nextFpu, &hlt_f64); + nextFpu++; + } else { + // On stack + args[i] = hl_make_dyn(stack_args, &hlt_f64); + stack_args += 8; + } + } else { + // Integer/pointer argument + if (nextCpu < CALL_NREGS) { + // In CPU register + if (hl_is_dynamic(t)) { + args[i] = *(vdynamic**)(regs + nextCpu); + } else { + args[i] = hl_make_dyn(regs + nextCpu, t); + } + nextCpu++; + } else { + // On stack + if (hl_is_dynamic(t)) { + args[i] = *(vdynamic**)stack_args; + } else { + args[i] = hl_make_dyn(stack_args, t); + } + stack_args += 8; + } + } + } + return hl_dyn_call(c->wrappedFun, args, nargs); +} + +/** + * Wrapper for pointer-returning HL->C calls + */ +static void *jit_wrapper_ptr(vclosure_wrapper *c, char *stack_args, void **regs) { + vdynamic *ret = jit_wrapper_call(c, stack_args, regs); + hl_type *tret = c->cl.t->fun->ret; + switch (tret->kind) { + case HVOID: + return NULL; + case HUI8: + case HUI16: + case HI32: + case HBOOL: + return (void*)(int_val)hl_dyn_casti(&ret, &hlt_dyn, tret); + case HI64: + case HGUID: + return (void*)(int_val)hl_dyn_casti64(&ret, &hlt_dyn); + default: + return hl_dyn_castp(&ret, &hlt_dyn, tret); + } +} + +/** + * Wrapper for float-returning HL->C calls + */ +static double jit_wrapper_d(vclosure_wrapper *c, char *stack_args, void **regs) { + vdynamic *ret = jit_wrapper_call(c, stack_args, regs); + return hl_dyn_castd(&ret, &hlt_dyn); +} + +/** + * Select which register to use for an argument based on type and position. + * Returns register ID or -1 if should go on stack. + */ +static int select_call_reg_c2hl(int *nextCpu, int *nextFpu, hl_type *t) { + if (t->kind == HF32 || t->kind == HF64) { + if (*nextFpu < CALL_NREGS) + return RCPU_COUNT + (*nextFpu)++; // FPU register + return -1; // Stack + } else { + if (*nextCpu < CALL_NREGS) + return (*nextCpu)++; // CPU register + return -1; // Stack + } +} + +/** + * Get the stack size for a type + */ +static int stack_size_c2hl(hl_type *t) { + switch (t->kind) { + case HUI8: + case HBOOL: + return 1; + case HUI16: + return 2; + case HI32: + case HF32: + return 4; + default: + return 8; + } +} + +/** + * Callback function that prepares arguments and calls the JIT trampoline. + * Called from C code to invoke JIT-compiled functions. + */ +static void *callback_c2hl(void *_f, hl_type *t, void **args, vdynamic *ret) { + void **f = (void**)_f; + // Stack layout: + // [0..size) = stack args (pushed in reverse) + // [size..size+CALL_NREGS*8) = integer register args (X0-X7) + // [size+CALL_NREGS*8..size+CALL_NREGS*16) = FP register args (V0-V7) + unsigned char stack[MAX_ARGS * 16]; + int nextCpu = 0, nextFpu = 0; + int mappedRegs[MAX_ARGS]; + + // Zero-initialize the stack to avoid passing garbage to unused registers + // The jit_c2hl trampoline loads ALL 8 int + 8 FP registers unconditionally + memset(stack, 0, sizeof(stack)); + + if (t->fun->nargs > MAX_ARGS) + hl_error("Too many arguments for dynamic call"); + + // First pass: determine register assignments and stack size + int i, size = 0; + for (i = 0; i < t->fun->nargs; i++) { + hl_type *at = t->fun->args[i]; + int creg = select_call_reg_c2hl(&nextCpu, &nextFpu, at); + mappedRegs[i] = creg; + if (creg < 0) { + int tsize = stack_size_c2hl(at); + if (tsize < 8) tsize = 8; // Align to 8 bytes on stack + size += tsize; + } + } + + // Align stack size to 16 bytes + int pad = (-size) & 15; + size += pad; + + // Second pass: copy arguments to appropriate locations + int pos = 0; + for (i = 0; i < t->fun->nargs; i++) { + hl_type *at = t->fun->args[i]; + void *v = args[i]; + int creg = mappedRegs[i]; + void *store; + + if (creg >= 0) { + if (creg >= RCPU_COUNT) { + // FP register - stored after integer registers + store = stack + size + CALL_NREGS * 8 + (creg - RCPU_COUNT) * 8; + } else { + // Integer register + store = stack + size + creg * 8; + } + switch (at->kind) { + case HBOOL: + case HUI8: + *(int64*)store = *(unsigned char*)v; + break; + case HUI16: + *(int64*)store = *(unsigned short*)v; + break; + case HI32: + *(int64*)store = *(int*)v; + break; + case HF32: + *(double*)store = *(float*)v; + break; + case HF64: + *(double*)store = *(double*)v; + break; + case HI64: + case HGUID: + *(int64*)store = *(int64*)v; + break; + default: + *(void**)store = v; + break; + } + } else { + // Stack argument + store = stack + pos; + int tsize = 8; + switch (at->kind) { + case HBOOL: + case HUI8: + *(int64*)store = *(unsigned char*)v; + break; + case HUI16: + *(int64*)store = *(unsigned short*)v; + break; + case HI32: + case HF32: + *(int64*)store = *(int*)v; + break; + case HF64: + *(double*)store = *(double*)v; + break; + case HI64: + case HGUID: + *(int64*)store = *(int64*)v; + break; + default: + *(void**)store = v; + break; + } + pos += tsize; + } + } + + pos += pad; + pos >>= 3; // Convert to 64-bit units + + // Call the trampoline with: function pointer, reg args pointer, stack args end + switch (t->fun->ret->kind) { + case HUI8: + case HUI16: + case HI32: + case HBOOL: + ret->v.i = ((int (*)(void *, void *, void *))call_jit_c2hl)(*f, (void**)stack + pos, stack); + return &ret->v.i; + case HI64: + case HGUID: + ret->v.i64 = ((int64 (*)(void *, void *, void *))call_jit_c2hl)(*f, (void**)stack + pos, stack); + return &ret->v.i64; + case HF32: + ret->v.f = ((float (*)(void *, void *, void *))call_jit_c2hl)(*f, (void**)stack + pos, stack); + return &ret->v.f; + case HF64: + ret->v.d = ((double (*)(void *, void *, void *))call_jit_c2hl)(*f, (void**)stack + pos, stack); + return &ret->v.d; + default: + return ((void *(*)(void *, void *, void *))call_jit_c2hl)(*f, (void**)stack + pos, stack); + } +} + +/** + * Generate the HL-to-C trampoline. + * Called from C code with a vclosure_wrapper* in X0 and native args in X1-X7, V0-V7. + * Saves registers and calls jit_wrapper_ptr or jit_wrapper_d based on return type. + */ +static void jit_hl2c(jit_ctx *ctx) { + hl_type_fun *ft = NULL; + + // Function prologue - save frame + // STP X29, X30, [SP, #-16]! + encode_ldp_stp(ctx, 0x02, 0, 0x03, -2, LR, SP_REG, FP); + // MOV X29, SP + mov_reg_reg(ctx, FP, SP_REG, true); + + // Allocate space for saved registers: 8 CPU regs + 8 FP regs = 16 * 8 = 128 bytes + // SUB SP, SP, #128 + encode_add_sub_imm(ctx, 1, 1, 0, 0, 128, SP_REG, SP_REG); + + // Trampoline marker: MOV W17, #0xE001 (HL2C trampoline) + EMIT32(ctx, 0x52800011 | (0xE001 << 5)); + + // Save integer argument registers X0-X7 at [SP, #0..63] + for (int i = 0; i < CALL_NREGS; i++) { + encode_ldr_str_imm(ctx, 0x03, 0, 0x00, i, SP_REG, i); // STR Xi, [SP, #i*8] + } + + // Save FP argument registers V0-V7 at [SP, #64..127] + for (int i = 0; i < CALL_NREGS; i++) { + encode_ldr_str_imm(ctx, 0x03, 1, 0x00, 8 + i, SP_REG, i); // STR Di, [SP, #(8+i)*8] + } + + // X0 = closure pointer (vclosure_wrapper*) + // Check return type: closure->t->fun->ret->kind + // X9 = X0->t + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, 0, X0, X9); + // X9 = X9->fun (hl_type->fun is at offset 8 on 64-bit) + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, 1, X9, X9); + // X9 = X9->ret (hl_type_fun->ret offset) + int ret_offset = (int)(int_val)&ft->ret; + if (ret_offset < 4096 && (ret_offset % 8) == 0) { + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, ret_offset / 8, X9, X9); + } else { + load_immediate(ctx, ret_offset, RTMP, true); + encode_ldr_str_reg(ctx, 0x03, 0, 0x01, RTMP, 0x03, 0, X9, X9); + } + // W9 = X9->kind (hl_type->kind is at offset 0, 32-bit) + encode_ldr_str_imm(ctx, 0x02, 0, 0x01, 0, X9, X9); + + // Compare with HF64 and HF32 + // CMP W9, #HF64 + encode_add_sub_imm(ctx, 0, 1, 1, 0, HF64, X9, XZR); + int jfloat1 = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); // B.EQ float_path + + // CMP W9, #HF32 + encode_add_sub_imm(ctx, 0, 1, 1, 0, HF32, X9, XZR); + int jfloat2 = BUF_POS(); + encode_branch_cond(ctx, 0, COND_EQ); // B.EQ float_path + + // Integer/pointer path: call jit_wrapper_ptr(closure, stack_args, regs) + // X0 = closure (reload from saved regs) + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, 0, SP_REG, X0); + // X1 = stack_args (FP + 16 is return address area, args start after saved frame) + encode_add_sub_imm(ctx, 1, 0, 0, 0, 16, FP, X1); + // X2 = regs pointer (SP) + mov_reg_reg(ctx, X2, SP_REG, true); + + load_immediate(ctx, (int64_t)jit_wrapper_ptr, RTMP, true); + EMIT32(ctx, 0xD63F0000 | (RTMP << 5)); // BLR RTMP + + // Result in X0, jump to exit + int jexit = BUF_POS(); + encode_branch_uncond(ctx, 0); // B exit + + // Float path + int float_pos = BUF_POS(); + patch_jump(ctx, jfloat1, float_pos); + patch_jump(ctx, jfloat2, float_pos); + + // X0 = closure (reload) + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, 0, SP_REG, X0); + // X1 = stack_args + encode_add_sub_imm(ctx, 1, 0, 0, 0, 16, FP, X1); + // X2 = regs pointer + mov_reg_reg(ctx, X2, SP_REG, true); + + load_immediate(ctx, (int64_t)jit_wrapper_d, RTMP, true); + EMIT32(ctx, 0xD63F0000 | (RTMP << 5)); // BLR RTMP + // Result in V0 + + // Exit path + int exit_pos = BUF_POS(); + patch_jump(ctx, jexit, exit_pos); + + // Restore frame and return + // MOV SP, X29 + mov_reg_reg(ctx, SP_REG, FP, true); + // LDP X29, X30, [SP], #16 + encode_ldp_stp(ctx, 0x02, 0, 0x01, 2, LR, SP_REG, FP); + // RET + encode_branch_reg(ctx, 0x02, LR); +} + +/** + * Generate the C-to-HL trampoline. + * Input: X0 = function pointer, X1 = reg args pointer, X2 = stack args end + * The trampoline loads arguments from the prepared stack and calls the function. + */ +static void jit_c2hl(jit_ctx *ctx) { + // Save callee-saved registers and set up frame + // STP X29, X30, [SP, #-16]! + encode_ldp_stp(ctx, 0x02, 0, 0x03, -2, LR, SP_REG, FP); + // MOV X29, SP + mov_reg_reg(ctx, FP, SP_REG, true); + + // Trampoline marker: MOV W17, #0xE002 (C2HL trampoline) + EMIT32(ctx, 0x52800011 | (0xE002 << 5)); + + // Save function pointer to X9 (caller-saved, will survive loads) + // MOV X9, X0 + mov_reg_reg(ctx, X9, X0, true); + + // Save stack args pointers to X10, X11 + // MOV X10, X1 (reg args pointer) + // MOV X11, X2 (stack args end) + mov_reg_reg(ctx, X10, X1, true); + mov_reg_reg(ctx, X11, X2, true); + + // Load integer register arguments X0-X7 from [X10] + for (int i = 0; i < CALL_NREGS; i++) { + // LDR Xi, [X10, #i*8] + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, i, X10, CALL_REGS[i]); + } + + // Load FP register arguments V0-V7 from [X10 + CALL_NREGS*8] + for (int i = 0; i < CALL_NREGS; i++) { + // LDR Di, [X10, #(CALL_NREGS + i)*8] + // Using 64-bit FP load: size=11, opc=01 + EMIT32(ctx,0xFD400000 | (((CALL_NREGS + i) & 0x1FF) << 10) | (X10 << 5) | FP_CALL_REGS[i]); + } + + // Push stack args: loop from X11 to X10, pushing each 8-byte value + // Calculate how many stack args: (X10 - X11) / 8 + // Compare X10 and X11 + int loop_start = BUF_POS(); + // CMP X10, X11 + encode_add_sub_reg(ctx, 1, 1, 1, 0, X11, 0, X10, XZR); + + // B.EQ done (if X10 == X11, no more stack args) + int beq_pos = BUF_POS(); + EMIT32(ctx,0x54000000 | (COND_EQ & 0xF)); // B.EQ (will patch) + + // SUB X10, X10, #8 + encode_add_sub_imm(ctx, 1, 1, 0, 0, 8, X10, X10); + + // LDR X12, [X10] + encode_ldr_str_imm(ctx, 0x03, 0, 0x01, 0, X10, X12); + + // STR X12, [SP, #-16]! (push with pre-decrement, keeping 16-byte alignment) + // We'll push pairs to maintain alignment - but for simplicity, push 16 at a time + // SUB SP, SP, #16 + encode_add_sub_imm(ctx, 1, 1, 0, 0, 16, SP_REG, SP_REG); + // STR X12, [SP] + encode_ldr_str_imm(ctx, 0x03, 0, 0x00, 0, SP_REG, X12); + + // B loop_start + int b_offset = (loop_start - BUF_POS()) / 4; + EMIT32(ctx,0x14000000 | (b_offset & 0x3FFFFFF)); + + // Patch the B.EQ to jump here + int done_pos = BUF_POS(); + int beq_offset = (done_pos - beq_pos) / 4; + ctx->buf.w = (unsigned int*)(ctx->startBuf + beq_pos); + EMIT32(ctx,0x54000000 | ((beq_offset & 0x7FFFF) << 5) | (COND_EQ & 0xF)); + ctx->buf.w = (unsigned int*)(ctx->startBuf + done_pos); + + // Call the function: BLR X9 + EMIT32(ctx,0xD63F0000 | (X9 << 5)); + + // Restore frame and return + // MOV SP, X29 + mov_reg_reg(ctx, SP_REG, FP, true); + // LDP X29, X30, [SP], #16 + encode_ldp_stp(ctx, 0x02, 0, 0x01, 2, LR, SP_REG, FP); + // RET + encode_branch_reg(ctx, 0x02, LR); +} + +/** + * Get wrapper function for HL-to-C calls. + * This is used for callbacks from C code back into HashLink. + * Returns the jit_hl2c trampoline address. + */ +static void *get_wrapper(hl_type *t) { + return call_jit_hl2c; +} + +// ============================================================================ +// JIT API Implementation +// ============================================================================ + +// Forward declaration +static void hl_jit_init_module(jit_ctx *ctx, hl_module *m); + +jit_ctx *hl_jit_alloc() { + jit_ctx *ctx = (jit_ctx*)malloc(sizeof(jit_ctx)); + if (ctx == NULL) + return NULL; + memset(ctx, 0, sizeof(jit_ctx)); + return ctx; +} + +void hl_jit_free(jit_ctx *ctx, h_bool can_reset) { + if (ctx == NULL) + return; + + if (ctx->startBuf) + free(ctx->startBuf); + if (ctx->vregs) + free(ctx->vregs); + if (ctx->opsPos) + free(ctx->opsPos); + + free(ctx); +} + +void hl_jit_reset(jit_ctx *ctx, hl_module *m) { + ctx->debug = NULL; + hl_jit_init_module(ctx, m); +} + +/** + * Build a JIT helper function, ensuring buffer is allocated. + * Returns the position in the buffer where the function starts. + */ +static int jit_build(jit_ctx *ctx, void (*fbuild)(jit_ctx *)) { + int pos; + jit_buf(ctx); // Ensure buffer is allocated + pos = BUF_POS(); + fbuild(ctx); + return pos; +} + +/** + * Initialize module-specific data in JIT context. + */ +static void hl_jit_init_module(jit_ctx *ctx, hl_module *m) { + int i; + ctx->m = m; + ctx->closure_list = NULL; + + // Allocate debug info array if bytecode has debug info + if (m->code->hasdebug && m->code->nfunctions > 0) { + ctx->debug = (hl_debug_infos*)malloc(sizeof(hl_debug_infos) * m->code->nfunctions); + if (ctx->debug) + memset(ctx->debug, 0, sizeof(hl_debug_infos) * m->code->nfunctions); + } + + // Store float constants in the code buffer (like x86 does) + for (i = 0; i < m->code->nfloats; i++) { + jit_buf(ctx); + *ctx->buf.d++ = m->code->floats[i]; + } +} + +void hl_jit_init(jit_ctx *ctx, hl_module *m) { + hl_jit_init_module(ctx, m); + + // Generate C↔HL trampolines + ctx->c2hl = jit_build(ctx, jit_c2hl); + ctx->hl2c = jit_build(ctx, jit_hl2c); +} + +/** + * Allocate a static closure object. + * For native functions, the function pointer is set immediately. + * For JIT functions, the function pointer is stored temporarily as the findex + * and the closure is added to closure_list for later patching. + */ +static vclosure *alloc_static_closure(jit_ctx *ctx, int fid) { + hl_module *m = ctx->m; + vclosure *c = hl_malloc(&m->ctx.alloc, sizeof(vclosure)); + int fidx = m->functions_indexes[fid]; + c->hasValue = 0; + if (fidx >= m->code->nfunctions) { + // Native function - pointer is already resolved + c->t = m->code->natives[fidx - m->code->nfunctions].t; + c->fun = m->functions_ptrs[fid]; + c->value = NULL; + } else { + // JIT function - store fid temporarily, add to closure_list for patching + c->t = m->code->functions[fidx].type; + c->fun = (void*)(int_val)fid; + c->value = ctx->closure_list; + ctx->closure_list = c; + } + return c; +} + +int hl_jit_function(jit_ctx *ctx, hl_module *m, hl_function *f) { + int i, size = 0, opCount; + int codePos = BUF_POS(); + int nargs = f->type->fun->nargs; + unsigned short *debug16 = NULL; + int *debug32 = NULL; + + ctx->f = f; + ctx->m = m; + ctx->allocOffset = 0; + + // Allocate virtual register array if needed + // Always reallocate to ensure clean state (no stale data from previous functions) + free(ctx->vregs); + ctx->vregs = (vreg*)calloc(f->nregs + 1, sizeof(vreg)); + if (ctx->vregs == NULL) { + ctx->maxRegs = 0; + return -1; + } + ctx->maxRegs = f->nregs; + + // Allocate opcode position array if needed + if (f->nops > ctx->maxOps) { + free(ctx->opsPos); + ctx->opsPos = (int*)malloc(sizeof(int) * (f->nops + 1)); + if (ctx->opsPos == NULL) { + ctx->maxOps = 0; + return -1; + } + ctx->maxOps = f->nops; + } + + memset(ctx->opsPos, 0, (f->nops + 1) * sizeof(int)); + + // Clear/initialize physical registers + for (i = 0; i < RCPU_COUNT; i++) { + preg *p = &ctx->pregs[i]; + p->kind = RCPU; + p->id = i; + p->holds = NULL; + p->lock = 0; + } + for (i = 0; i < RFPU_COUNT; i++) { + preg *p = &ctx->pregs[RCPU_COUNT + i]; + p->kind = RFPU; + p->id = i; + p->holds = NULL; + p->lock = 0; + } + + // Initialize virtual registers + for (i = 0; i < f->nregs; i++) { + vreg *r = R(i); + r->t = f->regs[i]; + r->size = hl_type_size(r->t); + r->stackPos = 0; + r->current = NULL; + r->stack.holds = NULL; + r->stack.id = i; + r->stack.kind = RSTACK; + r->stack.lock = 0; + } + + // Calculate stack layout + // Arguments: first 8 integer args in X0-X7, first 8 FP args in V0-V7 + // Additional args on stack + size = 0; + int argsSize = 0; + int int_arg_count = 0; + int fp_arg_count = 0; + + for (i = 0; i < nargs; i++) { + vreg *r = R(i); + bool is_fp = IS_FLOAT(r); + int *arg_count = is_fp ? &fp_arg_count : &int_arg_count; + + if (*arg_count < CALL_NREGS) { + // Argument is in register - allocate stack space for it + size += r->size; + size += hl_pad_size(size, r->t); + r->stackPos = -size; + (*arg_count)++; + } else { + // Argument is on stack (caller's frame) + // +80 for saved callee-saved (64 bytes) + FP/LR (16 bytes) + // Each stack arg occupies 8 bytes (matching caller's prepare_call_args) + r->stackPos = argsSize + 80; + argsSize += 8; + } + } + + // Local variables + for (i = nargs; i < f->nregs; i++) { + vreg *r = R(i); + size += r->size; + size += hl_pad_size(size, r->t); + r->stackPos = -size; + } + + // Align stack to 16 bytes + size += (-size) & 15; + ctx->totalRegsSize = size; + + jit_buf(ctx); + ctx->functionPos = BUF_POS(); + ctx->currentPos = 1; + + // Initialize Phase 2 callee-saved tracking + ctx->callee_saved_used = 0; + memset(ctx->stp_positions, 0, sizeof(ctx->stp_positions)); + memset(ctx->ldp_positions, 0, sizeof(ctx->ldp_positions)); + + // Function prologue - offset-based for selective NOP patching (Phase 2) + // Reserve space for callee-saved (64 bytes) + FP/LR (16 bytes) = 80 bytes + encode_add_sub_imm(ctx, 1, 1, 0, 0, 80, SP_REG, SP_REG); // SUB SP, SP, #80 + + // Save callee-saved at fixed offsets (NOPpable) - positions recorded for backpatching + ctx->stp_positions[0] = BUF_POS(); + stp_offset(ctx, X25, X26, SP_REG, 64); // STP X25, X26, [SP, #64] + + ctx->stp_positions[1] = BUF_POS(); + stp_offset(ctx, X23, X24, SP_REG, 48); // STP X23, X24, [SP, #48] + + ctx->stp_positions[2] = BUF_POS(); + stp_offset(ctx, X21, X22, SP_REG, 32); // STP X21, X22, [SP, #32] + + ctx->stp_positions[3] = BUF_POS(); + stp_offset(ctx, X19, X20, SP_REG, 16); // STP X19, X20, [SP, #16] + + // Save FP/LR at bottom (NOT NOPpable - always needed) + stp_offset(ctx, FP, LR, SP_REG, 0); // STP X29, X30, [SP, #0] + + // MOV X29, SP ; Set frame pointer (points to saved FP/LR) + mov_reg_reg(ctx, FP, SP_REG, true); + + // SUB SP, SP, #size ; Allocate stack space + if (size > 0) { + if (size < 4096) { + encode_add_sub_imm(ctx, 1, 1, 0, 0, size, SP_REG, SP_REG); + } else { + // Large stack frame - use multiple instructions + // Must use extended register form (UXTX) for SP, not shifted register + load_immediate(ctx, size, RTMP, true); + encode_add_sub_ext(ctx, 1, 1, 0, RTMP, 3, 0, SP_REG, SP_REG); // SUB SP, SP, RTMP, UXTX + } + } + + // Function marker: MOV W17, #(0xF000 | (findex & 0xFFF)) ; MOVK W17, #(findex >> 12), LSL #16 + // This encodes as 0xFnnnnnnn where nnnnnnn is the function index + // Distinguishes from opcode markers which are smaller numbers + { + int findex = f->findex; + int low12 = 0xF000 | (findex & 0xFFF); + int high = (findex >> 12) & 0xFFFF; + // MOV W17, #low12 + EMIT32(ctx, 0x52800011 | (low12 << 5)); + if (high != 0) { + // MOVK W17, #high, LSL #16 + EMIT32(ctx, 0x72A00011 | (high << 5)); + } + } + + // Store register arguments to their stack locations FIRST + // (before we clobber the argument registers with zero-init) + int_arg_count = 0; + fp_arg_count = 0; + for (i = 0; i < nargs && i < f->nregs; i++) { + vreg *r = R(i); + bool is_fp = IS_FLOAT(r); + int *arg_count = is_fp ? &fp_arg_count : &int_arg_count; + + if (*arg_count < CALL_NREGS) { + // This arg was in a register - store it to stack + // Skip void arguments (size 0) - they don't need storage + // but still consume a call register slot + if (r->size > 0) { + if (is_fp) { + str_stack_fp(ctx, FP_CALL_REGS[fp_arg_count], r->stackPos, r->size); + } else { + str_stack(ctx, CALL_REGS[int_arg_count], r->stackPos, r->size); + } + } + (*arg_count)++; + } + } + + // Zero-initialize local variables on stack (not arguments) + // This ensures reading unassigned locals returns null/0 + if (f->nregs > nargs) { + // Store zeros to each local variable slot using XZR + for (i = nargs; i < f->nregs; i++) { + vreg *r = R(i); + if (r->size > 0 && r->stackPos < 0) { + // Use str_stack with XZR as source + if (r->size == 8) { + load_immediate(ctx, r->stackPos, RTMP, true); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP, 0, RFP, RTMP); + encode_ldr_str_imm(ctx, 0x03, 0, 0x00, 0, RTMP, XZR); + } else if (r->size == 4) { + load_immediate(ctx, r->stackPos, RTMP, true); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP, 0, RFP, RTMP); + encode_ldr_str_imm(ctx, 0x02, 0, 0x00, 0, RTMP, XZR); + } else if (r->size == 2) { + load_immediate(ctx, r->stackPos, RTMP, true); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP, 0, RFP, RTMP); + encode_ldr_str_imm(ctx, 0x01, 0, 0x00, 0, RTMP, XZR); + } else if (r->size == 1) { + load_immediate(ctx, r->stackPos, RTMP, true); + encode_add_sub_reg(ctx, 1, 0, 0, 0, RTMP, 0, RFP, RTMP); + encode_ldr_str_imm(ctx, 0x00, 0, 0x00, 0, RTMP, XZR); + } + } + } + } + + ctx->opsPos[0] = BUF_POS(); + + // Initialize debug offset tracking + if (ctx->m->code->hasdebug) { + debug16 = (unsigned short*)malloc(sizeof(unsigned short) * (f->nops + 1)); + debug16[0] = (unsigned short)(BUF_POS() - codePos); + } + + // Main opcode translation loop + for (opCount = 0; opCount < f->nops; opCount++) { + hl_opcode *o = f->ops + opCount; + vreg *dst = R(o->p1); + vreg *ra = R(o->p2); + vreg *rb = R(o->p3); + + ctx->currentPos = opCount + 1; + jit_buf(ctx); + + // Emit opcode marker for debugging: MOV W17, #(opcode | (opCount << 8)) + // W17 is IP1, a scratch register. This encodes both the opcode type and index. + { + int marker = (o->op & 0xFF) | ((opCount & 0xFF) << 8); + EMIT32(ctx, 0x52800011 | ((marker & 0xFFFF) << 5)); // MOV W17, #marker + } + + // Before a label (merge point), spill dirty registers for fallthrough path. + // After spilling, update the label position so jumps bypass the spill code. + // discard_regs() in op_label just clears bindings (no code). + if (o->op == OLabel) { + spill_regs(ctx); + // Update label position AFTER spill - jumps should target here, + // not before the spill (which is only for fallthrough path) + ctx->opsPos[opCount] = BUF_POS(); + } + + // Emit code based on opcode + switch (o->op) { + case OMov: + case OUnsafeCast: + op_mov(ctx, dst, ra); + break; + + case OInt: + store_const(ctx, dst, m->code->ints[o->p2]); + break; + + case OBool: + store_const(ctx, dst, o->p2); + break; + + case ONull: + // Set register to NULL (0) + store_const(ctx, dst, 0); + break; + + case OFloat: { + // Load float constant from module + // Float constants are stored at the start of the code buffer (offset o->p2 * 8) + double float_val = m->code->floats[o->p2]; + preg *dst_reg = alloc_fpu(ctx); + + if (float_val == 0.0) { + // Zero out FP register: FMOV Dd, XZR + // FMOV Dd, XZR: sf=1, S=0, type=01, rmode=00, opcode=000111, Rn=31, Rd + EMIT32(ctx, (1 << 31) | (0 << 29) | (0x1E << 24) | (1 << 22) | (1 << 21) | (7 << 16) | (31 << 5) | dst_reg->id); + } else { + // Float constants are at the start of the code buffer + // Calculate PC-relative offset from current position to the float data + int float_offset = o->p2 * 8; // Offset from start of code buffer + int cur_pos = BUF_POS(); // Current position in code buffer + int pc_offset = float_offset - cur_pos; // PC-relative offset + + // LDR Dt,