/* * QBE Helper Functions * * Thin C wrappers called from QBE-generated code for operations * that are too complex to inline: float arithmetic, float comparison, * string comparison, bitwise ops on floats, and boolean conversion. */ #include "quickjs-internal.h" #include #include /* Non-inline wrappers for static inline functions in quickjs.h */ JSValue qbe_new_float64(JSContext *ctx, double d) { return __JS_NewFloat64(ctx, d); } JSValue qbe_new_string(JSContext *ctx, const char *str) { return JS_NewString(ctx, str); } /* Comparison op IDs (must match qbe.cm float_cmp_op_id values) */ enum { QBE_CMP_EQ = 0, QBE_CMP_NE = 1, QBE_CMP_LT = 2, QBE_CMP_LE = 3, QBE_CMP_GT = 4, QBE_CMP_GE = 5 }; /* ============================================================ Float binary arithmetic ============================================================ */ static inline JSValue qbe_float_binop(JSContext *ctx, JSValue a, JSValue b, double (*op)(double, double)) { double da, db; JS_ToFloat64(ctx, &da, a); JS_ToFloat64(ctx, &db, b); double r = op(da, db); if (!isfinite(r)) return JS_NULL; return JS_NewFloat64(ctx, r); } static double op_add(double a, double b) { return a + b; } static double op_sub(double a, double b) { return a - b; } static double op_mul(double a, double b) { return a * b; } JSValue qbe_float_add(JSContext *ctx, JSValue a, JSValue b) { return qbe_float_binop(ctx, a, b, op_add); } /* Generic add: concat if both text, float add if both numeric, else type error */ JSValue cell_rt_add(JSContext *ctx, JSValue a, JSValue b) { if (JS_IsText(a) && JS_IsText(b)) return JS_ConcatString(ctx, a, b); if (JS_IsNumber(a) && JS_IsNumber(b)) return qbe_float_binop(ctx, a, b, op_add); JS_ThrowTypeError(ctx, "cannot add incompatible types"); return JS_NULL; } JSValue qbe_float_sub(JSContext *ctx, JSValue a, JSValue b) { return qbe_float_binop(ctx, a, b, op_sub); } JSValue qbe_float_mul(JSContext *ctx, JSValue a, JSValue b) { return qbe_float_binop(ctx, a, b, op_mul); } JSValue qbe_float_div(JSContext *ctx, JSValue a, JSValue b) { double da, db; JS_ToFloat64(ctx, &da, a); JS_ToFloat64(ctx, &db, b); if (db == 0.0) return JS_NULL; double r = da / db; if (!isfinite(r)) return JS_NULL; return JS_NewFloat64(ctx, r); } JSValue qbe_float_mod(JSContext *ctx, JSValue a, JSValue b) { double da, db; JS_ToFloat64(ctx, &da, a); JS_ToFloat64(ctx, &db, b); if (db == 0.0) return JS_NULL; double r = fmod(da, db); if (!isfinite(r)) return JS_NULL; return JS_NewFloat64(ctx, r); } JSValue qbe_float_pow(JSContext *ctx, JSValue a, JSValue b) { double da, db; JS_ToFloat64(ctx, &da, a); JS_ToFloat64(ctx, &db, b); double r = pow(da, db); if (!isfinite(r) && isfinite(da) && isfinite(db)) return JS_NULL; return JS_NewFloat64(ctx, r); } /* ============================================================ Float unary ops ============================================================ */ JSValue qbe_float_neg(JSContext *ctx, JSValue v) { double d; JS_ToFloat64(ctx, &d, v); return JS_NewFloat64(ctx, -d); } JSValue qbe_float_inc(JSContext *ctx, JSValue v) { double d; JS_ToFloat64(ctx, &d, v); return JS_NewFloat64(ctx, d + 1); } JSValue qbe_float_dec(JSContext *ctx, JSValue v) { double d; JS_ToFloat64(ctx, &d, v); return JS_NewFloat64(ctx, d - 1); } /* ============================================================ Float comparison — returns 0 or 1 for QBE branching ============================================================ */ int qbe_float_cmp(JSContext *ctx, int op, JSValue a, JSValue b) { double da, db; JS_ToFloat64(ctx, &da, a); JS_ToFloat64(ctx, &db, b); switch (op) { case QBE_CMP_EQ: return da == db; case QBE_CMP_NE: return da != db; case QBE_CMP_LT: return da < db; case QBE_CMP_LE: return da <= db; case QBE_CMP_GT: return da > db; case QBE_CMP_GE: return da >= db; default: return 0; } } /* ============================================================ Boolean conversion wrapper ============================================================ */ int qbe_to_bool(JSContext *ctx, JSValue v) { return JS_ToBool(ctx, v); } /* ============================================================ Bitwise not on non-int (float -> int32 -> ~) ============================================================ */ JSValue qbe_bnot(JSContext *ctx, JSValue v) { int32_t i; JS_ToInt32(ctx, &i, v); return JS_NewInt32(ctx, ~i); } /* ============================================================ Bitwise binary ops on floats (convert both to int32, apply, re-tag) ============================================================ */ JSValue qbe_bitwise_and(JSContext *ctx, JSValue a, JSValue b) { int32_t ia, ib; JS_ToInt32(ctx, &ia, a); JS_ToInt32(ctx, &ib, b); return JS_NewInt32(ctx, ia & ib); } JSValue qbe_bitwise_or(JSContext *ctx, JSValue a, JSValue b) { int32_t ia, ib; JS_ToInt32(ctx, &ia, a); JS_ToInt32(ctx, &ib, b); return JS_NewInt32(ctx, ia | ib); } JSValue qbe_bitwise_xor(JSContext *ctx, JSValue a, JSValue b) { int32_t ia, ib; JS_ToInt32(ctx, &ia, a); JS_ToInt32(ctx, &ib, b); return JS_NewInt32(ctx, ia ^ ib); } /* ============================================================ Shift ops on floats (convert to int32, shift, re-tag) ============================================================ */ JSValue qbe_shift_shl(JSContext *ctx, JSValue a, JSValue b) { int32_t ia, ib; JS_ToInt32(ctx, &ia, a); JS_ToInt32(ctx, &ib, b); return JS_NewInt32(ctx, ia << (ib & 31)); } JSValue qbe_shift_sar(JSContext *ctx, JSValue a, JSValue b) { int32_t ia, ib; JS_ToInt32(ctx, &ia, a); JS_ToInt32(ctx, &ib, b); return JS_NewInt32(ctx, ia >> (ib & 31)); } JSValue qbe_shift_shr(JSContext *ctx, JSValue a, JSValue b) { int32_t ia, ib; JS_ToInt32(ctx, &ia, a); JS_ToInt32(ctx, &ib, b); return JS_NewInt32(ctx, (uint32_t)ia >> (ib & 31)); } /* ============================================================ cell_rt_* — Runtime support for QBE-compiled code ============================================================ */ #include #include /* --- Property access --- */ JSValue cell_rt_load_field(JSContext *ctx, JSValue obj, const char *name) { if (JS_IsFunction(obj)) { JS_ThrowTypeError(ctx, "cannot read property of function"); return JS_EXCEPTION; } return JS_GetPropertyStr(ctx, obj, name); } /* Like cell_rt_load_field but without the function guard. Used by load_dynamic when the key happens to be a static string. */ JSValue cell_rt_load_prop_str(JSContext *ctx, JSValue obj, const char *name) { return JS_GetPropertyStr(ctx, obj, name); } void cell_rt_store_field(JSContext *ctx, JSValue val, JSValue obj, const char *name) { JS_SetPropertyStr(ctx, obj, name, val); } JSValue cell_rt_load_dynamic(JSContext *ctx, JSValue obj, JSValue key) { if (JS_IsInt(key)) return JS_GetPropertyNumber(ctx, obj, (uint32_t)JS_VALUE_GET_INT(key)); return JS_GetProperty(ctx, obj, key); } void cell_rt_store_dynamic(JSContext *ctx, JSValue val, JSValue obj, JSValue key) { if (JS_IsInt(key)) { JS_SetPropertyNumber(ctx, obj, (uint32_t)JS_VALUE_GET_INT(key), val); } else if (JS_IsArray(obj) && !JS_IsInt(key)) { JS_ThrowTypeError(ctx, "array index must be a number"); } else if (JS_IsBool(key) || JS_IsNull(key) || JS_IsArray(key) || JS_IsFunction(key)) { JS_ThrowTypeError(ctx, "object key must be text"); } else { JS_SetProperty(ctx, obj, key, val); } } JSValue cell_rt_load_index(JSContext *ctx, JSValue arr, JSValue idx) { if (JS_IsInt(idx)) return JS_GetPropertyNumber(ctx, arr, (uint32_t)JS_VALUE_GET_INT(idx)); return JS_GetProperty(ctx, arr, idx); } void cell_rt_store_index(JSContext *ctx, JSValue val, JSValue arr, JSValue idx) { if (JS_IsInt(idx)) JS_SetPropertyNumber(ctx, arr, (uint32_t)JS_VALUE_GET_INT(idx), val); else JS_SetProperty(ctx, arr, idx, val); } /* --- Intrinsic/global lookup --- */ /* Native module environment — set before executing a native module's cell_main. Contains runtime functions (starts_with, ends_with, etc.) and use(). */ static JSGCRef g_native_env_ref; static int g_has_native_env = 0; void cell_rt_set_native_env(JSContext *ctx, JSValue env) { if (!JS_IsNull(env) && !JS_IsStone(env)) { fprintf(stderr, "cell_rt_set_native_env: ERROR env not stone\n"); abort(); } if (g_has_native_env) JS_DeleteGCRef(ctx, &g_native_env_ref); if (!JS_IsNull(env)) { JS_AddGCRef(ctx, &g_native_env_ref); g_native_env_ref.val = env; g_has_native_env = 1; } else { g_has_native_env = 0; } } JSValue cell_rt_get_intrinsic(JSContext *ctx, const char *name) { /* Check native env first (runtime-provided functions like log) */ if (g_has_native_env) { JSValue v = JS_GetPropertyStr(ctx, g_native_env_ref.val, name); if (!JS_IsNull(v)) return v; } /* Linear scan of global object — avoids hash mismatch issues with stoned records whose keys may be in cold storage */ JSValue gobj = ctx->global_obj; if (JS_IsRecord(gobj)) { JSRecord *rec = (JSRecord *)chase(gobj); uint64_t mask = objhdr_cap56(rec->mist_hdr); for (uint64_t i = 1; i <= mask; i++) { if (js_key_equal_str(rec->slots[i].key, name)) return rec->slots[i].val; } } JS_ThrowReferenceError(ctx, "'%s' is not defined", name); return JS_EXCEPTION; } /* --- Closure access --- Walk the outer_frame chain on JSFunction (JS_FUNC_KIND_NATIVE). The frame's function field links to the JSFunction, whose u.native.outer_frame points to the enclosing frame. GC traces outer_frame naturally — no registry needed. */ /* Get the outer frame's slots from a frame pointer. The frame's function must be JS_FUNC_KIND_NATIVE. */ static JSValue *get_outer_frame_slots(JSValue *fp) { /* fp points to frame->slots[0]; frame header is before it */ JSFrameRegister *frame = (JSFrameRegister *)((char *)fp - offsetof(JSFrameRegister, slots)); if (JS_IsNull(frame->function)) return NULL; JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); if (fn->kind != JS_FUNC_KIND_NATIVE) return NULL; JSValue outer = fn->u.native.outer_frame; if (JS_IsNull(outer)) return NULL; JSFrameRegister *outer_frame = (JSFrameRegister *)JS_VALUE_GET_PTR(outer); return (JSValue *)outer_frame->slots; } JSValue cell_rt_get_closure(JSContext *ctx, void *fp, int64_t depth, int64_t slot) { (void)ctx; JSValue *frame = (JSValue *)fp; for (int64_t d = 0; d < depth; d++) { frame = get_outer_frame_slots(frame); if (!frame) return JS_NULL; } return frame[slot]; } void cell_rt_put_closure(JSContext *ctx, void *fp, JSValue val, int64_t depth, int64_t slot) { (void)ctx; JSValue *frame = (JSValue *)fp; for (int64_t d = 0; d < depth; d++) { frame = get_outer_frame_slots(frame); if (!frame) return; } frame[slot] = val; } /* --- GC-managed AOT frame stack --- Each native dispatch loop pushes a GC ref so the GC can find and update the current frame pointer when it moves objects. cell_rt_refresh_fp re-derives the slot pointer after any GC call. */ #define MAX_AOT_DEPTH 8192 static JSGCRef g_aot_gc_refs[MAX_AOT_DEPTH]; static int g_aot_depth = 0; JSValue *cell_rt_enter_frame(JSContext *ctx, int64_t nr_slots) { if (g_aot_depth >= MAX_AOT_DEPTH) { JS_ThrowTypeError(ctx, "native call stack overflow (depth %d)", g_aot_depth); return NULL; } JSFrameRegister *frame = alloc_frame_register(ctx, (int)nr_slots); if (!frame) return NULL; JSGCRef *ref = &g_aot_gc_refs[g_aot_depth]; JS_AddGCRef(ctx, ref); ref->val = JS_MKPTR(frame); g_aot_depth++; return (JSValue *)frame->slots; } JSValue *cell_rt_refresh_fp(JSContext *ctx) { (void)ctx; if (g_aot_depth <= 0) { fprintf(stderr, "[BUG] cell_rt_refresh_fp: g_aot_depth=%d\n", g_aot_depth); abort(); } JSValue val = g_aot_gc_refs[g_aot_depth - 1].val; JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(val); if (!frame) { fprintf(stderr, "[BUG] cell_rt_refresh_fp: frame is NULL at depth=%d val=%lld\n", g_aot_depth, (long long)val); abort(); } return (JSValue *)frame->slots; } /* Combined refresh + exception check in a single call. */ JSValue *cell_rt_refresh_fp_checked(JSContext *ctx) { if (JS_HasException(ctx)) return NULL; if (g_aot_depth <= 0) { fprintf(stderr, "[BUG] cell_rt_refresh_fp_checked: g_aot_depth=%d\n", g_aot_depth); abort(); } JSValue val = g_aot_gc_refs[g_aot_depth - 1].val; JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(val); if (!frame) { fprintf(stderr, "[BUG] cell_rt_refresh_fp_checked: frame is NULL\n"); abort(); } return (JSValue *)frame->slots; } void cell_rt_leave_frame(JSContext *ctx) { g_aot_depth--; JS_DeleteGCRef(ctx, &g_aot_gc_refs[g_aot_depth]); } /* --- Function creation and calling --- */ typedef JSValue (*cell_compiled_fn)(JSContext *ctx, void *fp); /* Set before executing a native module's cell_main — used by cell_rt_make_function to resolve fn_ptr via dlsym */ static void *g_current_dl_handle = NULL; /* ============================================================ Dispatch loop — the core of native function execution. Each compiled cell_fn_N returns to this loop when it needs to call another function (instead of recursing via C stack). ============================================================ */ /* Pending call state — set by cell_rt_signal_call / cell_rt_signal_tail_call, read by the dispatch loop. */ static JSValue g_pending_callee_frame = 0; /* JSFrameRegister ptr */ static int g_pending_is_tail = 0; void cell_rt_signal_call(JSContext *ctx, void *fp, int64_t frame_slot) { (void)ctx; JSValue *slots = (JSValue *)fp; g_pending_callee_frame = slots[frame_slot]; g_pending_is_tail = 0; } void cell_rt_signal_tail_call(JSContext *ctx, void *fp, int64_t frame_slot) { (void)ctx; JSValue *slots = (JSValue *)fp; g_pending_callee_frame = slots[frame_slot]; g_pending_is_tail = 1; } /* Entry point called from JS_CallInternal / JS_Call / MACH_INVOKE for JS_FUNC_KIND_NATIVE functions. */ JSValue cell_native_dispatch(JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv) { JSFunction *f = JS_VALUE_GET_FUNCTION(func_obj); cell_compiled_fn fn = (cell_compiled_fn)f->u.native.fn_ptr; int nr_slots = f->u.native.nr_slots; int arity = f->length; /* Root func_obj across allocation — GC can move it */ JSGCRef func_ref; JS_PushGCRef(ctx, &func_ref); func_ref.val = func_obj; /* Allocate initial frame */ JSValue *fp = cell_rt_enter_frame(ctx, nr_slots); if (!fp) { JS_PopGCRef(ctx, &func_ref); return JS_EXCEPTION; } /* Re-derive func_obj after potential GC */ func_obj = func_ref.val; JS_PopGCRef(ctx, &func_ref); /* Set up frame: this in slot 0, args in slots 1..N */ fp[0] = this_obj; int copy = (argc < arity) ? argc : arity; if (copy < 0) copy = argc; /* variadic: copy all */ for (int i = 0; i < copy && i < nr_slots - 1; i++) fp[1 + i] = argv[i]; /* Link function to frame for closure access */ JSFrameRegister *frame = (JSFrameRegister *)((char *)fp - offsetof(JSFrameRegister, slots)); frame->function = func_obj; int base_depth = g_aot_depth; /* remember entry depth for return detection */ for (;;) { g_pending_callee_frame = 0; JSValue result = fn(ctx, fp); /* Re-derive frame after potential GC */ JSValue frame_val = g_aot_gc_refs[g_aot_depth - 1].val; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); fp = (JSValue *)frame->slots; if (g_pending_callee_frame != 0) { /* Function signaled a call — dispatch it */ JSValue callee_frame_val = g_pending_callee_frame; g_pending_callee_frame = 0; JSFrameRegister *callee_fr = (JSFrameRegister *)JS_VALUE_GET_PTR(callee_frame_val); int callee_argc = (int)objhdr_cap56(callee_fr->header); callee_argc = (callee_argc >= 2) ? callee_argc - 2 : 0; JSValue callee_fn_val = callee_fr->function; if (!JS_IsFunction(callee_fn_val)) { JS_ThrowTypeError(ctx, "not a function"); /* Resume caller with exception pending */ JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function); fn = (cell_compiled_fn)exc_fn->u.native.fn_ptr; continue; } JSFunction *callee_fn = JS_VALUE_GET_FUNCTION(callee_fn_val); if (callee_fn->kind == JS_FUNC_KIND_NATIVE) { /* Native-to-native call — no C stack growth */ cell_compiled_fn callee_ptr = (cell_compiled_fn)callee_fn->u.native.fn_ptr; int callee_slots = callee_fn->u.native.nr_slots; if (g_pending_is_tail) { /* Tail call: reuse or replace current frame */ if (callee_slots <= (int)objhdr_cap56(frame->header)) { /* Reuse current frame */ int cc = (callee_argc < callee_fn->length) ? callee_argc : callee_fn->length; if (cc < 0) cc = callee_argc; frame->slots[0] = callee_fr->slots[0]; /* this */ for (int i = 0; i < cc && i < callee_slots - 1; i++) frame->slots[1 + i] = callee_fr->slots[1 + i]; /* Null out remaining slots */ int cur_slots = (int)objhdr_cap56(frame->header); for (int i = 1 + cc; i < cur_slots; i++) frame->slots[i] = JS_NULL; frame->function = callee_fn_val; frame->address = JS_NewInt32(ctx, 0); fn = callee_ptr; /* fp stays the same (same frame) */ } else { /* Need bigger frame — save callee info, pop+push */ JSValue saved_caller = frame->caller; JSValue callee_this = callee_fr->slots[0]; int cc = (callee_argc < callee_fn->length) ? callee_argc : callee_fn->length; if (cc < 0) cc = callee_argc; JSValue callee_args[cc > 0 ? cc : 1]; for (int i = 0; i < cc; i++) callee_args[i] = callee_fr->slots[1 + i]; /* Pop old frame */ cell_rt_leave_frame(ctx); /* Push new right-sized frame */ JSValue *new_fp = cell_rt_enter_frame(ctx, callee_slots); if (!new_fp) return JS_EXCEPTION; JSFrameRegister *new_frame = (JSFrameRegister *)((char *)new_fp - offsetof(JSFrameRegister, slots)); new_frame->function = callee_fn_val; new_frame->caller = saved_caller; new_frame->slots[0] = callee_this; for (int i = 0; i < cc && i < callee_slots - 1; i++) new_frame->slots[1 + i] = callee_args[i]; frame = new_frame; fp = new_fp; fn = callee_ptr; } } else { /* Regular call: push new frame, link caller */ int ret_info = JS_VALUE_GET_INT(frame->address); int resume_seg = ret_info >> 16; int ret_slot = ret_info & 0xFFFF; /* Save callee info before allocation */ JSValue callee_this = callee_fr->slots[0]; int cc = (callee_argc < callee_fn->length) ? callee_argc : callee_fn->length; if (cc < 0) cc = callee_argc; JSValue callee_args[cc > 0 ? cc : 1]; for (int i = 0; i < cc; i++) callee_args[i] = callee_fr->slots[1 + i]; JSValue *new_fp = cell_rt_enter_frame(ctx, callee_slots); if (!new_fp) { /* Resume caller with exception pending */ frame_val = g_aot_gc_refs[g_aot_depth - 1].val; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); fp = (JSValue *)frame->slots; JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function); fn = (cell_compiled_fn)exc_fn->u.native.fn_ptr; continue; } /* Re-derive caller frame after alloc */ frame_val = g_aot_gc_refs[g_aot_depth - 2].val; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); JSFrameRegister *new_frame = (JSFrameRegister *)((char *)new_fp - offsetof(JSFrameRegister, slots)); new_frame->function = callee_fn_val; new_frame->caller = JS_MKPTR(frame); new_frame->slots[0] = callee_this; for (int i = 0; i < cc && i < callee_slots - 1; i++) new_frame->slots[1 + i] = callee_args[i]; /* Save return address in caller */ frame->address = JS_NewInt32(ctx, (resume_seg << 16) | ret_slot); frame = new_frame; fp = new_fp; fn = callee_ptr; } } else { /* Non-native callee (C function, register VM, etc.) — call it via the standard path and store the result */ JSValue ret; if (callee_fn->kind == JS_FUNC_KIND_C) ret = js_call_c_function(ctx, callee_fn_val, callee_fr->slots[0], callee_argc, &callee_fr->slots[1]); else ret = JS_CallInternal(ctx, callee_fn_val, callee_fr->slots[0], callee_argc, &callee_fr->slots[1], 0); /* Re-derive frame after call */ frame_val = g_aot_gc_refs[g_aot_depth - 1].val; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); fp = (JSValue *)frame->slots; if (JS_IsException(ret)) { /* Non-native callee threw — resume caller with exception pending. The caller's generated code checks JS_HasException at resume. */ if (!JS_HasException(ctx)) JS_Throw(ctx, JS_NULL); /* fn and fp still point to the calling native function's frame. Just resume it — it will detect the exception. */ JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function); fn = (cell_compiled_fn)exc_fn->u.native.fn_ptr; continue; } /* Clear stale exception */ if (JS_HasException(ctx)) JS_GetException(ctx); if (g_pending_is_tail) { /* Tail call to non-native: return its result up the chain */ /* Pop current frame and return to caller */ if (g_aot_depth <= base_depth) { cell_rt_leave_frame(ctx); return ret; } /* Pop current frame, return to caller frame */ JSValue caller_val = frame->caller; cell_rt_leave_frame(ctx); if (JS_IsNull(caller_val) || g_aot_depth < base_depth) { return ret; } frame = (JSFrameRegister *)JS_VALUE_GET_PTR(caller_val); /* Update GC ref to point to caller */ g_aot_gc_refs[g_aot_depth - 1].val = caller_val; fp = (JSValue *)frame->slots; int ret_info = JS_VALUE_GET_INT(frame->address); int ret_slot = ret_info & 0xFFFF; if (ret_slot != 0xFFFF) fp[ret_slot] = ret; /* Resume caller */ JSFunction *caller_fn = JS_VALUE_GET_FUNCTION(frame->function); fn = (cell_compiled_fn)caller_fn->u.native.fn_ptr; } else { /* Regular call: store result and resume current function */ int ret_info = JS_VALUE_GET_INT(frame->address); int ret_slot = ret_info & 0xFFFF; if (ret_slot != 0xFFFF) fp[ret_slot] = ret; /* fn stays the same — we resume the same function at next segment */ JSFunction *cur_fn = JS_VALUE_GET_FUNCTION(frame->function); fn = (cell_compiled_fn)cur_fn->u.native.fn_ptr; } } continue; } /* No pending call — function returned a value or exception */ if (result == JS_EXCEPTION) { /* Exception: pop this frame and propagate to caller. The caller's generated code has exception checks at resume points. */ if (!JS_HasException(ctx)) JS_Throw(ctx, JS_NULL); if (g_aot_depth <= base_depth) { cell_rt_leave_frame(ctx); return JS_EXCEPTION; } JSValue exc_caller_val = frame->caller; cell_rt_leave_frame(ctx); if (JS_IsNull(exc_caller_val) || g_aot_depth < base_depth) { return JS_EXCEPTION; } /* Resume caller — it will check JS_HasException and branch to handler */ frame = (JSFrameRegister *)JS_VALUE_GET_PTR(exc_caller_val); g_aot_gc_refs[g_aot_depth - 1].val = exc_caller_val; fp = (JSValue *)frame->slots; JSFunction *exc_caller_fn = JS_VALUE_GET_FUNCTION(frame->function); fn = (cell_compiled_fn)exc_caller_fn->u.native.fn_ptr; continue; } /* Normal return — pop frame and store result in caller */ if (g_aot_depth <= base_depth) { cell_rt_leave_frame(ctx); return result; } JSValue caller_val = frame->caller; cell_rt_leave_frame(ctx); if (JS_IsNull(caller_val) || g_aot_depth < base_depth) { return result; } /* Return to caller frame */ frame = (JSFrameRegister *)JS_VALUE_GET_PTR(caller_val); g_aot_gc_refs[g_aot_depth - 1].val = caller_val; fp = (JSValue *)frame->slots; int ret_info = JS_VALUE_GET_INT(frame->address); int ret_slot = ret_info & 0xFFFF; if (ret_slot != 0xFFFF) fp[ret_slot] = result; JSFunction *caller_fn = JS_VALUE_GET_FUNCTION(frame->function); fn = (cell_compiled_fn)caller_fn->u.native.fn_ptr; continue; } } /* Create a native function object from a compiled fn_idx. Called from QBE-generated code during function creation. */ JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx, void *outer_fp, int64_t nr_args, int64_t nr_slots) { if (!g_current_dl_handle) return JS_ThrowTypeError(ctx, "no native module loaded"); /* Resolve fn_ptr via dlsym at creation time — cached in the function object */ char name[64]; snprintf(name, sizeof(name), "cell_fn_%lld", (long long)fn_idx); void *fn_ptr = dlsym(g_current_dl_handle, name); if (!fn_ptr) return JS_ThrowTypeError(ctx, "native function %s not found in dylib", name); /* Get the current frame as outer_frame for closures */ JSValue outer_frame = JS_NULL; if (g_aot_depth > 0) outer_frame = g_aot_gc_refs[g_aot_depth - 1].val; return js_new_native_function(ctx, fn_ptr, g_current_dl_handle, (uint16_t)nr_slots, (int)nr_args, outer_frame); } /* --- Frame-based function calling --- Still used by QBE-generated code for building call frames before signaling the dispatch loop. */ JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int64_t nargs) { if (!JS_IsFunction(fn)) { JS_ThrowTypeError(ctx, "not a function"); return JS_EXCEPTION; } int nr_slots = (int)nargs + 2; JSFrameRegister *new_frame = alloc_frame_register(ctx, nr_slots); if (!new_frame) return JS_EXCEPTION; new_frame->function = fn; return JS_MKPTR(new_frame); } void cell_rt_setarg(JSValue frame_val, int64_t idx, JSValue val) { if (frame_val == JS_EXCEPTION || frame_val == JS_NULL) return; JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); fr->slots[idx] = val; } /* cell_rt_invoke — still used for non-dispatch-loop paths (e.g. old code) */ JSValue cell_rt_invoke(JSContext *ctx, JSValue frame_val) { if (frame_val == JS_EXCEPTION) return JS_EXCEPTION; JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); int nr_slots = (int)objhdr_cap56(fr->header); int c_argc = (nr_slots >= 2) ? nr_slots - 2 : 0; JSValue fn_val = fr->function; if (!JS_IsFunction(fn_val)) { JS_ThrowTypeError(ctx, "not a function"); return JS_EXCEPTION; } JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val); JSValue result; if (fn->kind == JS_FUNC_KIND_C) { result = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); } else if (fn->kind == JS_FUNC_KIND_NATIVE) { result = cell_native_dispatch(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); } else { JSValue args[c_argc > 0 ? c_argc : 1]; for (int i = 0; i < c_argc; i++) args[i] = fr->slots[i + 1]; result = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, args, 0); } if (JS_IsException(result)) return JS_EXCEPTION; if (JS_HasException(ctx)) JS_GetException(ctx); return result; } JSValue cell_rt_goframe(JSContext *ctx, JSValue fn, int64_t nargs) { return cell_rt_frame(ctx, fn, nargs); } JSValue cell_rt_goinvoke(JSContext *ctx, JSValue frame_val) { return cell_rt_invoke(ctx, frame_val); } /* --- Array push/pop --- */ JSValue cell_rt_push(JSContext *ctx, JSValue arr, JSValue val) { JS_ArrayPush(ctx, &arr, val); return arr; } JSValue cell_rt_pop(JSContext *ctx, JSValue arr) { return JS_ArrayPop(ctx, arr); } /* --- Delete --- */ JSValue cell_rt_delete(JSContext *ctx, JSValue obj, JSValue key) { int ret = JS_DeleteProperty(ctx, obj, key); if (ret < 0) return JS_EXCEPTION; return JS_NewBool(ctx, ret >= 0); } JSValue cell_rt_delete_str(JSContext *ctx, JSValue obj, const char *name) { JSValue key = JS_NewString(ctx, name); int ret = JS_DeleteProperty(ctx, obj, key); if (ret < 0) return JS_EXCEPTION; return JS_NewBool(ctx, ret >= 0); } /* --- Typeof --- */ JSValue cell_rt_typeof(JSContext *ctx, JSValue val) { if (JS_IsNull(val)) return JS_NewString(ctx, "null"); if (JS_IsInt(val) || JS_IsNumber(val)) return JS_NewString(ctx, "number"); if (JS_IsBool(val)) return JS_NewString(ctx, "logical"); if (JS_IsText(val)) return JS_NewString(ctx, "text"); if (JS_IsFunction(val)) return JS_NewString(ctx, "function"); if (JS_IsArray(val)) return JS_NewString(ctx, "array"); if (JS_IsRecord(val)) return JS_NewString(ctx, "object"); return JS_NewString(ctx, "unknown"); } /* --- Text comparison stubs (called from QBE type-dispatch branches) --- */ JSValue cell_rt_lt_text(JSContext *ctx, JSValue a, JSValue b) { const char *sa = JS_ToCString(ctx, a); const char *sb = JS_ToCString(ctx, b); int r = (sa && sb) ? strcmp(sa, sb) < 0 : 0; return JS_NewBool(ctx, r); } JSValue cell_rt_gt_text(JSContext *ctx, JSValue a, JSValue b) { const char *sa = JS_ToCString(ctx, a); const char *sb = JS_ToCString(ctx, b); int r = (sa && sb) ? strcmp(sa, sb) > 0 : 0; return JS_NewBool(ctx, r); } JSValue cell_rt_le_text(JSContext *ctx, JSValue a, JSValue b) { const char *sa = JS_ToCString(ctx, a); const char *sb = JS_ToCString(ctx, b); int r = (sa && sb) ? strcmp(sa, sb) <= 0 : 0; return JS_NewBool(ctx, r); } JSValue cell_rt_ge_text(JSContext *ctx, JSValue a, JSValue b) { const char *sa = JS_ToCString(ctx, a); const char *sb = JS_ToCString(ctx, b); int r = (sa && sb) ? strcmp(sa, sb) >= 0 : 0; return JS_NewBool(ctx, r); } static int cell_rt_tol_eq_inner(JSContext *ctx, JSValue a, JSValue b, JSValue tol) { if (JS_IsNumber(a) && JS_IsNumber(b) && JS_IsNumber(tol)) { double da, db, dt; JS_ToFloat64(ctx, &da, a); JS_ToFloat64(ctx, &db, b); JS_ToFloat64(ctx, &dt, tol); return fabs(da - db) <= dt; } if (JS_IsText(a) && JS_IsText(b) && JS_IsBool(tol) && JS_VALUE_GET_BOOL(tol)) { return js_string_compare_value_nocase(ctx, a, b) == 0; } /* Fallback to standard equality */ if (a == b) return 1; if (JS_IsText(a) && JS_IsText(b)) return js_string_compare_value(ctx, a, b, 1) == 0; if (JS_IsNumber(a) && JS_IsNumber(b)) { double da, db; JS_ToFloat64(ctx, &da, a); JS_ToFloat64(ctx, &db, b); return da == db; } return 0; } JSValue cell_rt_eq_tol(JSContext *ctx, JSValue a, JSValue b, JSValue tol) { return JS_NewBool(ctx, cell_rt_tol_eq_inner(ctx, a, b, tol)); } JSValue cell_rt_ne_tol(JSContext *ctx, JSValue a, JSValue b, JSValue tol) { return JS_NewBool(ctx, !cell_rt_tol_eq_inner(ctx, a, b, tol)); } /* --- Type check: is_proxy (function with arity 2) --- */ int cell_rt_is_proxy(JSContext *ctx, JSValue v) { (void)ctx; if (!JS_IsFunction(v)) return 0; JSFunction *fn = JS_VALUE_GET_FUNCTION(v); return fn->length == 2; } /* --- Identity check (chases forwarding pointers) --- */ JSValue cell_rt_is_identical(JSContext *ctx, JSValue a, JSValue b) { if (JS_IsPtr(a)) a = JS_MKPTR(chase(a)); if (JS_IsPtr(b)) b = JS_MKPTR(chase(b)); return JS_NewBool(ctx, a == b); } /* --- Short-circuit and/or (non-allocating) --- */ JSValue cell_rt_and(JSContext *ctx, JSValue left, JSValue right) { return JS_ToBool(ctx, left) ? right : left; } JSValue cell_rt_or(JSContext *ctx, JSValue left, JSValue right) { return JS_ToBool(ctx, left) ? left : right; } /* --- Exception checking --- After potentially-throwing runtime calls, QBE-generated code needs to check for pending exceptions and branch to the disruption handler. */ void cell_rt_clear_exception(JSContext *ctx) { if (JS_HasException(ctx)) JS_GetException(ctx); } /* --- Disruption --- */ /* Disrupt: silently set exception flag like the bytecode VM does. Does NOT call JS_ThrowTypeError — that would print to stderr even when a disruption handler will catch it. */ void cell_rt_disrupt(JSContext *ctx) { JS_Throw(ctx, JS_TRUE); } /* --- in: key in obj --- */ JSValue cell_rt_in(JSContext *ctx, JSValue key, JSValue obj) { int has = JS_HasProperty(ctx, obj, key); return JS_NewBool(ctx, has > 0); } /* --- regexp: create regex from pattern and flags --- */ JSValue cell_rt_regexp(JSContext *ctx, const char *pattern, const char *flags) { JSValue argv[2]; argv[0] = JS_NewString(ctx, pattern); argv[1] = JS_NewString(ctx, flags); JSValue re = js_regexp_constructor(ctx, JS_NULL, 2, argv); if (JS_IsException(re)) return JS_EXCEPTION; return re; } /* --- Module entry point --- Loads a native .cm module from a dylib handle. Looks up cell_main, builds a heap-allocated frame, sets g_current_dl_handle so closures register in the right module. */ /* Helper: run a native module's entry point through the dispatch loop. Creates a temporary JS_FUNC_KIND_NATIVE function so that the full dispatch loop (tail calls, closures, etc.) works for module-level code. */ static JSValue native_module_run(JSContext *ctx, void *dl_handle, cell_compiled_fn entry, int nr_slots) { void *prev_handle = g_current_dl_handle; g_current_dl_handle = dl_handle; /* Create a native function object for the entry point */ JSValue func_obj = js_new_native_function(ctx, (void *)entry, dl_handle, (uint16_t)nr_slots, 0, JS_NULL); if (JS_IsException(func_obj)) { g_current_dl_handle = prev_handle; return JS_EXCEPTION; } /* Clear any stale exception left by a previous interpreted run */ if (JS_HasException(ctx)) JS_GetException(ctx); JSValue result = cell_native_dispatch(ctx, func_obj, JS_NULL, 0, NULL); g_current_dl_handle = prev_handle; return result; } JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle, JSValue env) { cell_compiled_fn fn = (cell_compiled_fn)dlsym(dl_handle, "cell_main"); if (!fn) return JS_ThrowTypeError(ctx, "cell_main not found in native module dylib"); /* Make env available for cell_rt_get_intrinsic lookups */ cell_rt_set_native_env(ctx, env); /* Try to read nr_slots from the module (exported by emitter) */ int *slots_ptr = (int *)dlsym(dl_handle, "cell_main_nr_slots"); int nr_slots = slots_ptr ? *slots_ptr : 512; return native_module_run(ctx, dl_handle, fn, nr_slots); } /* Load a native module from a dylib handle, trying a named symbol first. Falls back to cell_main if the named symbol is not found. */ JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name, JSValue env) { cell_compiled_fn fn = NULL; const char *used_name = NULL; if (sym_name) { fn = (cell_compiled_fn)dlsym(dl_handle, sym_name); if (fn) used_name = sym_name; } if (!fn) { fn = (cell_compiled_fn)dlsym(dl_handle, "cell_main"); used_name = "cell_main"; } if (!fn) return JS_ThrowTypeError(ctx, "symbol not found in native module dylib"); /* Make env available for cell_rt_get_intrinsic lookups */ cell_rt_set_native_env(ctx, env); /* Try to read nr_slots from the module */ char slots_sym[128]; snprintf(slots_sym, sizeof(slots_sym), "%s_nr_slots", used_name); int *slots_ptr = (int *)dlsym(dl_handle, slots_sym); int nr_slots = slots_ptr ? *slots_ptr : 512; return native_module_run(ctx, dl_handle, fn, nr_slots); } /* Backward-compat: uses RTLD_DEFAULT (works when dylib opened with RTLD_GLOBAL) */ JSValue cell_rt_module_entry(JSContext *ctx) { void *handle = dlopen(NULL, RTLD_LAZY); return cell_rt_native_module_load(ctx, handle, JS_NULL); }