From e73152bc368646d207bf74b8d1642ef22aaf3a62 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 21:49:22 -0600 Subject: [PATCH 1/5] better error when use without tet --- internal/shop.cm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/shop.cm b/internal/shop.cm index 78065c96..c8545949 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -1294,6 +1294,10 @@ function get_module(path, package_context) { } Shop.use = function use(path, _pkg_ctx) { + if (!is_text(path)) { + log.error("use() expects a text module path, but received a non-text value") + disrupt + } var package_context = is_core_dir(_pkg_ctx) ? 'core' : _pkg_ctx // Check for embedded module (static builds) var embed_key = 'embedded:' + path From 652e8a19f02172adb718f9f5f63e44a9381a8a3c Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 21:59:17 -0600 Subject: [PATCH 2/5] better fn error --- source/mach.c | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/source/mach.c b/source/mach.c index 93341ac9..61bd67f3 100644 --- a/source/mach.c +++ b/source/mach.c @@ -494,6 +494,37 @@ static int mach_check_call_arity(JSContext *ctx, JSFunction *fn, int argc) { return 1; } +/* Scan backwards from pc to find what loaded the callee register. + Returns the name in buf, or NULL if unknown. */ +static const char *mach_callee_name(JSContext *ctx, JSCodeRegister *code, + uint32_t pc, int reg, + char *buf, size_t buf_size) { + int hops = 4; /* limit move-chain depth */ + for (int i = (int)pc - 2; i >= 0 && hops > 0; i--) { + MachInstr32 prev = code->instructions[i]; + int prev_op = MACH_GET_OP(prev); + int prev_a = MACH_GET_A(prev); + if (prev_a != reg) continue; + if (prev_op == MACH_GETENV || prev_op == MACH_GETINTRINSIC) { + int bx = MACH_GET_Bx(prev); + if ((uint32_t)bx < code->cpool_count && JS_IsText(code->cpool[bx])) + return JS_KeyGetStr(ctx, buf, buf_size, code->cpool[bx]); + } + if (prev_op == MACH_LOAD_FIELD) { + int ci = MACH_GET_C(prev); + if ((uint32_t)ci < code->cpool_count && JS_IsText(code->cpool[ci])) + return JS_KeyGetStr(ctx, buf, buf_size, code->cpool[ci]); + } + if (prev_op == MACH_MOVE) { + reg = MACH_GET_B(prev); + hops--; + continue; + } + break; /* some other op wrote to this reg — give up */ + } + return NULL; +} + /* ---- Link pass: resolve GETNAME to GETINTRINSIC or GETENV ---- */ static void mach_link_code(JSContext *ctx, JSCodeRegister *code, JSValue env) { @@ -2560,7 +2591,12 @@ vm_dispatch: /* A=frame_slot, B=func_reg, C=argc */ JSValue func_val = frame->slots[b]; if (!mist_is_function(func_val)) { - JS_RaiseDisrupt(ctx, "not a function"); + char nbuf[KEY_GET_STR_BUF_SIZE]; + const char *name = mach_callee_name(ctx, code, pc, b, nbuf, sizeof(nbuf)); + if (name) + JS_RaiseDisrupt(ctx, "%s is not a function", name); + else + JS_RaiseDisrupt(ctx, "not a function"); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); goto disrupt; } From 93aaaa43a11235dc9aac6d552c91d63541d71572 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 22:02:26 -0600 Subject: [PATCH 3/5] lexical this --- mcode.cm | 8 ++++++++ tests/suite.cm | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/mcode.cm b/mcode.cm index e5cbb58f..d993acd1 100644 --- a/mcode.cm +++ b/mcode.cm @@ -2767,6 +2767,7 @@ var mcode = function(ast) { var result = null var saved_label = 0 var saved_func = 0 + var captured_this = 0 push(parent_states, saved) @@ -2818,6 +2819,13 @@ var mcode = function(ast) { s_max_slot = s_next_temp_slot } + // Arrow functions capture the enclosing this via closure + if (is_arrow) { + captured_this = alloc_slot() + emit_3("get", captured_this, saved.this_slot, 1) + s_this_slot = captured_this + } + // Default parameter initialization ps = 1 _i = 0 diff --git a/tests/suite.cm b/tests/suite.cm index 0eb68b82..7e975153 100644 --- a/tests/suite.cm +++ b/tests/suite.cm @@ -3498,4 +3498,59 @@ return { } }, + // ============================================================================ + // ARROW FUNCTION LEXICAL THIS + // ============================================================================ + + test_arrow_captures_this: function() { + var obj = { + value: 42, + getIt: function() { + var arrow = () => this.value + return arrow() + } + } + if (obj.getIt() != 42) return "arrow should capture enclosing this" + }, + + test_arrow_captures_this_nested: function() { + var obj = { + value: 99, + getIt: function() { + var outer = () => { + var inner = () => this.value + return inner() + } + return outer() + } + } + if (obj.getIt() != 99) return "nested arrows should capture enclosing this" + }, + + test_arrow_this_not_rebound_by_method_call: function() { + var obj = { + value: 10, + getIt: function() { + var arrow = () => this.value + return arrow() + } + } + var other = { value: 20, stolen: obj.getIt } + if (other.stolen() != 20) return "setup failed" + if (obj.getIt() != 10) return "arrow this should come from enclosing method" + }, + + test_arrow_this_with_regular_function_this: function() { + var obj = { + value: 7, + getIt: function() { + var arrow = () => this.value + var regular = function() { return this } + if (regular() != null) return "regular fn direct call should have null this" + return arrow() + } + } + if (obj.getIt() != 7) return "arrow should capture this while regular fn does not" + }, + } From 34cb19c357e2a98102f28eac514e73b4452dfec3 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 23:07:47 -0600 Subject: [PATCH 4/5] fix gc closure shortening --- streamline.cm | 27 +++++++++ vm_suite.ce | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/streamline.cm b/streamline.cm index f7858c78..4a873d10 100644 --- a/streamline.cm +++ b/streamline.cm @@ -1598,6 +1598,8 @@ var streamline = function(ir, log) { var found = false var anc_remap = null var old_slot = 0 + var max_close = null + var needed = 0 var fi = 0 var i = 0 var j = 0 @@ -1703,6 +1705,8 @@ var streamline = function(ir, log) { } // Fix get/put parent_slot references using ancestor remap tables + // and track the max close slot per ancestor for nr_close_slots update + max_close = array(func_count + 1, -1) fi = 0 while (fi < func_count) { instrs = functions[fi].instructions @@ -1725,6 +1729,9 @@ var streamline = function(ir, log) { instr[2] = anc_remap[old_slot] } } + if (ancestor >= 0 && instr[2] > max_close[ancestor]) { + max_close[ancestor] = instr[2] + } } i = i + 1 } @@ -1732,6 +1739,26 @@ var streamline = function(ir, log) { fi = fi + 1 } + // Update nr_close_slots for functions whose close slots were remapped. + // Frame shortening keeps 1 + nr_args + nr_close_slots slots, so + // nr_close_slots must cover the highest-numbered close slot. + fi = 0 + while (fi < func_count) { + if (max_close[fi] >= 0) { + needed = max_close[fi] - (functions[fi].nr_args != null ? functions[fi].nr_args : 0) + if (needed > functions[fi].nr_close_slots) { + functions[fi].nr_close_slots = needed + } + } + fi = fi + 1 + } + if (max_close[func_count] >= 0 && ir.main != null) { + needed = max_close[func_count] - (ir.main.nr_args != null ? ir.main.nr_args : 0) + if (needed > ir.main.nr_close_slots) { + ir.main.nr_close_slots = needed + } + } + return null } diff --git a/vm_suite.ce b/vm_suite.ce index 0a391df1..79454a8c 100644 --- a/vm_suite.ce +++ b/vm_suite.ce @@ -5539,6 +5539,157 @@ run("gc blob forward pointer chase", function() { } }) +// ============================================================================ +// GC CLOSURE FRAME SHORTENING +// Verify that closure-captured variables survive GC collection, particularly +// when the streamline optimizer remaps close slots to different positions. +// ============================================================================ + +var force_gc = function() { + var _g = 0 + var _gx = null + for (_g = 0; _g < 200; _g = _g + 1) { + _gx = {a: _g, b: [1, 2, 3], c: "garbage"} + } +} + +run("gc closure basic - captured function survives gc", function() { + var make = function() { + function helper() { return 42 } + var obj = { call() { return helper() } } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.call(), 42, "captured function should survive GC") +}) + +run("gc closure - captured variable survives gc", function() { + var make = function() { + var val = 99 + var obj = { get() { return val } } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.get(), 99, "captured variable should survive GC") +}) + +run("gc closure - multiple captured variables survive gc", function() { + var make = function() { + var a = 10 + var b = 20 + var c = 30 + var obj = { + sum() { return a + b + c } + } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.sum(), 60, "all captured vars should survive GC") +}) + +run("gc closure - captured function and var survive gc", function() { + var make = function() { + function double(x) { return x * 2 } + var base = 5 + var obj = { compute() { return double(base) } } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.compute(), 10, "captured fn and var should survive GC") +}) + +run("gc closure - nested closure chain survives gc", function() { + var outer = function() { + var x = 7 + var mid = function() { + var y = 3 + var inner = function() { return x + y } + return inner + } + return mid() + } + var fn = outer() + force_gc() + assert_eq(fn(), 10, "nested closure chain should survive GC") +}) + +run("gc closure - multiple methods share captured frame", function() { + var make = function() { + var count = 0 + function inc() { count = count + 1 } + function get() { return count } + var obj = { + increment() { inc() }, + value() { return get() } + } + return obj + } + var obj = make() + obj.increment() + obj.increment() + force_gc() + obj.increment() + assert_eq(obj.value(), 3, "shared closure frame should survive GC") +}) + +run("gc closure - closure survives repeated gc cycles", function() { + var make = function() { + var val = 123 + var obj = { get() { return val } } + return obj + } + var obj = make() + force_gc() + force_gc() + force_gc() + assert_eq(obj.get(), 123, "closure should survive repeated GC cycles") +}) + +run("gc closure - object literal method with temp slot reuse", function() { + var make = function() { + function helper() { return "ok" } + var temp = [1, 2, 3] + var unused = {x: temp} + var obj = { call() { return helper() } } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.call(), "ok", "closure should work after temp slots discarded") +}) + +run("gc closure - closure array survives gc", function() { + var make = function() { + var items = [10, 20, 30] + var obj = { + first() { return items[0] }, + last() { return items[2] } + } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.first(), 10, "captured array first element") + assert_eq(obj.last(), 30, "captured array last element") +}) + +run("gc closure - factory pattern survives gc", function() { + var factory = function(name) { + function greet() { return "hello " + name } + var obj = { say() { return greet() } } + return obj + } + var a = factory("alice") + var b = factory("bob") + force_gc() + assert_eq(a.say(), "hello alice", "first factory closure") + assert_eq(b.say(), "hello bob", "second factory closure") +}) + // ============================================================================ // SUMMARY // ============================================================================ From 5ea0de9fbb7d27b6195bcef45de1430df6c6967e Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 23:14:43 -0600 Subject: [PATCH 5/5] crash vm trace --- source/cell.c | 10 ++++++ source/cell_internal.h | 4 +++ source/quickjs.h | 5 +++ source/runtime.c | 71 +++++++++++++++++++++++++++++++++++++++--- source/scheduler.c | 5 +++ 5 files changed, 91 insertions(+), 4 deletions(-) diff --git a/source/cell.c b/source/cell.c index 4d244220..3f2190db 100644 --- a/source/cell.c +++ b/source/cell.c @@ -28,6 +28,7 @@ static int run_test_suite(size_t heap_size); cell_rt *root_cell = NULL; static char *shop_path = NULL; +volatile JSContext *g_crash_ctx = NULL; static char *core_path = NULL; static int native_mode = 0; static int warn_mode = 1; @@ -408,6 +409,13 @@ static void signal_handler(int sig) #endif if (!str) return; + /* Reset handler to default so a double-fault terminates immediately */ + signal(sig, SIG_DFL); + + /* Try to print the JS stack (best-effort, signal-safe) */ + if (g_crash_ctx) + JS_CrashPrintStack((JSContext *)g_crash_ctx); + exit_handler(); } @@ -689,7 +697,9 @@ int cell_init(int argc, char **argv) JS_DeleteGCRef(ctx, &args_ref); JSValue hidden_env = JS_Stone(ctx, env_ref.val); + g_crash_ctx = ctx; JSValue result = JS_RunMachBin(ctx, (const uint8_t *)bin_data, bin_size, hidden_env); + g_crash_ctx = NULL; JS_DeleteGCRef(ctx, &env_ref); free(bin_data); diff --git a/source/cell_internal.h b/source/cell_internal.h index 6f5d429c..77ed2f12 100644 --- a/source/cell_internal.h +++ b/source/cell_internal.h @@ -76,6 +76,10 @@ typedef struct cell_rt { cell_hook trace_hook; } cell_rt; +/* Set by actor_turn/CLI before entering the VM, cleared after. + Read by signal_handler to print JS stack on crash. */ +extern volatile JSContext *g_crash_ctx; + cell_rt *create_actor(void *wota); const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar); void actor_disrupt(cell_rt *actor); diff --git a/source/quickjs.h b/source/quickjs.h index de64a852..125da3c5 100644 --- a/source/quickjs.h +++ b/source/quickjs.h @@ -1054,6 +1054,11 @@ MachCode *mach_compile_mcode(struct cJSON *mcode_json); Does NOT clear reg_current_frame — caller is responsible if needed. */ JSValue JS_GetStack (JSContext *ctx); +/* Crash-safe stack printer: walks JS frames and writes to stderr. + Uses only write() (async-signal-safe). No GC allocations. + Safe to call from signal handlers. */ +void JS_CrashPrintStack(JSContext *ctx); + #undef js_unlikely #undef inline diff --git a/source/runtime.c b/source/runtime.c index 8847f571..99d9077a 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -25,6 +25,7 @@ #define BLOB_IMPLEMENTATION #include "quickjs-internal.h" +#include // #define DUMP_BUDDY #ifdef DUMP_BUDDY @@ -11640,10 +11641,10 @@ JSValue JS_GetStack(JSContext *ctx) { JSGCRef item_ref; JS_PushGCRef(ctx, &item_ref); item_ref.val = JS_NewObject(ctx); - JS_SetPropertyStr(ctx, item_ref.val, "fn", - JS_NewString(ctx, frames[i].fn ? frames[i].fn : "")); - JS_SetPropertyStr(ctx, item_ref.val, "file", - JS_NewString(ctx, frames[i].file ? frames[i].file : "")); + JSValue fn_str = JS_NewString(ctx, frames[i].fn ? frames[i].fn : ""); + JS_SetPropertyStr(ctx, item_ref.val, "fn", fn_str); + JSValue file_str = JS_NewString(ctx, frames[i].file ? frames[i].file : ""); + JS_SetPropertyStr(ctx, item_ref.val, "file", file_str); JS_SetPropertyStr(ctx, item_ref.val, "line", JS_NewInt32(ctx, frames[i].line)); JS_SetPropertyStr(ctx, item_ref.val, "col", JS_NewInt32(ctx, frames[i].col)); JS_SetPropertyNumber(ctx, arr_ref.val, i, item_ref.val); @@ -11653,3 +11654,65 @@ JSValue JS_GetStack(JSContext *ctx) { JS_PopGCRef(ctx, &arr_ref); return result; } + +/* Crash-safe stack printer: walks JS frames and writes to stderr + using only write() (async-signal-safe). No GC allocations. + Best-effort: if the heap is corrupted, we may double-fault. */ +void JS_CrashPrintStack(JSContext *ctx) { + if (!ctx) return; + if (JS_IsNull(ctx->reg_current_frame)) return; + + static const char hdr[] = "\n--- JS Stack at crash ---\n"; + write(STDERR_FILENO, hdr, sizeof(hdr) - 1); + + JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(ctx->reg_current_frame); + uint32_t cur_pc = ctx->current_register_pc; + int is_first = 1; + int depth = 0; + + while (frame && depth < 32) { + if (!JS_IsFunction(frame->function)) break; + JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); + + if (fn->kind == JS_FUNC_KIND_REGISTER && JS_VALUE_GET_CODE(fn->u.cell.code)->u.reg.code) { + JSCodeRegister *code = JS_VALUE_GET_CODE(fn->u.cell.code)->u.reg.code; + uint32_t pc = is_first ? cur_pc : (uint32_t)(JS_VALUE_GET_INT(frame->address) >> 16); + + const char *name = code->name_cstr ? code->name_cstr : ""; + const char *file = code->filename_cstr ? code->filename_cstr : ""; + int line = 0; + if (code->line_table && pc < code->instr_count) + line = code->line_table[pc].line; + + /* Format: " at name (file:line)\n" using only write() */ + char buf[512]; + int pos = 0; + buf[pos++] = ' '; buf[pos++] = ' '; buf[pos++] = ' '; buf[pos++] = ' '; + buf[pos++] = 'a'; buf[pos++] = 't'; buf[pos++] = ' '; + for (const char *p = name; *p && pos < 200; p++) buf[pos++] = *p; + buf[pos++] = ' '; buf[pos++] = '('; + for (const char *p = file; *p && pos < 440; p++) buf[pos++] = *p; + buf[pos++] = ':'; + /* itoa for line number */ + if (line == 0) { + buf[pos++] = '0'; + } else { + char digits[12]; + int d = 0; + int n = line; + while (n > 0) { digits[d++] = '0' + (n % 10); n /= 10; } + for (int i = d - 1; i >= 0; i--) buf[pos++] = digits[i]; + } + buf[pos++] = ')'; buf[pos++] = '\n'; + write(STDERR_FILENO, buf, pos); + } + + if (JS_IsNull(frame->caller)) break; + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); + is_first = 0; + depth++; + } + + static const char ftr[] = "--- End JS Stack ---\n"; + write(STDERR_FILENO, ftr, sizeof(ftr) - 1); +} diff --git a/source/scheduler.c b/source/scheduler.c index 97b4736f..415d694e 100644 --- a/source/scheduler.c +++ b/source/scheduler.c @@ -716,6 +716,7 @@ void actor_turn(cell_rt *actor) if (actor->vm_suspended) { /* RESUME path: continue suspended turn under kill timer only */ + g_crash_ctx = actor->context; atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed); JS_SetPauseFlag(actor->context, 0); actor->turn_start_ns = cell_ns(); @@ -783,6 +784,8 @@ void actor_turn(cell_rt *actor) pthread_cond_signal(&engine.timer_cond); pthread_mutex_unlock(&engine.lock); + g_crash_ctx = actor->context; + if (l.type == LETTER_BLOB) { size_t size = blob_length(l.blob_data) / 8; JSValue arg = js_new_blob_stoned_copy(actor->context, @@ -816,6 +819,7 @@ void actor_turn(cell_rt *actor) actor->slow_strikes = 0; /* completed within fast timer */ ENDTURN: + g_crash_ctx = NULL; /* Invalidate any outstanding pause/kill timers for this turn */ atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed); actor->state = ACTOR_IDLE; @@ -842,6 +846,7 @@ ENDTURN: return; ENDTURN_SLOW: + g_crash_ctx = NULL; #ifdef ACTOR_TRACE fprintf(stderr, "[ACTOR_TRACE] %s: suspended mid-turn -> SLOW\n", actor->name ? actor->name : actor->id);