diff --git a/compile.ce b/compile.ce index 6075f0f2..a7b95dd3 100644 --- a/compile.ce +++ b/compile.ce @@ -55,8 +55,10 @@ if (rc != 0) { return } -// Append wrapper function -var wrapper_cmd = `printf '\nexport function l $` + symbol + `(l %%ctx) {\n@entry\n %%frame =l alloc8 4096\n %%result =l call $cell_main(l %%ctx, l %%frame)\n ret %%result\n}\n' >> ` + tmp + `_fixed.ssa` +// Append wrapper function — called as symbol(ctx) by os.dylib_symbol. +// Delegates to cell_rt_module_entry which heap-allocates a frame +// (so closures survive) and calls cell_main. +var wrapper_cmd = `printf '\nexport function l $` + symbol + `(l %%ctx) {\n@entry\n %%result =l call $cell_rt_module_entry(l %%ctx)\n ret %%result\n}\n' >> ` + tmp + `_fixed.ssa` rc = os.system(wrapper_cmd) if (rc != 0) { print('wrapper append failed') diff --git a/internal/bootstrap.mach b/internal/bootstrap.mach index 7276d31a..f2696e69 100644 Binary files a/internal/bootstrap.mach and b/internal/bootstrap.mach differ diff --git a/parse.mach b/parse.mach index 22336711..1da26295 100644 Binary files a/parse.mach and b/parse.mach differ diff --git a/qbe_emit.cm b/qbe_emit.cm index bf94d71c..8ff6d0f9 100644 --- a/qbe_emit.cm +++ b/qbe_emit.cm @@ -89,6 +89,9 @@ var qbe_emit = function(ir, qbe) { var pn = null var sl = null var fop_id = 0 + var nr_elems = 0 + var ei = 0 + var elem_slot = 0 // Function signature: (ctx, frame_ptr) → JSValue emit(`export function l $${name}(l %ctx, l %fp) {`) @@ -105,6 +108,11 @@ var qbe_emit = function(ir, qbe) { i = i + 1 } + // Write-back: store SSA var to frame slot so closures see updates + var wb = function(slot) { + emit(` storel ${s(slot)}, %p${text(slot)}`) + } + // Walk instructions i = 0 while (i < length(instrs)) { @@ -126,18 +134,22 @@ var qbe_emit = function(ir, qbe) { if (op == "int") { emit(` ${s(a1)} =l copy ${text(a2 * 2)}`) + wb(a1) continue } if (op == "null") { emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`) + wb(a1) continue } if (op == "true") { emit(` ${s(a1)} =l copy ${text(qbe.js_true)}`) + wb(a1) continue } if (op == "false") { emit(` ${s(a1)} =l copy ${text(qbe.js_false)}`) + wb(a1) continue } if (op == "access") { @@ -177,6 +189,7 @@ var qbe_emit = function(ir, qbe) { } else { emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`) } + wb(a1) continue } @@ -184,6 +197,7 @@ var qbe_emit = function(ir, qbe) { if (op == "move") { emit(` ${s(a1)} =l copy ${s(a2)}`) + wb(a1) continue } @@ -193,30 +207,35 @@ var qbe_emit = function(ir, qbe) { p = fresh() emit(qbe.add_int(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "sub_int") { p = fresh() emit(qbe.sub_int(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "mul_int") { p = fresh() emit(qbe.mul_int(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "div_int") { p = fresh() emit(qbe.div_int(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "mod_int") { p = fresh() emit(qbe.mod_int(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } @@ -226,30 +245,35 @@ var qbe_emit = function(ir, qbe) { p = fresh() emit(qbe.add_float(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "sub_float") { p = fresh() emit(qbe.sub_float(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "mul_float") { p = fresh() emit(qbe.mul_float(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "div_float") { p = fresh() emit(qbe.div_float(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "mod_float") { p = fresh() emit(qbe.mod_float(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } @@ -259,6 +283,7 @@ var qbe_emit = function(ir, qbe) { p = fresh() emit(qbe.concat(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } @@ -269,6 +294,7 @@ var qbe_emit = function(ir, qbe) { emit(qbe.is_int(p, s(a2))) emit(qbe.new_bool(p + ".r", "%" + p)) emit(` ${s(a1)} =l copy %${p}.r`) + wb(a1) continue } if (op == "is_text") { @@ -276,6 +302,7 @@ var qbe_emit = function(ir, qbe) { emit(qbe.is_imm_text(p, s(a2))) emit(qbe.new_bool(p + ".r", "%" + p)) emit(` ${s(a1)} =l copy %${p}.r`) + wb(a1) continue } if (op == "is_num") { @@ -283,6 +310,7 @@ var qbe_emit = function(ir, qbe) { emit(qbe.is_number(p, s(a2))) emit(qbe.new_bool(p + ".r", "%" + p)) emit(` ${s(a1)} =l copy %${p}.r`) + wb(a1) continue } if (op == "is_bool") { @@ -290,6 +318,7 @@ var qbe_emit = function(ir, qbe) { emit(qbe.is_bool(p, s(a2))) emit(qbe.new_bool(p + ".r", "%" + p)) emit(` ${s(a1)} =l copy %${p}.r`) + wb(a1) continue } if (op == "is_null") { @@ -297,12 +326,14 @@ var qbe_emit = function(ir, qbe) { emit(qbe.is_null(p, s(a2))) emit(qbe.new_bool(p + ".r", "%" + p)) emit(` ${s(a1)} =l copy %${p}.r`) + wb(a1) continue } if (op == "is_identical") { p = fresh() emit(qbe.is_identical(p, s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } @@ -312,36 +343,42 @@ var qbe_emit = function(ir, qbe) { p = fresh() emit(qbe.eq_int(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "ne_int") { p = fresh() emit(qbe.ne_int(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "lt_int") { p = fresh() emit(qbe.lt_int(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "gt_int") { p = fresh() emit(qbe.gt_int(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "le_int") { p = fresh() emit(qbe.le_int(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "ge_int") { p = fresh() emit(qbe.ge_int(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } @@ -351,12 +388,14 @@ var qbe_emit = function(ir, qbe) { p = fresh() emit(qbe.eq_float(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "ne_float") { p = fresh() emit(qbe.ne_float(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "lt_float" || op == "gt_float" || op == "le_float" || op == "ge_float") { @@ -368,39 +407,46 @@ var qbe_emit = function(ir, qbe) { else if (op == "ge_float") fop_id = 5 emit(qbe.cmp_float != null ? cmp_float(p, "%ctx", s(a2), s(a3), fop_id) : ` %${p} =l call $qbe_float_cmp(l %ctx, w ${text(fop_id)}, l ${s(a2)}, l ${s(a3)})`) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "eq_text") { p = fresh() emit(qbe.eq_text(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "ne_text") { p = fresh() emit(qbe.ne_text(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "lt_text" || op == "gt_text" || op == "le_text" || op == "ge_text") { p = fresh() emit(` ${s(a1)} =l call $cell_rt_${op}(l %ctx, l ${s(a2)}, l ${s(a3)})`) + wb(a1) continue } if (op == "eq_bool") { p = fresh() emit(qbe.eq_bool(p, s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "ne_bool") { p = fresh() emit(qbe.ne_bool(p, s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "eq_tol" || op == "ne_tol") { emit(` ${s(a1)} =l call $cell_rt_${op}(l %ctx, l ${s(a2)}, l ${s(a3)})`) + wb(a1) continue } @@ -410,14 +456,17 @@ var qbe_emit = function(ir, qbe) { p = fresh() emit(qbe.lnot(p, "%ctx", s(a2))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "and") { emit(` ${s(a1)} =l and ${s(a2)}, ${s(a3)}`) + wb(a1) continue } if (op == "or") { emit(` ${s(a1)} =l or ${s(a2)}, ${s(a3)}`) + wb(a1) continue } @@ -427,42 +476,49 @@ var qbe_emit = function(ir, qbe) { p = fresh() emit(qbe.bnot(p, "%ctx", s(a2))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "bitand") { p = fresh() emit(qbe.band(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "bitor") { p = fresh() emit(qbe.bor(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "bitxor") { p = fresh() emit(qbe.bxor(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "shl") { p = fresh() emit(qbe.shl(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "shr") { p = fresh() emit(qbe.shr(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } if (op == "ushr") { p = fresh() emit(qbe.ushr(p, "%ctx", s(a2), s(a3))) emit(` ${s(a1)} =l copy %${p}`) + wb(a1) continue } @@ -476,32 +532,38 @@ var qbe_emit = function(ir, qbe) { } else { emit(` ${s(a1)} =l call $cell_rt_load_dynamic(l %ctx, l ${s(a2)}, l ${s(a3)})`) } + wb(a1) continue } if (op == "load_index") { emit(` ${s(a1)} =l call $cell_rt_load_index(l %ctx, l ${s(a2)}, l ${s(a3)})`) + wb(a1) continue } if (op == "load_dynamic") { emit(` ${s(a1)} =l call $cell_rt_load_dynamic(l %ctx, l ${s(a2)}, l ${s(a3)})`) + wb(a1) continue } if (op == "store_field") { + // IR: ["store_field", obj, val, prop] → C: (ctx, val, obj, name) pn = prop_name(a3) if (pn != null) { sl = intern_str(pn) - emit(` call $cell_rt_store_field(l %ctx, l ${s(a1)}, l ${s(a2)}, l ${sl})`) + emit(` call $cell_rt_store_field(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${sl})`) } else { - emit(` call $cell_rt_store_dynamic(l %ctx, l ${s(a1)}, l ${s(a2)}, l ${s(a3)})`) + emit(` call $cell_rt_store_dynamic(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${s(a3)})`) } continue } if (op == "store_index") { - emit(` call $cell_rt_store_index(l %ctx, l ${s(a1)}, l ${s(a2)}, l ${s(a3)})`) + // IR: ["store_index", obj, val, idx] → C: (ctx, val, obj, idx) + emit(` call $cell_rt_store_index(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${s(a3)})`) continue } if (op == "store_dynamic") { - emit(` call $cell_rt_store_dynamic(l %ctx, l ${s(a1)}, l ${s(a2)}, l ${s(a3)})`) + // IR: ["store_dynamic", obj, val, key] → C: (ctx, val, obj, key) + emit(` call $cell_rt_store_dynamic(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${s(a3)})`) continue } @@ -509,6 +571,7 @@ var qbe_emit = function(ir, qbe) { if (op == "get") { emit(` ${s(a1)} =l call $cell_rt_get_closure(l %ctx, l %fp, l ${text(a2)}, l ${text(a3)})`) + wb(a1) continue } if (op == "put") { @@ -569,6 +632,7 @@ var qbe_emit = function(ir, qbe) { if (op == "frame") { emit(` ${s(a1)} =l call $cell_rt_frame(l %ctx, l ${s(a2)}, l ${text(a3)})`) + wb(a1) continue } if (op == "setarg") { @@ -577,10 +641,12 @@ var qbe_emit = function(ir, qbe) { } if (op == "invoke") { emit(` ${s(a2)} =l call $cell_rt_invoke(l %ctx, l ${s(a1)})`) + wb(a2) continue } if (op == "goframe") { emit(` ${s(a1)} =l call $cell_rt_goframe(l %ctx, l ${s(a2)}, l ${text(a3)})`) + wb(a1) continue } if (op == "goinvoke") { @@ -591,7 +657,28 @@ var qbe_emit = function(ir, qbe) { // --- Function object creation --- if (op == "function") { - emit(` ${s(a1)} =l call $cell_rt_make_function(l %ctx, l ${text(a2)})`) + emit(` ${s(a1)} =l call $cell_rt_make_function(l %ctx, l ${text(a2)}, l %fp)`) + wb(a1) + continue + } + + // --- Record/Array creation --- + + if (op == "record") { + emit(` ${s(a1)} =l call $JS_NewObject(l %ctx)`) + wb(a1) + continue + } + if (op == "array") { + nr_elems = a2 != null ? a2 : 0 + emit(` ${s(a1)} =l call $JS_NewArray(l %ctx)`) + ei = 0 + while (ei < nr_elems) { + elem_slot = instr[3 + ei] + emit(` call $JS_SetPropertyUint32(l %ctx, l ${s(a1)}, l ${text(ei)}, l ${s(elem_slot)})`) + ei = ei + 1 + } + wb(a1) continue } @@ -603,6 +690,7 @@ var qbe_emit = function(ir, qbe) { } if (op == "pop") { emit(` ${s(a1)} =l call $cell_rt_pop(l %ctx, l ${s(a2)})`) + wb(a1) continue } @@ -619,10 +707,12 @@ var qbe_emit = function(ir, qbe) { } if (op == "delete") { emit(` ${s(a1)} =l call $cell_rt_delete(l %ctx, l ${s(a2)}, l ${s(a3)})`) + wb(a1) continue } if (op == "typeof") { emit(` ${s(a1)} =l call $cell_rt_typeof(l %ctx, l ${s(a2)})`) + wb(a1) continue } diff --git a/qbe_emit.mach b/qbe_emit.mach index a3bccfc2..7a41d105 100644 Binary files a/qbe_emit.mach and b/qbe_emit.mach differ diff --git a/qbe_rt.c b/qbe_rt.c index e6f50529..1f3a3bda 100644 --- a/qbe_rt.c +++ b/qbe_rt.c @@ -1,16 +1,16 @@ /* - * qbe_rt.c - Runtime support for QBE-compiled ƿit modules + * qbe_rt.c - Non-inline wrappers for QBE-compiled ƿit modules * * Provides non-inline versions of static-inline quickjs functions - * (which QBE-generated code calls as external symbols) and stub - * implementations of cell_rt_* helper functions. + * (which QBE-generated code calls as external symbols). + * + * All cell_rt_* runtime functions are implemented in source/qbe_helpers.c + * (compiled into the cell binary) and resolved via -undefined dynamic_lookup. */ #include #include #include -#include -#include typedef uint64_t JSValue; typedef struct JSContext JSContext; @@ -80,112 +80,3 @@ extern JSValue JS_NewStringLen(JSContext *ctx, const char *str, size_t len); JSValue JS_NewString(JSContext *ctx, const char *str) { return JS_NewStringLen(ctx, str, strlen(str)); } - -/* ============================================================ - cell_rt_* stubs — error/fallback paths for QBE-compiled code - These are called from type-mismatch branches that should not - be reached in pure numeric code. - ============================================================ */ - -extern JSValue JS_ThrowTypeError(JSContext *ctx, const char *fmt, ...); - -void cell_rt_disrupt(JSContext *ctx) { - JS_ThrowTypeError(ctx, "type error in native code"); -} - -JSValue cell_rt_lt_text(JSContext *ctx, JSValue a, JSValue b) { - return JS_VAL_NULL; -} -JSValue cell_rt_gt_text(JSContext *ctx, JSValue a, JSValue b) { - return JS_VAL_NULL; -} -JSValue cell_rt_le_text(JSContext *ctx, JSValue a, JSValue b) { - return JS_VAL_NULL; -} -JSValue cell_rt_ge_text(JSContext *ctx, JSValue a, JSValue b) { - return JS_VAL_NULL; -} -JSValue cell_rt_eq_tol(JSContext *ctx, JSValue a, JSValue b) { - return JS_VAL_NULL; -} -JSValue cell_rt_ne_tol(JSContext *ctx, JSValue a, JSValue b) { - return JS_VAL_NULL; -} - -JSValue cell_rt_get_intrinsic(JSContext *ctx, const char *name) { - return JS_VAL_NULL; -} -JSValue cell_rt_load_field(JSContext *ctx, JSValue obj, const char *name) { - return JS_VAL_NULL; -} -JSValue cell_rt_load_dynamic(JSContext *ctx, JSValue obj, JSValue key) { - return JS_VAL_NULL; -} -JSValue cell_rt_load_index(JSContext *ctx, JSValue arr, JSValue idx) { - return JS_VAL_NULL; -} -void cell_rt_store_field(JSContext *ctx, JSValue val, JSValue obj, - const char *name) {} -void cell_rt_store_dynamic(JSContext *ctx, JSValue val, JSValue obj, - JSValue key) {} -void cell_rt_store_index(JSContext *ctx, JSValue val, JSValue arr, - JSValue idx) {} -JSValue cell_rt_get_closure(JSContext *ctx, void *fp, int64_t depth, - int64_t index) { - return JS_VAL_NULL; -} -void cell_rt_put_closure(JSContext *ctx, void *fp, JSValue val, int64_t depth, - int64_t index) {} -JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int64_t nargs) { - return JS_VAL_NULL; -} -void cell_rt_setarg(JSValue frame, int64_t idx, JSValue val) {} -JSValue cell_rt_invoke(JSContext *ctx, JSValue frame) { return JS_VAL_NULL; } -JSValue cell_rt_goframe(JSContext *ctx, JSValue fn, int64_t nargs) { - return JS_VAL_NULL; -} -void cell_rt_goinvoke(JSContext *ctx, JSValue frame) {} -/* - * cell_rt_make_function — create a callable JS function wrapping a - * QBE-compiled cell_fn_N. Uses magic to store fn_idx, and dlsym to - * look up the compiled symbol at call time. - */ - -typedef JSValue (*cell_compiled_fn)(JSContext *ctx, void *fp); - -/* JS_CFUNC_generic_magic = 1 in the JSCFunctionEnum */ -#define QBE_JS_CFUNC_GENERIC_MAGIC 1 - -extern JSValue JS_NewCFunction2(JSContext *ctx, void *func, - const char *name, int length, - int cproto, int magic); - -static JSValue cell_fn_trampoline(JSContext *ctx, JSValue this_val, - int argc, JSValue *argv, int magic) { - char name[64]; - snprintf(name, sizeof(name), "cell_fn_%d", magic); - - cell_compiled_fn fn = (cell_compiled_fn)dlsym(RTLD_DEFAULT, name); - if (!fn) - return JS_ThrowTypeError(ctx, "native function %s not found", name); - - /* Allocate frame: slot 0 = this, slots 1..argc = args, rest zeroed */ - JSValue frame[512]; - memset(frame, 0, sizeof(frame)); - frame[0] = this_val; - for (int i = 0; i < argc && i < 511; i++) - frame[1 + i] = argv[i]; - - return fn(ctx, frame); -} - -JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx) { - return JS_NewCFunction2(ctx, (void *)cell_fn_trampoline, "native_fn", - 0, QBE_JS_CFUNC_GENERIC_MAGIC, (int)fn_idx); -} -void cell_rt_push(JSContext *ctx, JSValue arr, JSValue val) {} -JSValue cell_rt_pop(JSContext *ctx, JSValue arr) { return JS_VAL_NULL; } -JSValue cell_rt_delete(JSContext *ctx, JSValue obj, JSValue key) { - return JS_VAL_NULL; -} -JSValue cell_rt_typeof(JSContext *ctx, JSValue val) { return JS_VAL_NULL; } diff --git a/source/qbe_helpers.c b/source/qbe_helpers.c index 210dc677..d5aab35d 100644 --- a/source/qbe_helpers.c +++ b/source/qbe_helpers.c @@ -192,3 +192,261 @@ JSValue qbe_shift_shr(JSContext *ctx, JSValue a, JSValue b) { JS_ToInt32(ctx, &ib, b); return JS_NewInt32(ctx, (uint32_t)ia >> (ib & 31)); } + +/* ============================================================ + cell_rt_* — Runtime support for QBE-compiled code + ============================================================ */ + +#include +#include + +/* --- Property access --- */ + +JSValue cell_rt_load_field(JSContext *ctx, JSValue obj, const char *name) { + 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_GetPropertyUint32(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_SetPropertyUint32(ctx, obj, (uint32_t)JS_VALUE_GET_INT(key), val); + else + JS_SetProperty(ctx, obj, key, val); +} + +JSValue cell_rt_load_index(JSContext *ctx, JSValue arr, JSValue idx) { + if (JS_IsInt(idx)) + return JS_GetPropertyUint32(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_SetPropertyUint32(ctx, arr, (uint32_t)JS_VALUE_GET_INT(idx), val); + else + JS_SetProperty(ctx, arr, idx, val); +} + +/* --- Intrinsic/global lookup --- */ + +JSValue cell_rt_get_intrinsic(JSContext *ctx, const char *name) { + return JS_GetPropertyStr(ctx, ctx->global_obj, name); +} + +/* --- Closure access --- + Slot 511 in each frame stores a pointer to the enclosing frame. + Walking depth levels up the chain gives the target frame. */ + +#define QBE_FRAME_OUTER_SLOT 511 + +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++) { + void *outer = (void *)(uintptr_t)frame[QBE_FRAME_OUTER_SLOT]; + if (!outer) return JS_NULL; + frame = (JSValue *)outer; + } + 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++) { + void *outer = (void *)(uintptr_t)frame[QBE_FRAME_OUTER_SLOT]; + if (!outer) return; + frame = (JSValue *)outer; + } + frame[slot] = val; +} + +/* --- Function creation and calling --- */ + +typedef JSValue (*cell_compiled_fn)(JSContext *ctx, void *fp); + +/* Table mapping fn_idx → outer_fp at creation time. + Valid for single-threaded, non-recursive closure patterns. */ +#define MAX_QBE_FUNCTIONS 256 +static void *g_outer_fp[MAX_QBE_FUNCTIONS]; + +static JSValue cell_fn_trampoline(JSContext *ctx, JSValue this_val, + int argc, JSValue *argv, int magic) { + char name[64]; + snprintf(name, sizeof(name), "cell_fn_%d", magic); + + cell_compiled_fn fn = (cell_compiled_fn)dlsym(RTLD_DEFAULT, name); + if (!fn) + return JS_ThrowTypeError(ctx, "native function %s not found", name); + + /* Allocate frame: slot 0 = this, slots 1..argc = args */ + JSValue frame[512]; + memset(frame, 0, sizeof(frame)); + frame[0] = this_val; + for (int i = 0; i < argc && i < 510; i++) + frame[1 + i] = argv[i]; + + /* Link to outer frame for closure access */ + if (magic >= 0 && magic < MAX_QBE_FUNCTIONS) + frame[QBE_FRAME_OUTER_SLOT] = (JSValue)(uintptr_t)g_outer_fp[magic]; + + return fn(ctx, frame); +} + +JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx, void *outer_fp) { + if (fn_idx >= 0 && fn_idx < MAX_QBE_FUNCTIONS) + g_outer_fp[fn_idx] = outer_fp; + return JS_NewCFunction2(ctx, (JSCFunction *)cell_fn_trampoline, "native_fn", + 255, JS_CFUNC_generic_magic, (int)fn_idx); +} + +/* --- Frame-based function calling --- */ + +JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int64_t nargs) { + if (!JS_IsFunction(fn)) { + JS_ThrowTypeError(ctx, "not a function"); + return JS_EXCEPTION; + } + int nr_slots = (int)nargs + 2; + JSFrameRegister *new_frame = alloc_frame_register(ctx, nr_slots); + if (!new_frame) return JS_EXCEPTION; + new_frame->function = fn; + return JS_MKPTR(new_frame); +} + +void cell_rt_setarg(JSValue frame_val, int64_t idx, JSValue val) { + JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); + fr->slots[idx] = val; +} + +JSValue cell_rt_invoke(JSContext *ctx, JSValue frame_val) { + JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); + int nr_slots = (int)objhdr_cap56(fr->hdr); + int c_argc = (nr_slots >= 2) ? nr_slots - 2 : 0; + + /* Push args onto value stack (GC-safe) */ + int vs_base = ctx->value_stack_top; + for (int i = 0; i < c_argc; i++) + ctx->value_stack[vs_base + i] = fr->slots[i + 1]; + ctx->value_stack_top = vs_base + c_argc; + + JSValue result = JS_Call(ctx, fr->function, fr->slots[0], + c_argc, &ctx->value_stack[vs_base]); + ctx->value_stack_top = vs_base; + if (JS_IsException(result)) + return JS_EXCEPTION; + return result; +} + +JSValue cell_rt_goframe(JSContext *ctx, JSValue fn, int64_t nargs) { + return cell_rt_frame(ctx, fn, nargs); +} + +void cell_rt_goinvoke(JSContext *ctx, JSValue frame_val) { + cell_rt_invoke(ctx, frame_val); +} + +/* --- Array push/pop --- */ + +void cell_rt_push(JSContext *ctx, JSValue arr, JSValue val) { + JS_ArrayPush(ctx, &arr, val); +} + +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); + 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); +} + +JSValue cell_rt_eq_tol(JSContext *ctx, JSValue a, JSValue b) { + return JS_NewBool(ctx, a == b); +} + +JSValue cell_rt_ne_tol(JSContext *ctx, JSValue a, JSValue b) { + return JS_NewBool(ctx, a != b); +} + +/* --- Disruption --- */ + +void cell_rt_disrupt(JSContext *ctx) { + JS_ThrowTypeError(ctx, "type error in native code"); +} + +/* --- Module entry point --- + Called as symbol(ctx) by os.dylib_symbol. Looks up cell_main + in the loaded dylib, builds a heap-allocated frame (so closures + can reference it after the module returns), and runs the module body. */ + +JSValue cell_rt_module_entry(JSContext *ctx) { + cell_compiled_fn fn = (cell_compiled_fn)dlsym(RTLD_DEFAULT, "cell_main"); + if (!fn) + return JS_ThrowTypeError(ctx, "cell_main not found in loaded dylib"); + + /* Heap-allocate so closures created in cell_main can reference + this frame after the module entry returns. */ + JSValue *frame = calloc(512, sizeof(JSValue)); + if (!frame) + return JS_ThrowTypeError(ctx, "frame allocation failed"); + + return fn(ctx, frame); +}