Files
cell/source/qbe_helpers.c

1162 lines
39 KiB
C

/*
* 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 <math.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#if defined(__GNUC__) || defined(__clang__)
#define CELL_THREAD_LOCAL __thread
#else
#define CELL_THREAD_LOCAL _Thread_local
#endif
/* 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_RaiseDisrupt(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 <dlfcn.h>
#include <stdio.h>
/* --- Property access --- */
JSValue cell_rt_load_field(JSContext *ctx, JSValue obj, const char *name) {
if (JS_IsFunction(obj)) {
JS_RaiseDisrupt(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_RaiseDisrupt(ctx, "array index must be a number");
} else if (JS_IsBool(key) || JS_IsNull(key) || JS_IsArray(key) || JS_IsFunction(key)) {
JS_RaiseDisrupt(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 CELL_THREAD_LOCAL JSGCRef g_native_env_ref;
static CELL_THREAD_LOCAL 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_RaiseDisrupt(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. */
// 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];
} AOTGCRefChunk;
static CELL_THREAD_LOCAL AOTGCRefChunk **g_aot_gc_ref_chunks = NULL;
static CELL_THREAD_LOCAL int g_aot_gc_ref_chunk_count = 0;
static CELL_THREAD_LOCAL int g_aot_depth = 0;
int cell_rt_native_active(void) {
return g_aot_depth > 0;
}
static int ensure_aot_gc_ref_slot(JSContext *ctx, int depth_index) {
if (depth_index < 0)
return 0;
int needed_chunks = (depth_index / AOT_GC_REF_CHUNK_SIZE) + 1;
if (needed_chunks <= g_aot_gc_ref_chunk_count)
return 1;
AOTGCRefChunk **new_chunks =
(AOTGCRefChunk **)realloc(g_aot_gc_ref_chunks,
(size_t)needed_chunks * sizeof(*new_chunks));
if (!new_chunks) {
JS_RaiseOOM(ctx);
return 0;
}
g_aot_gc_ref_chunks = new_chunks;
for (int i = g_aot_gc_ref_chunk_count; i < needed_chunks; i++) {
g_aot_gc_ref_chunks[i] = (AOTGCRefChunk *)calloc(1, sizeof(AOTGCRefChunk));
if (!g_aot_gc_ref_chunks[i]) {
JS_RaiseOOM(ctx);
return 0;
}
}
g_aot_gc_ref_chunk_count = needed_chunks;
return 1;
}
static inline JSGCRef *aot_gc_ref_at(int depth_index) {
int chunk_index = depth_index / AOT_GC_REF_CHUNK_SIZE;
int slot_index = depth_index % AOT_GC_REF_CHUNK_SIZE;
return &g_aot_gc_ref_chunks[chunk_index]->refs[slot_index];
}
JSValue *cell_rt_enter_frame(JSContext *ctx, int64_t nr_slots) {
if (!ensure_aot_gc_ref_slot(ctx, g_aot_depth)) {
return NULL;
}
JSFrameRegister *frame = alloc_frame_register(ctx, (int)nr_slots);
if (!frame) return NULL;
JSGCRef *ref = aot_gc_ref_at(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 = aot_gc_ref_at(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 = aot_gc_ref_at(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) {
if (g_aot_depth <= 0) {
fprintf(stderr, "[BUG] cell_rt_leave_frame underflow\n");
abort();
}
g_aot_depth--;
JS_DeleteGCRef(ctx, aot_gc_ref_at(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 CELL_THREAD_LOCAL 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 CELL_THREAD_LOCAL JSValue g_pending_callee_frame = 0; /* JSFrameRegister ptr */
static CELL_THREAD_LOCAL int g_pending_is_tail = 0;
/* Poll pause state on taken backward jumps (AOT backedges).
MACH can suspend/resume a register VM frame at pc granularity; native AOT
does not currently have an equivalent resume point, so we acknowledge timer
pauses by clearing pause_flag and continuing the current turn. */
int cell_rt_check_backedge(JSContext *ctx) {
int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
if (pf >= 1) {
atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);
}
return 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;
void *prev_dl_handle = g_current_dl_handle;
g_current_dl_handle = f->u.native.dl_handle;
#define RETURN_DISPATCH(v) \
do { \
atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed); \
g_current_dl_handle = prev_dl_handle; \
return (v); \
} while (0)
/* 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 */
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;
g_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)
g_current_dl_handle = cur_fn->u.native.dl_handle;
}
JSValue result = fn(ctx, fp);
/* Re-derive frame after potential GC */
if (g_aot_depth <= 0) {
fprintf(stderr, "[BUG] native dispatch lost frame depth after fn call\n");
abort();
}
JSValue frame_val = aot_gc_ref_at(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;
int pending_is_tail = g_pending_is_tail;
g_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 = (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_RaiseDisrupt(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;
JS_PopGCRef(ctx, &callee_ref);
continue;
}
JSGCRef callee_fn_ref;
JS_PushGCRef(ctx, &callee_fn_ref);
callee_fn_ref.val = callee_fn_val;
JSFunction *callee_fn = JS_VALUE_GET_FUNCTION(callee_fn_ref.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 (pending_is_tail) {
/* Tail call: replace frame instead of mutating in place.
In-place reuse breaks closures that captured the caller frame. */
JSValue saved_caller = frame->caller;
int cc = (callee_argc < callee_fn->length) ? callee_argc : callee_fn->length;
if (cc < 0) cc = callee_argc;
/* 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) {
JS_PopGCRef(ctx, &callee_fn_ref);
JS_PopGCRef(ctx, &callee_ref);
RETURN_DISPATCH(JS_EXCEPTION);
}
callee_fr = (JSFrameRegister *)JS_VALUE_GET_PTR(callee_ref.val);
JSFrameRegister *new_frame = (JSFrameRegister *)((char *)new_fp - offsetof(JSFrameRegister, slots));
callee_fn_val = callee_fn_ref.val;
new_frame->function = callee_fn_val;
new_frame->caller = saved_caller;
new_frame->slots[0] = callee_fr->slots[0];
for (int i = 0; i < cc && i < callee_slots - 1; i++)
new_frame->slots[1 + i] = callee_fr->slots[1 + 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;
int cc = (callee_argc < callee_fn->length) ? callee_argc : callee_fn->length;
if (cc < 0) cc = callee_argc;
JSValue *new_fp = cell_rt_enter_frame(ctx, callee_slots);
if (!new_fp) {
/* Resume caller with exception pending */
frame_val = aot_gc_ref_at(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;
JS_PopGCRef(ctx, &callee_fn_ref);
JS_PopGCRef(ctx, &callee_ref);
continue;
}
callee_fr = (JSFrameRegister *)JS_VALUE_GET_PTR(callee_ref.val);
/* Re-derive caller frame after alloc */
if (g_aot_depth <= 1) {
fprintf(stderr, "[BUG] native dispatch bad depth while linking caller: %d\n", g_aot_depth);
abort();
}
frame_val = aot_gc_ref_at(g_aot_depth - 2)->val;
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
JSFrameRegister *new_frame = (JSFrameRegister *)((char *)new_fp - offsetof(JSFrameRegister, slots));
callee_fn_val = callee_fn_ref.val;
new_frame->function = callee_fn_val;
new_frame->caller = JS_MKPTR(frame);
new_frame->slots[0] = callee_fr->slots[0];
for (int i = 0; i < cc && i < callee_slots - 1; i++)
new_frame->slots[1 + i] = callee_fr->slots[1 + 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 = aot_gc_ref_at(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_Disrupt(ctx);
/* 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;
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 (g_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 (g_aot_depth < base_depth) {
JS_PopGCRef(ctx, &callee_ref);
RETURN_DISPATCH(ret);
}
frame_val = aot_gc_ref_at(g_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)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;
}
}
JS_PopGCRef(ctx, &callee_fn_ref);
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 (g_aot_depth <= base_depth) {
cell_rt_leave_frame(ctx);
RETURN_DISPATCH(JS_EXCEPTION);
}
cell_rt_leave_frame(ctx);
if (g_aot_depth < base_depth) {
RETURN_DISPATCH(JS_EXCEPTION);
}
/* Resume caller — it will check JS_HasException and branch to handler */
frame_val = aot_gc_ref_at(g_aot_depth - 1)->val;
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_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_DISPATCH(result);
}
cell_rt_leave_frame(ctx);
if (g_aot_depth < base_depth) {
RETURN_DISPATCH(result);
}
/* Return to caller frame */
frame_val = aot_gc_ref_at(g_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)caller_fn->u.native.fn_ptr;
continue;
}
#undef RETURN_DISPATCH
}
/* 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_RaiseDisrupt(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_RaiseDisrupt(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 = aot_gc_ref_at(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_RaiseDisrupt(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_RaiseDisrupt(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_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, 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_RaiseDisrupt(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_RaiseDisrupt(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);
}