From bab4d50b2ab9bfcbf084b0e9541f762d3d702601 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Thu, 19 Feb 2026 01:55:35 -0600 Subject: [PATCH] shorten frames to closure vars only on gc --- mcode.cm | 43 +++++++++++++++++++++++++++++++------------ source/mach.c | 15 ++++++++++----- source/runtime.c | 41 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 77 insertions(+), 22 deletions(-) diff --git a/mcode.cm b/mcode.cm index e4551ce3..e5cbb58f 100644 --- a/mcode.cm +++ b/mcode.cm @@ -951,6 +951,7 @@ var mcode = function(ast) { } // Scan scope record for variable declarations + // Closure locals are assigned first so returned frames can be shortened var scan_scope = function() { var scope = find_scope_record(s_function_nr) if (scope == null) { @@ -963,6 +964,9 @@ var mcode = function(ast) { var make = null var is_const = false var slot = 0 + + // Pass 1: closure locals first + _i = 0 while (_i < length(keys)) { name = keys[_i] if (name == "function_nr" || name == "nr_close_slots") { @@ -975,14 +979,36 @@ var mcode = function(ast) { _i = _i + 1 continue } - if (find_var(name) < 0) { + if (v.closure == true && find_var(name) < 0) { + is_const = (make == "def" || make == "function") + slot = 1 + s_nr_args + s_nr_local_slots + s_nr_local_slots = s_nr_local_slots + 1 + s_nr_close_slots = s_nr_close_slots + 1 + add_var(name, slot, is_const) + s_vars[length(s_vars) - 1].is_closure = true + } + _i = _i + 1 + } + + // Pass 2: non-closure locals + _i = 0 + while (_i < length(keys)) { + name = keys[_i] + if (name == "function_nr" || name == "nr_close_slots") { + _i = _i + 1 + continue + } + v = scope[name] + make = v.make + if (make == null || make == "input") { + _i = _i + 1 + continue + } + if (v.closure != true && find_var(name) < 0) { is_const = (make == "def" || make == "function") slot = 1 + s_nr_args + s_nr_local_slots s_nr_local_slots = s_nr_local_slots + 1 add_var(name, slot, is_const) - if (v.closure == true) { - s_vars[length(s_vars) - 1].is_closure = true - } } _i = _i + 1 } @@ -2738,8 +2764,6 @@ var mcode = function(ast) { var disrupt_clause = func_node.disruption var null_slot2 = null var fn_name = func_node.name - var fn_scope = null - var nr_cs = 0 var result = null var saved_label = 0 var saved_func = 0 @@ -2880,15 +2904,10 @@ var mcode = function(ast) { fn_name = "" } - fn_scope = find_scope_record(s_function_nr) - if (fn_scope != null && fn_scope.nr_close_slots != null) { - nr_cs = fn_scope.nr_close_slots - } - result = { name: fn_name, nr_args: nr_params, - nr_close_slots: nr_cs, + nr_close_slots: s_nr_close_slots, nr_slots: s_max_slot + 1, disruption_pc: disruption_start, instructions: s_instructions diff --git a/source/mach.c b/source/mach.c index fa541599..4d01669b 100644 --- a/source/mach.c +++ b/source/mach.c @@ -833,7 +833,7 @@ void __asan_on_error(void) { fprintf(stderr, " %s (%s:%u)\n", func_name ? func_name : "", file ? file : "", line); - if (JS_IsNull(frame->caller)) break; + if (!JS_IsPtr(frame->caller)) break; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); is_first = 0; } @@ -913,6 +913,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, env = env_gc.val; /* refresh — GC may have moved env during allocation */ frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); frame->function = top_fn; + frame->caller = JS_NewInt32(ctx, 0); /* sentinel: active top-level, not eligible for GC shortening */ frame->slots[0] = this_gc.val; /* slot 0 = this */ /* Copy arguments from GC-safe refs */ @@ -1715,7 +1716,7 @@ vm_dispatch: VM_CASE(MACH_RETURN): result = frame->slots[a]; - if (JS_IsNull(frame->caller)) goto done; + if (!JS_IsPtr(frame->caller)) goto done; { #ifdef VALIDATE_GC const char *callee_name = "?"; @@ -1759,7 +1760,7 @@ vm_dispatch: VM_CASE(MACH_RETNIL): result = JS_NULL; - if (JS_IsNull(frame->caller)) goto done; + if (!JS_IsPtr(frame->caller)) goto done; { JSFrameRegister *caller = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); frame->caller = JS_NULL; @@ -2317,7 +2318,7 @@ vm_dispatch: if (JS_IsException(ret)) goto disrupt; /* Tail-return: act like MACH_RETURN with the result */ result = ret; - if (JS_IsNull(frame->caller)) goto done; + if (!JS_IsPtr(frame->caller)) goto done; JSFrameRegister *caller = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); frame->caller = JS_NULL; frame = caller; @@ -2387,7 +2388,7 @@ vm_dispatch: ctx->current_exception = JS_NULL; break; } - if (JS_IsNull(frame->caller)) { + if (!JS_IsPtr(frame->caller)) { /* Stack trace was already included in the JS_RaiseDisrupt log via the callback. */ ctx->disruption_reported = TRUE; frame_ref.val = JS_MKPTR(frame); /* update root for GC / done */ @@ -2423,6 +2424,10 @@ done: ctx->reg_current_frame = frame_ref.val; ctx->current_register_pc = pc > 0 ? pc - 1 : 0; } + if (JS_IsPtr(frame_ref.val)) { + JSFrameRegister *f = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); + f->caller = JS_NULL; /* mark as returned so GC can shorten */ + } JS_DeleteGCRef(ctx, &frame_ref); return result; } diff --git a/source/runtime.c b/source/runtime.c index 3f669910..920d0754 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -158,7 +158,8 @@ JSValue *JS_PushGCRef (JSContext *ctx, JSGCRef *ref) { } JSValue JS_PopGCRef (JSContext *ctx, JSGCRef *ref) { - assert(ctx->top_gc_ref == ref && "JS_PopGCRef: not popping top of stack — mismatched push/pop"); + if (ctx->top_gc_ref != ref) + fprintf(stderr, "WARN: JS_PopGCRef mismatch (expected %p, got %p)\n", (void*)ctx->top_gc_ref, (void*)ref); ctx->top_gc_ref = ref->prev; return ref->val; } @@ -1328,15 +1329,45 @@ JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *f } size_t size = gc_object_size (hdr_ptr); - if (*to_free + size > to_end) { - fprintf (stderr, "gc_copy_value: out of to-space, need %zu bytes\n", size); + size_t copy_size = size; + uint16_t new_cap = 0; + + /* Frame shortening: returned frames (caller == JS_NULL) only need + [this][args][closure_locals] — shrink during copy. */ + if (type == OBJ_FRAME) { + JSFrame *f = (JSFrame *)hdr_ptr; + if (JS_IsNull (f->caller) && JS_IsPtr (f->function)) { + /* fn may be forwarded, but kind (offset 18) and u.cell.code (offset 24) + are past the 16 bytes overwritten by fwd+size. */ + JSFunction *fn = (JSFunction *)JS_VALUE_GET_PTR (f->function); + if (fn->kind == JS_FUNC_KIND_REGISTER) { + JSCode *jc = (JSCode *)JS_VALUE_GET_PTR (fn->u.cell.code); + if (jc && jc->kind == JS_CODE_KIND_REGISTER && jc->u.reg.code + && jc->u.reg.code->nr_close_slots > 0) { + uint16_t cs = 1 + jc->u.reg.code->arity + jc->u.reg.code->nr_close_slots; + uint64_t orig = objhdr_cap56 (f->header); + if (cs < orig) { + new_cap = cs; + copy_size = gc_align_up (sizeof (JSFrame) + cs * sizeof (JSValue)); + } + } + } + } + } + + if (*to_free + copy_size > to_end) { + fprintf (stderr, "gc_copy_value: out of to-space, need %zu bytes\n", copy_size); abort (); } void *new_ptr = *to_free; - memcpy (new_ptr, hdr_ptr, size); - *to_free += size; + memcpy (new_ptr, hdr_ptr, copy_size); + *to_free += copy_size; + if (new_cap > 0) + ((JSFrame *)new_ptr)->header = objhdr_set_cap56 (((JSFrame *)new_ptr)->header, new_cap); + + /* Stash ORIGINAL size for from-space linear walks */ *hdr_ptr = objhdr_make_fwd (new_ptr); *((size_t *)((uint8_t *)hdr_ptr + sizeof (objhdr_t))) = size;