864 lines
28 KiB
C
864 lines
28 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>
|
|
|
|
/* 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 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_RaiseDisrupt(ctx, "'%s' is not defined", name);
|
|
return JS_EXCEPTION;
|
|
}
|
|
|
|
/* --- Closure access ---
|
|
Slot 511 in each frame stores the magic ID (registry index) of the
|
|
function that owns this frame. cell_rt_get/put_closure re-derive
|
|
the enclosing frame from the function's GC ref at call time, so
|
|
pointers stay valid even if GC moves frames. */
|
|
|
|
#define QBE_FRAME_OUTER_SLOT 511
|
|
|
|
static JSValue *derive_outer_fp(int magic);
|
|
|
|
JSValue cell_rt_get_closure(JSContext *ctx, void *fp, int64_t depth,
|
|
int64_t slot) {
|
|
JSValue *frame = (JSValue *)fp;
|
|
for (int64_t d = 0; d < depth; d++) {
|
|
/* fp[511] stores the magic ID (registry index) of the function
|
|
that owns this frame. derive_outer_fp re-derives the enclosing
|
|
frame from the function's GC ref, so it's always current even
|
|
if GC moved the frame. */
|
|
int magic = (int)(int64_t)frame[QBE_FRAME_OUTER_SLOT];
|
|
frame = derive_outer_fp(magic);
|
|
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) {
|
|
JSValue *frame = (JSValue *)fp;
|
|
for (int64_t d = 0; d < depth; d++) {
|
|
int magic = (int)(int64_t)frame[QBE_FRAME_OUTER_SLOT];
|
|
frame = derive_outer_fp(magic);
|
|
if (!frame) return;
|
|
}
|
|
frame[slot] = val;
|
|
}
|
|
|
|
/* --- GC-managed AOT frame stack ---
|
|
Each AOT function call pushes a GC ref so the GC can find and
|
|
update frame pointers when it moves objects. cell_rt_refresh_fp
|
|
re-derives the slot pointer after any GC-triggering call. */
|
|
|
|
#define MAX_AOT_DEPTH 65536
|
|
static JSGCRef g_aot_gc_refs[MAX_AOT_DEPTH];
|
|
static int g_aot_depth = 0;
|
|
|
|
/* Check remaining C stack space to prevent segfaults from deep recursion */
|
|
static int stack_space_ok(void) {
|
|
#ifdef __APPLE__
|
|
char local;
|
|
void *stack_addr = pthread_get_stackaddr_np(pthread_self());
|
|
size_t stack_size = pthread_get_stacksize_np(pthread_self());
|
|
/* stack_addr is the TOP of the stack (highest address); stack grows down */
|
|
uintptr_t stack_bottom = (uintptr_t)stack_addr - stack_size;
|
|
uintptr_t current = (uintptr_t)&local;
|
|
/* Keep 128KB of reserve for unwinding and error handling */
|
|
return (current - stack_bottom) > (128 * 1024);
|
|
#else
|
|
return g_aot_depth < MAX_AOT_DEPTH;
|
|
#endif
|
|
}
|
|
|
|
JSValue *cell_rt_enter_frame(JSContext *ctx, int64_t nr_slots) {
|
|
if (g_aot_depth >= MAX_AOT_DEPTH || !stack_space_ok()) {
|
|
JS_RaiseDisrupt(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.
|
|
Returns the refreshed fp, or NULL if there is a pending exception.
|
|
This avoids QBE register-allocation issues from two consecutive calls. */
|
|
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);
|
|
|
|
/* Per-module function registry.
|
|
Each native .cm module gets its own dylib. When a module creates closures
|
|
via cell_rt_make_function, we record the dylib handle so the trampoline
|
|
can look up the correct cell_fn_N in the right dylib. */
|
|
#define MAX_NATIVE_FN 32768
|
|
|
|
static struct {
|
|
void *dl_handle;
|
|
int fn_idx;
|
|
JSGCRef frame_ref; /* independent GC ref for enclosing frame */
|
|
int has_frame_ref;
|
|
} g_native_fn_registry[MAX_NATIVE_FN];
|
|
|
|
static int g_native_fn_count = 0;
|
|
|
|
/* Set before executing a native module's cell_main */
|
|
static void *g_current_dl_handle = NULL;
|
|
|
|
/* Derive the outer frame's slots pointer from the closure's own GC ref.
|
|
Each closure keeps an independent GC ref so the enclosing frame
|
|
survives even after cell_rt_leave_frame pops the stack ref. */
|
|
static JSValue *derive_outer_fp(int magic) {
|
|
if (!g_native_fn_registry[magic].has_frame_ref) return NULL;
|
|
JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(
|
|
g_native_fn_registry[magic].frame_ref.val);
|
|
return (JSValue *)frame->slots;
|
|
}
|
|
|
|
static void reclaim_native_fns(JSContext *ctx, int saved_count) {
|
|
/* Free GC refs for temporary closures created during a call */
|
|
for (int i = saved_count; i < g_native_fn_count; i++) {
|
|
if (g_native_fn_registry[i].has_frame_ref) {
|
|
JS_DeleteGCRef(ctx, &g_native_fn_registry[i].frame_ref);
|
|
g_native_fn_registry[i].has_frame_ref = 0;
|
|
}
|
|
}
|
|
g_native_fn_count = saved_count;
|
|
}
|
|
|
|
static JSValue cell_fn_trampoline(JSContext *ctx, JSValue this_val,
|
|
int argc, JSValue *argv, int magic) {
|
|
if (magic < 0 || magic >= g_native_fn_count)
|
|
return JS_RaiseDisrupt(ctx, "invalid native function id %d", magic);
|
|
|
|
void *handle = g_native_fn_registry[magic].dl_handle;
|
|
int fn_idx = g_native_fn_registry[magic].fn_idx;
|
|
|
|
char name[64];
|
|
snprintf(name, sizeof(name), "cell_fn_%d", fn_idx);
|
|
|
|
cell_compiled_fn fn = (cell_compiled_fn)dlsym(handle, name);
|
|
if (!fn)
|
|
return JS_RaiseDisrupt(ctx, "native function %s not found in dylib", name);
|
|
|
|
/* Allocate GC-managed frame: slot 0 = this, slots 1..argc = args */
|
|
JSValue *fp = cell_rt_enter_frame(ctx, 512);
|
|
if (!fp) return JS_EXCEPTION;
|
|
fp[0] = this_val;
|
|
for (int i = 0; i < argc && i < 510; i++)
|
|
fp[1 + i] = argv[i];
|
|
|
|
/* Store the magic ID (registry index) so cell_rt_get/put_closure
|
|
can re-derive the enclosing frame from the GC ref at call time,
|
|
surviving GC moves */
|
|
fp[QBE_FRAME_OUTER_SLOT] = (JSValue)(int64_t)magic;
|
|
|
|
/* Set g_current_dl_handle so any closures created during this call
|
|
(e.g. inner functions returned by factory functions) are registered
|
|
against the correct dylib */
|
|
void *prev_handle = g_current_dl_handle;
|
|
g_current_dl_handle = handle;
|
|
|
|
/* At top-level (depth 1 = this is the outermost native call),
|
|
save the fn count so we can reclaim temporary closures after */
|
|
int saved_fn_count = (g_aot_depth == 1) ? g_native_fn_count : -1;
|
|
|
|
JSValue result = fn(ctx, fp);
|
|
cell_rt_leave_frame(ctx);
|
|
g_current_dl_handle = prev_handle;
|
|
|
|
/* Reclaim temporary closures created during this top-level call */
|
|
if (saved_fn_count >= 0)
|
|
reclaim_native_fns(ctx, saved_fn_count);
|
|
|
|
if (result == JS_EXCEPTION) {
|
|
/* Ensure there is a pending exception. QBE @_exc_ret returns 15
|
|
but may not have set one (e.g. if cell_rt_enter_frame failed). */
|
|
if (!JS_HasException(ctx))
|
|
ctx->current_exception = JS_NULL;
|
|
return JS_EXCEPTION;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx, void *outer_fp,
|
|
int64_t nr_args) {
|
|
(void)outer_fp;
|
|
if (g_native_fn_count >= MAX_NATIVE_FN)
|
|
return JS_RaiseDisrupt(ctx, "too many native functions (max %d)", MAX_NATIVE_FN);
|
|
|
|
int global_id = g_native_fn_count++;
|
|
g_native_fn_registry[global_id].dl_handle = g_current_dl_handle;
|
|
g_native_fn_registry[global_id].fn_idx = (int)fn_idx;
|
|
|
|
/* Create independent GC ref so the enclosing frame survives
|
|
even after cell_rt_leave_frame pops the stack ref */
|
|
if (g_aot_depth > 0) {
|
|
JSGCRef *ref = &g_native_fn_registry[global_id].frame_ref;
|
|
JS_AddGCRef(ctx, ref);
|
|
ref->val = g_aot_gc_refs[g_aot_depth - 1].val;
|
|
g_native_fn_registry[global_id].has_frame_ref = 1;
|
|
} else {
|
|
g_native_fn_registry[global_id].has_frame_ref = 0;
|
|
}
|
|
|
|
return JS_NewCFunction2(ctx, (JSCFunction *)cell_fn_trampoline, "native_fn",
|
|
(int)nr_args, JS_CFUNC_generic_magic, global_id);
|
|
}
|
|
|
|
/* --- Frame-based function calling --- */
|
|
|
|
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;
|
|
}
|
|
|
|
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) {
|
|
/* Match MACH_INVOKE: C functions go directly to js_call_c_function,
|
|
bypassing JS_Call's arity check. Extra args are silently available. */
|
|
result = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]);
|
|
} else {
|
|
/* Register/bytecode functions — use JS_CallInternal (no arity gate) */
|
|
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;
|
|
/* Clear any stale exception left by functions that returned a valid
|
|
value despite internal error (e.g., sign("text") returns null
|
|
but JS_ToFloat64 leaves an exception flag) */
|
|
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 --- */
|
|
|
|
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. */
|
|
|
|
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");
|
|
|
|
/* Set current handle so cell_rt_make_function registers closures
|
|
against this module's dylib */
|
|
void *prev_handle = g_current_dl_handle;
|
|
g_current_dl_handle = dl_handle;
|
|
|
|
/* Make env available for cell_rt_get_intrinsic lookups */
|
|
cell_rt_set_native_env(ctx, env);
|
|
|
|
/* GC-managed frame for module execution */
|
|
JSValue *fp = cell_rt_enter_frame(ctx, 512);
|
|
if (!fp) {
|
|
g_current_dl_handle = prev_handle;
|
|
return JS_RaiseDisrupt(ctx, "frame allocation failed");
|
|
}
|
|
|
|
/* Clear any stale exception left by a previous interpreted run */
|
|
if (JS_HasException(ctx))
|
|
JS_GetException(ctx);
|
|
|
|
JSValue result = fn(ctx, fp);
|
|
cell_rt_leave_frame(ctx); /* safe — closures have independent GC refs */
|
|
g_current_dl_handle = prev_handle;
|
|
if (result == JS_EXCEPTION)
|
|
return JS_EXCEPTION;
|
|
return result;
|
|
}
|
|
|
|
/* 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;
|
|
if (sym_name)
|
|
fn = (cell_compiled_fn)dlsym(dl_handle, sym_name);
|
|
if (!fn)
|
|
fn = (cell_compiled_fn)dlsym(dl_handle, "cell_main");
|
|
if (!fn)
|
|
return JS_RaiseDisrupt(ctx, "symbol not found in native module dylib");
|
|
|
|
void *prev_handle = g_current_dl_handle;
|
|
g_current_dl_handle = dl_handle;
|
|
|
|
/* Make env available for cell_rt_get_intrinsic lookups */
|
|
cell_rt_set_native_env(ctx, env);
|
|
|
|
JSValue *fp = cell_rt_enter_frame(ctx, 512);
|
|
if (!fp) {
|
|
g_current_dl_handle = prev_handle;
|
|
return JS_RaiseDisrupt(ctx, "frame allocation failed");
|
|
}
|
|
|
|
JSValue result = fn(ctx, fp);
|
|
cell_rt_leave_frame(ctx); /* safe — closures have independent GC refs */
|
|
g_current_dl_handle = prev_handle;
|
|
if (result == JS_EXCEPTION)
|
|
return JS_EXCEPTION;
|
|
return result;
|
|
}
|
|
|
|
/* 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);
|
|
}
|