From 7af8ffe4e0cb7160784b1f2834db099c0e302a80 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Wed, 31 Dec 2025 16:01:02 -0600 Subject: [PATCH] attempt --- source/quickjs.c | 1077 +++++++++++++++++++++------------------------- 1 file changed, 489 insertions(+), 588 deletions(-) diff --git a/source/quickjs.c b/source/quickjs.c index 1eec13ce..5ecc272f 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -13046,17 +13046,19 @@ static void dump_stack_chain(JSContext *ctx, JSStackFrame *start, int max_frames } #endif +/* Forward declaration for unified interpreter loop */ +static JSValue vm_run(JSContext *caller_ctx, int initial_frame_top); + /* Trampoline VM dispatcher - runs frames without C recursion */ +/* Entry point for calling bytecode functions. + Handles initial setup and delegates to unified vm_run loop. */ static JSValue JS_CallTrampoline(JSContext *caller_ctx, JSValueConst func_obj, JSValueConst this_obj, JSValueConst new_target, int argc, JSValue *argv, int flags) { JSRuntime *rt = caller_ctx->rt; JSObject *p; - JSValue ret_val = JS_NULL; struct VMFrame *frame; - VMExecState state; - VMCallInfo call_info; int initial_frame_top; if (js_poll_interrupts(caller_ctx)) @@ -13066,6 +13068,7 @@ static JSValue JS_CallTrampoline(JSContext *caller_ctx, JSValueConst func_obj, p = JS_VALUE_GET_OBJ(func_obj); if (unlikely(p->class_id != JS_CLASS_BYTECODE_FUNCTION)) { + /* C function - call directly */ JSClassCall *call_func = rt->class_array[p->class_id].call; if (!call_func) return JS_ThrowTypeError(caller_ctx, "not a function"); @@ -13081,394 +13084,14 @@ static JSValue JS_CallTrampoline(JSContext *caller_ctx, JSValueConst func_obj, initial_frame_top = caller_ctx->frame_stack_top; - for (;;) { - /* Always re-fetch frame pointer in case of realloc (though current push doesn't realloc) */ - frame = &caller_ctx->frame_stack[caller_ctx->frame_stack_top]; - memset(&call_info, 0, sizeof(call_info)); - - state = vm_execute_frame(caller_ctx, frame, &ret_val, &call_info); - - switch (state) { - case VM_EXEC_RETURN: { - /* 1. Capture continuation and constructor info before popping */ - const uint8_t *ret_pc = frame->ret_pc; - int ret_sp_offset = frame->ret_sp_offset; - int was_constructor = frame->is_constructor; - JSValue ctor_this_obj = frame->ctor_this_obj; - - /* Clear ctor_this_obj in frame so vm_pop_frame doesn't free it */ - frame->ctor_this_obj = JS_NULL; - - /* 2. Handle constructor return value semantics */ - if (was_constructor) { - /* JS constructor semantics: - - If return value is an object, use it - - Otherwise, use the constructed 'this' object */ - if (JS_VALUE_GET_TAG(ret_val) == JS_TAG_OBJECT) { - /* Return value is object, free the constructed this */ - JS_FreeValue(caller_ctx, ctor_this_obj); -#ifdef DEBUG_VM - printf("[CTOR-RET] constructor returned object, using ret_val tag=%d\n", - JS_VALUE_GET_TAG(ret_val)); -#endif - } else { - /* Return value is not an object, use constructed this */ - JS_FreeValue(caller_ctx, ret_val); - ret_val = ctor_this_obj; -#ifdef DEBUG_VM - printf("[CTOR-RET] constructor returned non-object (tag=%d), using ctor_this_obj\n", - JS_VALUE_GET_TAG(ret_val)); -#endif - } - } - - /* 3. Pop the frame */ - vm_pop_frame(caller_ctx); - - /* 4. Check if finished */ - if (caller_ctx->frame_stack_top < initial_frame_top) { - return ret_val; - } - - /* 5. Resume caller */ - frame = &caller_ctx->frame_stack[caller_ctx->frame_stack_top]; - - /* 6. Clean up caller stack (pop Func, This, Args) using the saved offset */ - JSValue *stack_base = &caller_ctx->value_stack[frame->value_stack_base]; - JSValue *current_sp = stack_base + frame->sp_offset; - JSValue *target_sp = stack_base + ret_sp_offset; - - while (current_sp > target_sp) { - JS_FreeValue(caller_ctx, *--current_sp); - } - - /* 7. Push return value */ - *current_sp++ = ret_val; - - /* 8. Restore state */ - frame->sp_offset = current_sp - stack_base; - -#ifdef DEBUG_VM - { - const uint8_t *bc = frame->b->byte_code_buf; - const uint8_t *end = bc + frame->b->byte_code_len; - if (ret_pc < bc || ret_pc >= end) { - printf("BAD ret_pc: caller=%s ret_pc=%p not in [%p,%p)\n", - frame->b->func_name ? JS_AtomToCString(caller_ctx, frame->b->func_name) : "", - ret_pc, bc, end); - abort(); - } - uint8_t op = *ret_pc; - if (op == 0 || op == OP_invalid) { - ptrdiff_t off = ret_pc - bc; - printf("BAD resume opcode: caller=%s off=%td op=0x%02x\n", - frame->b->func_name ? JS_AtomToCString(caller_ctx, frame->b->func_name) : "", - off, op); - /* dump a few bytes */ - for (int di = -8; di < 16; di++) - printf("%02x ", ret_pc[di]); - printf("\n"); - abort(); - } - } -#endif - - frame->pc = ret_pc; - break; - } - - case VM_EXEC_CALL: { - JSObject *callee_p; - - if (unlikely(JS_VALUE_GET_TAG(call_info.func_obj) != JS_TAG_OBJECT)) { - JS_ThrowTypeError(caller_ctx, "not a function"); - goto exception_unwind; - } - - callee_p = JS_VALUE_GET_OBJ(call_info.func_obj); - if (unlikely(callee_p->class_id != JS_CLASS_BYTECODE_FUNCTION)) { - /* C function call */ - JSClassCall *call_func = rt->class_array[callee_p->class_id].call; - if (!call_func) { - JS_ThrowTypeError(caller_ctx, "not a function"); - goto exception_unwind; - } - - int call_flags = 0; - JSValueConst this_for_call = call_info.this_obj; - - if (call_info.is_constructor) { - /* Constructor call: use JS_CALL_FLAG_CONSTRUCTOR and pass new_target as this - (QuickJS convention for C constructors) */ - call_flags = JS_CALL_FLAG_CONSTRUCTOR; - this_for_call = call_info.new_target; -#ifdef DEBUG_VM - printf("[C-CTOR] calling C constructor class_id=%d argc=%d new_target_tag=%d\n", - callee_p->class_id, call_info.argc, JS_VALUE_GET_TAG(call_info.new_target)); -#endif - } - - ret_val = call_func(caller_ctx, call_info.func_obj, this_for_call, - call_info.argc, (JSValueConst *)call_info.argv, call_flags); - -#ifdef DEBUG_VM - if (call_info.is_constructor) { - printf("[C-CTOR] returned tag=%d exception=%d\n", - JS_VALUE_GET_TAG(ret_val), JS_IsException(ret_val)); - } -#endif - - if (JS_IsException(ret_val)) { -#ifdef DEBUG_VM - printf("[EXCEPTION] C call failed: caller=%s is_ctor=%d callee_class=%d\n", - frame->b->func_name ? JS_AtomToCString(caller_ctx, frame->b->func_name) : "", - call_info.is_constructor, callee_p->class_id); -#endif - goto exception_unwind; - } - - if (call_info.is_tail_call) { - /* Tail-call to C function: call completed, now we must return - from the current frame (not resume it). A tail-call has no - continuation in the calling function. */ - const uint8_t *ret_pc = frame->ret_pc; - int ret_sp_offset = frame->ret_sp_offset; - -#ifdef DEBUG_VM - { - JSFunctionBytecode *caller_b = frame->b; - printf("[TAILCALL-C] C func returned, popping frame=%s " - "will_return_to ret_pc=%p ret_sp_offset=%d\n", - caller_b->func_name ? JS_AtomToCString(caller_ctx, caller_b->func_name) : "", - ret_pc, ret_sp_offset); - } -#endif - - vm_pop_frame(caller_ctx); - - if (caller_ctx->frame_stack_top < initial_frame_top) { - /* Tail-call from the initial frame: finish execution. */ - return ret_val; - } - - frame = &caller_ctx->frame_stack[caller_ctx->frame_stack_top]; - -#ifdef DEBUG_VM - { - JSFunctionBytecode *parent_b = frame->b; - const uint8_t *bc = parent_b->byte_code_buf; - int bc_len = parent_b->byte_code_len; - printf("[TAILCALL-C] resuming frame=%s ret_pc_offset=%td bc_len=%d\n", - parent_b->func_name ? JS_AtomToCString(caller_ctx, parent_b->func_name) : "", - ret_pc - bc, bc_len); - if (ret_pc < bc || ret_pc >= bc + bc_len) { - printf(" ERROR: ret_pc=%p not in [%p, %p)\n", ret_pc, bc, bc + bc_len); - } - } -#endif - - JSValue *stack_base = &caller_ctx->value_stack[frame->value_stack_base]; - JSValue *current_sp = stack_base + frame->sp_offset; - JSValue *target_sp = stack_base + ret_sp_offset; - - while (current_sp > target_sp) JS_FreeValue(caller_ctx, *--current_sp); - - *current_sp++ = ret_val; - frame->sp_offset = current_sp - stack_base; - frame->pc = ret_pc; - -#ifdef DEBUG_VM - { - JSFunctionBytecode *fb = frame->b; - const uint8_t *bc = fb->byte_code_buf; - int bc_len = fb->byte_code_len; - if (frame->pc < bc || frame->pc >= bc + bc_len) { - printf("[TAILCALL-C] ERROR: frame->pc=%p not in [%p, %p) func=%s\n", - frame->pc, bc, bc + bc_len, - fb->func_name ? JS_AtomToCString(caller_ctx, fb->func_name) : ""); - abort(); - } - uint8_t op = *frame->pc; - if (op == 0 || op == OP_invalid) { - printf("[TAILCALL-C] BAD resume opcode=0x%02x at offset=%td func=%s\n", - op, frame->pc - bc, - fb->func_name ? JS_AtomToCString(caller_ctx, fb->func_name) : ""); - abort(); - } - } -#endif - break; - } - - - /* Manual stack cleanup for regular C call (not tail-call) */ - frame = &caller_ctx->frame_stack[caller_ctx->frame_stack_top]; - JSValue *stack_base = &caller_ctx->value_stack[frame->value_stack_base]; - JSValue *current_sp = stack_base + frame->sp_offset; - JSValue *target_sp = stack_base + call_info.ret_sp_offset; - - while (current_sp > target_sp) { - JS_FreeValue(caller_ctx, *--current_sp); - } - *current_sp++ = ret_val; - frame->sp_offset = current_sp - stack_base; - frame->pc = call_info.ret_pc; - -#ifdef DEBUG_VM - { - JSFunctionBytecode *fb = frame->b; - const uint8_t *bc = fb->byte_code_buf; - int bc_len = fb->byte_code_len; - if (frame->pc < bc || frame->pc >= bc + bc_len) { - printf("[C-CALL] ERROR: frame->pc=%p not in [%p, %p) func=%s\n", - frame->pc, bc, bc + bc_len, - fb->func_name ? JS_AtomToCString(caller_ctx, fb->func_name) : ""); - abort(); - } - uint8_t op = *frame->pc; - if (op == 0 || op == OP_invalid) { - printf("[C-CALL] BAD resume opcode=0x%02x at offset=%td func=%s\n", - op, frame->pc - bc, - fb->func_name ? JS_AtomToCString(caller_ctx, fb->func_name) : ""); - abort(); - } - } -#endif - break; - } - - if (call_info.is_tail_call) { - /* Tail call optimization for bytecode callee. - Key insight: a tail-call has no continuation in the current function. - The callee should return directly to our caller, so we save our - frame's continuation BEFORE popping and use that for the new frame. */ - JSValue *saved_argv = NULL; - JSValue func_saved = JS_DupValue(caller_ctx, call_info.func_obj); - JSValue this_saved = JS_DupValue(caller_ctx, call_info.this_obj); - JSValue new_target_saved = JS_DupValue(caller_ctx, call_info.new_target); - - /* Save current frame's continuation BEFORE popping - this is where - the tail-called function should return to (our caller). */ - const uint8_t *saved_ret_pc = frame->ret_pc; - int saved_ret_sp_offset = frame->ret_sp_offset; - int saved_call_argc = frame->call_argc; - int saved_call_has_this = frame->call_has_this; - -#ifdef DEBUG_VM - { - JSFunctionBytecode *caller_b = frame->b; - printf("[TAILCALL-BC] popping frame=%s will_return_to ret_pc=%p " - "ret_sp_offset=%d (owner=caller's caller)\n", - caller_b->func_name ? JS_AtomToCString(caller_ctx, caller_b->func_name) : "", - saved_ret_pc, saved_ret_sp_offset); - } -#endif - - if (call_info.argc > 0) { - saved_argv = js_malloc(caller_ctx, sizeof(JSValue) * call_info.argc); - if (!saved_argv) { - JS_FreeValue(caller_ctx, func_saved); - JS_FreeValue(caller_ctx, this_saved); - JS_FreeValue(caller_ctx, new_target_saved); - JS_ThrowOutOfMemory(caller_ctx); - goto exception_unwind; - } - for (int i = 0; i < call_info.argc; i++) - saved_argv[i] = JS_DupValue(caller_ctx, call_info.argv[i]); - } - - vm_pop_frame(caller_ctx); - - /* Push new frame with the saved continuation from the popped frame. - This makes the tail-called function return directly to our caller. */ - frame = vm_push_frame(caller_ctx, func_saved, this_saved, new_target_saved, - call_info.argc, saved_argv, JS_CALL_FLAG_COPY_ARGV, - saved_ret_pc, saved_ret_sp_offset, - saved_call_argc, saved_call_has_this); - - JS_FreeValue(caller_ctx, func_saved); - JS_FreeValue(caller_ctx, this_saved); - JS_FreeValue(caller_ctx, new_target_saved); - for (int i = 0; i < call_info.argc; i++) JS_FreeValue(caller_ctx, saved_argv[i]); - js_free(caller_ctx, saved_argv); - } else { - /* Regular call (including constructor calls) */ - JSValue this_for_call = call_info.this_obj; - JSValue ctor_this_obj = JS_NULL; - - /* Handle bytecode constructor: create 'this' object */ - if (call_info.is_constructor) { - JSObject *callee_obj = JS_VALUE_GET_OBJ(call_info.func_obj); - JSFunctionBytecode *callee_b = callee_obj->u.func.function_bytecode; - - if (callee_b->is_derived_class_constructor) { - /* Derived class: this is null until super() is called */ - this_for_call = JS_NULL; -#ifdef DEBUG_VM - printf("[CTOR-BC] derived class constructor, this=NULL until super()\n"); -#endif - } else { - /* Non-derived: create 'this' object from prototype */ - ctor_this_obj = js_create_from_ctor(caller_ctx, call_info.new_target, JS_CLASS_OBJECT); - if (JS_IsException(ctor_this_obj)) { -#ifdef DEBUG_VM - printf("[CTOR-BC] js_create_from_ctor FAILED\n"); -#endif - goto exception_unwind; - } - this_for_call = ctor_this_obj; -#ifdef DEBUG_VM - printf("[CTOR-BC] created this object: tag=%d ptr=%p new_target_tag=%d\n", - JS_VALUE_GET_TAG(ctor_this_obj), - JS_VALUE_GET_PTR(ctor_this_obj), - JS_VALUE_GET_TAG(call_info.new_target)); -#endif - } - } - - frame = vm_push_frame(caller_ctx, call_info.func_obj, - this_for_call, call_info.new_target, - call_info.argc, call_info.argv, - JS_CALL_FLAG_COPY_ARGV, - call_info.ret_pc, call_info.ret_sp_offset, - call_info.call_argc, call_info.call_has_this); - - if (!frame) { - if (!JS_IsNull(ctor_this_obj)) - JS_FreeValue(caller_ctx, ctor_this_obj); - JS_ThrowStackOverflow(caller_ctx); - goto exception_unwind; - } - - /* Store constructor info in the frame for return handling */ - if (call_info.is_constructor) { - frame->is_constructor = 1; - frame->ctor_this_obj = ctor_this_obj; /* ownership transferred to frame */ - } - } - break; - } - - case VM_EXEC_EXCEPTION: - exception_unwind: - while (caller_ctx->frame_stack_top >= initial_frame_top) { - vm_pop_frame(caller_ctx); - } - return JS_EXCEPTION; - - default: break; - } - } + /* Run the unified interpreter loop */ + return vm_run(caller_ctx, initial_frame_top); } -/* Execute a single frame - runs bytecode until call/return/exception. - This is the core bytecode interpreter, using VMFrame's value_stack - instead of alloca. When a bytecode-to-bytecode call is needed, it returns - VM_EXEC_CALL; the trampoline dispatcher then pushes a new frame. -*/ -/* Updated Execute Frame - removes recursion */ -static VMExecState vm_execute_frame(JSContext *caller_ctx, struct VMFrame *frame, - JSValue *return_value, VMCallInfo *call_info) +/* Unified interpreter loop - handles bytecode->bytecode calls inline. + Returns final value when initial frame returns, or JS_EXCEPTION on error. + initial_frame_top is the frame_stack_top AFTER the initial frame was pushed. */ +static JSValue vm_run(JSContext *caller_ctx, int initial_frame_top) { JSRuntime *rt = caller_ctx->rt; JSContext *ctx; @@ -13479,8 +13102,56 @@ static VMExecState vm_execute_frame(JSContext *caller_ctx, struct VMFrame *frame JSValue *stack_buf, *var_buf, *arg_buf, *sp, ret_val, *pval; JSVarRef **var_refs; JSValue this_obj, new_target; + struct VMFrame *frame; - /* Load execution state */ + /* Macro to commit current execution state back to the heap frame. + Moves var refs from sf (C stack) -> frame (heap) before suspending. */ +#define COMMIT_FRAME_STATE() do { \ + frame->pc = pc; \ + frame->sp_offset = sp - stack_buf; \ + if (!list_empty(&sf->var_ref_list)) { \ + /* Move refs from sf -> frame (splice INTO frame's list) */ \ + list_splice(&sf->var_ref_list, &frame->var_ref_list); \ + init_list_head(&sf->var_ref_list); \ + } \ + frame->bt_sf.cur_func = sf->cur_func; \ + frame->bt_sf.cur_pc = sf->cur_pc; \ + frame->bt_sf.js_mode = sf->js_mode; \ + rt->current_stack_frame = &frame->bt_sf; \ + } while(0) + + /* Macro to reload execution state from the current heap frame. + Moves var refs from frame (heap) -> sf (C stack) when resuming. */ +#define RELOAD_FRAME_STATE() do { \ + frame = &caller_ctx->frame_stack[caller_ctx->frame_stack_top]; \ + b = frame->b; \ + ctx = frame->ctx; \ + pc = frame->pc; \ + var_refs = frame->var_refs; \ + this_obj = frame->this_obj; \ + new_target = frame->new_target; \ + stack_buf = &caller_ctx->value_stack[frame->value_stack_base]; \ + arg_buf = (frame->arg_buf_offset >= 0) ? stack_buf + frame->arg_buf_offset : stack_buf; \ + var_buf = stack_buf + frame->var_buf_offset; \ + sp = stack_buf + frame->sp_offset; \ + sf->js_mode = frame->js_mode; \ + sf->cur_func = frame->cur_func; \ + sf->arg_count = frame->arg_count; \ + sf->var_buf = var_buf; \ + sf->arg_buf = arg_buf; \ + sf->cur_pc = pc; \ + sf->prev_frame = frame->bt_sf.prev_frame; \ + init_list_head(&sf->var_ref_list); \ + if (!list_empty(&frame->var_ref_list)) { \ + /* Move refs from frame -> sf (splice INTO sf's list) */ \ + list_splice(&frame->var_ref_list, &sf->var_ref_list); \ + init_list_head(&frame->var_ref_list); \ + } \ + rt->current_stack_frame = sf; \ + } while(0) + + /* Initial load - use RELOAD_FRAME_STATE to properly transfer var_ref_list */ + frame = &caller_ctx->frame_stack[caller_ctx->frame_stack_top]; b = frame->b; ctx = frame->ctx; pc = frame->pc; @@ -13504,10 +13175,11 @@ static VMExecState vm_execute_frame(JSContext *caller_ctx, struct VMFrame *frame sf->cur_pc = pc; sf->prev_frame = frame->bt_sf.prev_frame; /* link to caller's bt_sf, not whatever is current */ - /* Restore var_ref_list from the frame state */ + /* Restore var_ref_list from the frame state (move frame -> sf) */ init_list_head(&sf->var_ref_list); if (!list_empty(&frame->var_ref_list)) { - list_splice(&sf->var_ref_list, &frame->var_ref_list); + list_splice(&frame->var_ref_list, &sf->var_ref_list); + init_list_head(&frame->var_ref_list); } /* While executing, sf is the active frame for C functions to see */ @@ -13796,249 +13468,418 @@ CASE(OP_push_i32): CASE(OP_call): CASE(OP_tail_call): { + int is_tail_call = (opcode == OP_tail_call); + JSValue func_obj; + JSObject *callee_p; + int ret_sp_offset; + call_argc = get_u16(pc); pc += 2; goto has_call_argc; has_call_argc: call_argv = sp - call_argc; sf->cur_pc = pc; + func_obj = call_argv[-1]; + ret_sp_offset = (call_argv - 1) - stack_buf; - /* Save current frame state for return */ - frame->pc = pc; - frame->sp_offset = sp - stack_buf; - - /* Setup call info */ - call_info->func_obj = call_argv[-1]; - call_info->this_obj = JS_NULL; - call_info->new_target = JS_NULL; - call_info->argc = call_argc; - call_info->argv = call_argv; - call_info->ret_pc = pc; - /* Note: ret_sp_offset points to where FuncObj is on stack */ - call_info->ret_sp_offset = (call_argv - 1) - stack_buf; - call_info->call_argc = call_argc; - call_info->call_has_this = 0; - call_info->is_tail_call = (opcode == OP_tail_call); - call_info->is_constructor = 0; /* not a constructor call */ - call_info->pre_argc = 1; /* 1 slot before argv: func */ - - /* Move refs back to heap frame */ - if (!list_empty(&sf->var_ref_list)) { - list_splice(&frame->var_ref_list, &sf->var_ref_list); - init_list_head(&sf->var_ref_list); /* reinit source after splice */ + /* Check if callee is valid */ + if (unlikely(JS_VALUE_GET_TAG(func_obj) != JS_TAG_OBJECT)) { + JS_ThrowTypeError(ctx, "not a function"); + goto exception; } - /* Sync backtrace frame - keep this frame visible while call is in progress */ - frame->bt_sf.cur_func = sf->cur_func; - frame->bt_sf.cur_pc = sf->cur_pc; /* points to call instruction for line info */ - frame->bt_sf.js_mode = sf->js_mode; - rt->current_stack_frame = &frame->bt_sf; + callee_p = JS_VALUE_GET_OBJ(func_obj); -#ifdef DEBUG_VM - /* Debug: print call info with tail-call awareness */ - { - int pc_offset = (int)(pc - b->byte_code_buf); - int ret_pc_offset = call_info->ret_pc ? (int)(call_info->ret_pc - b->byte_code_buf) : -1; - int bc_len = b->byte_code_len; - int at_end = (ret_pc_offset >= bc_len); + if (callee_p->class_id == JS_CLASS_BYTECODE_FUNCTION) { + /* Bytecode callee - handle inline without returning */ + struct VMFrame *new_frame; - printf("[VM_EXEC_CALL] caller=%s bc_len=%d pc_off=%d ret_pc_off=%d", - b->func_name ? JS_AtomToCString(ctx, b->func_name) : "", - bc_len, pc_offset, ret_pc_offset); + if (is_tail_call) { + /* Tail call: pop current frame, reuse its continuation */ + const uint8_t *saved_ret_pc = frame->ret_pc; + int saved_ret_sp_offset = frame->ret_sp_offset; + int saved_call_argc = frame->call_argc; + int saved_call_has_this = frame->call_has_this; - if (call_info->is_tail_call) { - printf(" TAIL_CALL=1"); - if (at_end) { - printf(" (ret_pc at END - expected for tail call, will use frame->ret_pc instead)"); + /* Save args before popping (they're on the stack we're about to free) */ + JSValue *saved_argv = NULL; + JSValue func_saved = JS_DupValue(ctx, func_obj); + if (call_argc > 0) { + saved_argv = js_malloc(ctx, sizeof(JSValue) * call_argc); + if (!saved_argv) { + JS_FreeValue(ctx, func_saved); + JS_ThrowOutOfMemory(ctx); + goto exception; + } + for (int j = 0; j < call_argc; j++) + saved_argv[j] = JS_DupValue(ctx, call_argv[j]); } - } - printf(" bytes=["); - if (call_info->ret_pc && !at_end) { - for (int di = 0; di < 8 && call_info->ret_pc + di < b->byte_code_buf + b->byte_code_len; di++) - printf("%02x ", call_info->ret_pc[di]); - } else if (at_end) { - printf("END"); - } - printf("] argc=%d has_this=%d ", - call_info->call_argc, call_info->call_has_this); + /* Close var refs before popping */ + if (unlikely(!list_empty(&sf->var_ref_list))) { + close_var_refs(rt, sf); + } - if (JS_VALUE_GET_TAG(call_info->func_obj) == JS_TAG_OBJECT) { - JSObject *callee_p = JS_VALUE_GET_OBJ(call_info->func_obj); - if (callee_p->class_id == JS_CLASS_BYTECODE_FUNCTION) { - JSFunctionBytecode *callee_b = callee_p->u.func.function_bytecode; - printf("callee=bytecode:%s\n", - callee_b->func_name ? JS_AtomToCString(ctx, callee_b->func_name) : ""); - } else { - printf("callee=C:class_id=%d\n", callee_p->class_id); + vm_pop_frame(caller_ctx); + + /* Push new frame with saved continuation */ + new_frame = vm_push_frame(caller_ctx, func_saved, JS_NULL, JS_NULL, + call_argc, saved_argv, JS_CALL_FLAG_COPY_ARGV, + saved_ret_pc, saved_ret_sp_offset, + saved_call_argc, saved_call_has_this); + + JS_FreeValue(caller_ctx, func_saved); + if (saved_argv) { + for (int j = 0; j < call_argc; j++) + JS_FreeValue(caller_ctx, saved_argv[j]); + js_free(caller_ctx, saved_argv); + } + + if (!new_frame) { + JS_ThrowStackOverflow(caller_ctx); + goto exception_unwind; } } else { - printf("callee=non-object\n"); - } - } -#endif + /* Regular call: push new frame with current pc as return point */ + frame->pc = pc; + frame->sp_offset = sp - stack_buf; + COMMIT_FRAME_STATE(); - return VM_EXEC_CALL; + new_frame = vm_push_frame(caller_ctx, func_obj, JS_NULL, JS_NULL, + call_argc, call_argv, JS_CALL_FLAG_COPY_ARGV, + pc, ret_sp_offset, call_argc, 0); + + if (!new_frame) { + JS_ThrowStackOverflow(caller_ctx); + goto exception; + } + } + + /* Switch to new frame and continue dispatching */ + RELOAD_FRAME_STATE(); + goto restart; + } else { + /* C function callee */ + JSClassCall *call_func = rt->class_array[callee_p->class_id].call; + if (!call_func) { + JS_ThrowTypeError(ctx, "not a function"); + goto exception; + } + + /* Commit state for backtrace during C call */ + frame->pc = pc; + frame->sp_offset = sp - stack_buf; + sf->cur_pc = pc; + frame->bt_sf.cur_pc = pc; + rt->current_stack_frame = &frame->bt_sf; + + ret_val = call_func(ctx, func_obj, JS_NULL, + call_argc, (JSValueConst *)call_argv, 0); + + if (JS_IsException(ret_val)) + goto exception; + + if (is_tail_call) { + /* Tail-call to C: we must return from current frame */ + const uint8_t *saved_ret_pc = frame->ret_pc; + int saved_ret_sp_offset = frame->ret_sp_offset; + + /* Close var refs */ + if (unlikely(!list_empty(&sf->var_ref_list))) { + close_var_refs(rt, sf); + } + + vm_pop_frame(caller_ctx); + + if (caller_ctx->frame_stack_top < initial_frame_top) { + /* Initial frame done */ + return ret_val; + } + + /* Resume caller with result */ + RELOAD_FRAME_STATE(); + + /* Clean up caller stack */ + JSValue *target_sp = stack_buf + saved_ret_sp_offset; + while (sp > target_sp) { + JS_FreeValue(ctx, *--sp); + } + *sp++ = ret_val; + frame->sp_offset = sp - stack_buf; + frame->pc = saved_ret_pc; + pc = saved_ret_pc; + goto restart; + } + + /* Regular C call: clean up stack and push result */ + JSValue *target_sp = stack_buf + ret_sp_offset; + while (sp > target_sp) { + JS_FreeValue(ctx, *--sp); + } + *sp++ = ret_val; + rt->current_stack_frame = sf; + } } BREAK; CASE(OP_call_constructor): { + JSValue func_obj, new_target_val; + JSObject *callee_p; + int ret_sp_offset; + call_argc = get_u16(pc); pc += 2; call_argv = sp - call_argc; sf->cur_pc = pc; - frame->pc = pc; - frame->sp_offset = sp - stack_buf; - - call_info->func_obj = call_argv[-2]; - call_info->this_obj = JS_NULL; /* constructor creates its own this */ - call_info->new_target = call_argv[-1]; - call_info->argc = call_argc; - call_info->argv = call_argv; - call_info->ret_pc = pc; /* call_argv[-2] is func, call_argv[-1] is new.target */ - call_info->ret_sp_offset = (call_argv - 2) - stack_buf; - call_info->call_argc = call_argc; - call_info->call_has_this = 0; /* NOT a this-call, it's a constructor */ - call_info->is_tail_call = 0; - call_info->is_constructor = 1; /* this is a constructor call (new) */ - call_info->pre_argc = 2; /* 2 slots before argv: func + new_target */ + func_obj = call_argv[-2]; + new_target_val = call_argv[-1]; + ret_sp_offset = (call_argv - 2) - stack_buf; - /* Move refs back to heap frame */ - if (!list_empty(&sf->var_ref_list)) { - list_splice(&frame->var_ref_list, &sf->var_ref_list); - init_list_head(&sf->var_ref_list); /* reinit source after splice */ + /* Check if callee is valid */ + if (unlikely(JS_VALUE_GET_TAG(func_obj) != JS_TAG_OBJECT)) { + JS_ThrowTypeError(ctx, "not a constructor"); + goto exception; } - /* Sync backtrace frame - keep this frame visible while call is in progress */ - frame->bt_sf.cur_func = sf->cur_func; - frame->bt_sf.cur_pc = sf->cur_pc; - frame->bt_sf.js_mode = sf->js_mode; - rt->current_stack_frame = &frame->bt_sf; + callee_p = JS_VALUE_GET_OBJ(func_obj); -#ifdef DEBUG_VM - /* Debug: print call info for constructor */ - { - int pc_offset = (int)(pc - b->byte_code_buf); - int ret_pc_offset = call_info->ret_pc ? (int)(call_info->ret_pc - b->byte_code_buf) : -1; - int sp_off_before = frame->sp_offset; + if (callee_p->class_id == JS_CLASS_BYTECODE_FUNCTION) { + /* Bytecode constructor - handle inline */ + struct VMFrame *new_frame; + JSFunctionBytecode *callee_b = callee_p->u.func.function_bytecode; + JSValue this_for_call = JS_NULL; + JSValue ctor_this_obj = JS_NULL; - printf("[VM_EXEC_CALL] caller=%s bc_len=%d pc_off=%d ret_pc_off=%d " - "sp_off=%d ret_sp_off=%d IS_CTOR=1 new_target_tag=%d bytes=[", - b->func_name ? JS_AtomToCString(ctx, b->func_name) : "", - b->byte_code_len, pc_offset, ret_pc_offset, - sp_off_before, call_info->ret_sp_offset, - JS_VALUE_GET_TAG(call_info->new_target)); - if (call_info->ret_pc) { - for (int di = 0; di < 8 && call_info->ret_pc + di < b->byte_code_buf + b->byte_code_len; di++) - printf("%02x ", call_info->ret_pc[di]); - } - printf("] argc=%d pre_argc=%d ", - call_info->call_argc, call_info->pre_argc); - - if (JS_VALUE_GET_TAG(call_info->func_obj) == JS_TAG_OBJECT) { - JSObject *callee_p = JS_VALUE_GET_OBJ(call_info->func_obj); - if (callee_p->class_id == JS_CLASS_BYTECODE_FUNCTION) { - JSFunctionBytecode *callee_b = callee_p->u.func.function_bytecode; - printf("callee=bytecode:%s\n", - callee_b->func_name ? JS_AtomToCString(ctx, callee_b->func_name) : ""); - } else { - printf("callee=C:class_id=%d\n", callee_p->class_id); - } + /* Handle constructor 'this' creation */ + if (callee_b->is_derived_class_constructor) { + /* Derived class: this is null until super() is called */ + this_for_call = JS_NULL; } else { - printf("callee=non-object\n"); + /* Non-derived: create 'this' object from prototype */ + ctor_this_obj = js_create_from_ctor(ctx, new_target_val, JS_CLASS_OBJECT); + if (JS_IsException(ctor_this_obj)) + goto exception; + this_for_call = ctor_this_obj; } - } -#endif - return VM_EXEC_CALL; + /* Commit current frame state */ + frame->pc = pc; + frame->sp_offset = sp - stack_buf; + COMMIT_FRAME_STATE(); + + /* Push new frame (call_has_this=1 because 2 pre-arg slots: func + new_target) */ + new_frame = vm_push_frame(caller_ctx, func_obj, this_for_call, new_target_val, + call_argc, call_argv, JS_CALL_FLAG_COPY_ARGV, + pc, ret_sp_offset, call_argc, 1); + + if (!new_frame) { + if (!JS_IsNull(ctor_this_obj)) + JS_FreeValue(ctx, ctor_this_obj); + JS_ThrowStackOverflow(caller_ctx); + goto exception; + } + + /* Store constructor info in the frame for return handling */ + new_frame->is_constructor = 1; + new_frame->ctor_this_obj = ctor_this_obj; /* ownership transferred to frame */ + + /* Switch to new frame and continue dispatching */ + RELOAD_FRAME_STATE(); + goto restart; + } else { + /* C function constructor */ + JSClassCall *call_func = rt->class_array[callee_p->class_id].call; + if (!call_func) { + JS_ThrowTypeError(ctx, "not a constructor"); + goto exception; + } + + /* Commit state for backtrace during C call */ + frame->pc = pc; + frame->sp_offset = sp - stack_buf; + sf->cur_pc = pc; + frame->bt_sf.cur_pc = pc; + rt->current_stack_frame = &frame->bt_sf; + + /* For C constructors, pass new_target as this (QuickJS convention) */ + ret_val = call_func(ctx, func_obj, new_target_val, + call_argc, (JSValueConst *)call_argv, JS_CALL_FLAG_CONSTRUCTOR); + + if (JS_IsException(ret_val)) + goto exception; + + /* Clean up stack and push result */ + JSValue *target_sp = stack_buf + ret_sp_offset; + while (sp > target_sp) { + JS_FreeValue(ctx, *--sp); + } + *sp++ = ret_val; + rt->current_stack_frame = sf; + } } BREAK; CASE(OP_call_method): CASE(OP_tail_call_method): { + int is_tail_call = (opcode == OP_tail_call_method); + JSValue func_obj, this_val; + JSObject *callee_p; + int ret_sp_offset; + call_argc = get_u16(pc); pc += 2; call_argv = sp - call_argc; sf->cur_pc = pc; - frame->pc = pc; - frame->sp_offset = sp - stack_buf; - - call_info->func_obj = call_argv[-1]; - call_info->this_obj = call_argv[-2]; - call_info->new_target = JS_NULL; - call_info->argc = call_argc; - call_info->argv = call_argv; - call_info->ret_pc = pc; /* call_argv[-2] is this, call_argv[-1] is func */ - call_info->ret_sp_offset = (call_argv - 2) - stack_buf; - call_info->call_argc = call_argc; - call_info->call_has_this = 1; /* consumes 2 slots before args */ - call_info->is_tail_call = (opcode == OP_tail_call_method); - call_info->is_constructor = 0; /* not a constructor call */ - call_info->pre_argc = 2; /* 2 slots before argv: this + func */ + this_val = call_argv[-2]; + func_obj = call_argv[-1]; + ret_sp_offset = (call_argv - 2) - stack_buf; - /* Move refs back to heap frame */ - if (!list_empty(&sf->var_ref_list)) { - list_splice(&frame->var_ref_list, &sf->var_ref_list); - init_list_head(&sf->var_ref_list); /* reinit source after splice */ + /* Check if callee is valid */ + if (unlikely(JS_VALUE_GET_TAG(func_obj) != JS_TAG_OBJECT)) { + JS_ThrowTypeError(ctx, "not a function"); + goto exception; } - /* Sync backtrace frame - keep this frame visible while call is in progress */ - frame->bt_sf.cur_func = sf->cur_func; - frame->bt_sf.cur_pc = sf->cur_pc; - frame->bt_sf.js_mode = sf->js_mode; - rt->current_stack_frame = &frame->bt_sf; + callee_p = JS_VALUE_GET_OBJ(func_obj); -#ifdef DEBUG_VM - /* Debug: print call info with tail-call awareness */ - { - int pc_offset = (int)(pc - b->byte_code_buf); - int ret_pc_offset = call_info->ret_pc ? (int)(call_info->ret_pc - b->byte_code_buf) : -1; - int bc_len = b->byte_code_len; - int at_end = (ret_pc_offset >= bc_len); + if (callee_p->class_id == JS_CLASS_BYTECODE_FUNCTION) { + /* Bytecode callee - handle inline without returning */ + struct VMFrame *new_frame; - printf("[VM_EXEC_CALL] caller=%s bc_len=%d pc_off=%d ret_pc_off=%d", - b->func_name ? JS_AtomToCString(ctx, b->func_name) : "", - bc_len, pc_offset, ret_pc_offset); + if (is_tail_call) { + /* Tail call: pop current frame, reuse its continuation */ + const uint8_t *saved_ret_pc = frame->ret_pc; + int saved_ret_sp_offset = frame->ret_sp_offset; + int saved_call_argc = frame->call_argc; + int saved_call_has_this = frame->call_has_this; - if (call_info->is_tail_call) { - printf(" TAIL_CALL=1"); - if (at_end) { - printf(" (ret_pc at END - expected for tail call, will use frame->ret_pc instead)"); + /* Save args before popping */ + JSValue *saved_argv = NULL; + JSValue func_saved = JS_DupValue(ctx, func_obj); + JSValue this_saved = JS_DupValue(ctx, this_val); + if (call_argc > 0) { + saved_argv = js_malloc(ctx, sizeof(JSValue) * call_argc); + if (!saved_argv) { + JS_FreeValue(ctx, func_saved); + JS_FreeValue(ctx, this_saved); + JS_ThrowOutOfMemory(ctx); + goto exception; + } + for (int j = 0; j < call_argc; j++) + saved_argv[j] = JS_DupValue(ctx, call_argv[j]); } - } - printf(" bytes=["); - if (call_info->ret_pc && !at_end) { - for (int di = 0; di < 8 && call_info->ret_pc + di < b->byte_code_buf + b->byte_code_len; di++) - printf("%02x ", call_info->ret_pc[di]); - } else if (at_end) { - printf("END"); - } - printf("] argc=%d has_this=%d ", - call_info->call_argc, call_info->call_has_this); + /* Close var refs before popping */ + if (unlikely(!list_empty(&sf->var_ref_list))) { + close_var_refs(rt, sf); + } - if (JS_VALUE_GET_TAG(call_info->func_obj) == JS_TAG_OBJECT) { - JSObject *callee_p = JS_VALUE_GET_OBJ(call_info->func_obj); - if (callee_p->class_id == JS_CLASS_BYTECODE_FUNCTION) { - JSFunctionBytecode *callee_b = callee_p->u.func.function_bytecode; - printf("callee=bytecode:%s\n", - callee_b->func_name ? JS_AtomToCString(ctx, callee_b->func_name) : ""); - } else { - printf("callee=C:class_id=%d\n", callee_p->class_id); + vm_pop_frame(caller_ctx); + + /* Push new frame with saved continuation */ + new_frame = vm_push_frame(caller_ctx, func_saved, this_saved, JS_NULL, + call_argc, saved_argv, JS_CALL_FLAG_COPY_ARGV, + saved_ret_pc, saved_ret_sp_offset, + saved_call_argc, saved_call_has_this); + + JS_FreeValue(caller_ctx, func_saved); + JS_FreeValue(caller_ctx, this_saved); + if (saved_argv) { + for (int j = 0; j < call_argc; j++) + JS_FreeValue(caller_ctx, saved_argv[j]); + js_free(caller_ctx, saved_argv); + } + + if (!new_frame) { + JS_ThrowStackOverflow(caller_ctx); + goto exception_unwind; } } else { - printf("callee=non-object\n"); - } - } -#endif + /* Regular method call: push new frame with current pc as return point */ + frame->pc = pc; + frame->sp_offset = sp - stack_buf; + COMMIT_FRAME_STATE(); - return VM_EXEC_CALL; + new_frame = vm_push_frame(caller_ctx, func_obj, this_val, JS_NULL, + call_argc, call_argv, JS_CALL_FLAG_COPY_ARGV, + pc, ret_sp_offset, call_argc, 1); + + if (!new_frame) { + JS_ThrowStackOverflow(caller_ctx); + goto exception; + } + } + + /* Switch to new frame and continue dispatching */ + RELOAD_FRAME_STATE(); + goto restart; + } else { + /* C function callee */ + JSClassCall *call_func = rt->class_array[callee_p->class_id].call; + if (!call_func) { + JS_ThrowTypeError(ctx, "not a function"); + goto exception; + } + + /* Commit state for backtrace during C call */ + frame->pc = pc; + frame->sp_offset = sp - stack_buf; + sf->cur_pc = pc; + frame->bt_sf.cur_pc = pc; + rt->current_stack_frame = &frame->bt_sf; + + ret_val = call_func(ctx, func_obj, this_val, + call_argc, (JSValueConst *)call_argv, 0); + + if (JS_IsException(ret_val)) + goto exception; + + if (is_tail_call) { + /* Tail-call to C: we must return from current frame */ + const uint8_t *saved_ret_pc = frame->ret_pc; + int saved_ret_sp_offset = frame->ret_sp_offset; + + /* Close var refs */ + if (unlikely(!list_empty(&sf->var_ref_list))) { + close_var_refs(rt, sf); + } + + vm_pop_frame(caller_ctx); + + if (caller_ctx->frame_stack_top < initial_frame_top) { + /* Initial frame done */ + return ret_val; + } + + /* Resume caller with result */ + RELOAD_FRAME_STATE(); + + /* Clean up caller stack */ + JSValue *target_sp = stack_buf + saved_ret_sp_offset; + while (sp > target_sp) { + JS_FreeValue(ctx, *--sp); + } + *sp++ = ret_val; + frame->sp_offset = sp - stack_buf; + frame->pc = saved_ret_pc; + pc = saved_ret_pc; + goto restart; + } + + /* Regular C call: clean up stack and push result */ + JSValue *target_sp = stack_buf + ret_sp_offset; + while (sp > target_sp) { + JS_FreeValue(ctx, *--sp); + } + *sp++ = ret_val; + rt->current_stack_frame = sf; + } } BREAK; CASE(OP_array_from): @@ -14084,10 +13925,70 @@ CASE(OP_push_i32): BREAK; CASE(OP_return): ret_val = *--sp; - goto done; + goto do_return; CASE(OP_return_undef): ret_val = JS_NULL; - goto done; + do_return: + { + /* Handle constructor return value semantics */ + int was_constructor = frame->is_constructor; + JSValue ctor_this_obj = frame->ctor_this_obj; + + /* Clear ctor_this_obj in frame so vm_pop_frame doesn't free it */ + frame->ctor_this_obj = JS_NULL; + + if (was_constructor) { + /* Prevent vm_pop_frame from freeing constructed this + (it lives in frame->this_obj too) */ + frame->this_obj = JS_NULL; + this_obj = JS_NULL; + + /* JS constructor semantics: + - If return value is an object, use it + - Otherwise, use the constructed 'this' object */ + if (JS_VALUE_GET_TAG(ret_val) == JS_TAG_OBJECT) { + /* Return value is object, free the constructed this */ + JS_FreeValue(ctx, ctor_this_obj); + } else { + /* Return value is not an object, use constructed this */ + JS_FreeValue(ctx, ret_val); + ret_val = ctor_this_obj; + } + } + + /* Close var refs before popping */ + if (unlikely(!list_empty(&sf->var_ref_list))) { + close_var_refs(rt, sf); + } + + /* Capture continuation before popping */ + const uint8_t *ret_pc = frame->ret_pc; + int ret_sp_offset = frame->ret_sp_offset; + + /* Pop the frame */ + vm_pop_frame(caller_ctx); + + /* Check if finished - all frames above initial have returned */ + if (caller_ctx->frame_stack_top < initial_frame_top) { + return ret_val; + } + + /* Resume caller */ + RELOAD_FRAME_STATE(); + + /* Clean up caller stack (pop Func, This, Args) using the saved offset */ + JSValue *target_sp = stack_buf + ret_sp_offset; + while (sp > target_sp) { + JS_FreeValue(ctx, *--sp); + } + + /* Push return value */ + *sp++ = ret_val; + frame->sp_offset = sp - stack_buf; + frame->pc = ret_pc; + pc = ret_pc; + goto restart; + } CASE(OP_throw): JS_Throw(ctx, *--sp); @@ -15984,6 +15885,7 @@ exception: build_backtrace(ctx, rt->current_exception, NULL, 0, 0, 0); } if (!rt->current_exception_is_uncatchable) { + /* Try to find a catch handler in current frame */ while (sp > stack_buf) { JSValue val = *--sp; JS_FreeValue(ctx, val); @@ -16002,24 +15904,23 @@ exception: } } } - ret_val = JS_EXCEPTION; + /* No catch handler found in current frame - propagate exception up */ + /* Fall through to exception_unwind */ -done: +exception_unwind: + /* Close any var refs still on the C stack frame */ if (unlikely(!list_empty(&sf->var_ref_list))) { close_var_refs(rt, sf); } - frame->sp_offset = sp - stack_buf; - frame->pc = pc; - /* Sync backtrace frame before returning to trampoline. - This keeps our frame visible in stack traces while suspended. */ - frame->bt_sf.cur_func = sf->cur_func; - frame->bt_sf.cur_pc = sf->cur_pc; - frame->bt_sf.js_mode = sf->js_mode; - rt->current_stack_frame = &frame->bt_sf; + /* Unwind all frames back to initial and return exception */ + while (caller_ctx->frame_stack_top >= initial_frame_top) { + vm_pop_frame(caller_ctx); + } + return JS_EXCEPTION; - *return_value = ret_val; - return JS_IsException(ret_val) ? VM_EXEC_EXCEPTION : VM_EXEC_RETURN; +#undef COMMIT_FRAME_STATE +#undef RELOAD_FRAME_STATE } /* Trampoline-based JS_CallInternal - eliminates C stack recursion */