diff --git a/api-test.c b/api-test.c index 7aa7dc8a6..a61858e52 100644 --- a/api-test.c +++ b/api-test.c @@ -1013,6 +1013,144 @@ static void get_uint8array(void) JS_FreeRuntime(rt); } +static struct { + int call_count; + int last_line; + int last_col; + char last_filename[256]; + char last_funcname[256]; + int stack_depth; + int max_local_count; + int abort_at; /* abort (return -1) on this call, 0 = never */ +} trace_state; + +static int debug_trace_cb(JSContext *ctx, + JSAtom filename, + JSAtom funcname, + int line, + int col, + int flags, + void *opaque) +{ + (void)flags; + trace_state.call_count++; + trace_state.last_line = line; + trace_state.last_col = col; + /* Convert while the atom is still valid (within callback lifetime). + Embedders who only need to compare against known breakpoint atoms + can skip this conversion entirely. */ + const char *fn = JS_AtomToCString(ctx, filename); + if (fn) { + snprintf(trace_state.last_filename, sizeof(trace_state.last_filename), + "%s", fn); + JS_FreeCString(ctx, fn); + } + const char *fnn = JS_AtomToCString(ctx, funcname); + if (fnn) { + snprintf(trace_state.last_funcname, sizeof(trace_state.last_funcname), + "%s", fnn); + JS_FreeCString(ctx, fnn); + } + trace_state.stack_depth = JS_GetStackDepth(ctx); + int count = 0; + JSDebugLocalVar *vars = NULL; + assert(JS_GetLocalVariablesAtLevel(ctx, 0, &vars, &count) == 0); + if (count > trace_state.max_local_count) + trace_state.max_local_count = count; + if (vars) + JS_FreeLocalVariables(ctx, vars, count); + if (trace_state.abort_at > 0 && + trace_state.call_count >= trace_state.abort_at) + return -1; + return 0; +} + +static void debug_trace(void) +{ + JSRuntime *rt = JS_NewRuntime(); + JSContext *ctx = JS_NewContext(rt); + + memset(&trace_state, 0, sizeof(trace_state)); + { + JSValue ret = eval(ctx, "1+2"); + assert(!JS_IsException(ret)); + JS_FreeValue(ctx, ret); + assert(trace_state.call_count == 0); + } + + JS_SetDebugTraceHandler(ctx, debug_trace_cb, NULL); + memset(&trace_state, 0, sizeof(trace_state)); + { + JSValue ret = eval(ctx, "var x = 1; x + 2"); + assert(!JS_IsException(ret)); + JS_FreeValue(ctx, ret); + assert(trace_state.call_count > 0); + assert(!strcmp(trace_state.last_filename, "")); + } + + { + JSDebugLocalVar *vars = NULL; + int count = -1; + assert(JS_GetLocalVariablesAtLevel(ctx, 0, &vars, &count) == 0); + assert(vars == NULL); + assert(count == 0); + } + + memset(&trace_state, 0, sizeof(trace_state)); + { + static const char code[] = + "function outer() {\n" + " function inner() {\n" + " return 42;\n" + " }\n" + " return inner();\n" + "}\n" + "outer();\n"; + JSValue ret = eval(ctx, code); + assert(!JS_IsException(ret)); + JS_FreeValue(ctx, ret); + assert(trace_state.call_count > 0); + assert(trace_state.stack_depth >= 1); + } + + memset(&trace_state, 0, sizeof(trace_state)); + { + static const char code[] = + "function f(a, b) {\n" + " var c = a + b;\n" + " return c;\n" + "}\n" + "f(10, 20);\n"; + JSValue ret = eval(ctx, code); + assert(!JS_IsException(ret)); + JS_FreeValue(ctx, ret); + assert(trace_state.call_count > 0); + assert(trace_state.max_local_count >= 2); + } + + memset(&trace_state, 0, sizeof(trace_state)); + trace_state.abort_at = 1; + { + JSValue ret = eval(ctx, "1+2; 3+4"); + assert(JS_IsException(ret)); + JS_FreeValue(ctx, ret); + JSValue exc = JS_GetException(ctx); + JS_FreeValue(ctx, exc); + } + + JS_SetDebugTraceHandler(ctx, NULL, NULL); + memset(&trace_state, 0, sizeof(trace_state)); + { + JSValue ret = eval(ctx, "1+2"); + assert(!JS_IsException(ret)); + JS_FreeValue(ctx, ret); + assert(trace_state.call_count == 0); + } + + JS_FreeContext(ctx); + JS_FreeRuntime(rt); +} + static void new_symbol(void) { JSRuntime *rt = new_runtime(); @@ -1089,6 +1227,7 @@ int main(void) slice_string_tocstring(); immutable_array_buffer(); get_uint8array(); + debug_trace(); new_symbol(); return 0; } diff --git a/quickjs-opcode.h b/quickjs-opcode.h index ec2a5ad91..ce954cbb9 100644 --- a/quickjs-opcode.h +++ b/quickjs-opcode.h @@ -372,6 +372,9 @@ DEF( is_null, 1, 1, 1, none) DEF(typeof_is_undefined, 1, 1, 1, none) DEF( typeof_is_function, 1, 1, 1, none) +DEF( debug, 1, 0, 0, none) +DEF( debugger_stmt, 1, 0, 0, none) + #undef DEF #undef def #endif /* DEF */ diff --git a/quickjs.c b/quickjs.c index 38a724fbb..cabfcb107 100644 --- a/quickjs.c +++ b/quickjs.c @@ -539,6 +539,9 @@ struct JSContext { const char *input, size_t input_len, const char *filename, int line, int flags, int scope_idx); void *user_opaque; + + JSDebugTraceFunc *debug_trace; + void *debug_trace_opaque; }; typedef union JSFloat64Union { @@ -1390,6 +1393,7 @@ static void js_async_function_resolve_mark(JSRuntime *rt, JSValueConst val, static JSValue JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, const char *input, size_t input_len, const char *filename, int line, int flags, int scope_idx); +static const char *JS_AtomGetStr(JSContext *ctx, char *buf, int buf_size, JSAtom atom); static void js_free_module_def(JSContext *ctx, JSModuleDef *m); static void js_mark_module_def(JSRuntime *rt, JSModuleDef *m, JS_MarkFunc *mark_func); @@ -2593,6 +2597,309 @@ JSValue JS_GetFunctionProto(JSContext *ctx) return js_dup(ctx->function_proto); } +void JS_SetDebugTraceHandler(JSContext *ctx, JSDebugTraceFunc *cb, void *opaque) +{ + ctx->debug_trace = cb; + ctx->debug_trace_opaque = opaque; +} + +static JSStackFrame *js_get_stack_frame_at_level(JSContext *ctx, int level) +{ + JSRuntime *rt = ctx->rt; + JSStackFrame *sf = rt->current_stack_frame; + int current_level = 0; + + while (sf != NULL && current_level < level) { + sf = sf->prev_frame; + current_level++; + } + return sf; +} + +int JS_GetStackDepth(JSContext *ctx) +{ + JSRuntime *rt = ctx->rt; + JSStackFrame *sf = rt->current_stack_frame; + int depth = 0; + + while (sf != NULL) { + depth++; + sf = sf->prev_frame; + } + return depth; +} + +int JS_GetLocalVariablesAtLevel(JSContext *ctx, int level, + JSDebugLocalVar **pvars, int *pcount) +{ + if (pvars) + *pvars = NULL; + if (pcount) + *pcount = 0; + if (!pvars) { + JS_ThrowTypeError(ctx, "pvars must not be NULL"); + return -1; + } + + JSStackFrame *sf = js_get_stack_frame_at_level(ctx, level); + if (sf == NULL) + return 0; + + JSValue func = sf->cur_func; + if (JS_VALUE_GET_TAG(func) != JS_TAG_OBJECT) + return 0; + + JSObject *p = JS_VALUE_GET_OBJ(func); + if (p->class_id != JS_CLASS_BYTECODE_FUNCTION) + return 0; + + JSFunctionBytecode *b = p->u.func.function_bytecode; + int total_vars = b->arg_count + b->var_count + b->closure_var_count; + + if (total_vars == 0) + return 0; + + JSDebugLocalVar *vars = js_malloc(ctx, sizeof(JSDebugLocalVar) * total_vars); + if (!vars) + return -1; + + int idx = 0; + +#define APPEND_VAR(vd_, value_, is_arg_) \ + do { \ + JSAtom name_ = (vd_)->var_name; \ + const char *name_str_; \ + if (name_ != JS_ATOM_NULL) { \ + char tmp_[32]; \ + JS_AtomGetStr(ctx, tmp_, sizeof(tmp_), name_); \ + if (tmp_[0] == '<') \ + break; \ + } \ + name_str_ = JS_AtomToCString(ctx, name_); \ + if (unlikely(!name_str_)) \ + goto fail; \ + vars[idx].name = name_str_; \ + /* Do not expose the internal TDZ sentinel to C callers. */ \ + if (JS_VALUE_GET_TAG(value_) == JS_TAG_UNINITIALIZED) \ + vars[idx].value = JS_UNDEFINED; \ + else \ + vars[idx].value = js_dup(value_); \ + vars[idx].is_arg = (is_arg_); \ + vars[idx].is_closure = false; \ + vars[idx].scope_level = (vd_)->scope_level; \ + idx++; \ + } while (0) + + for (int i = 0; i < b->arg_count; i++) { + JSVarDef *vd = &b->vardefs[i]; + APPEND_VAR(vd, sf->arg_buf[i], true); + } + + for (int i = 0; i < b->var_count; i++) { + JSVarDef *vd = &b->vardefs[b->arg_count + i]; + APPEND_VAR(vd, sf->var_buf[i], false); + } + +#undef APPEND_VAR + + /* Append closure variables captured from enclosing scopes. */ + { + JSVarRef **var_refs = p->u.func.var_refs; + for (int i = 0; i < b->closure_var_count; i++) { + JSClosureVar *cv = &b->closure_var[i]; + JSAtom name = cv->var_name; + if (name != JS_ATOM_NULL) { + char tmp[32]; + JS_AtomGetStr(ctx, tmp, sizeof(tmp), name); + if (tmp[0] == '<') + continue; + } + const char *name_str = JS_AtomToCString(ctx, name); + if (unlikely(!name_str)) + goto fail; + JSValue cv_val = JS_UNDEFINED; + if (var_refs && var_refs[i] && var_refs[i]->pvalue) { + JSValue v = *var_refs[i]->pvalue; + if (JS_VALUE_GET_TAG(v) != JS_TAG_UNINITIALIZED) + cv_val = js_dup(v); + } + vars[idx].name = name_str; + vars[idx].value = cv_val; + vars[idx].is_arg = false; + vars[idx].is_closure = true; + vars[idx].scope_level = 0; + idx++; + } + } + + if (idx == 0) { + js_free(ctx, vars); + return 0; + } + + if (pvars) + *pvars = vars; + if (pcount) + *pcount = idx; + return 0; + +fail: + for (int i = 0; i < idx; i++) { + JS_FreeCString(ctx, vars[i].name); + JS_FreeValue(ctx, vars[i].value); + } + js_free(ctx, vars); + return -1; +} + +void JS_FreeLocalVariables(JSContext *ctx, JSDebugLocalVar *vars, int count) +{ + if (!vars) + return; + for (int i = 0; i < count; i++) { + JS_FreeCString(ctx, vars[i].name); + JS_FreeValue(ctx, vars[i].value); + } + js_free(ctx, vars); +} + +int JS_SetVariableAtLevel(JSContext *ctx, int level, + const char *name, JSValue value) +{ + if (!name) { + JS_FreeValue(ctx, value); + return -3; + } + + JSStackFrame *sf = js_get_stack_frame_at_level(ctx, level); + if (sf == NULL) { + JS_FreeValue(ctx, value); + return -1; + } + + JSValue func = sf->cur_func; + if (JS_VALUE_GET_TAG(func) != JS_TAG_OBJECT) { + JS_FreeValue(ctx, value); + return -1; + } + JSObject *p = JS_VALUE_GET_OBJ(func); + if (p->class_id != JS_CLASS_BYTECODE_FUNCTION) { + JS_FreeValue(ctx, value); + return -1; + } + JSFunctionBytecode *b = p->u.func.function_bytecode; + + JSAtom target_atom = JS_NewAtom(ctx, name); + if (target_atom == JS_ATOM_NULL) { + JS_FreeValue(ctx, value); + return -3; + } + + int rc = -1; + + /* Search arguments. */ + for (int i = 0; i < b->arg_count; i++) { + if (b->vardefs[i].var_name == target_atom) { + JSValue old = sf->arg_buf[i]; + sf->arg_buf[i] = js_dup(value); + JS_FreeValue(ctx, old); + rc = 0; + goto done; + } + } + + /* Search locals. */ + for (int i = 0; i < b->var_count; i++) { + JSVarDef *vd = &b->vardefs[b->arg_count + i]; + if (vd->var_name == target_atom) { + if (vd->is_const) { + rc = -2; + goto done; + } + JSValue old = sf->var_buf[i]; + sf->var_buf[i] = js_dup(value); + JS_FreeValue(ctx, old); + rc = 0; + goto done; + } + } + + /* Search closure vars. */ + { + JSVarRef **var_refs = p->u.func.var_refs; + for (int i = 0; i < b->closure_var_count; i++) { + JSClosureVar *cv = &b->closure_var[i]; + if (cv->var_name == target_atom) { + if (cv->is_const) { + rc = -2; + goto done; + } + if (var_refs && var_refs[i] && var_refs[i]->pvalue) { + set_value(ctx, var_refs[i]->pvalue, js_dup(value)); + rc = 0; + } + goto done; + } + } + } + +done: + JS_FreeAtom(ctx, target_atom); + JS_FreeValue(ctx, value); + return rc; +} + +JSValue JS_EvalInStackFrame(JSContext *ctx, int level, + const char *input, size_t input_len, + const char *filename) +{ + if (!input) + return JS_ThrowTypeError(ctx, "input must not be NULL"); + + /* Reuse the normal direct-eval pipeline: temporarily swap the runtime's + current stack frame to the target frame so that JS_EVAL_TYPE_DIRECT + picks up its bytecode, var_refs and closure chain via the same path + used by the `eval(...)` operator at runtime. */ + JSStackFrame *target = js_get_stack_frame_at_level(ctx, level); + if (!target) + return JS_ThrowReferenceError(ctx, "no stack frame at level %d", level); + + if (JS_VALUE_GET_TAG(target->cur_func) != JS_TAG_OBJECT) + return JS_ThrowTypeError(ctx, + "stack frame at level %d is not a JS function", + level); + JSObject *p = JS_VALUE_GET_OBJ(target->cur_func); + if (p->class_id != JS_CLASS_BYTECODE_FUNCTION) + return JS_ThrowTypeError(ctx, + "stack frame at level %d is not a JS function", + level); + JSFunctionBytecode *b = p->u.func.function_bytecode; + + /* Pick the deepest lexical scope index so add_closure_variables() walks + through every enclosing block scope, exposing let/const bindings as + well as args, var-declared locals and closure refs. */ + int scope_idx = -1; + int max_level = 0; + for (int i = 0; i < b->var_count; i++) { + JSVarDef *vd = &b->vardefs[b->arg_count + i]; + if (vd->scope_level > max_level) { + max_level = vd->scope_level; + scope_idx = i; + } + } + + JSRuntime *rt = ctx->rt; + JSStackFrame *saved = rt->current_stack_frame; + rt->current_stack_frame = target; + + JSValue ret = JS_EvalInternal(ctx, JS_UNDEFINED, input, input_len, + filename ? filename : "", + 1, JS_EVAL_TYPE_DIRECT, scope_idx); + + rt->current_stack_frame = saved; + return ret; +} + typedef enum JSFreeModuleEnum { JS_FREE_MODULE_ALL, JS_FREE_MODULE_NOT_RESOLVED, @@ -17687,6 +17994,35 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, JSValue *call_argv; SWITCH(pc) { + CASE(OP_debug): + CASE(OP_debugger_stmt): + if (unlikely(ctx->debug_trace)) { + int col_num = 0; + int line_num = -1; + uint32_t pc_index = (uint32_t)(pc - b->byte_code_buf - 1); + int flags = (pc[-1] == OP_debugger_stmt) + ? JS_DEBUG_TRACE_DEBUGGER_STMT : 0; + line_num = find_line_num(ctx, b, pc_index, &col_num); + + /* Pass the JSAtom values directly — no heap allocation. + The atoms are valid for the lifetime of the bytecode + object, which outlives the callback. The embedder must + call JS_DupAtom() if it needs to retain them. */ + int ret = ctx->debug_trace(ctx, b->filename, b->func_name, + line_num, col_num, flags, + ctx->debug_trace_opaque); + + if (ret != 0 || JS_HasException(ctx)) { + /* If the callback indicated failure but did not raise + an exception itself, synthesize a default one so the + caller never observes JS_UNINITIALIZED via + JS_GetException(). */ + if (ret != 0 && !JS_HasException(ctx)) + JS_ThrowInternalError(ctx, "aborted by debugger"); + goto exception; + } + } + BREAK; CASE(OP_push_i32): *sp++ = js_int32(get_u32(pc)); pc += 4; @@ -23410,6 +23746,20 @@ static void emit_source_loc(JSParseState *s) emit_source_loc_at(s, s->token.line_num, s->token.col_num); } +static void emit_debug(JSParseState *s) +{ + if (unlikely(s->ctx->debug_trace)) + dbuf_putc(&s->cur_func->byte_code, OP_debug); +} + +static void emit_source_loc_debug(JSParseState *s) +{ + if (unlikely(s->ctx->debug_trace)) { + emit_source_loc(s); + emit_debug(s); + } +} + static void emit_op(JSParseState *s, uint8_t val) { JSFunctionDef *fd = s->cur_func; @@ -28774,6 +29124,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, goto fail; break; case TOK_RETURN: + emit_source_loc_debug(s); if (s->cur_func->is_eval) { js_parse_error(s, "return not in a function"); goto fail; @@ -28802,6 +29153,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, goto fail; } emit_source_loc(s); + emit_debug(s); if (js_parse_expr(s)) goto fail; emit_op(s, OP_throw); @@ -28825,6 +29177,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, goto fail; } s->cur_func->has_await = true; + emit_source_loc_debug(s); if (next_token(s)) /* skip 'using' */ goto fail; if (js_parse_var(s, PF_IN_ACCEPTED | PF_AWAIT_USING, TOK_USING, /*export_flag*/false)) @@ -28847,6 +29200,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, } /* fall thru */ case TOK_VAR: + emit_source_loc_debug(s); if (next_token(s)) goto fail; if (js_parse_var(s, PF_IN_ACCEPTED, tok, /*export_flag*/false)) @@ -28857,6 +29211,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, case TOK_IF: { int label1, label2, mask; + emit_source_loc_debug(s); if (next_token(s)) goto fail; /* create a new scope for `let f;if(1) function f(){}` */ @@ -28967,6 +29322,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, int source_line_num, source_col_num; bool is_async; + emit_source_loc_debug(s); source_line_num = s->token.line_num; source_col_num = s->token.col_num; if (next_token(s)) @@ -29202,6 +29558,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, int default_label_pos; BlockEnv break_entry; + emit_source_loc_debug(s); if (next_token(s)) goto fail; @@ -29553,6 +29910,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, js_parse_error(s, "using declaration is not allowed at the top level of a script"); goto fail; } + emit_source_loc_debug(s); if (next_token(s)) goto fail; if (js_parse_var(s, PF_IN_ACCEPTED, TOK_USING, /*export_flag*/false)) @@ -29590,7 +29948,14 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, break; case TOK_DEBUGGER: - /* currently no debugger, so just skip the keyword */ + /* Emit OP_source_loc + OP_debugger_stmt unconditionally so that an + attached debugger can pause on the `debugger` statement even when + the handler was not yet set at parse time. The dedicated opcode + lets the trace handler distinguish a real `debugger;` statement + from a statement-boundary OP_debug via the JS_DEBUG_TRACE_DEBUGGER_STMT + flag. */ + emit_source_loc(s); + dbuf_putc(&s->cur_func->byte_code, OP_debugger_stmt); if (next_token(s)) goto fail; if (js_parse_expect_semi(s)) @@ -29606,6 +29971,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, default: hasexpr: emit_source_loc(s); + emit_debug(s); if (js_parse_expr(s)) goto fail; if (s->cur_func->eval_ret_idx >= 0) { @@ -33969,6 +34335,8 @@ static bool code_match(CodeContext *s, int pos, ...) line_num = get_u32(tab + pos + 1); col_num = get_u32(tab + pos + 5); pos = pos_next; + } else if (op == OP_debug || op == OP_debugger_stmt) { + pos = pos_next; } else { break; } @@ -34256,6 +34624,10 @@ static int get_label_pos(JSFunctionDef *s, int label) case OP_source_loc: pos += 9; continue; + case OP_debug: + case OP_debugger_stmt: + pos += 1; + continue; case OP_label: pos += 5; continue; @@ -35017,6 +35389,19 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) col_num = get_u32(bc_buf + pos + 5); break; + case OP_debug: + /* record pc2line so the debugger can resolve the source + location when OP_debug is hit at runtime */ + add_pc2line_info(s, bc_out.size, line_num, col_num); + dbuf_putc(&bc_out, OP_debug); + break; + + case OP_debugger_stmt: + /* same as OP_debug but carries the `debugger;` flag at runtime */ + add_pc2line_info(s, bc_out.size, line_num, col_num); + dbuf_putc(&bc_out, OP_debugger_stmt); + break; + case OP_label: { label = get_u32(bc_buf + pos + 1); diff --git a/quickjs.h b/quickjs.h index b9ed27560..19e881394 100644 --- a/quickjs.h +++ b/quickjs.h @@ -523,6 +523,97 @@ JS_EXTERN void JS_SetClassProto(JSContext *ctx, JSClassID class_id, JSValue obj) JS_EXTERN JSValue JS_GetClassProto(JSContext *ctx, JSClassID class_id); JS_EXTERN JSValue JS_GetFunctionProto(JSContext *ctx); +/* Debug callback - invoked when the interpreter hits an OP_debug opcode. + Return 0 to continue execution. Return non-zero to abort execution at + this point: the engine will jump to the exception handler. The + callback may itself call JS_Throw* to provide a specific exception; + if the callback returns non-zero without having raised one, the engine + will synthesize a default InternalError("aborted by debugger"). If + the callback raises an exception via JS_Throw* but returns 0, the + engine still treats it as a request to abort. + + OP_debug opcodes are only emitted at statement boundaries when a debug + trace handler is registered at parse time. Therefore only code that + is parsed (e.g. by JS_Eval / JS_Compile) AFTER JS_SetDebugTraceHandler + has been called will be instrumented; previously compiled bytecode + will not invoke the callback. In practice, install the handler before + evaluating any application code. + + 'filename' and 'funcname' are JSAtom values identifying the source file + and enclosing function name. They are valid only for the duration of + the callback; call JS_DupAtom() if you need to retain them. Either + may be JS_ATOM_NULL (0) for anonymous functions or eval code. Use + JS_AtomToCString() / JS_AtomToString() to convert to a C string or + JSValue when needed. Accepting JSAtom avoids a heap allocation on + every instrumented statement when the embedder only needs to compare + against a known set of breakpoint locations. */ +/* Flags passed to JSDebugTraceFunc. Use bitwise-AND to test specific bits. */ +#define JS_DEBUG_TRACE_DEBUGGER_STMT (1 << 0) /* triggered by `debugger;` statement */ + +typedef int JSDebugTraceFunc(JSContext *ctx, + JSAtom filename, + JSAtom funcname, + int line, + int col, + int flags, + void *opaque); + +/* Set (or clear) the debug trace handler on a context. Pass NULL to + disable. Works with any context, including those created with + JS_NewContextRaw. See JSDebugTraceFunc above for the parse-time + instrumentation contract. */ +JS_EXTERN void JS_SetDebugTraceHandler(JSContext *ctx, + JSDebugTraceFunc *cb, + void *opaque); + +/* Debug API: Get local variables in stack frames */ +typedef struct JSDebugLocalVar { + const char *name; + JSValue value; + bool is_arg; + bool is_closure; /* true if captured from an enclosing scope */ + int scope_level; +} JSDebugLocalVar; + +/* Get the call stack depth (0 when no frames are active). */ +JS_EXTERN int JS_GetStackDepth(JSContext *ctx); + +/* Get local variables at a specific stack level (0 = current frame, 1 = caller, etc.). + On success, *pvars receives an allocated array of JSDebugLocalVar entries + that must be freed with JS_FreeLocalVariables(), and *pcount receives the + entry count. If no variables are available, *pvars is set to NULL and + *pcount is set to 0. Returns -1 on exception. + + The returned array contains arguments first, then locals, then any closure + variables captured from enclosing scopes (with `is_closure = true`). */ +JS_EXTERN int JS_GetLocalVariablesAtLevel(JSContext *ctx, int level, + JSDebugLocalVar **pvars, + int *pcount); + +/* Free local variables array returned by JS_GetLocalVariablesAtLevel */ +JS_EXTERN void JS_FreeLocalVariables(JSContext *ctx, JSDebugLocalVar *vars, int count); + +/* Set a local or closure variable in a stack frame by name. + Returns 0 on success, -1 if the variable is not found, -2 if it is a const + binding (read-only), or -3 on type/argument errors. */ +JS_EXTERN int JS_SetVariableAtLevel(JSContext *ctx, int level, + const char *name, JSValue value); + +/* Evaluate an expression in the context of the given stack frame. + The expression has access to the frame's local and closure variables. + On success, returns the resulting JSValue. On error, returns + JS_EXCEPTION (the exception is set on `ctx`). + + This is a best-effort implementation: simple identifiers are resolved + against the frame's bindings, and otherwise the expression is evaluated + in a synthetic scope that surfaces the frame variables as globals on a + transient object. Modifications to those bindings are NOT propagated + back to the underlying stack frame -- use JS_SetVariableAtLevel for + that. */ +JS_EXTERN JSValue JS_EvalInStackFrame(JSContext *ctx, int level, + const char *input, size_t input_len, + const char *filename); + /* the following functions are used to select the intrinsic object to save memory */ JS_EXTERN JSContext *JS_NewContextRaw(JSRuntime *rt);