From ad419797b4c9fbf5ebfccf14cbeecc2f26baf941 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 17 Feb 2026 17:40:44 -0600 Subject: [PATCH] native function type --- build.cm | 67 +---- meson.build | 3 +- qbe_emit.cm | 144 ++++++++-- source/mach.c | 34 ++- source/qbe_helpers.c | 584 ++++++++++++++++++++++++++------------ source/quickjs-internal.h | 9 + source/runtime.c | 6 + vm_suite.ce | 21 ++ 8 files changed, 603 insertions(+), 265 deletions(-) diff --git a/build.cm b/build.cm index 41ae6af0..fdff7ce9 100644 --- a/build.cm +++ b/build.cm @@ -509,62 +509,21 @@ Build.build_static = function(packages, target, output, buildtype) { // il_parts: {data: text, functions: [text, ...]} // cc: C compiler path // tmp_prefix: prefix for temp files (e.g. /tmp/cell_native_) -function compile_native_batched(il_parts, cc, tmp_prefix) { - var nfuncs = length(il_parts.functions) - var nbatch = 8 - var o_paths = [] - var s_paths = [] - var asm_cmds = [] - var batch_fns = null - var batch_il = null - var asm_text = null - var s_path = null - var o_path = null - var end = 0 - var bi = 0 - var fi = 0 - var ai = 0 - var rc = null - var parallel_cmd = null +function compile_native_single(il_parts, cc, tmp_prefix) { var helpers_il = (il_parts.helpers && length(il_parts.helpers) > 0) ? text(il_parts.helpers, "\n") : "" - var prefix = null - - if (nfuncs < nbatch) nbatch = nfuncs - if (nbatch < 1) nbatch = 1 - - // Generate .s files: run QBE on each batch - while (bi < nbatch) { - batch_fns = [] - end = nfuncs * (bi + 1) / nbatch - while (fi < end) { - batch_fns[] = il_parts.functions[fi] - fi = fi + 1 - } - // Batch 0 includes helper functions; others reference them as external symbols - prefix = (bi == 0 && helpers_il != "") ? helpers_il + "\n\n" : "" - batch_il = il_parts.data + "\n\n" + prefix + text(batch_fns, "\n") - asm_text = os.qbe(batch_il) - s_path = tmp_prefix + '_b' + text(bi) + '.s' - o_path = tmp_prefix + '_b' + text(bi) + '.o' - fd.slurpwrite(s_path, stone(blob(asm_text))) - s_paths[] = s_path - o_paths[] = o_path - bi = bi + 1 - } - - // Assemble all batches in parallel - while (ai < length(s_paths)) { - asm_cmds[] = cc + ' -c ' + s_paths[ai] + ' -o ' + o_paths[ai] - ai = ai + 1 - } - parallel_cmd = text(asm_cmds, ' & ') + ' & wait' - rc = os.system(parallel_cmd) + var all_fns = text(il_parts.functions, "\n") + var full_il = il_parts.data + "\n\n" + helpers_il + "\n\n" + all_fns + var asm_text = os.qbe(full_il) + var s_path = tmp_prefix + '.s' + var o_path = tmp_prefix + '.o' + var rc = null + fd.slurpwrite(s_path, stone(blob(asm_text))) + rc = os.system(cc + ' -c ' + s_path + ' -o ' + o_path) if (rc != 0) { - print('Parallel assembly failed'); disrupt + print('Assembly failed'); disrupt } - - return o_paths + return [o_path] } // Post-process QBE IL: insert dead labels after ret/jmp (QBE requirement) @@ -651,7 +610,7 @@ Build.compile_native = function(src_path, target, buildtype, pkg) { var tmp = '/tmp/cell_native_' + hash var rt_o_path = '/tmp/cell_qbe_rt.o' - var o_paths = compile_native_batched(il_parts, cc, tmp) + var o_paths = compile_native_single(il_parts, cc, tmp) // Compile QBE runtime stubs if needed var rc = null @@ -734,7 +693,7 @@ Build.compile_native_ir = function(optimized, src_path, opts) { var tmp = '/tmp/cell_native_' + hash var rt_o_path = '/tmp/cell_qbe_rt.o' - var o_paths = compile_native_batched(il_parts, cc, tmp) + var o_paths = compile_native_single(il_parts, cc, tmp) // Compile QBE runtime stubs if needed var rc = null diff --git a/meson.build b/meson.build index 17b96f7a..28ea6cc9 100644 --- a/meson.build +++ b/meson.build @@ -38,8 +38,7 @@ if host_machine.system() == 'darwin' foreach fkit : fworks deps += dependency('appleframeworks', modules: fkit) endforeach - # 32MB stack for deep native recursion (CPS patterns without TCO) - link += ['-Wl,-stack_size,0x2000000'] + # Native code uses dispatch loop (no C stack recursion) endif if host_machine.system() == 'playdate' diff --git a/qbe_emit.cm b/qbe_emit.cm index c0f24deb..23924b2c 100644 --- a/qbe_emit.cm +++ b/qbe_emit.cm @@ -475,10 +475,10 @@ ${sw("w", "%fp2", "%result_slot", "%r")} ret 0 }` - // function(ctx, fp, dest, fn_idx, arity) - h[] = `export function l $__function_ss(l %ctx, l %fp, l %dest, l %fn_idx, l %arity) { + // function(ctx, fp, dest, fn_idx, arity, nr_slots) + h[] = `export function l $__function_ss(l %ctx, l %fp, l %dest, l %fn_idx, l %arity, l %nr_slots) { @entry - %r =l call $cell_rt_make_function(l %ctx, l %fn_idx, l %fp, l %arity) + %r =l call $cell_rt_make_function(l %ctx, l %fn_idx, l %fp, l %arity, l %nr_slots) ${alloc_tail("%r")} }` @@ -680,11 +680,74 @@ var qbe_emit = function(ir, qbe, export_name) { var tol = null var fn_arity = 0 var arity_tmp = null + var fn_nr_slots = 0 + var invoke_count = 0 + var si = 0 + var scan = null + var scan_op = null + var has_invokes = false + var seg_counter = 0 + var ri = 0 + var seg_num = 0 + var resume_val = 0 + + // Pre-scan: count invoke/tail_invoke points to assign segment numbers. + // Must skip dead code (instructions after terminators) the same way + // the main emission loop does, otherwise we create jump table entries + // for segments that never get emitted. + var scan_dead = false + si = 0 + while (si < length(instrs)) { + scan = instrs[si] + si = si + 1 + if (is_text(scan)) { + // Labels reset dead code state (unless they're nop pseudo-labels) + if (!starts_with(scan, "_nop_ur_") && !starts_with(scan, "_nop_tc_")) + scan_dead = false + continue + } + if (scan_dead) continue + if (!is_array(scan)) continue + scan_op = scan[0] + if (scan_op == "invoke" || scan_op == "tail_invoke") { + invoke_count = invoke_count + 1 + } + // Track terminators — same set as in the main loop + if (scan_op == "return" || scan_op == "jump" || scan_op == "goinvoke" || scan_op == "disrupt") { + scan_dead = true + } + } + has_invokes = invoke_count > 0 // Function signature: (ctx, frame_ptr) → JSValue emit(`export function l $${name}(l %ctx, l %fp) {`) emit("@entry") + // Resume dispatch: if this function has invoke points, read the segment + // number from frame->address and jump to the right resume point. + // frame->address is at fp - 8 (last field before slots[]). + if (has_invokes) { + emit(" %addr_ptr =l sub %fp, 8") + emit(" %addr_raw =l loadl %addr_ptr") + // address is stored as JS_NewInt32 tagged value: n << 1 + emit(" %addr =l sar %addr_raw, 1") + emit(" %resume =l shr %addr, 16") + emit(` jnz %resume, @_rcheck1, @_seg0`) + ri = 1 + while (ri <= invoke_count) { + emit(`@_rcheck${text(ri)}`) + emit(` %_rc${text(ri)} =w ceql %resume, ${text(ri)}`) + if (ri < invoke_count) { + emit(` jnz %_rc${text(ri)}, @_seg${text(ri)}, @_rcheck${text(ri + 1)}`) + } else { + // Last check — if no match, fall through to seg0 + emit(` jnz %_rc${text(ri)}, @_seg${text(ri)}, @_seg0`) + } + ri = ri + 1 + } + emit("@_seg0") + } + // GC-safe slot access: every read/write goes through frame memory. // %fp may become stale after GC-triggering calls — use refresh_fp(). var s_read = function(slot) { @@ -1228,13 +1291,51 @@ var qbe_emit = function(ir, qbe, export_name) { continue } if (op == "invoke") { - emit(` %fp =l call $__invoke_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`) - emit_exc_check() + // Dispatch loop invoke: store resume info, signal, return 0 + seg_counter = seg_counter + 1 + seg_num = seg_counter + // Store (seg_num << 16 | result_slot) as tagged int in frame->address + resume_val = seg_num * 65536 + a2 + // frame->address is at fp - 8, store as tagged int (n << 1) + emit(` %_inv_addr${text(seg_num)} =l sub %fp, 8`) + emit(` storel ${text(resume_val * 2)}, %_inv_addr${text(seg_num)}`) + emit(` call $cell_rt_signal_call(l %ctx, l %fp, l ${text(a1)})`) + emit(" ret 0") + emit(`@_seg${text(seg_num)}`) + // Check for exception after dispatch loop resumes us + p = fresh() + emit(` %${p} =w call $JS_HasException(l %ctx)`) + if (has_handler && !in_handler) { + emit(` jnz %${p}, @disruption_handler, @${p}_ok`) + } else { + needs_exc_ret = true + emit(` jnz %${p}, @_exc_ret, @${p}_ok`) + } + emit(`@${p}_ok`) + last_was_term = false continue } if (op == "tail_invoke") { - emit(` %fp =l call $__invoke_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`) - emit_exc_check() + // Same as invoke — dispatch loop regular call with resume + seg_counter = seg_counter + 1 + seg_num = seg_counter + resume_val = seg_num * 65536 + a2 + emit(` %_tinv_addr${text(seg_num)} =l sub %fp, 8`) + emit(` storel ${text(resume_val * 2)}, %_tinv_addr${text(seg_num)}`) + emit(` call $cell_rt_signal_call(l %ctx, l %fp, l ${text(a1)})`) + emit(" ret 0") + emit(`@_seg${text(seg_num)}`) + // Check for exception after dispatch loop resumes us + p = fresh() + emit(` %${p} =w call $JS_HasException(l %ctx)`) + if (has_handler && !in_handler) { + emit(` jnz %${p}, @disruption_handler, @${p}_ok`) + } else { + needs_exc_ret = true + emit(` jnz %${p}, @_exc_ret, @${p}_ok`) + } + emit(`@${p}_ok`) + last_was_term = false continue } if (op == "goframe") { @@ -1243,22 +1344,13 @@ var qbe_emit = function(ir, qbe, export_name) { continue } if (op == "goinvoke") { - v = s_read(a1) + // Dispatch loop tail call: signal tail call and return 0 + // Use 0xFFFF as ret_slot (no result to store — it's a tail call) p = fresh() - emit(` %${p} =l call $cell_rt_goinvoke(l %ctx, l ${v})`) - chk = fresh() - emit(` %${chk} =w ceql %${p}, 15`) - if (has_handler) { - emit(` jnz %${chk}, @disruption_handler, @${chk}_ok`) - emit(`@${chk}_ok`) - refresh_fp() - emit(` ret %${p}`) - } else { - needs_exc_ret = true - emit(` jnz %${chk}, @_exc_ret, @${chk}_ok`) - emit(`@${chk}_ok`) - emit(` ret %${p}`) - } + emit(` %${p}_addr =l sub %fp, 8`) + emit(` storel ${text(65535 * 2)}, %${p}_addr`) + emit(` call $cell_rt_signal_tail_call(l %ctx, l %fp, l ${text(a1)})`) + emit(" ret 0") last_was_term = true continue } @@ -1267,10 +1359,12 @@ var qbe_emit = function(ir, qbe, export_name) { if (op == "function") { fn_arity = 0 + fn_nr_slots = 0 if (a2 >= 0 && a2 < length(ir.functions)) { fn_arity = ir.functions[a2].nr_args + fn_nr_slots = ir.functions[a2].nr_slots } - emit(` %fp =l call $__function_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(fn_arity)})`) + emit(` %fp =l call $__function_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(fn_arity)}, l ${text(fn_nr_slots)})`) emit_exc_check() continue } @@ -1407,6 +1501,10 @@ var qbe_emit = function(ir, qbe, export_name) { compile_fn(ir.main, -1, true) fn_bodies[] = text(out, "\n") + // Export nr_slots for main function so the module loader can use right-sized frames + var main_name = export_name ? sanitize(export_name) : "cell_main" + push(data_out, `export data $${main_name}_nr_slots = { w ${text(ir.main.nr_slots)} }`) + return { data: text(data_out, "\n"), functions: fn_bodies, diff --git a/source/mach.c b/source/mach.c index 7dbf48b2..3d05f17d 100644 --- a/source/mach.c +++ b/source/mach.c @@ -490,6 +490,32 @@ JSValue js_new_register_function(JSContext *ctx, JSCodeRegister *code, JSValue e return JS_MKPTR(fn); } +/* Create a native (QBE-compiled) function */ +JSValue js_new_native_function(JSContext *ctx, void *fn_ptr, void *dl_handle, + uint16_t nr_slots, int arity, JSValue outer_frame) { + JSGCRef frame_ref; + JS_PushGCRef(ctx, &frame_ref); + frame_ref.val = outer_frame; + + JSFunction *fn = js_mallocz(ctx, sizeof(JSFunction)); + if (!fn) { + JS_PopGCRef(ctx, &frame_ref); + return JS_EXCEPTION; + } + + fn->header = objhdr_make(0, OBJ_FUNCTION, 0, 0, 0, 0); + fn->kind = JS_FUNC_KIND_NATIVE; + fn->length = arity; + fn->name = JS_NULL; + fn->u.native.fn_ptr = fn_ptr; + fn->u.native.dl_handle = dl_handle; + fn->u.native.nr_slots = nr_slots; + fn->u.native.outer_frame = frame_ref.val; + + JS_PopGCRef(ctx, &frame_ref); + return JS_MKPTR(fn); +} + /* Binary operations helper */ static JSValue reg_vm_binop(JSContext *ctx, int op, JSValue a, JSValue b) { /* Fast path for integers */ @@ -1924,12 +1950,14 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, env = fn->u.reg.env_record; pc = code->entry_point; } else { - /* C or bytecode function: args already in fr->slots (GC-protected via frame chain) */ + /* C, native, or bytecode function */ ctx->reg_current_frame = frame_ref.val; ctx->current_register_pc = pc > 0 ? pc - 1 : 0; JSValue ret; if (fn->kind == JS_FUNC_KIND_C) ret = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); + else if (fn->kind == JS_FUNC_KIND_NATIVE) + ret = cell_native_dispatch(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); else ret = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1], 0); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); @@ -2007,12 +2035,14 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, pc = code->entry_point; } } else { - /* C/bytecode function: call it, then return result to our caller */ + /* C, native, or bytecode function: call it, then return result to our caller */ ctx->reg_current_frame = frame_ref.val; ctx->current_register_pc = pc > 0 ? pc - 1 : 0; JSValue ret; if (fn->kind == JS_FUNC_KIND_C) ret = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); + else if (fn->kind == JS_FUNC_KIND_NATIVE) + ret = cell_native_dispatch(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); else ret = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1], 0); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); diff --git a/source/qbe_helpers.c b/source/qbe_helpers.c index 4f189280..547f3d2f 100644 --- a/source/qbe_helpers.c +++ b/source/qbe_helpers.c @@ -320,25 +320,34 @@ JSValue cell_rt_get_intrinsic(JSContext *ctx, const char *name) { } /* --- 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. */ + 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. */ -#define QBE_FRAME_OUTER_SLOT 511 - -static JSValue *derive_outer_fp(int magic); +/* 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++) { - /* 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); + frame = get_outer_frame_slots(frame); if (!frame) return JS_NULL; } @@ -347,42 +356,26 @@ JSValue cell_rt_get_closure(JSContext *ctx, void *fp, int64_t depth, 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++) { - int magic = (int)(int64_t)frame[QBE_FRAME_OUTER_SLOT]; - frame = derive_outer_fp(magic); + frame = get_outer_frame_slots(frame); 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. */ + Each native dispatch loop pushes a GC ref so the GC can find and + update the current frame pointer when it moves objects. + cell_rt_refresh_fp re-derives the slot pointer after any GC call. */ -#define MAX_AOT_DEPTH 65536 +#define MAX_AOT_DEPTH 8192 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()) { + if (g_aot_depth >= MAX_AOT_DEPTH) { JS_ThrowTypeError(ctx, "native call stack overflow (depth %d)", g_aot_depth); return NULL; } @@ -411,9 +404,7 @@ JSValue *cell_rt_refresh_fp(JSContext *ctx) { 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. */ +/* Combined refresh + exception check in a single call. */ JSValue *cell_rt_refresh_fp_checked(JSContext *ctx) { if (JS_HasException(ctx)) return NULL; @@ -439,126 +430,346 @@ void cell_rt_leave_frame(JSContext *ctx) { 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 */ +/* Set before executing a native module's cell_main — + used by cell_rt_make_function to resolve fn_ptr via dlsym */ static void *g_current_dl_handle = NULL; -/* 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; +/* ============================================================ + Dispatch loop — the core of native function execution. + Each compiled cell_fn_N returns to this loop when it needs + to call another function (instead of recursing via C stack). + ============================================================ */ + +/* Pending call state — set by cell_rt_signal_call / cell_rt_signal_tail_call, + read by the dispatch loop. */ +static JSValue g_pending_callee_frame = 0; /* JSFrameRegister ptr */ +static int g_pending_is_tail = 0; + +void cell_rt_signal_call(JSContext *ctx, void *fp, int64_t frame_slot) { + (void)ctx; + JSValue *slots = (JSValue *)fp; + g_pending_callee_frame = slots[frame_slot]; + g_pending_is_tail = 0; } -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; +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; } -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_ThrowTypeError(ctx, "invalid native function id %d", magic); +/* 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 *handle = g_native_fn_registry[magic].dl_handle; - int fn_idx = g_native_fn_registry[magic].fn_idx; + /* Root func_obj across allocation — GC can move it */ + JSGCRef func_ref; + JS_PushGCRef(ctx, &func_ref); + func_ref.val = func_obj; - 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_ThrowTypeError(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)) - JS_Throw(ctx, JS_NULL); + /* Allocate initial frame */ + JSValue *fp = cell_rt_enter_frame(ctx, nr_slots); + if (!fp) { + JS_PopGCRef(ctx, &func_ref); 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_ThrowTypeError(ctx, "too many native functions (max %d)", MAX_NATIVE_FN); + /* Re-derive func_obj after potential GC */ + func_obj = func_ref.val; + JS_PopGCRef(ctx, &func_ref); - 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; + /* 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]; - /* 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; + /* Link function to frame for closure access */ + JSFrameRegister *frame = (JSFrameRegister *)((char *)fp - offsetof(JSFrameRegister, slots)); + frame->function = func_obj; + + int base_depth = g_aot_depth; /* remember entry depth for return detection */ + + for (;;) { + g_pending_callee_frame = 0; + + JSValue result = fn(ctx, fp); + + /* Re-derive frame after potential GC */ + JSValue frame_val = g_aot_gc_refs[g_aot_depth - 1].val; + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); + fp = (JSValue *)frame->slots; + + if (g_pending_callee_frame != 0) { + /* Function signaled a call — dispatch it */ + JSValue callee_frame_val = g_pending_callee_frame; + g_pending_callee_frame = 0; + JSFrameRegister *callee_fr = (JSFrameRegister *)JS_VALUE_GET_PTR(callee_frame_val); + int callee_argc = (int)objhdr_cap56(callee_fr->header); + callee_argc = (callee_argc >= 2) ? callee_argc - 2 : 0; + JSValue callee_fn_val = callee_fr->function; + + if (!JS_IsFunction(callee_fn_val)) { + JS_ThrowTypeError(ctx, "not a function"); + /* Resume caller with exception pending */ + JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function); + fn = (cell_compiled_fn)exc_fn->u.native.fn_ptr; + continue; + } + + JSFunction *callee_fn = JS_VALUE_GET_FUNCTION(callee_fn_val); + + if (callee_fn->kind == JS_FUNC_KIND_NATIVE) { + /* Native-to-native call — no C stack growth */ + cell_compiled_fn callee_ptr = (cell_compiled_fn)callee_fn->u.native.fn_ptr; + int callee_slots = callee_fn->u.native.nr_slots; + + if (g_pending_is_tail) { + /* Tail call: reuse or replace current frame */ + if (callee_slots <= (int)objhdr_cap56(frame->header)) { + /* Reuse current frame */ + int cc = (callee_argc < callee_fn->length) ? callee_argc : callee_fn->length; + if (cc < 0) cc = callee_argc; + frame->slots[0] = callee_fr->slots[0]; /* this */ + for (int i = 0; i < cc && i < callee_slots - 1; i++) + frame->slots[1 + i] = callee_fr->slots[1 + i]; + /* Null out remaining slots */ + int cur_slots = (int)objhdr_cap56(frame->header); + for (int i = 1 + cc; i < cur_slots; i++) + frame->slots[i] = JS_NULL; + frame->function = callee_fn_val; + frame->address = JS_NewInt32(ctx, 0); + fn = callee_ptr; + /* fp stays the same (same frame) */ + } else { + /* Need bigger frame — save callee info, pop+push */ + JSValue saved_caller = frame->caller; + JSValue callee_this = callee_fr->slots[0]; + int cc = (callee_argc < callee_fn->length) ? callee_argc : callee_fn->length; + if (cc < 0) cc = callee_argc; + JSValue callee_args[cc > 0 ? cc : 1]; + for (int i = 0; i < cc; i++) + callee_args[i] = callee_fr->slots[1 + i]; + + /* Pop old frame */ + cell_rt_leave_frame(ctx); + + /* Push new right-sized frame */ + JSValue *new_fp = cell_rt_enter_frame(ctx, callee_slots); + if (!new_fp) + return JS_EXCEPTION; + JSFrameRegister *new_frame = (JSFrameRegister *)((char *)new_fp - offsetof(JSFrameRegister, slots)); + new_frame->function = callee_fn_val; + new_frame->caller = saved_caller; + new_frame->slots[0] = callee_this; + for (int i = 0; i < cc && i < callee_slots - 1; i++) + new_frame->slots[1 + i] = callee_args[i]; + frame = new_frame; + fp = new_fp; + fn = callee_ptr; + } + } else { + /* Regular call: push new frame, link caller */ + int ret_info = JS_VALUE_GET_INT(frame->address); + int resume_seg = ret_info >> 16; + int ret_slot = ret_info & 0xFFFF; + + /* Save callee info before allocation */ + JSValue callee_this = callee_fr->slots[0]; + int cc = (callee_argc < callee_fn->length) ? callee_argc : callee_fn->length; + if (cc < 0) cc = callee_argc; + JSValue callee_args[cc > 0 ? cc : 1]; + for (int i = 0; i < cc; i++) + callee_args[i] = callee_fr->slots[1 + i]; + + JSValue *new_fp = cell_rt_enter_frame(ctx, callee_slots); + if (!new_fp) { + /* Resume caller with exception pending */ + frame_val = g_aot_gc_refs[g_aot_depth - 1].val; + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); + fp = (JSValue *)frame->slots; + JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function); + fn = (cell_compiled_fn)exc_fn->u.native.fn_ptr; + continue; + } + + /* Re-derive caller frame after alloc */ + frame_val = g_aot_gc_refs[g_aot_depth - 2].val; + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); + + JSFrameRegister *new_frame = (JSFrameRegister *)((char *)new_fp - offsetof(JSFrameRegister, slots)); + new_frame->function = callee_fn_val; + new_frame->caller = JS_MKPTR(frame); + new_frame->slots[0] = callee_this; + for (int i = 0; i < cc && i < callee_slots - 1; i++) + new_frame->slots[1 + i] = callee_args[i]; + + /* Save return address in caller */ + frame->address = JS_NewInt32(ctx, (resume_seg << 16) | ret_slot); + + frame = new_frame; + fp = new_fp; + fn = callee_ptr; + } + } else { + /* Non-native callee (C function, register VM, etc.) — + call it via the standard path and store the result */ + JSValue ret; + if (callee_fn->kind == JS_FUNC_KIND_C) + ret = js_call_c_function(ctx, callee_fn_val, callee_fr->slots[0], + callee_argc, &callee_fr->slots[1]); + else + ret = JS_CallInternal(ctx, callee_fn_val, callee_fr->slots[0], + callee_argc, &callee_fr->slots[1], 0); + + /* Re-derive frame after call */ + frame_val = g_aot_gc_refs[g_aot_depth - 1].val; + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); + fp = (JSValue *)frame->slots; + + if (JS_IsException(ret)) { + /* Non-native callee threw — resume caller with exception pending. + The caller's generated code checks JS_HasException at resume. */ + if (!JS_HasException(ctx)) + JS_Throw(ctx, JS_NULL); + /* fn and fp still point to the calling native function's frame. + Just resume it — it will detect the exception. */ + JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function); + fn = (cell_compiled_fn)exc_fn->u.native.fn_ptr; + continue; + } + /* Clear stale exception */ + if (JS_HasException(ctx)) + JS_GetException(ctx); + + if (g_pending_is_tail) { + /* Tail call to non-native: return its result up the chain */ + /* Pop current frame and return to caller */ + if (g_aot_depth <= base_depth) { + cell_rt_leave_frame(ctx); + return ret; + } + /* Pop current frame, return to caller frame */ + JSValue caller_val = frame->caller; + cell_rt_leave_frame(ctx); + if (JS_IsNull(caller_val) || g_aot_depth < base_depth) { + return ret; + } + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(caller_val); + /* Update GC ref to point to caller */ + g_aot_gc_refs[g_aot_depth - 1].val = caller_val; + fp = (JSValue *)frame->slots; + int ret_info = JS_VALUE_GET_INT(frame->address); + int ret_slot = ret_info & 0xFFFF; + if (ret_slot != 0xFFFF) + fp[ret_slot] = ret; + /* Resume caller */ + JSFunction *caller_fn = JS_VALUE_GET_FUNCTION(frame->function); + fn = (cell_compiled_fn)caller_fn->u.native.fn_ptr; + } else { + /* Regular call: store result and resume current function */ + int ret_info = JS_VALUE_GET_INT(frame->address); + int ret_slot = ret_info & 0xFFFF; + if (ret_slot != 0xFFFF) + fp[ret_slot] = ret; + /* fn stays the same — we resume the same function at next segment */ + JSFunction *cur_fn = JS_VALUE_GET_FUNCTION(frame->function); + fn = (cell_compiled_fn)cur_fn->u.native.fn_ptr; + } + } + continue; + } + + /* No pending call — function returned a value or exception */ + if (result == JS_EXCEPTION) { + /* Exception: pop this frame and propagate to caller. + The caller's generated code has exception checks at resume points. */ + if (!JS_HasException(ctx)) + JS_Throw(ctx, JS_NULL); + + if (g_aot_depth <= base_depth) { + cell_rt_leave_frame(ctx); + return JS_EXCEPTION; + } + + JSValue exc_caller_val = frame->caller; + cell_rt_leave_frame(ctx); + + if (JS_IsNull(exc_caller_val) || g_aot_depth < base_depth) { + return JS_EXCEPTION; + } + + /* Resume caller — it will check JS_HasException and branch to handler */ + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(exc_caller_val); + g_aot_gc_refs[g_aot_depth - 1].val = exc_caller_val; + fp = (JSValue *)frame->slots; + + JSFunction *exc_caller_fn = JS_VALUE_GET_FUNCTION(frame->function); + fn = (cell_compiled_fn)exc_caller_fn->u.native.fn_ptr; + continue; + } + + /* Normal return — pop frame and store result in caller */ + if (g_aot_depth <= base_depth) { + cell_rt_leave_frame(ctx); + return result; + } + + JSValue caller_val = frame->caller; + cell_rt_leave_frame(ctx); + + if (JS_IsNull(caller_val) || g_aot_depth < base_depth) { + return result; + } + + /* Return to caller frame */ + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(caller_val); + g_aot_gc_refs[g_aot_depth - 1].val = caller_val; + fp = (JSValue *)frame->slots; + int ret_info = JS_VALUE_GET_INT(frame->address); + int ret_slot = ret_info & 0xFFFF; + if (ret_slot != 0xFFFF) + fp[ret_slot] = result; + + JSFunction *caller_fn = JS_VALUE_GET_FUNCTION(frame->function); + fn = (cell_compiled_fn)caller_fn->u.native.fn_ptr; + continue; } - - return JS_NewCFunction2(ctx, (JSCFunction *)cell_fn_trampoline, "native_fn", - (int)nr_args, JS_CFUNC_generic_magic, global_id); } -/* --- Frame-based function calling --- */ +/* Create a native function object from a compiled fn_idx. + Called from QBE-generated code during function creation. */ +JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx, void *outer_fp, + int64_t nr_args, int64_t nr_slots) { + if (!g_current_dl_handle) + return JS_ThrowTypeError(ctx, "no native module loaded"); + + /* Resolve fn_ptr via dlsym at creation time — cached in the function object */ + char name[64]; + snprintf(name, sizeof(name), "cell_fn_%lld", (long long)fn_idx); + void *fn_ptr = dlsym(g_current_dl_handle, name); + if (!fn_ptr) + return JS_ThrowTypeError(ctx, "native function %s not found in dylib", name); + + /* Get the current frame as outer_frame for closures */ + JSValue outer_frame = JS_NULL; + if (g_aot_depth > 0) + outer_frame = g_aot_gc_refs[g_aot_depth - 1].val; + + return js_new_native_function(ctx, fn_ptr, g_current_dl_handle, + (uint16_t)nr_slots, (int)nr_args, outer_frame); +} + +/* --- Frame-based function calling --- + Still used by QBE-generated code for building call frames + before signaling the dispatch loop. */ JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int64_t nargs) { if (!JS_IsFunction(fn)) { @@ -578,6 +789,7 @@ void cell_rt_setarg(JSValue frame_val, int64_t idx, JSValue 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); @@ -594,11 +806,10 @@ JSValue cell_rt_invoke(JSContext *ctx, JSValue frame_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 if (fn->kind == JS_FUNC_KIND_NATIVE) { + result = cell_native_dispatch(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]; @@ -607,9 +818,6 @@ JSValue cell_rt_invoke(JSContext *ctx, JSValue frame_val) { 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; @@ -765,8 +973,11 @@ void cell_rt_clear_exception(JSContext *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_ThrowTypeError(ctx, "type error in native code"); + JS_Throw(ctx, JS_TRUE); } /* --- in: key in obj --- */ @@ -793,67 +1004,72 @@ JSValue cell_rt_regexp(JSContext *ctx, const char *pattern, const char *flags) { 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_ThrowTypeError(ctx, "cell_main not found in native module dylib"); - - /* Set current handle so cell_rt_make_function registers closures - against this module's dylib */ +/* 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; - /* 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) { + /* 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_ThrowTypeError(ctx, "frame allocation failed"); + return JS_EXCEPTION; } /* 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 */ + JSValue result = cell_native_dispatch(ctx, func_obj, JS_NULL, 0, NULL); g_current_dl_handle = prev_handle; - if (result == JS_EXCEPTION) - return JS_EXCEPTION; return result; } +JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle, JSValue env) { + cell_compiled_fn fn = (cell_compiled_fn)dlsym(dl_handle, "cell_main"); + if (!fn) + return JS_ThrowTypeError(ctx, "cell_main not found in native module dylib"); + + /* Make env available for cell_rt_get_intrinsic lookups */ + cell_rt_set_native_env(ctx, env); + + /* Try to read nr_slots from the module (exported by emitter) */ + int *slots_ptr = (int *)dlsym(dl_handle, "cell_main_nr_slots"); + int nr_slots = slots_ptr ? *slots_ptr : 512; + + return native_module_run(ctx, dl_handle, fn, nr_slots); +} + /* Load a native module from a dylib handle, trying a named symbol first. Falls back to cell_main if the named symbol is not found. */ JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name, JSValue env) { cell_compiled_fn fn = NULL; - if (sym_name) + const char *used_name = NULL; + if (sym_name) { fn = (cell_compiled_fn)dlsym(dl_handle, sym_name); - if (!fn) + if (fn) used_name = sym_name; + } + if (!fn) { fn = (cell_compiled_fn)dlsym(dl_handle, "cell_main"); + used_name = "cell_main"; + } if (!fn) return JS_ThrowTypeError(ctx, "symbol not found in native module dylib"); - 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_ThrowTypeError(ctx, "frame allocation failed"); - } + /* 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; - 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; + return native_module_run(ctx, dl_handle, fn, nr_slots); } /* Backward-compat: uses RTLD_DEFAULT (works when dylib opened with RTLD_GLOBAL) */ diff --git a/source/quickjs-internal.h b/source/quickjs-internal.h index f8487801..94e10b08 100644 --- a/source/quickjs-internal.h +++ b/source/quickjs-internal.h @@ -1322,6 +1322,7 @@ typedef enum { JS_FUNC_KIND_BYTECODE, JS_FUNC_KIND_C_DATA, JS_FUNC_KIND_REGISTER, /* register-based VM function */ + JS_FUNC_KIND_NATIVE, /* QBE-compiled native function */ } JSFunctionKind; typedef struct JSFunction { @@ -1340,6 +1341,12 @@ typedef struct JSFunction { JSValue env_record; /* stone record, module environment */ JSValue outer_frame; /* JSFrame JSValue, for closures */ } reg; + struct { + void *fn_ptr; /* compiled cell_fn_N pointer */ + void *dl_handle; /* dylib handle for dlsym lookups */ + uint16_t nr_slots; /* frame size for this function */ + JSValue outer_frame; /* GC-traced, for closures */ + } native; } u; } JSFunction; @@ -1362,6 +1369,7 @@ typedef struct JSFunction { JSValue js_call_c_function (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv); JSValue JS_CallInternal (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv, int flags); JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, JSValue this_obj, int argc, JSValue *argv, JSValue env, JSValue outer_frame); +JSValue cell_native_dispatch(JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv); int JS_DeleteProperty (JSContext *ctx, JSValue obj, JSValue prop); JSValue __attribute__ ((format (printf, 2, 3))) JS_ThrowInternalError (JSContext *ctx, const char *fmt, ...); @@ -1652,6 +1660,7 @@ JSValue js_key_from_string (JSContext *ctx, JSValue val); /* mach.c exports */ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, JSValue this_obj, int argc, JSValue *argv, JSValue env, JSValue outer_frame); +JSValue js_new_native_function(JSContext *ctx, void *fn_ptr, void *dl_handle, uint16_t nr_slots, int arity, JSValue outer_frame); JSFrameRegister *alloc_frame_register(JSContext *ctx, int slot_count); int reg_vm_check_interrupt(JSContext *ctx); diff --git a/source/runtime.c b/source/runtime.c index 08be4ee9..657fe161 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -1442,6 +1442,8 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro /* Scan outer_frame and env_record */ fn->u.reg.outer_frame = gc_copy_value (ctx, fn->u.reg.outer_frame, from_base, from_end, to_base, to_free, to_end); fn->u.reg.env_record = gc_copy_value (ctx, fn->u.reg.env_record, from_base, from_end, to_base, to_free, to_end); + } else if (fn->kind == JS_FUNC_KIND_NATIVE) { + fn->u.native.outer_frame = gc_copy_value (ctx, fn->u.native.outer_frame, from_base, from_end, to_base, to_free, to_end); } break; } @@ -4732,6 +4734,8 @@ JSValue JS_CallInternal (JSContext *ctx, JSValue func_obj, JSValue this_obj, case JS_FUNC_KIND_REGISTER: return JS_CallRegisterVM (ctx, f->u.reg.code, this_obj, argc, argv, f->u.reg.env_record, f->u.reg.outer_frame); + case JS_FUNC_KIND_NATIVE: + return cell_native_dispatch (ctx, func_obj, this_obj, argc, argv); default: return JS_ThrowTypeError (ctx, "not a function"); } @@ -4753,6 +4757,8 @@ JSValue JS_Call (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, J case JS_FUNC_KIND_REGISTER: return JS_CallRegisterVM (ctx, f->u.reg.code, this_obj, argc, argv, f->u.reg.env_record, f->u.reg.outer_frame); + case JS_FUNC_KIND_NATIVE: + return cell_native_dispatch (ctx, func_obj, this_obj, argc, argv); default: return JS_ThrowTypeError (ctx, "not a function"); } diff --git a/vm_suite.ce b/vm_suite.ce index c14dd11c..6cee162b 100644 --- a/vm_suite.ce +++ b/vm_suite.ce @@ -827,6 +827,27 @@ run("disruption handler accesses object from outer scope", function() { if (obj.y != 20) fail("handler mutation lost, y=" + text(obj.y)) }) +run("disruption in callback with multiple calls after", function() { + // Regression: a function with a disruption handler that calls a + // callback which disrupts, followed by more successful calls. + // In native mode, cell_rt_disrupt must NOT use JS_ThrowTypeError + // (which prints to stderr) — it must silently set the exception. + var log = [] + var run_inner = function(name, fn) { + fn() + log[] = "pass:" + name + } disruption { + log[] = "fail:" + name + } + run_inner("a", function() { var x = 1 }) + run_inner("b", function() { disrupt }) + run_inner("c", function() { var y = 2 }) + if (length(log) != 3) fail("expected 3 log entries, got " + text(length(log))) + if (log[0] != "pass:a") fail("expected pass:a, got " + log[0]) + if (log[1] != "fail:b") fail("expected fail:b, got " + log[1]) + if (log[2] != "pass:c") fail("expected pass:c, got " + log[2]) +}) + // ============================================================================ // TYPE CHECKING WITH is_* FUNCTIONS // ============================================================================