From 30a9cfee795704704b3140d5f748adca469cd5e1 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 13 Feb 2026 02:33:25 -0600 Subject: [PATCH 1/7] simplify gc model --- source/mach.c | 6 +- source/qbe_helpers.c | 2 +- source/quickjs-internal.h | 36 ++-- source/runtime.c | 410 +++++++++++++------------------------- 4 files changed, 162 insertions(+), 292 deletions(-) diff --git a/source/mach.c b/source/mach.c index ea57276e..bc6e8e9f 100644 --- a/source/mach.c +++ b/source/mach.c @@ -442,7 +442,7 @@ JSFrameRegister *alloc_frame_register(JSContext *ctx, int slot_count) { if (!frame) return NULL; /* cap56 = slot count (used by gc_object_size) */ - frame->hdr = objhdr_make(slot_count, OBJ_FRAME, 0, 0, 0, 0); + frame->header = objhdr_make(slot_count, OBJ_FRAME, 0, 0, 0, 0); frame->function = JS_NULL; frame->caller = JS_NULL; frame->address = JS_NewInt32(ctx, 0); @@ -1695,7 +1695,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, case MACH_INVOKE: { /* A=frame_slot, B=result_slot */ JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]); - int nr = (int)objhdr_cap56(fr->hdr); + int nr = (int)objhdr_cap56(fr->header); int c_argc = (nr >= 2) ? nr - 2 : 0; JSValue fn_val = fr->function; JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val); @@ -1746,7 +1746,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, case MACH_GOINVOKE: { /* Async invoke: call and discard result */ JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]); - int nr = (int)objhdr_cap56(fr->hdr); + int nr = (int)objhdr_cap56(fr->header); int c_argc = (nr >= 2) ? nr - 2 : 0; ctx->reg_current_frame = frame_ref.val; ctx->current_register_pc = pc > 0 ? pc - 1 : 0; diff --git a/source/qbe_helpers.c b/source/qbe_helpers.c index a5419095..e4f4cf5f 100644 --- a/source/qbe_helpers.c +++ b/source/qbe_helpers.c @@ -333,7 +333,7 @@ void cell_rt_setarg(JSValue frame_val, int64_t idx, JSValue 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 nr_slots = (int)objhdr_cap56(fr->header); int c_argc = (nr_slots >= 2) ? nr_slots - 2 : 0; /* Copy args to C stack */ diff --git a/source/quickjs-internal.h b/source/quickjs-internal.h index 2b6959de..e5922f25 100644 --- a/source/quickjs-internal.h +++ b/source/quickjs-internal.h @@ -361,13 +361,7 @@ struct JSClass { #define JS_MODE_BACKTRACE_BARRIER \ (1 << 3) /* stop backtrace before this frame */ -typedef struct JSFrameRegister { - objhdr_t hdr; // capacity in this is the total number of words of the object, including the 4 words of overhead and all slots - JSValue function; // JSFunction, function object being invoked - JSValue caller; // JSFrameRegister, the frame that called this one - JSValue address; // address of the instruction in the code that should be executed upon return - JSValue slots[]; // inline memory. order is [this][input args][closed over vars][non closed over vars][temporaries] -} JSFrameRegister; /// extra note: when this frame returns, caller should be set to 0. If caller is found to be 0, then the GC can reduce this frame's slots down to [this][input_args][closed over vars]; if no closed over vars it can be totally removed; may happen naturally in GC since it would have no refs? +/* JSFrameRegister is now an alias for JSFrame — see JSFrame definition below */ /* ============================================================ Register-Based VM Data Structures @@ -823,17 +817,18 @@ typedef struct JSCodeRegister { } JSCodeRegister; -/* Frame for closures - used by link-time relocation model where closures - reference outer frames via (depth, slot) addressing. - Stores function as JSValue to survive GC movements. */ +/* Unified frame struct — used by the register VM and closures. + All fields are JSValues so the GC can scan them uniformly. */ typedef struct JSFrame { objhdr_t header; /* OBJ_FRAME, cap56 = slot count */ - JSValue function; /* JSValue for GC safety (use JS_VALUE_GET_FUNCTION) */ - JSValue caller; /* JSValue for GC safety (unused currently) */ - uint32_t return_pc; + JSValue function; /* JSFunction, function object being invoked */ + JSValue caller; /* JSFrame, the frame that called this one */ + JSValue address; /* return PC stored as JS_NewInt32 */ JSValue slots[]; /* [this][args][captured][locals][temps] */ } JSFrame; +typedef JSFrame JSFrameRegister; + static inline objhdr_t objhdr_set_s (objhdr_t h, bool s) { return s ? (h | OBJHDR_S_MASK) : (h & ~OBJHDR_S_MASK); } @@ -903,7 +898,8 @@ typedef struct JSArray { JSValue values[]; /* inline flexible array member */ } JSArray; -/* JSBlob - binary data per memory.md */ +/* JSBlob — not allocated on GC heap (blobs use JSRecord + opaque). + Struct kept for reference; gc_object_size/gc_scan_object do not handle OBJ_BLOB. */ typedef struct JSBlob { objhdr_t mist_hdr; word_t length; @@ -926,7 +922,7 @@ typedef slot JSRecordEntry; typedef struct JSRecord { objhdr_t mist_hdr; - struct JSRecord *proto; + JSValue proto; /* prototype as JSValue (JS_NULL if none) */ word_t len; /* number of entries */ slot slots[]; /* slots[0] reserved: key low32=class_id, key high32=rec_id, val=opaque */ } JSRecord; @@ -1071,6 +1067,13 @@ static JS_BOOL JSText_equal_ascii (const JSText *text, JSValue imm) { enough to call the interrupt callback often. */ #define JS_INTERRUPT_COUNTER_INIT 10000 +/* Auto-rooted C call argv — GC updates values in-place */ +typedef struct CCallRoot { + JSValue *argv; /* points to C-stack-local array */ + int argc; + struct CCallRoot *prev; /* stack for nesting (C -> JS -> C -> ...) */ +} CCallRoot; + struct JSContext { JSRuntime *rt; @@ -1102,6 +1105,7 @@ struct JSContext { JSGCRef *top_gc_ref; /* used to reference temporary GC roots (stack top) */ JSGCRef *last_gc_ref; /* used to reference temporary GC roots (list) */ + CCallRoot *c_call_root; /* stack of auto-rooted C call argv arrays */ int class_count; /* size of class_array and class_proto */ JSClass *class_array; @@ -1234,7 +1238,7 @@ typedef struct JSRegExp { /* Get prototype from object (works for both JSRecord and JSRecord since they * share layout) */ -#define JS_OBJ_GET_PROTO(p) ((JSRecord *)((JSRecord *)(p))->proto) +#define JS_OBJ_GET_PROTO(p) (JS_IsNull(((JSRecord *)(p))->proto) ? NULL : (JSRecord *)JS_VALUE_GET_PTR(((JSRecord *)(p))->proto)) /* Initial capacity for new records (mask = 7, 8 slots total) */ #define JS_RECORD_INITIAL_MASK 7 diff --git a/source/runtime.c b/source/runtime.c index 0b03f3cb..5f0d69ad 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -510,7 +510,7 @@ JSValue rec_get (JSContext *ctx, JSRecord *rec, JSValue k) { while (p) { int slot = rec_find_slot (p, k); if (slot > 0) { return p->slots[slot].val; } - p = p->proto; + p = JS_IsNull (p->proto) ? NULL : JS_VALUE_GET_RECORD (p->proto); } return JS_NULL; } @@ -540,7 +540,7 @@ int rec_resize (JSContext *ctx, JSValue *pobj, uint64_t new_mask) { /* Initialize new record */ new_rec->mist_hdr = objhdr_make (new_mask, OBJ_RECORD, false, false, false, false); - new_rec->proto = rec->proto; + new_rec->proto = rec->proto; /* JSValue — copies directly */ new_rec->len = 0; /* Initialize all slots to empty */ @@ -646,7 +646,7 @@ JSRecord *js_new_record_class (JSContext *ctx, uint32_t initial_mask, JSClassID if (!rec) return NULL; rec->mist_hdr = objhdr_make (initial_mask, OBJ_RECORD, false, false, false, false); - rec->proto = NULL; + rec->proto = JS_NULL; rec->len = 0; /* Initialize all slots to empty (JS_NULL) */ @@ -1038,7 +1038,7 @@ JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *f continue; } - if (type != OBJ_ARRAY && type != OBJ_TEXT && type != OBJ_RECORD && type != OBJ_FUNCTION && type != OBJ_BLOB && type != OBJ_FRAME) { + if (type != OBJ_ARRAY && type != OBJ_TEXT && type != OBJ_RECORD && type != OBJ_FUNCTION && type != OBJ_FRAME) { fprintf (stderr, "gc_copy_value: invalid object type %d at %p (hdr=0x%llx)\n", type, ptr, (unsigned long long)hdr); fprintf (stderr, " This may be an interior pointer or corrupt root\n"); fflush (stderr); @@ -1079,15 +1079,11 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro JSRecord *rec = (JSRecord *)ptr; uint32_t mask = (uint32_t)objhdr_cap56 (rec->mist_hdr); #ifdef DUMP_GC_DETAIL - printf(" record: slots=%u used=%u proto=%p\n", mask + 1, (uint32_t)rec->len, (void*)rec->proto); + printf(" record: slots=%u used=%u proto=0x%llx\n", mask + 1, (uint32_t)rec->len, (unsigned long long)rec->proto); fflush(stdout); #endif /* Copy prototype */ - if (rec->proto) { - JSValue proto_val = JS_MKPTR (rec->proto); - proto_val = gc_copy_value (ctx, proto_val, from_base, from_end, to_base, to_free, to_end); - rec->proto = (JSRecord *)JS_VALUE_GET_PTR (proto_val); - } + rec->proto = gc_copy_value (ctx, rec->proto, from_base, from_end, to_base, to_free, to_end); /* Copy table entries */ for (uint32_t i = 0; i <= mask; i++) { JSValue k = rec->slots[i].key; @@ -1127,15 +1123,14 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro break; } case OBJ_TEXT: - case OBJ_BLOB: /* No internal references to scan */ break; case OBJ_FRAME: { - /* JSFrame - scan function, caller, and slots */ + /* JSFrame - scan function, caller, address, and slots */ JSFrame *frame = (JSFrame *)ptr; - /* function and caller are now JSValues - copy them directly */ frame->function = gc_copy_value (ctx, frame->function, from_base, from_end, to_base, to_free, to_end); frame->caller = gc_copy_value (ctx, frame->caller, from_base, from_end, to_base, to_free, to_end); + frame->address = gc_copy_value (ctx, frame->address, from_base, from_end, to_base, to_free, to_end); /* Scan all slots */ uint64_t slot_count = objhdr_cap56 (frame->header); for (uint64_t i = 0; i < slot_count; i++) { @@ -1251,6 +1246,13 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) { ref->val = gc_copy_value (ctx, ref->val, from_base, from_end, to_base, &to_free, to_end); } + /* Copy auto-rooted C call argv arrays */ + for (CCallRoot *r = ctx->c_call_root; r != NULL; r = r->prev) { + for (int i = 0; i < r->argc; i++) { + r->argv[i] = gc_copy_value (ctx, r->argv[i], from_base, from_end, to_base, &to_free, to_end); + } + } + /* Cheney scan: scan copied objects to find more references */ uint8_t *scan = to_base; #ifdef DUMP_GC_DETAIL @@ -1410,6 +1412,7 @@ JSContext *JS_NewContextRawWithHeapSize (JSRuntime *rt, size_t heap_size) { /* Initialize register VM frame root */ ctx->reg_current_frame = JS_NULL; + ctx->c_call_root = NULL; /* Initialize per-context execution state (moved from JSRuntime) */ ctx->current_exception = JS_NULL; @@ -2205,7 +2208,7 @@ JSValue JS_NewObjectProtoClass (JSContext *ctx, JSValue proto_val, JSClassID cla /* Set prototype if provided */ if (JS_IsRecord (proto_val)) { - rec->proto = JS_VALUE_GET_RECORD (proto_val); + rec->proto = proto_val; } return JS_MKPTR (rec); @@ -2685,8 +2688,8 @@ int JS_HasProperty (JSContext *ctx, JSValue obj, JSValue prop) { for (;;) { ret = JS_GetOwnPropertyInternal (ctx, NULL, p, prop); if (ret != 0) return ret; - p = p->proto; /* Direct pointer chase is safe - no allocation */ - if (!p) break; + if (JS_IsNull (p->proto)) break; + p = JS_VALUE_GET_RECORD (p->proto); } return FALSE; } @@ -2965,7 +2968,8 @@ int JS_HasPropertyKey (JSContext *ctx, JSValue obj, JSValue key) { /* Check own and prototype chain */ while (rec) { if (rec_find_slot (rec, key) > 0) return TRUE; - rec = rec->proto; + if (JS_IsNull (rec->proto)) break; + rec = JS_VALUE_GET_RECORD (rec->proto); } return FALSE; } @@ -3902,8 +3906,8 @@ __maybe_unused void JS_DumpObject (JSRuntime *rt, JSRecord *rec) { printf ("%14p ", (void *)rec); /* Print prototype from JSRecord */ - if (rec->proto) { - printf ("%14p ", (void *)rec->proto); + if (!JS_IsNull (rec->proto)) { + printf ("%14p ", JS_VALUE_GET_PTR (rec->proto)); } else { printf ("%14s ", "-"); } @@ -3993,20 +3997,20 @@ static JSValue js_throw_type_error (JSContext *ctx, JSValue this_val, int argc, } JSValue js_call_c_function (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv) { - JSCFunctionType func; - JSFunction *f; + JSFunction *f = JS_VALUE_GET_FUNCTION (func_obj); + JSCFunctionEnum cproto = f->u.cfunc.cproto; + JSCFunctionType func = f->u.cfunc.c_function; JSValue ret_val; - JSValue *arg_buf; - int arg_count, i; - JSCFunctionEnum cproto; - f = JS_VALUE_GET_FUNCTION (func_obj); - cproto = f->u.cfunc.cproto; - arg_count = f->length; + /* Auto-root argv: copy to C stack and register as GC root. + GC will update arg_copy[] in-place, so C functions can read + argv[n] across allocation points without manual guarding. */ + JSValue arg_copy[argc > 0 ? argc : 1]; + if (argc > 0) + memcpy (arg_copy, argv, argc * sizeof (JSValue)); - arg_buf = argv; - - func = f->u.cfunc.c_function; + CCallRoot root = { arg_copy, argc, ctx->c_call_root }; + ctx->c_call_root = &root; if (unlikely (ctx->trace_hook) && (ctx->trace_type & JS_HOOK_CALL)) { js_debug dbg; @@ -4016,16 +4020,14 @@ JSValue js_call_c_function (JSContext *ctx, JSValue func_obj, JSValue this_obj, switch (cproto) { case JS_CFUNC_generic: - ret_val = func.generic (ctx, this_obj, argc, arg_buf); + ret_val = func.generic (ctx, this_obj, argc, arg_copy); break; case JS_CFUNC_generic_magic: - ret_val - = func.generic_magic (ctx, this_obj, argc, arg_buf, f->u.cfunc.magic); + ret_val = func.generic_magic (ctx, this_obj, argc, arg_copy, f->u.cfunc.magic); break; case JS_CFUNC_f_f: { double d1; - - if (unlikely (JS_ToFloat64 (ctx, &d1, arg_buf[0]))) { + if (unlikely (JS_ToFloat64 (ctx, &d1, arg_copy[0]))) { ret_val = JS_EXCEPTION; break; } @@ -4033,56 +4035,57 @@ JSValue js_call_c_function (JSContext *ctx, JSValue func_obj, JSValue this_obj, } break; case JS_CFUNC_f_f_f: { double d1, d2; - - if (unlikely (JS_ToFloat64 (ctx, &d1, arg_buf[0]))) { + if (unlikely (JS_ToFloat64 (ctx, &d1, arg_copy[0]))) { ret_val = JS_EXCEPTION; break; } - if (unlikely (JS_ToFloat64 (ctx, &d2, arg_buf[1]))) { + if (unlikely (JS_ToFloat64 (ctx, &d2, arg_copy[1]))) { ret_val = JS_EXCEPTION; break; } ret_val = JS_NewFloat64 (ctx, func.f_f_f (d1, d2)); } break; - /* Fixed-arity fast paths - direct call without argc/argv marshaling */ + /* Fixed-arity fast paths — args passed by value at dispatch time */ case JS_CFUNC_0: ret_val = func.f0 (ctx, this_obj); break; case JS_CFUNC_1: - ret_val = func.f1 (ctx, this_obj, arg_buf[0]); + ret_val = func.f1 (ctx, this_obj, arg_copy[0]); break; case JS_CFUNC_2: - ret_val = func.f2 (ctx, this_obj, arg_buf[0], arg_buf[1]); + ret_val = func.f2 (ctx, this_obj, arg_copy[0], arg_copy[1]); break; case JS_CFUNC_3: - ret_val = func.f3 (ctx, this_obj, arg_buf[0], arg_buf[1], arg_buf[2]); + ret_val = func.f3 (ctx, this_obj, arg_copy[0], arg_copy[1], arg_copy[2]); break; case JS_CFUNC_4: - ret_val = func.f4 (ctx, this_obj, arg_buf[0], arg_buf[1], arg_buf[2], arg_buf[3]); + ret_val = func.f4 (ctx, this_obj, arg_copy[0], arg_copy[1], arg_copy[2], arg_copy[3]); break; /* Pure functions (no this_val) */ case JS_CFUNC_PURE: - ret_val = func.pure (ctx, argc, arg_buf); + ret_val = func.pure (ctx, argc, arg_copy); break; case JS_CFUNC_PURE_0: ret_val = func.pure0 (ctx); break; case JS_CFUNC_PURE_1: - ret_val = func.pure1 (ctx, arg_buf[0]); + ret_val = func.pure1 (ctx, arg_copy[0]); break; case JS_CFUNC_PURE_2: - ret_val = func.pure2 (ctx, arg_buf[0], arg_buf[1]); + ret_val = func.pure2 (ctx, arg_copy[0], arg_copy[1]); break; case JS_CFUNC_PURE_3: - ret_val = func.pure3 (ctx, arg_buf[0], arg_buf[1], arg_buf[2]); + ret_val = func.pure3 (ctx, arg_copy[0], arg_copy[1], arg_copy[2]); break; case JS_CFUNC_PURE_4: - ret_val = func.pure4 (ctx, arg_buf[0], arg_buf[1], arg_buf[2], arg_buf[3]); + ret_val = func.pure4 (ctx, arg_copy[0], arg_copy[1], arg_copy[2], arg_copy[3]); break; default: abort (); } + ctx->c_call_root = root.prev; + if (unlikely (ctx->trace_hook) && (ctx->trace_type & JS_HOOK_RET)) ctx->trace_hook (ctx, JS_HOOK_RET, NULL, ctx->trace_data); @@ -7868,20 +7871,12 @@ static JSValue js_cell_array (JSContext *ctx, JSValue this_val, int argc, JSValu } /* array.reduce(arr, fn, initial, reverse) */ -/* GC-safe reduce: re-chase array after each call */ static JSValue js_cell_array_reduce (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; if (!JS_IsArray (argv[0])) return JS_NULL; if (!JS_IsFunction (argv[1])) return JS_NULL; - /* GC-safe: root argv[0] and argv[1] for the duration of this function */ - JSGCRef arr_ref, func_ref; - JS_PushGCRef (ctx, &arr_ref); - JS_PushGCRef (ctx, &func_ref); - arr_ref.val = argv[0]; - func_ref.val = argv[1]; - - JSArray *arr = JS_VALUE_GET_ARRAY (arr_ref.val); + JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]); word_t len = arr->len; int reverse = argc > 3 && JS_ToBool (ctx, argv[3]); @@ -7889,65 +7884,63 @@ static JSValue js_cell_array_reduce (JSContext *ctx, JSValue this_val, int argc, JSValue acc; if (argc < 3 || JS_IsNull (argv[2])) { - if (len == 0) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_NULL; } - if (len == 1) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return arr->values[0]; } + if (len == 0) return JS_NULL; + if (len == 1) return arr->values[0]; if (reverse) { acc = arr->values[len - 1]; for (word_t i = len - 1; i > 0; i--) { - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if (i - 1 >= arr->len) continue; JSValue args[2] = { acc, arr->values[i - 1] }; JS_PUSH_VALUE (ctx, acc); - JSValue new_acc = JS_CallInternal (ctx, func_ref.val, JS_NULL, 2, args, 0); + JSValue new_acc = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0); JS_POP_VALUE (ctx, acc); - if (JS_IsException (new_acc)) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } + if (JS_IsException (new_acc)) return JS_EXCEPTION; acc = new_acc; } } else { acc = arr->values[0]; for (word_t i = 1; i < len; i++) { - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if (i >= arr->len) break; JSValue args[2] = { acc, arr->values[i] }; JS_PUSH_VALUE (ctx, acc); - JSValue new_acc = JS_CallInternal (ctx, func_ref.val, JS_NULL, 2, args, 0); + JSValue new_acc = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0); JS_POP_VALUE (ctx, acc); - if (JS_IsException (new_acc)) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } + if (JS_IsException (new_acc)) return JS_EXCEPTION; acc = new_acc; } } } else { - if (len == 0) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return argv[2]; } + if (len == 0) return argv[2]; acc = argv[2]; if (reverse) { for (word_t i = len; i > 0; i--) { - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if (i - 1 >= arr->len) continue; JSValue args[2] = { acc, arr->values[i - 1] }; JS_PUSH_VALUE (ctx, acc); - JSValue new_acc = JS_CallInternal (ctx, func_ref.val, JS_NULL, 2, args, 0); + JSValue new_acc = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0); JS_POP_VALUE (ctx, acc); - if (JS_IsException (new_acc)) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } + if (JS_IsException (new_acc)) return JS_EXCEPTION; acc = new_acc; } } else { for (word_t i = 0; i < len; i++) { - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if (i >= arr->len) break; JSValue args[2] = { acc, arr->values[i] }; JS_PUSH_VALUE (ctx, acc); - JSValue new_acc = JS_CallInternal (ctx, func_ref.val, JS_NULL, 2, args, 0); + JSValue new_acc = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0); JS_POP_VALUE (ctx, acc); - if (JS_IsException (new_acc)) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } + if (JS_IsException (new_acc)) return JS_EXCEPTION; acc = new_acc; } } } - JS_PopGCRef (ctx, &func_ref); - JS_PopGCRef (ctx, &arr_ref); return acc; } @@ -7957,185 +7950,139 @@ static JSValue js_cell_array_for (JSContext *ctx, JSValue this_val, int argc, JS if (!JS_IsArray (argv[0])) return JS_NULL; if (!JS_IsFunction (argv[1])) return JS_NULL; - /* GC-safe: root argv[0] and argv[1] */ - JSGCRef arr_ref, func_ref; - JS_PushGCRef (ctx, &arr_ref); - JS_PushGCRef (ctx, &func_ref); - arr_ref.val = argv[0]; - func_ref.val = argv[1]; - - JSArray *arr = JS_VALUE_GET_ARRAY (arr_ref.val); + JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]); word_t len = arr->len; - if (len == 0) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_NULL; } + if (len == 0) return JS_NULL; int reverse = argc > 2 && JS_ToBool (ctx, argv[2]); JSValue exit_val = argc > 3 ? argv[3] : JS_NULL; /* Determine function arity */ - int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (func_ref.val))->length; + int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (argv[1]))->length; if (reverse) { for (word_t i = len; i > 0; i--) { - /* Re-chase array each iteration */ - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if (i - 1 >= arr->len) continue; JSValue result; if (arity == 1) { JSValue item = arr->values[i - 1]; - result = JS_CallInternal (ctx, func_ref.val, JS_NULL, 1, &item, 0); + result = JS_CallInternal (ctx, argv[1], JS_NULL, 1, &item, 0); } else { JSValue args[2]; args[0] = arr->values[i - 1]; args[1] = JS_NewInt32 (ctx, (int32_t)(i - 1)); - result = JS_CallInternal (ctx, func_ref.val, JS_NULL, 2, args, 0); + result = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0); } - - if (JS_IsException (result)) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } - if (!JS_IsNull (exit_val) && js_strict_eq (ctx, result, exit_val)) { - JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); + if (JS_IsException (result)) return JS_EXCEPTION; + if (!JS_IsNull (exit_val) && js_strict_eq (ctx, result, exit_val)) return result; - } } } else { for (word_t i = 0; i < len; i++) { - /* Re-chase array each iteration */ - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if (i >= arr->len) break; JSValue result; if (arity == 1) { JSValue item = arr->values[i]; - result = JS_CallInternal (ctx, func_ref.val, JS_NULL, 1, &item, 0); + result = JS_CallInternal (ctx, argv[1], JS_NULL, 1, &item, 0); } else { JSValue args[2]; args[0] = arr->values[i]; args[1] = JS_NewInt32 (ctx, (int32_t)i); - result = JS_CallInternal (ctx, func_ref.val, JS_NULL, 2, args, 0); + result = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0); } - - if (JS_IsException (result)) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } - if (!JS_IsNull (exit_val) && js_strict_eq (ctx, result, exit_val)) { - JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); + if (JS_IsException (result)) return JS_EXCEPTION; + if (!JS_IsNull (exit_val) && js_strict_eq (ctx, result, exit_val)) return result; - } } } - JS_PopGCRef (ctx, &func_ref); - JS_PopGCRef (ctx, &arr_ref); return JS_NULL; } -/* array.find(arr, fn, reverse, from) */ -/* array.find(arr, fn, reverse, from) - GC-safe */ +/* array.find(arr, fn_or_value, reverse, from) */ static JSValue js_cell_array_find (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; if (!JS_IsArray (argv[0])) return JS_NULL; - /* GC-safe: root argv[0] */ - JSGCRef arr_ref; - JS_PushGCRef (ctx, &arr_ref); - arr_ref.val = argv[0]; - - JSArray *arr = JS_VALUE_GET_ARRAY (arr_ref.val); + JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]); word_t len = arr->len; int reverse = argc > 2 && JS_ToBool (ctx, argv[2]); int32_t from; if (argc > 3 && !JS_IsNull (argv[3])) { - if (JS_ToInt32 (ctx, &from, argv[3])) { JS_PopGCRef (ctx, &arr_ref); return JS_NULL; } + if (JS_ToInt32 (ctx, &from, argv[3])) return JS_NULL; } else { from = reverse ? (int32_t)(len - 1) : 0; } if (!JS_IsFunction (argv[1])) { - /* Compare exactly - no GC concerns since no calls */ JSValue target = argv[1]; if (reverse) { for (int32_t i = from; i >= 0; i--) { - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if ((word_t)i >= arr->len) continue; - if (js_strict_eq (ctx, arr->values[i], target)) { - JS_PopGCRef (ctx, &arr_ref); + if (js_strict_eq (ctx, arr->values[i], target)) return JS_NewInt32 (ctx, i); - } } } else { for (word_t i = (word_t)from; i < len; i++) { - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if (i >= arr->len) break; - if (js_strict_eq (ctx, arr->values[i], target)) { - JS_PopGCRef (ctx, &arr_ref); + if (js_strict_eq (ctx, arr->values[i], target)) return JS_NewInt32 (ctx, (int32_t)i); - } } } - JS_PopGCRef (ctx, &arr_ref); return JS_NULL; } - /* Use function predicate - must re-chase after each call */ - JSGCRef func_ref; - JS_PushGCRef (ctx, &func_ref); - func_ref.val = argv[1]; - int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (func_ref.val))->length; + /* Use function predicate — re-chase after each call */ + int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (argv[1]))->length; if (arity == 2) { if (reverse) { for (int32_t i = from; i >= 0; i--) { - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if ((word_t)i >= arr->len) continue; JSValue args[2] = { arr->values[i], JS_NewInt32 (ctx, i) }; - JSValue result = JS_CallInternal (ctx, func_ref.val, JS_NULL, 2, args, 0); - if (JS_IsException (result)) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } - if (JS_ToBool (ctx, result)) { - JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); - return JS_NewInt32 (ctx, i); - } + JSValue result = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0); + if (JS_IsException (result)) return JS_EXCEPTION; + if (JS_ToBool (ctx, result)) return JS_NewInt32 (ctx, i); } } else { for (word_t i = (word_t)from; i < len; i++) { - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if (i >= arr->len) break; JSValue args[2] = { arr->values[i], JS_NewInt32 (ctx, (int32_t)i) }; - JSValue result = JS_CallInternal (ctx, func_ref.val, JS_NULL, 2, args, 0); - if (JS_IsException (result)) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } - if (JS_ToBool (ctx, result)) { - JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); - return JS_NewInt32 (ctx, (int32_t)i); - } + JSValue result = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0); + if (JS_IsException (result)) return JS_EXCEPTION; + if (JS_ToBool (ctx, result)) return JS_NewInt32 (ctx, (int32_t)i); } } } else { if (reverse) { for (int32_t i = from; i >= 0; i--) { - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if ((word_t)i >= arr->len) continue; JSValue item = arr->values[i]; - JSValue result = JS_CallInternal (ctx, func_ref.val, JS_NULL, 1, &item, 0); - if (JS_IsException (result)) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } - if (JS_ToBool (ctx, result)) { - JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); - return JS_NewInt32 (ctx, i); - } + JSValue result = JS_CallInternal (ctx, argv[1], JS_NULL, 1, &item, 0); + if (JS_IsException (result)) return JS_EXCEPTION; + if (JS_ToBool (ctx, result)) return JS_NewInt32 (ctx, i); } } else { for (word_t i = (word_t)from; i < len; i++) { - arr = JS_VALUE_GET_ARRAY (arr_ref.val); + arr = JS_VALUE_GET_ARRAY (argv[0]); if (i >= arr->len) break; JSValue item = arr->values[i]; - JSValue result = JS_CallInternal (ctx, func_ref.val, JS_NULL, 1, &item, 0); - if (JS_IsException (result)) { JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } - if (JS_ToBool (ctx, result)) { - JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &arr_ref); - return JS_NewInt32 (ctx, (int32_t)i); - } + JSValue result = JS_CallInternal (ctx, argv[1], JS_NULL, 1, &item, 0); + if (JS_IsException (result)) return JS_EXCEPTION; + if (JS_ToBool (ctx, result)) return JS_NewInt32 (ctx, (int32_t)i); } } } - JS_PopGCRef (ctx, &func_ref); - JS_PopGCRef (ctx, &arr_ref); return JS_NULL; } @@ -8604,55 +8551,29 @@ static JSValue js_cell_fn_apply (JSContext *ctx, JSValue this_val, int argc, JSV if (argc < 1) return JS_NULL; if (!JS_IsFunction (argv[0])) return argv[0]; - JSGCRef func_ref, args_ref; - JS_PushGCRef (ctx, &func_ref); - JS_PushGCRef (ctx, &args_ref); - func_ref.val = argv[0]; - args_ref.val = argc >= 2 ? argv[1] : JS_NULL; + if (argc < 2) + return JS_CallInternal (ctx, argv[0], JS_NULL, 0, NULL, 0); - if (argc < 2) { - JSValue result = JS_CallInternal (ctx, func_ref.val, JS_NULL, 0, NULL, 0); - JS_PopGCRef (ctx, &args_ref); - JS_PopGCRef (ctx, &func_ref); - return result; - } + if (!JS_IsArray (argv[1])) + return JS_CallInternal (ctx, argv[0], JS_NULL, 1, &argv[1], 0); - if (!JS_IsArray (args_ref.val)) { - /* Wrap single value in array */ - JSValue result = JS_CallInternal (ctx, func_ref.val, JS_NULL, 1, &args_ref.val, 0); - JS_PopGCRef (ctx, &args_ref); - JS_PopGCRef (ctx, &func_ref); - return result; - } - - JSArray *arr = JS_VALUE_GET_ARRAY (args_ref.val); + JSArray *arr = JS_VALUE_GET_ARRAY (argv[1]); int len = arr->len; - if (len == 0) { - JSValue result = JS_CallInternal (ctx, func_ref.val, JS_NULL, 0, NULL, 0); - JS_PopGCRef (ctx, &args_ref); - JS_PopGCRef (ctx, &func_ref); - return result; - } + if (len == 0) + return JS_CallInternal (ctx, argv[0], JS_NULL, 0, NULL, 0); JSValue *args = js_malloc (ctx, sizeof (JSValue) * len); - if (!args) { - JS_PopGCRef (ctx, &args_ref); - JS_PopGCRef (ctx, &func_ref); - return JS_EXCEPTION; - } - arr = JS_VALUE_GET_ARRAY (args_ref.val); /* re-chase after malloc via rooted ref */ + if (!args) return JS_EXCEPTION; + arr = JS_VALUE_GET_ARRAY (argv[1]); /* re-chase after malloc */ for (int i = 0; i < len; i++) { args[i] = arr->values[i]; } - JSValue result = JS_CallInternal (ctx, func_ref.val, JS_NULL, len, args, 0); + JSValue result = JS_CallInternal (ctx, argv[0], JS_NULL, len, args, 0); js_free (ctx, args); - - JS_PopGCRef (ctx, &args_ref); - JS_PopGCRef (ctx, &func_ref); return result; } @@ -9233,15 +9154,9 @@ static JSValue js_mach_load (JSContext *ctx, JSValue this_val, int argc, JSValue JSValue env = (argc >= 2 && mist_is_gc_object (argv[1])) ? argv[1] : JS_NULL; - JSGCRef env_ref; - JS_PushGCRef (ctx, &env_ref); - env_ref.val = env; - - JSCodeRegister *code = JS_LoadMachCode (ctx, mc, env_ref.val); + JSCodeRegister *code = JS_LoadMachCode (ctx, mc, env); JS_FreeMachCode (mc); - JSValue result = JS_CallRegisterVM (ctx, code, ctx->global_obj, 0, NULL, env_ref.val, JS_NULL); - JS_PopGCRef (ctx, &env_ref); - return result; + return JS_CallRegisterVM (ctx, code, ctx->global_obj, 0, NULL, env, JS_NULL); } /* mach_eval_mcode(name, mcode_json, env?) - compile mcode IR and run via register VM */ @@ -9281,14 +9196,9 @@ static JSValue js_mach_eval_mcode (JSContext *ctx, JSValue this_val, int argc, J JSValue env = (argc >= 3 && mist_is_gc_object (argv[2])) ? argv[2] : JS_NULL; - JSGCRef env_ref; - JS_PushGCRef (ctx, &env_ref); - env_ref.val = env; - - JSCodeRegister *code = JS_LoadMachCode (ctx, mc, env_ref.val); + JSCodeRegister *code = JS_LoadMachCode (ctx, mc, env); JS_FreeMachCode (mc); - JSValue result = JS_CallRegisterVM (ctx, code, ctx->global_obj, 0, NULL, env_ref.val, JS_NULL); - JS_PopGCRef (ctx, &env_ref); + JSValue result = JS_CallRegisterVM (ctx, code, ctx->global_obj, 0, NULL, env, JS_NULL); JS_FreeCString (ctx, name); return result; } @@ -9328,18 +9238,13 @@ static JSValue js_mach_dump_mcode (JSContext *ctx, JSValue this_val, int argc, J JSValue env = (argc >= 3 && mist_is_gc_object (argv[2])) ? argv[2] : JS_NULL; - JSGCRef env_ref; - JS_PushGCRef (ctx, &env_ref); - env_ref.val = env; - /* Serialize to binary then dump */ size_t bin_size; uint8_t *bin = JS_SerializeMachCode (mc, &bin_size); JS_FreeMachCode (mc); - JS_DumpMachBin (ctx, bin, bin_size, env_ref.val); + JS_DumpMachBin (ctx, bin, bin_size, env); sys_free (bin); - JS_PopGCRef (ctx, &env_ref); JS_FreeCString (ctx, name); return JS_NULL; } @@ -9888,19 +9793,11 @@ static JSValue js_cell_push (JSContext *ctx, JSValue this_val, int argc, JSValue if (!JS_IsArray (argv[0])) return JS_NULL; - JSGCRef arr_ref; - JS_PushGCRef (ctx, &arr_ref); - arr_ref.val = argv[0]; - for (int i = 1; i < argc; i++) { - if (js_intrinsic_array_push (ctx, &arr_ref.val, argv[i]) < 0) { - JS_PopGCRef (ctx, &arr_ref); + if (js_intrinsic_array_push (ctx, &argv[0], argv[i]) < 0) return JS_EXCEPTION; - } } - argv[0] = arr_ref.val; - JS_PopGCRef (ctx, &arr_ref); return JS_NULL; } @@ -10171,50 +10068,34 @@ static JSValue js_cell_call (JSContext *ctx, JSValue this_val, int argc, JSValue if (argc < 1) return JS_ThrowTypeError (ctx, "call requires a function argument"); - JSGCRef func_ref, this_ref, args_ref; - JS_PushGCRef (ctx, &func_ref); - JS_PushGCRef (ctx, &this_ref); - JS_PushGCRef (ctx, &args_ref); - func_ref.val = argv[0]; - this_ref.val = argc >= 2 ? argv[1] : JS_NULL; - args_ref.val = argc >= 3 ? argv[2] : JS_NULL; - - if (!JS_IsFunction (func_ref.val)) { - JS_PopGCRef (ctx, &args_ref); - JS_PopGCRef (ctx, &this_ref); - JS_PopGCRef (ctx, &func_ref); + if (!JS_IsFunction (argv[0])) return JS_ThrowTypeError (ctx, "first argument must be a function"); - } - if (argc < 3 || JS_IsNull (args_ref.val)) { - JSValue ret = JS_CallInternal (ctx, func_ref.val, this_ref.val, 0, NULL, 0); - JS_PopGCRef (ctx, &args_ref); + JSGCRef this_ref; + JS_PushGCRef (ctx, &this_ref); + this_ref.val = argc >= 2 ? argv[1] : JS_NULL; + + if (argc < 3 || JS_IsNull (argv[2])) { + JSValue ret = JS_CallInternal (ctx, argv[0], this_ref.val, 0, NULL, 0); JS_PopGCRef (ctx, &this_ref); - JS_PopGCRef (ctx, &func_ref); return ret; } - if (!JS_IsArray (args_ref.val)) { - JS_PopGCRef (ctx, &args_ref); + if (!JS_IsArray (argv[2])) { JS_PopGCRef (ctx, &this_ref); - JS_PopGCRef (ctx, &func_ref); return JS_ThrowTypeError (ctx, "third argument must be an array"); } uint32_t len; - JSValue *tab = build_arg_list (ctx, &len, &args_ref.val); + JSValue *tab = build_arg_list (ctx, &len, &argv[2]); if (!tab) { - JS_PopGCRef (ctx, &args_ref); JS_PopGCRef (ctx, &this_ref); - JS_PopGCRef (ctx, &func_ref); return JS_EXCEPTION; } - JSValue ret = JS_CallInternal (ctx, func_ref.val, this_ref.val, len, tab, 0); + JSValue ret = JS_CallInternal (ctx, argv[0], this_ref.val, len, tab, 0); free_arg_list (ctx, tab, len); - JS_PopGCRef (ctx, &args_ref); JS_PopGCRef (ctx, &this_ref); - JS_PopGCRef (ctx, &func_ref); return ret; } @@ -10395,32 +10276,17 @@ static JSValue js_cell_ends_with(JSContext *ctx, JSValue this_val, int argc, JSV } /* every(arr, pred) — find(arr, x => !pred(x)) == null */ -static JSValue js_cell_every(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { +static JSValue js_cell_every (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; - if (!JS_IsArray(argv[0]) || !JS_IsFunction(argv[1])) return JS_NULL; - JSGCRef arr_ref, fn_ref; - JS_PushGCRef(ctx, &arr_ref); - JS_PushGCRef(ctx, &fn_ref); - arr_ref.val = argv[0]; - fn_ref.val = argv[1]; - JSArray *arr = JS_VALUE_GET_ARRAY(arr_ref.val); + if (!JS_IsArray (argv[0]) || !JS_IsFunction (argv[1])) return JS_NULL; + JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]); for (int i = 0; i < arr->len; i++) { JSValue elem = arr->values[i]; - JSValue r = JS_CallInternal(ctx, fn_ref.val, JS_NULL, 1, &elem, 0); - arr = JS_VALUE_GET_ARRAY(arr_ref.val); - if (JS_IsException(r)) { - JS_PopGCRef(ctx, &fn_ref); - JS_PopGCRef(ctx, &arr_ref); - return r; - } - if (!JS_ToBool(ctx, r)) { - JS_PopGCRef(ctx, &fn_ref); - JS_PopGCRef(ctx, &arr_ref); - return JS_FALSE; - } + JSValue r = JS_CallInternal (ctx, argv[1], JS_NULL, 1, &elem, 0); + arr = JS_VALUE_GET_ARRAY (argv[0]); + if (JS_IsException (r)) return r; + if (!JS_ToBool (ctx, r)) return JS_FALSE; } - JS_PopGCRef(ctx, &fn_ref); - JS_PopGCRef(ctx, &arr_ref); return JS_TRUE; } From f296a0c10dcc5039f614c729fa7519c806a86bf0 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 13 Feb 2026 03:08:27 -0600 Subject: [PATCH 2/7] fix segv --- source/mach.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/mach.c b/source/mach.c index bc6e8e9f..394bd343 100644 --- a/source/mach.c +++ b/source/mach.c @@ -767,9 +767,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, /* Setup initial frame — wrap top-level code in a function object so that returning from a called register function can read code/env from frame */ JSValue top_fn = js_new_register_function(ctx, code, env_gc.val, of_gc.val); - JS_PopGCRef(ctx, &of_gc); env = env_gc.val; /* refresh — GC may have moved env during allocation */ - JS_PopGCRef(ctx, &env_gc); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); frame->function = top_fn; frame->slots[0] = this_gc.val; /* slot 0 = this */ @@ -780,6 +778,8 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, } for (int i = nargs_copy - 1; i >= 0; i--) JS_PopGCRef(ctx, &arg_gcs[i]); JS_PopGCRef(ctx, &this_gc); + JS_PopGCRef(ctx, &of_gc); + JS_PopGCRef(ctx, &env_gc); uint32_t pc = code->entry_point; JSValue result = JS_NULL; From cb9d6e0c0e275cccff5679ea7473413c2098ca4c Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 13 Feb 2026 04:03:36 -0600 Subject: [PATCH 3/7] mmap for poison heap --- source/cell.c | 2 +- source/quickjs-internal.h | 10 +++++++++- source/runtime.c | 19 ++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/source/cell.c b/source/cell.c index e356b35e..622d35da 100644 --- a/source/cell.c +++ b/source/cell.c @@ -377,7 +377,7 @@ int cell_init(int argc, char **argv) free(boot_data); return 1; } - JSContext *ctx = JS_NewContextWithHeapSize(g_runtime, 16 * 1024 * 1024); + JSContext *ctx = JS_NewContextWithHeapSize(g_runtime, 1024 * 1024); if (!ctx) { printf("Failed to create JS context\n"); free(boot_data); JS_FreeRuntime(g_runtime); diff --git a/source/quickjs-internal.h b/source/quickjs-internal.h index e5922f25..afb5bb8a 100644 --- a/source/quickjs-internal.h +++ b/source/quickjs-internal.h @@ -78,7 +78,7 @@ */ // #define DUMP_BYTECODE (1) /* dump GC summary: old/new heap, recovery %, heap growth */ -// #define DUMP_GC +#define DUMP_GC /* dump detailed GC: roots, scanning, object traversal (implies DUMP_GC) */ // #define DUMP_GC_DETAIL #ifdef DUMP_GC_DETAIL @@ -115,6 +115,14 @@ #define gc_poison_region(addr, size) ((void)0) #define gc_unpoison_region(addr, size) ((void)0) #endif + +#include +#include + +static inline size_t poison_page_align(size_t size) { + size_t ps = (size_t)sysconf(_SC_PAGESIZE); + return (size + ps - 1) & ~(ps - 1); +} #endif /* POISON_HEAP */ #ifdef HAVE_ASAN diff --git a/source/runtime.c b/source/runtime.c index 5f0d69ad..069bb0f6 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -935,7 +935,10 @@ void buddy_destroy (BuddyAllocator *b) { static void *heap_block_alloc(JSRuntime *rt, size_t size) { #ifdef POISON_HEAP (void)rt; - return malloc(size); + size = poison_page_align(size); + void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_ANON | MAP_PRIVATE, -1, 0); + return (p == MAP_FAILED) ? NULL : p; #else return buddy_alloc(&rt->buddy, size); #endif @@ -944,8 +947,9 @@ static void *heap_block_alloc(JSRuntime *rt, size_t size) { static void heap_block_free(JSRuntime *rt, void *ptr, size_t size) { #ifdef POISON_HEAP (void)rt; - (void)size; - /* Don't free - leave it poisoned to catch stale accesses */ + /* mmap'd memory is intentionally never munmap'd so virtual addresses + are never reused (preventing stale pointer aliasing). Pages stay + resident because chase() reads forwarding pointers from old blocks. */ gc_poison_region(ptr, size); #else buddy_free(&rt->buddy, ptr, size); @@ -1174,10 +1178,16 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) { while (new_size < need && new_size < (1ULL << BUDDY_MAX_ORDER)) new_size *= 2; } +#ifdef POISON_HEAP + new_size = poison_page_align(new_size); +#endif uint8_t *new_block = heap_block_alloc (rt, new_size); if (!new_block) { /* Try with same size */ new_size = ctx->current_block_size; +#ifdef POISON_HEAP + new_size = poison_page_align(new_size); +#endif new_block = heap_block_alloc (rt, new_size); if (!new_block) return -1; } @@ -1432,6 +1442,9 @@ JSContext *JS_NewContextRawWithHeapSize (JSRuntime *rt, size_t heap_size) { } /* Allocate initial heap block for bump allocation */ +#ifdef POISON_HEAP + heap_size = poison_page_align(heap_size); +#endif ctx->current_block_size = heap_size; ctx->next_block_size = ctx->current_block_size; ctx->heap_base = heap_block_alloc (rt, ctx->current_block_size); From 9f0fd84f4fe36853ee362a73d19a3216ff263e0b Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 13 Feb 2026 04:33:32 -0600 Subject: [PATCH 4/7] fix growing gc --- source/runtime.c | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/source/runtime.c b/source/runtime.c index 069bb0f6..aa3b30f1 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -724,10 +724,27 @@ void *js_malloc (JSContext *ctx, size_t size) { JS_ThrowOutOfMemory (ctx); return NULL; } - /* Re-check after GC */ + /* Re-check after GC — if still no room, grow and retry. + The second GC is cheap: data was just compacted so there is + almost no garbage to skip. */ if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) { - JS_ThrowOutOfMemory (ctx); - return NULL; + size_t need = (size_t)((uint8_t *)ctx->heap_free - (uint8_t *)ctx->heap_base) + size; + size_t ns = ctx->current_block_size; + while (ns < need && ns < (1ULL << BUDDY_MAX_ORDER)) + ns *= 2; + ctx->next_block_size = ns; + if (ctx_gc (ctx, 1, size) < 0) { + JS_ThrowOutOfMemory (ctx); + return NULL; + } + /* The retry pass relocates compacted data — 0% recovery is expected. + Reset next_block_size so the poor-recovery heuristic inside ctx_gc + doesn't cascade into further unnecessary doubling. */ + ctx->next_block_size = ctx->current_block_size; + if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) { + JS_ThrowOutOfMemory (ctx); + return NULL; + } } } #endif @@ -1167,15 +1184,17 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) { printf("ctx_gc: from_base=%p from_end=%p size=%zu\n", (void*)from_base, (void*)from_end, old_heap_size); #endif - /* Request new block from runtime. - When allow_grow is set and the pending allocation won't fit in the - current next_block_size, jump straight to a block that can hold - live_data + alloc_size instead of doubling one step at a time. */ - size_t new_size = ctx->next_block_size; + /* Size the new block. Start at current_block_size (guaranteed >= used + portion, so all live data fits). Only grow when: + - next_block_size was bumped by the poor-recovery heuristic, or + - alloc_size alone exceeds the block (rare large allocation). + Crucially, do NOT add live_est to the sizing — it counts garbage + and causes exponential heap growth even with excellent recovery. */ + size_t new_size = ctx->current_block_size; if (allow_grow) { - size_t live_est = (size_t)(from_end - from_base); /* upper bound on live data */ - size_t need = live_est + alloc_size; - while (new_size < need && new_size < (1ULL << BUDDY_MAX_ORDER)) + if (ctx->next_block_size > new_size) + new_size = ctx->next_block_size; + while (new_size < alloc_size && new_size < (1ULL << BUDDY_MAX_ORDER)) new_size *= 2; } #ifdef POISON_HEAP @@ -1324,12 +1343,15 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) { } #ifdef DUMP_GC - printf ("\nGC: %zu -> %zu bytes, recovered %zu (%.1f%%)%s\n", + printf ("\nGC: %zu -> %zu bytes (used %zu -> %zu), recovered %zu (%.1f%%)%s\n", old_heap_size, new_size, + old_used, + new_used, recovered, old_used > 0 ? (recovered * 100.0 / old_used) : 0.0, will_grow ? ", heap will grow" : ""); + fflush(stdout); #endif return 0; From 0a680a0cd3df77a07ba43e7fc624488d5f6e03ff Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 13 Feb 2026 05:03:45 -0600 Subject: [PATCH 5/7] gc print --- source/runtime.c | 55 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/source/runtime.c b/source/runtime.c index aa3b30f1..56de2c58 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -728,10 +728,16 @@ void *js_malloc (JSContext *ctx, size_t size) { The second GC is cheap: data was just compacted so there is almost no garbage to skip. */ if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) { - size_t need = (size_t)((uint8_t *)ctx->heap_free - (uint8_t *)ctx->heap_base) + size; + size_t live = (size_t)((uint8_t *)ctx->heap_free - (uint8_t *)ctx->heap_base); + size_t need = live + size; size_t ns = ctx->current_block_size; while (ns < need && ns < (1ULL << BUDDY_MAX_ORDER)) ns *= 2; +#ifdef DUMP_GC + printf (" growing %zu -> %zu for %zu byte alloc (live %zu)\n", + ctx->current_block_size, ns, size, live); + fflush (stdout); +#endif ctx->next_block_size = ns; if (ctx_gc (ctx, 1, size) < 0) { JS_ThrowOutOfMemory (ctx); @@ -1332,7 +1338,7 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) { #ifdef DUMP_GC int will_grow = 0; #endif - if (allow_grow && old_used > 0 && recovered < old_used / 5) { + if (allow_grow && recovered > 0 && old_used > 0 && recovered < old_used / 5) { size_t doubled = new_size * 2; if (doubled <= (1ULL << BUDDY_MAX_ORDER)) { ctx->next_block_size = doubled; @@ -1343,15 +1349,42 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) { } #ifdef DUMP_GC - printf ("\nGC: %zu -> %zu bytes (used %zu -> %zu), recovered %zu (%.1f%%)%s\n", - old_heap_size, - new_size, - old_used, - new_used, - recovered, - old_used > 0 ? (recovered * 100.0 / old_used) : 0.0, - will_grow ? ", heap will grow" : ""); - fflush(stdout); + { + /* Walk to-space and tally memory by object type */ + static const char *type_names[] = { + [OBJ_ARRAY] = "array", + [OBJ_TEXT] = "text", + [OBJ_RECORD] = "record", + [OBJ_FUNCTION] = "function", + [OBJ_FRAME] = "frame", + }; + size_t type_bytes[8] = {0}; + size_t type_count[8] = {0}; + uint8_t *p = to_base; + while (p < to_free) { + uint8_t t = objhdr_type (*(objhdr_t *)p); + size_t sz = gc_object_size (p); + type_bytes[t] += sz; + type_count[t]++; + p += sz; + } + + printf ("\nGC: %zu -> %zu bytes, recovered %zu (%.1f%%)%s\n", + old_heap_size, + new_size, + recovered, + old_used > 0 ? (recovered * 100.0 / old_used) : 0.0, + will_grow ? ", will grow (poor recovery)" : ""); + printf (" live %zu / %zu bytes (%.0f%% full):", + new_used, new_size, + new_size > 0 ? (new_used * 100.0 / new_size) : 0.0); + for (int i = 0; i < 7; i++) { + if (type_count[i] == 0) continue; + printf (" %s %zu(%zu)", type_names[i], type_bytes[i], type_count[i]); + } + printf ("\n"); + fflush (stdout); + } #endif return 0; From b960d03eeb2815e470c24501422cb941b21a6bda Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 13 Feb 2026 05:35:11 -0600 Subject: [PATCH 6/7] immediate ascii for string path --- source/quickjs-internal.h | 2 +- source/runtime.c | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/source/quickjs-internal.h b/source/quickjs-internal.h index afb5bb8a..681e9453 100644 --- a/source/quickjs-internal.h +++ b/source/quickjs-internal.h @@ -78,7 +78,7 @@ */ // #define DUMP_BYTECODE (1) /* dump GC summary: old/new heap, recovery %, heap growth */ -#define DUMP_GC +// #define DUMP_GC /* dump detailed GC: roots, scanning, object traversal (implies DUMP_GC) */ // #define DUMP_GC_DETAIL #ifdef DUMP_GC_DETAIL diff --git a/source/runtime.c b/source/runtime.c index 56de2c58..232aa778 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -1732,6 +1732,21 @@ static JSValue js_sub_string (JSContext *ctx, JSText *p, int start, int end) { return JS_MKPTR (p); } + /* Try immediate ASCII for short substrings (avoids heap allocation) */ + if (len <= MIST_ASCII_MAX_LEN && len > 0) { + char buf[MIST_ASCII_MAX_LEN]; + int all_ascii = 1; + for (i = 0; i < len; i++) { + uint32_t c = string_get (p, start + i); + if (c >= 128) { all_ascii = 0; break; } + buf[i] = (char)c; + } + if (all_ascii) { + JSValue imm = MIST_TryNewImmediateASCII (buf, len); + if (!JS_IsNull (imm)) return imm; + } + } + /* Root the source string as a JSValue so it survives js_alloc_string GC */ JSGCRef src_ref; JS_PushGCRef (ctx, &src_ref); From 77c02bf9bf1f2d562038e28007da37c9191b369d Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 13 Feb 2026 05:59:01 -0600 Subject: [PATCH 7/7] simplify text --- source/runtime.c | 101 ++++++++++------------------------------------- 1 file changed, 20 insertions(+), 81 deletions(-) diff --git a/source/runtime.c b/source/runtime.c index 232aa778..c02e2d3d 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -1732,21 +1732,6 @@ static JSValue js_sub_string (JSContext *ctx, JSText *p, int start, int end) { return JS_MKPTR (p); } - /* Try immediate ASCII for short substrings (avoids heap allocation) */ - if (len <= MIST_ASCII_MAX_LEN && len > 0) { - char buf[MIST_ASCII_MAX_LEN]; - int all_ascii = 1; - for (i = 0; i < len; i++) { - uint32_t c = string_get (p, start + i); - if (c >= 128) { all_ascii = 0; break; } - buf[i] = (char)c; - } - if (all_ascii) { - JSValue imm = MIST_TryNewImmediateASCII (buf, len); - if (!JS_IsNull (imm)) return imm; - } - } - /* Root the source string as a JSValue so it survives js_alloc_string GC */ JSGCRef src_ref; JS_PushGCRef (ctx, &src_ref); @@ -1775,20 +1760,10 @@ static JSValue js_sub_string_val (JSContext *ctx, JSValue src, int start, int en if (len <= 0) return JS_NewString (ctx, ""); if (MIST_IsImmediateASCII (src)) { - /* IMM: extract chars directly, try to return IMM */ - if (len <= MIST_ASCII_MAX_LEN) { - char buf[MIST_ASCII_MAX_LEN + 1]; - for (int i = 0; i < len; i++) - buf[i] = (char)MIST_GetImmediateASCIIChar (src, start + i); - return js_new_string8_len (ctx, buf, len); - } - /* Longer than 7 — shouldn't happen for IMM (max 7 chars) but handle it */ - JSText *str = js_alloc_string (ctx, len); - if (!str) return JS_EXCEPTION; + char buf[MIST_ASCII_MAX_LEN + 1]; for (int i = 0; i < len; i++) - string_put (str, i, MIST_GetImmediateASCIIChar (src, start + i)); - str->length = len; - return pretext_end (ctx, str); + buf[i] = (char)MIST_GetImmediateASCIIChar (src, start + i); + return js_new_string8_len (ctx, buf, len); } /* Heap string — delegate to existing js_sub_string */ @@ -1914,7 +1889,7 @@ JSText *pretext_concat_value (JSContext *ctx, JSText *s, JSValue v) { return pretext_write8 (ctx, s, (const uint8_t *)buf, len); } if (JS_IsText (v)) { - JSText *p = JS_VALUE_GET_PTR (v); + JSText *p = JS_VALUE_GET_STRING (v); return pretext_concat (ctx, s, p, 0, (uint32_t)JSText_len (p)); } JSValue v1 = JS_ToString (ctx, v); @@ -1942,6 +1917,20 @@ JSValue pretext_end (JSContext *ctx, JSText *s) { js_free (ctx, s); return JS_KEY_empty; } + /* Promote short ASCII strings to immediate values */ + if (len <= MIST_ASCII_MAX_LEN) { + char buf[MIST_ASCII_MAX_LEN]; + int all_ascii = 1; + for (int i = 0; i < len; i++) { + uint32_t c = string_get (s, i); + if (c >= 0x80) { all_ascii = 0; break; } + buf[i] = (char)c; + } + if (all_ascii) { + JSValue imm = MIST_TryNewImmediateASCII (buf, len); + if (!JS_IsNull (imm)) return imm; + } + } /* Set final length in capacity field and clear length for hash storage */ s->hdr = objhdr_set_cap56 (s->hdr, len); s->length = 0; @@ -2120,50 +2109,6 @@ void JS_FreeCString (JSContext *ctx, const char *ptr) { (void)ptr; } -JSValue JS_ConcatString1 (JSContext *ctx, const JSText *p1, const JSText *p2) { - JSText *p; - uint32_t len; - int len1 = (int)JSText_len (p1); - int len2 = (int)JSText_len (p2); - - len = len1 + len2; - /* len is uint32_t, JS_STRING_LEN_MAX is 56 bits, so this always fits */ - p = js_alloc_string (ctx, len); - if (!p) return JS_EXCEPTION; - /* Pack first string */ - { - int i; - for (i = 0; i < len1; i++) - string_put (p, i, string_get (p1, i)); - for (i = 0; i < len2; i++) - string_put (p, len1 + i, string_get (p2, i)); - } - return JS_MKPTR (p); -} - -// TODO: this function is fucked. -BOOL JS_ConcatStringInPlace (JSContext *ctx, JSText *p1, JSValue op2) { - (void)ctx; - if (JS_VALUE_GET_TAG (op2) == JS_TAG_STRING) { - JSText *p2 = JS_VALUE_GET_STRING (op2); - int64_t new_len; - int64_t len1 = JSText_len (p1); - int64_t len2 = JSText_len (p2); - - if (len2 == 0) return TRUE; - - new_len = len1 + len2; - - /* Append p2's characters using string_put/string_get */ - for (int64_t i = 0; i < len2; i++) { - string_put (p1, len1 + i, string_get (p2, i)); - } - p1->hdr = objhdr_set_cap56 (p1->hdr, new_len); - return TRUE; - } - return FALSE; -} - /* Helper for string value comparison (handles immediate and heap strings) */ int js_string_compare_value (JSContext *ctx, JSValue op1, JSValue op2, BOOL eq_only) { (void)ctx; @@ -2270,7 +2215,7 @@ JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2) { string_put (p, len1 + i, js_string_value_get (op2, i)); } p->length = new_len; - ret_val = JS_MKPTR (p); + ret_val = pretext_end (ctx, p); } return ret_val; @@ -2895,13 +2840,7 @@ JSValue JS_GetPropertyStr (JSContext *ctx, JSValue this_obj, const char *prop) { JS_PushGCRef (ctx, &obj_ref); obj_ref.val = this_obj; - /* Try immediate ASCII first */ - if (len <= MIST_ASCII_MAX_LEN) { - key = MIST_TryNewImmediateASCII (prop, len); - if (JS_IsNull (key)) { key = JS_NewStringLen (ctx, prop, len); } - } else { - key = JS_NewStringLen (ctx, prop, len); - } + key = JS_NewStringLen (ctx, prop, len); if (JS_IsException (key)) { JS_PopGCRef (ctx, &obj_ref); return JS_EXCEPTION;