/* * 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 #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); } /* 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 }; /* Generic comparison helper matching MACH eq/ne/lt/le/gt/ge semantics. */ JSValue cell_rt_cmp(JSContext *ctx, int op, JSValue a, JSValue b) { if (JS_VALUE_IS_BOTH_INT(a, b)) { int32_t ia = JS_VALUE_GET_INT(a); int32_t ib = JS_VALUE_GET_INT(b); switch (op) { case QBE_CMP_EQ: return JS_NewBool(ctx, ia == ib); case QBE_CMP_NE: return JS_NewBool(ctx, ia != ib); case QBE_CMP_LT: return JS_NewBool(ctx, ia < ib); case QBE_CMP_LE: return JS_NewBool(ctx, ia <= ib); case QBE_CMP_GT: return JS_NewBool(ctx, ia > ib); case QBE_CMP_GE: return JS_NewBool(ctx, ia >= ib); default: return JS_NewBool(ctx, 0); } } /* Fast path: identity after chasing forward pointers (matches MACH). */ { JSValue ca = JS_IsPtr(a) ? JS_MKPTR(chase(a)) : a; JSValue cb = JS_IsPtr(b) ? JS_MKPTR(chase(b)) : b; if (ca == cb) { if (op == QBE_CMP_EQ || op == QBE_CMP_LE || op == QBE_CMP_GE) return JS_TRUE; if (op == QBE_CMP_NE) return JS_FALSE; } } if (JS_IsNumber(a) && JS_IsNumber(b)) { double da, db; JS_ToFloat64(ctx, &da, a); JS_ToFloat64(ctx, &db, b); switch (op) { case QBE_CMP_EQ: return JS_NewBool(ctx, da == db); case QBE_CMP_NE: return JS_NewBool(ctx, da != db); case QBE_CMP_LT: return JS_NewBool(ctx, da < db); case QBE_CMP_LE: return JS_NewBool(ctx, da <= db); case QBE_CMP_GT: return JS_NewBool(ctx, da > db); case QBE_CMP_GE: return JS_NewBool(ctx, da >= db); default: return JS_NewBool(ctx, 0); } } if (mist_is_text(a) && mist_is_text(b)) { int cmp = js_string_compare_value(ctx, a, b, FALSE); switch (op) { case QBE_CMP_EQ: return JS_NewBool(ctx, cmp == 0); case QBE_CMP_NE: return JS_NewBool(ctx, cmp != 0); case QBE_CMP_LT: return JS_NewBool(ctx, cmp < 0); case QBE_CMP_LE: return JS_NewBool(ctx, cmp <= 0); case QBE_CMP_GT: return JS_NewBool(ctx, cmp > 0); case QBE_CMP_GE: return JS_NewBool(ctx, cmp >= 0); default: return JS_NewBool(ctx, 0); } } if (JS_IsNull(a) && JS_IsNull(b)) { if (op == QBE_CMP_EQ || op == QBE_CMP_LE || op == QBE_CMP_GE) return JS_TRUE; return JS_FALSE; } if (JS_IsBool(a) && JS_IsBool(b)) { int ba = JS_VALUE_GET_BOOL(a); int bb = JS_VALUE_GET_BOOL(b); switch (op) { case QBE_CMP_EQ: return JS_NewBool(ctx, ba == bb); case QBE_CMP_NE: return JS_NewBool(ctx, ba != bb); case QBE_CMP_LT: return JS_NewBool(ctx, ba < bb); case QBE_CMP_LE: return JS_NewBool(ctx, ba <= bb); case QBE_CMP_GT: return JS_NewBool(ctx, ba > bb); case QBE_CMP_GE: return JS_NewBool(ctx, ba >= bb); default: return JS_NewBool(ctx, 0); } } if (op == QBE_CMP_EQ) return JS_NewBool(ctx, 0); if (op == QBE_CMP_NE) return JS_NewBool(ctx, 1); JS_RaiseDisrupt(ctx, "cannot compare: operands must be same type"); return JS_EXCEPTION; } /* ============================================================ 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); } /* Concat helper matching MACH_CONCAT semantics exactly. */ JSValue cell_rt_concat(JSContext *ctx, JSValue left, JSValue right, int self_assign) { if (self_assign) { if (JS_IsPtr(left)) { JSText *s = (JSText *)chase(left); int slen = (int)s->length; int rlen = js_string_value_len(right); int cap = (int)objhdr_cap56(s->hdr); if (objhdr_type(s->hdr) == OBJ_TEXT && !(s->hdr & OBJHDR_S_MASK) && slen + rlen <= cap) { for (int i = 0; i < rlen; i++) string_put(s, slen + i, js_string_value_get(right, i)); s->length = slen + rlen; return left; } } JSValue res = JS_ConcatStringGrow(ctx, left, right); if (JS_IsException(res)) return JS_EXCEPTION; return res; } JSValue res = JS_ConcatString(ctx, left, right); if (JS_IsException(res)) return JS_EXCEPTION; return res; } /* ============================================================ cell_rt_* — Runtime support for QBE-compiled code ============================================================ */ #include #include /* --- Property access --- */ typedef struct { void *dl_handle; JSContext *ctx; JSGCRef *vals; int count; } AOTLiteralPool; typedef struct { const char *name; JSValue key; } AOTKeyCacheEntry; typedef struct { void *dl_handle; int64_t fn_idx; JSValue code; } AOTCodeCacheEntry; typedef struct AOTGCRefChunk AOTGCRefChunk; typedef struct { void *current_dl_handle; AOTLiteralPool lit_pool; AOTKeyCacheEntry *key_cache; int key_cache_count; int key_cache_cap; AOTCodeCacheEntry *code_cache; int code_cache_count; int code_cache_cap; AOTGCRefChunk **gc_ref_chunks; int gc_ref_chunk_count; int aot_depth; JSValue pending_callee_frame; int pending_is_tail; } NativeRTState; static NativeRTState *native_state(JSContext *ctx) { NativeRTState *st = (NativeRTState *)ctx->native_state; if (st) return st; st = js_mallocz_rt(sizeof(*st)); if (!st) { JS_RaiseOOM(ctx); return NULL; } ctx->native_state = st; return st; } static void aot_clear_lit_pool(JSContext *ctx, NativeRTState *st) { if (!st) return; if (st->lit_pool.vals) { for (int i = 0; i < st->lit_pool.count; i++) JS_DeleteGCRef(ctx, &st->lit_pool.vals[i]); free(st->lit_pool.vals); } st->lit_pool.dl_handle = NULL; st->lit_pool.ctx = NULL; st->lit_pool.vals = NULL; st->lit_pool.count = 0; } static int aot_load_lit_pool(JSContext *ctx, NativeRTState *st, void *dl_handle) { aot_clear_lit_pool(ctx, st); st->lit_pool.dl_handle = dl_handle; st->lit_pool.ctx = ctx; if (!dl_handle) return 1; int *count_ptr = (int *)dlsym(dl_handle, "cell_lit_count"); const char **table_ptr = (const char **)dlsym(dl_handle, "cell_lit_table"); int count = count_ptr ? *count_ptr : 0; if (count <= 0 || !table_ptr) return 1; st->lit_pool.vals = (JSGCRef *)calloc((size_t)count, sizeof(JSGCRef)); if (!st->lit_pool.vals) { JS_RaiseOOM(ctx); return 0; } st->lit_pool.count = 0; for (int i = 0; i < count; i++) { const char *cstr = table_ptr[i] ? table_ptr[i] : ""; JS_AddGCRef(ctx, &st->lit_pool.vals[i]); st->lit_pool.count = i + 1; st->lit_pool.vals[i].val = js_key_new(ctx, cstr); if (JS_IsException(st->lit_pool.vals[i].val)) { aot_clear_lit_pool(ctx, st); return 0; } } return 1; } static JSValue aot_lit_from_index(JSContext *ctx, int64_t lit_idx) { NativeRTState *st = native_state(ctx); if (!st) return JS_EXCEPTION; if (lit_idx < 0) { JS_RaiseDisrupt(ctx, "literal index out of range"); return JS_EXCEPTION; } if (st->lit_pool.dl_handle != st->current_dl_handle || st->lit_pool.ctx != ctx) { if (!aot_load_lit_pool(ctx, st, st->current_dl_handle)) return JS_EXCEPTION; } if (lit_idx >= st->lit_pool.count) { JS_RaiseDisrupt(ctx, "literal index out of range"); return JS_EXCEPTION; } return st->lit_pool.vals[lit_idx].val; } /* Convert a static C string to an interned JSValue key. Uses a small per-actor cache keyed by C-string pointer to avoid repeated UTF-8 decoding in hot property paths. */ static JSValue aot_key_from_cstr(JSContext *ctx, const char *name) { NativeRTState *st = native_state(ctx); if (!st) return JS_EXCEPTION; if (!name) return JS_NULL; for (int i = 0; i < st->key_cache_count; i++) { if (st->key_cache[i].name == name) return st->key_cache[i].key; } JSValue key = js_key_new(ctx, name); if (JS_IsNull(key)) return JS_RaiseDisrupt(ctx, "invalid property key"); if (st->key_cache_count >= st->key_cache_cap) { int new_cap = st->key_cache_cap ? (st->key_cache_cap * 2) : 64; AOTKeyCacheEntry *new_cache = (AOTKeyCacheEntry *)realloc(st->key_cache, (size_t)new_cap * sizeof(*new_cache)); if (!new_cache) return JS_RaiseOOM(ctx); st->key_cache = new_cache; st->key_cache_cap = new_cap; } st->key_cache[st->key_cache_count].name = name; st->key_cache[st->key_cache_count].key = key; st->key_cache_count++; return key; } static JSValue cell_rt_load_field_key(JSContext *ctx, JSValue obj, JSValue key) { if (JS_IsFunction(obj)) { JS_RaiseDisrupt(ctx, "cannot read property of function"); return JS_EXCEPTION; } return JS_GetProperty(ctx, obj, key); } JSValue cell_rt_load_field_lit(JSContext *ctx, JSValue obj, int64_t lit_idx) { JSValue key = aot_lit_from_index(ctx, lit_idx); if (JS_IsException(key)) return JS_EXCEPTION; return cell_rt_load_field_key(ctx, obj, key); } static int cell_rt_store_field_key(JSContext *ctx, JSValue val, JSValue obj, JSValue key) { int ret = JS_SetProperty(ctx, obj, key, val); return (ret < 0 || JS_HasException(ctx)) ? 0 : 1; } int cell_rt_store_field_lit(JSContext *ctx, JSValue val, JSValue obj, int64_t lit_idx) { JSValue key = aot_lit_from_index(ctx, lit_idx); if (JS_IsException(key)) return 0; return cell_rt_store_field_key(ctx, val, obj, key); } JSValue cell_rt_access_lit(JSContext *ctx, int64_t lit_idx) { return aot_lit_from_index(ctx, lit_idx); } 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); } int cell_rt_store_dynamic(JSContext *ctx, JSValue val, JSValue obj, JSValue key) { int ret = 0; JSValue nr = JS_NULL; if (JS_IsInt(key)) { nr = JS_SetPropertyNumber(ctx, obj, (uint32_t)JS_VALUE_GET_INT(key), val); return JS_IsException(nr) ? 0 : 1; } else if (JS_IsArray(obj) && !JS_IsInt(key)) { JS_RaiseDisrupt(ctx, "array index must be a number"); return 0; } else if (JS_IsBool(key) || JS_IsNull(key) || JS_IsArray(key) || JS_IsFunction(key)) { JS_RaiseDisrupt(ctx, "object key must be text"); return 0; } else { ret = JS_SetProperty(ctx, obj, key, val); return (ret < 0 || JS_HasException(ctx)) ? 0 : 1; } } /* --- Env-based variable lookup (env at frame slot 0) --- */ JSValue cell_rt_access_env(JSContext *ctx, JSValue env, int64_t lit_idx) { JSValue key = aot_lit_from_index(ctx, lit_idx); if (JS_IsException(key)) return JS_EXCEPTION; /* Try env first (linear scan for stoned records) */ if (!JS_IsNull(env) && JS_IsRecord(env)) { JSRecord *rec = (JSRecord *)chase(env); uint64_t mask = objhdr_cap56(rec->mist_hdr); for (uint64_t i = 1; i <= mask; i++) { if (js_key_equal(rec->slots[i].key, key)) return rec->slots[i].val; } } /* Fallback to global object */ 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(rec->slots[i].key, key)) return rec->slots[i].val; } } const char *kstr = JS_ToCString(ctx, key); JS_RaiseDisrupt(ctx, "'%s' is not defined", kstr ? kstr : "?"); if (kstr) JS_FreeCString(ctx, kstr); return JS_EXCEPTION; } /* --- 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. */ // Keep GC roots for native frames in stable heap chunks (no fixed depth cap). #define AOT_GC_REF_CHUNK_SIZE 1024 typedef struct AOTGCRefChunk { JSGCRef refs[AOT_GC_REF_CHUNK_SIZE]; uint8_t inited[AOT_GC_REF_CHUNK_SIZE]; } AOTGCRefChunk; int cell_rt_native_active(JSContext *ctx) { NativeRTState *st = (NativeRTState *)ctx->native_state; return st ? (st->aot_depth > 0) : 0; } static int ensure_aot_gc_ref_slot(JSContext *ctx, NativeRTState *st, int depth_index) { if (depth_index < 0) return 0; int needed_chunks = (depth_index / AOT_GC_REF_CHUNK_SIZE) + 1; if (needed_chunks <= st->gc_ref_chunk_count) return 1; AOTGCRefChunk **new_chunks = (AOTGCRefChunk **)realloc(st->gc_ref_chunks, (size_t)needed_chunks * sizeof(*new_chunks)); if (!new_chunks) { JS_RaiseOOM(ctx); return 0; } st->gc_ref_chunks = new_chunks; for (int i = st->gc_ref_chunk_count; i < needed_chunks; i++) { st->gc_ref_chunks[i] = (AOTGCRefChunk *)calloc(1, sizeof(AOTGCRefChunk)); if (!st->gc_ref_chunks[i]) { JS_RaiseOOM(ctx); return 0; } } st->gc_ref_chunk_count = needed_chunks; return 1; } static inline JSGCRef *aot_gc_ref_at(NativeRTState *st, int depth_index) { int chunk_index = depth_index / AOT_GC_REF_CHUNK_SIZE; int slot_index = depth_index % AOT_GC_REF_CHUNK_SIZE; return &st->gc_ref_chunks[chunk_index]->refs[slot_index]; } static inline uint8_t *aot_gc_ref_inited_at(NativeRTState *st, int depth_index) { int chunk_index = depth_index / AOT_GC_REF_CHUNK_SIZE; int slot_index = depth_index % AOT_GC_REF_CHUNK_SIZE; return &st->gc_ref_chunks[chunk_index]->inited[slot_index]; } static inline void aot_gc_ref_activate(JSContext *ctx, NativeRTState *st, int depth_index) { JSGCRef *ref = aot_gc_ref_at(st, depth_index); uint8_t *inited = aot_gc_ref_inited_at(st, depth_index); if (!*inited) { JS_AddGCRef(ctx, ref); *inited = 1; } } JSValue *cell_rt_enter_frame(JSContext *ctx, int64_t nr_slots) { NativeRTState *st = native_state(ctx); if (!st) return NULL; if (!ensure_aot_gc_ref_slot(ctx, st, st->aot_depth)) { return NULL; } JSFrameRegister *frame = alloc_frame_register(ctx, (int)nr_slots); if (!frame) return NULL; aot_gc_ref_activate(ctx, st, st->aot_depth); JSGCRef *ref = aot_gc_ref_at(st, st->aot_depth); ref->val = JS_MKPTR(frame); st->aot_depth++; return (JSValue *)frame->slots; } /* Push an already-allocated frame onto the active AOT frame stack. */ static int cell_rt_push_existing_frame(JSContext *ctx, JSValue frame_val) { NativeRTState *st = native_state(ctx); if (!st) return 0; if (!ensure_aot_gc_ref_slot(ctx, st, st->aot_depth)) return 0; aot_gc_ref_activate(ctx, st, st->aot_depth); JSGCRef *ref = aot_gc_ref_at(st, st->aot_depth); ref->val = frame_val; st->aot_depth++; return 1; } JSValue *cell_rt_refresh_fp(JSContext *ctx) { NativeRTState *st = native_state(ctx); if (!st) return NULL; if (st->aot_depth <= 0) { fprintf(stderr, "[BUG] cell_rt_refresh_fp: aot_depth=%d\n", st->aot_depth); abort(); } JSValue val = aot_gc_ref_at(st, st->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", st->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) { NativeRTState *st = native_state(ctx); if (!st) return NULL; if (JS_HasException(ctx)) return NULL; if (st->aot_depth <= 0) { fprintf(stderr, "[BUG] cell_rt_refresh_fp_checked: aot_depth=%d\n", st->aot_depth); abort(); } JSValue val = aot_gc_ref_at(st, st->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) { NativeRTState *st = native_state(ctx); if (!st) return; if (st->aot_depth <= 0) { fprintf(stderr, "[BUG] cell_rt_leave_frame underflow\n"); abort(); } st->aot_depth--; aot_gc_ref_at(st, st->aot_depth)->val = JS_NULL; } /* --- Function creation and calling --- */ typedef JSValue (*cell_compiled_fn)(JSContext *ctx, void *fp); /* ============================================================ 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). ============================================================ */ void cell_rt_signal_call(JSContext *ctx, void *fp, int64_t frame_slot) { NativeRTState *st = native_state(ctx); if (!st) return; JSValue *slots = (JSValue *)fp; st->pending_callee_frame = slots[frame_slot]; st->pending_is_tail = 0; } void cell_rt_signal_tail_call(JSContext *ctx, void *fp, int64_t frame_slot) { NativeRTState *st = native_state(ctx); if (!st) return; JSValue *slots = (JSValue *)fp; st->pending_callee_frame = slots[frame_slot]; st->pending_is_tail = 1; } static int cell_check_call_arity(JSContext *ctx, JSFunction *fn, int argc) { if (unlikely(fn->length >= 0 && argc > fn->length)) { char buf[KEY_GET_STR_BUF_SIZE]; JS_RaiseDisrupt(ctx, "too many arguments for %s: expected %d, got %d", JS_KeyGetStr(ctx, buf, KEY_GET_STR_BUF_SIZE, fn->name), fn->length, argc); return 0; } return 1; } JSValue cell_rt_apply(JSContext *ctx, JSValue fn_val, JSValue arg_val) { if (!mist_is_function(fn_val)) return fn_val; JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val); if (!mist_is_array(arg_val)) { if (!cell_check_call_arity(ctx, fn, 1)) return JS_EXCEPTION; return JS_CallInternal(ctx, fn_val, JS_NULL, 1, &arg_val, 0); } JSArray *arr = JS_VALUE_GET_ARRAY(arg_val); int len = arr->len; if (!cell_check_call_arity(ctx, fn, len)) return JS_EXCEPTION; if (len == 0) return JS_CallInternal(ctx, fn_val, JS_NULL, 0, NULL, 0); JSValue args[len]; for (int i = 0; i < len; i++) args[i] = arr->values[i]; return JS_CallInternal(ctx, fn_val, JS_NULL, len, args, 0); } static inline void cell_copy_args_0_4(JSValue *fp, JSValue *argv, int copy) { /* fp[0] is `this`; copy args into fp[1..4] */ switch (copy) { case 4: fp[4] = argv[3]; case 3: fp[3] = argv[2]; case 2: fp[2] = argv[1]; case 1: fp[1] = argv[0]; case 0: break; default: break; } } static inline void cell_sync_dl_from_native_fn(NativeRTState *st, JSFunction *fn) { st->current_dl_handle = JS_VALUE_GET_CODE(fn->u.cell.code)->u.native.dl_handle; } /* 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) { NativeRTState *st = native_state(ctx); if (!st) return JS_EXCEPTION; JSFunction *f = JS_VALUE_GET_FUNCTION(func_obj); JSCode *f_code = JS_VALUE_GET_CODE(FN_READ_CODE(f)); cell_compiled_fn fn = (cell_compiled_fn)f_code->u.native.fn_ptr; int nr_slots = f_code->u.native.nr_slots; int arity = f->length; void *prev_dl_handle = st->current_dl_handle; st->current_dl_handle = f_code->u.native.dl_handle; #define RETURN_DISPATCH(v) \ do { \ atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed); \ st->current_dl_handle = prev_dl_handle; \ return (v); \ } while (0) if (!cell_check_call_arity(ctx, f, argc)) RETURN_DISPATCH(JS_EXCEPTION); /* 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_DISPATCH(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 */ if (copy > nr_slots - 1) copy = nr_slots - 1; if (unlikely(copy > 4)) { JS_RaiseDisrupt(ctx, "native calls support at most 4 arguments"); RETURN_DISPATCH(JS_EXCEPTION); } if (copy > 0 && argv) cell_copy_args_0_4(fp, argv, copy); /* Link function to frame for closure access */ JSFrameRegister *frame = (JSFrameRegister *)((char *)fp - offsetof(JSFrameRegister, slots)); frame->function = func_obj; int base_depth = st->aot_depth; /* remember entry depth for return detection */ for (;;) { st->pending_callee_frame = 0; st->pending_is_tail = 0; if (atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed) >= 1) atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed); /* Keep closure creation bound to the currently executing native module. */ if (JS_IsFunction(frame->function)) { JSFunction *cur_fn = JS_VALUE_GET_FUNCTION(frame->function); if (cur_fn->kind == JS_FUNC_KIND_NATIVE) st->current_dl_handle = JS_VALUE_GET_CODE(FN_READ_CODE(cur_fn))->u.native.dl_handle; } JSValue result = fn(ctx, fp); /* Re-derive frame after potential GC */ if (st->aot_depth <= 0) { fprintf(stderr, "[BUG] native dispatch lost frame depth after fn call\n"); abort(); } JSValue frame_val = aot_gc_ref_at(st, st->aot_depth - 1)->val; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); fp = (JSValue *)frame->slots; if (st->pending_callee_frame != 0) { /* Function signaled a call — dispatch it */ JSValue callee_frame_val = st->pending_callee_frame; st->pending_callee_frame = 0; int pending_is_tail = st->pending_is_tail; st->pending_is_tail = 0; JSGCRef callee_ref; JS_PushGCRef(ctx, &callee_ref); callee_ref.val = callee_frame_val; JSFrameRegister *callee_fr = (JSFrameRegister *)JS_VALUE_GET_PTR(callee_ref.val); int callee_argc = JS_VALUE_GET_INT(callee_fr->address); if (callee_argc < 0) callee_argc = 0; JSValue callee_fn_val = callee_fr->function; if (!JS_IsFunction(callee_fn_val)) { JS_RaiseDisrupt(ctx, "not a function"); { int ret_info = JS_VALUE_GET_INT(frame->address); int ret_slot = ret_info & 0xFFFF; if (ret_slot != 0xFFFF) fp[ret_slot] = JS_EXCEPTION; } /* Resume caller with exception pending */ JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function); fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(exc_fn))->u.native.fn_ptr; cell_sync_dl_from_native_fn(st, exc_fn); JS_PopGCRef(ctx, &callee_ref); continue; } JSFunction *callee_fn = JS_VALUE_GET_FUNCTION(callee_fn_val); if (!cell_check_call_arity(ctx, callee_fn, callee_argc)) { int ret_info = JS_VALUE_GET_INT(frame->address); int ret_slot = ret_info & 0xFFFF; if (ret_slot != 0xFFFF) fp[ret_slot] = JS_EXCEPTION; JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function); fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(exc_fn))->u.native.fn_ptr; cell_sync_dl_from_native_fn(st, exc_fn); JS_PopGCRef(ctx, &callee_ref); continue; } if (callee_fn->kind == JS_FUNC_KIND_NATIVE) { /* Native-to-native call — no C stack growth */ cell_compiled_fn callee_ptr = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(callee_fn))->u.native.fn_ptr; if (pending_is_tail) { /* Tail call: replace current frame with the prepared callee frame. */ JSValue saved_caller = frame->caller; /* Pop old frame */ cell_rt_leave_frame(ctx); callee_fr = (JSFrameRegister *)JS_VALUE_GET_PTR(callee_ref.val); callee_fr->function = callee_fn_val; callee_fr->caller = saved_caller; callee_fr->address = JS_NewInt32(ctx, 0); if (!cell_rt_push_existing_frame(ctx, callee_ref.val)) { JS_PopGCRef(ctx, &callee_ref); RETURN_DISPATCH(JS_EXCEPTION); } frame = (JSFrameRegister *)JS_VALUE_GET_PTR(aot_gc_ref_at(st, st->aot_depth - 1)->val); fp = (JSValue *)frame->slots; fn = callee_ptr; cell_sync_dl_from_native_fn(st, callee_fn); } else { /* Regular call: link caller and push prepared callee frame. */ int ret_info = JS_VALUE_GET_INT(frame->address); int resume_seg = ret_info >> 16; int ret_slot = ret_info & 0xFFFF; /* Save return address in caller */ frame->address = JS_NewInt32(ctx, (resume_seg << 16) | ret_slot); callee_fr = (JSFrameRegister *)JS_VALUE_GET_PTR(callee_ref.val); callee_fr->function = callee_fn_val; callee_fr->caller = JS_MKPTR(frame); callee_fr->address = JS_NewInt32(ctx, 0); if (!cell_rt_push_existing_frame(ctx, callee_ref.val)) { /* Resume caller with exception pending */ frame_val = aot_gc_ref_at(st, st->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)JS_VALUE_GET_CODE(FN_READ_CODE(exc_fn))->u.native.fn_ptr; cell_sync_dl_from_native_fn(st, exc_fn); JS_PopGCRef(ctx, &callee_ref); continue; } frame = (JSFrameRegister *)JS_VALUE_GET_PTR(aot_gc_ref_at(st, st->aot_depth - 1)->val); fp = (JSValue *)frame->slots; fn = callee_ptr; cell_sync_dl_from_native_fn(st, callee_fn); } } 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 = aot_gc_ref_at(st, st->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. Tag the pending return slot with JS_EXCEPTION so generated code can branch without an extra JS_HasException C call. */ if (!JS_HasException(ctx)) JS_Disrupt(ctx); int ret_info = JS_VALUE_GET_INT(frame->address); int ret_slot = ret_info & 0xFFFF; if (ret_slot != 0xFFFF) fp[ret_slot] = JS_EXCEPTION; /* fn and fp still point to the calling native function's frame. Just resume it — it will detect JS_EXCEPTION in the return slot. */ JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function); fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(exc_fn))->u.native.fn_ptr; cell_sync_dl_from_native_fn(st, exc_fn); JS_PopGCRef(ctx, &callee_ref); continue; } /* Clear stale exception */ if (JS_HasException(ctx)) JS_GetException(ctx); if (pending_is_tail) { /* Tail call to non-native: return its result up the chain */ /* Pop current frame and return to caller */ if (st->aot_depth <= base_depth) { cell_rt_leave_frame(ctx); JS_PopGCRef(ctx, &callee_ref); RETURN_DISPATCH(ret); } /* Pop current frame, return to caller frame */ cell_rt_leave_frame(ctx); if (st->aot_depth < base_depth) { JS_PopGCRef(ctx, &callee_ref); RETURN_DISPATCH(ret); } frame_val = aot_gc_ref_at(st, st->aot_depth - 1)->val; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_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)JS_VALUE_GET_CODE(FN_READ_CODE(caller_fn))->u.native.fn_ptr; cell_sync_dl_from_native_fn(st, caller_fn); } 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)JS_VALUE_GET_CODE(FN_READ_CODE(cur_fn))->u.native.fn_ptr; cell_sync_dl_from_native_fn(st, cur_fn); } } JS_PopGCRef(ctx, &callee_ref); 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_Disrupt(ctx); if (st->aot_depth <= base_depth) { cell_rt_leave_frame(ctx); RETURN_DISPATCH(JS_EXCEPTION); } cell_rt_leave_frame(ctx); if (st->aot_depth < base_depth) { RETURN_DISPATCH(JS_EXCEPTION); } /* Resume caller and tag the return slot with JS_EXCEPTION. */ frame_val = aot_gc_ref_at(st, st->aot_depth - 1)->val; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_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] = JS_EXCEPTION; JSFunction *exc_caller_fn = JS_VALUE_GET_FUNCTION(frame->function); fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(exc_caller_fn))->u.native.fn_ptr; cell_sync_dl_from_native_fn(st, exc_caller_fn); continue; } /* Normal return — pop frame and store result in caller */ if (st->aot_depth <= base_depth) { cell_rt_leave_frame(ctx); RETURN_DISPATCH(result); } cell_rt_leave_frame(ctx); if (st->aot_depth < base_depth) { RETURN_DISPATCH(result); } /* Return to caller frame */ frame_val = aot_gc_ref_at(st, st->aot_depth - 1)->val; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_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)JS_VALUE_GET_CODE(FN_READ_CODE(caller_fn))->u.native.fn_ptr; cell_sync_dl_from_native_fn(st, caller_fn); continue; } #undef RETURN_DISPATCH } static JSValue aot_get_or_create_native_code(JSContext *ctx, NativeRTState *st, void *dl_handle, int64_t fn_idx, int arity, uint16_t nr_slots) { for (int i = 0; i < st->code_cache_count; i++) { AOTCodeCacheEntry *e = &st->code_cache[i]; if (e->dl_handle == dl_handle && e->fn_idx == fn_idx) return e->code; } char name[64]; snprintf(name, sizeof(name), "cell_fn_%lld", (long long)fn_idx); void *fn_ptr = dlsym(dl_handle, name); if (!fn_ptr) return JS_RaiseDisrupt(ctx, "native function %s not found in dylib", name); JSCode *code = ct_alloc(ctx, sizeof(JSCode), 8); if (!code) return JS_EXCEPTION; memset(code, 0, sizeof(*code)); code->header = objhdr_make(0, OBJ_CODE, 0, 0, 0, 0); code->kind = JS_CODE_KIND_NATIVE; code->arity = (int16_t)arity; code->u.native.fn_ptr = fn_ptr; code->u.native.dl_handle = dl_handle; code->u.native.nr_slots = nr_slots; JSValue code_obj = JS_MKPTR(code); if (st->code_cache_count >= st->code_cache_cap) { int new_cap = st->code_cache_cap ? (st->code_cache_cap * 2) : 128; AOTCodeCacheEntry *new_cache = (AOTCodeCacheEntry *)realloc(st->code_cache, (size_t)new_cap * sizeof(*new_cache)); if (!new_cache) return JS_RaiseOOM(ctx); st->code_cache = new_cache; st->code_cache_cap = new_cap; } st->code_cache[st->code_cache_count].dl_handle = dl_handle; st->code_cache[st->code_cache_count].fn_idx = fn_idx; st->code_cache[st->code_cache_count].code = code_obj; st->code_cache_count++; return code_obj; } /* 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) { NativeRTState *st = native_state(ctx); if (!st) return JS_EXCEPTION; if (!st->current_dl_handle) return JS_RaiseDisrupt(ctx, "no native module loaded"); JSValue code_obj = aot_get_or_create_native_code( ctx, st, st->current_dl_handle, fn_idx, (int)nr_args, (uint16_t)nr_slots); if (JS_IsException(code_obj)) return JS_EXCEPTION; /* Get the current frame as outer_frame for closures */ JSValue outer_frame = JS_NULL; if (st->aot_depth > 0) outer_frame = aot_gc_ref_at(st, st->aot_depth - 1)->val; /* Read env from the parent frame's function object */ JSValue env = JS_NULL; if (outer_fp) { JSFrameRegister *parent = (JSFrameRegister *)((char *)outer_fp - offsetof(JSFrameRegister, slots)); if (JS_IsFunction(parent->function)) { JSFunction *parent_fn = JS_VALUE_GET_FUNCTION(parent->function); if (parent_fn->kind == JS_FUNC_KIND_NATIVE) env = parent_fn->u.cell.env_record; } } return js_new_native_function_with_code(ctx, code_obj, (int)nr_args, outer_frame, env); } /* --- 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_RaiseDisrupt(ctx, "not a function"); return JS_EXCEPTION; } int nr_slots = (int)nargs + 2; JSFunction *f = JS_VALUE_GET_FUNCTION(fn); if (f->kind == JS_FUNC_KIND_NATIVE && JS_VALUE_GET_CODE(FN_READ_CODE(f))->u.native.nr_slots > nr_slots) nr_slots = JS_VALUE_GET_CODE(FN_READ_CODE(f))->u.native.nr_slots; JSFrameRegister *new_frame = alloc_frame_register(ctx, nr_slots); if (!new_frame) return JS_EXCEPTION; new_frame->function = fn; new_frame->address = JS_NewInt32(ctx, (int)nargs); return JS_MKPTR(new_frame); } JSValue cell_rt_goframe(JSContext *ctx, JSValue fn, int64_t nargs) { return cell_rt_frame(ctx, fn, nargs); } /* --- 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); } static JSValue cell_rt_delete_key(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_lit(JSContext *ctx, JSValue obj, int64_t lit_idx) { JSValue key = aot_lit_from_index(ctx, lit_idx); if (JS_IsException(key)) return JS_EXCEPTION; return cell_rt_delete_key(ctx, obj, key); } 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)); } int cell_rt_is_actor(JSContext *ctx, JSValue v) { int result = 0; if (mist_is_record(v) && !JS_IsNull(ctx->actor_sym)) result = JS_HasPropertyKey(ctx, v, ctx->actor_sym) > 0; return result; } /* --- 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_RaiseDisrupt(ctx, "type error in native code"); } /* --- 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, and records active module handle in per-actor native state. */ /* 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, JSValue env) { NativeRTState *st = native_state(ctx); if (!st) return JS_EXCEPTION; void *prev_handle = st->current_dl_handle; st->current_dl_handle = dl_handle; /* Create a native function object for the entry point, with env on the function */ JSValue func_obj = js_new_native_function(ctx, (void *)entry, dl_handle, (uint16_t)nr_slots, 0, JS_NULL, env); if (JS_IsException(func_obj)) { st->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); st->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_RaiseDisrupt(ctx, "cell_main not found in native module dylib"); /* 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, env); } /* 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_RaiseDisrupt(ctx, "symbol '%s' (and fallback 'cell_main') not found in native module dylib", sym_name ? sym_name : "(null)"); } /* 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, env); } void cell_rt_free_native_state(JSContext *ctx) { NativeRTState *st = (NativeRTState *)ctx->native_state; if (!st) return; aot_clear_lit_pool(ctx, st); for (int ci = 0; ci < st->gc_ref_chunk_count; ci++) { AOTGCRefChunk *chunk = st->gc_ref_chunks[ci]; if (!chunk) continue; for (int si = 0; si < AOT_GC_REF_CHUNK_SIZE; si++) { if (chunk->inited[si]) { JS_DeleteGCRef(ctx, &chunk->refs[si]); chunk->inited[si] = 0; chunk->refs[si].val = JS_NULL; } } free(chunk); } free(st->gc_ref_chunks); free(st->key_cache); free(st->code_cache); js_free_rt(st); ctx->native_state = NULL; } /* 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); }