diff --git a/api-test.c b/api-test.c index d17c49acd..88fdf63b1 100644 --- a/api-test.c +++ b/api-test.c @@ -962,6 +962,130 @@ 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, + const char *filename, + const char *funcname, + int line, + int col) +{ + trace_state.call_count++; + trace_state.last_line = line; + trace_state.last_col = col; + snprintf(trace_state.last_filename, sizeof(trace_state.last_filename), + "%s", filename); + snprintf(trace_state.last_funcname, sizeof(trace_state.last_funcname), + "%s", funcname); + trace_state.stack_depth = JS_GetStackDepth(ctx); + int count = 0; + JSDebugLocalVar *vars = JS_GetLocalVariablesAtLevel(ctx, 0, &count); + 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); + + /* no handler set: eval should work and call_count stays 0 */ + 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); + } + + /* set handler: callback fires for each statement */ + JS_SetDebugTraceHandler(ctx, debug_trace_cb); + 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, "")); + } + + /* stack depth inside a nested call */ + 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); + /* the deepest invocation should have a stack depth > 1 */ + /* (just verify we got a sane value; exact depth depends on internals) */ + assert(trace_state.stack_depth >= 1); + } + + /* local variables are visible inside the callback */ + 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); + /* inside f() we should see locals (a, b, c) at some point */ + assert(trace_state.max_local_count >= 2); + } + + /* returning non-zero aborts execution */ + memset(&trace_state, 0, sizeof(trace_state)); + trace_state.abort_at = 1; /* abort on first callback */ + { + 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); + } + + /* clear handler: callbacks no longer fire */ + JS_SetDebugTraceHandler(ctx, 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 = JS_NewRuntime(); @@ -1037,6 +1161,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 909fd718f..76265d5a3 100644 --- a/quickjs-opcode.h +++ b/quickjs-opcode.h @@ -364,6 +364,8 @@ 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) /* debugger trace point */ + #undef DEF #undef def #endif /* DEF */ diff --git a/quickjs.c b/quickjs.c index 3bf342e31..331f942e0 100644 --- a/quickjs.c +++ b/quickjs.c @@ -536,6 +536,8 @@ 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; }; typedef union JSFloat64Union { @@ -2560,6 +2562,104 @@ JSValue JS_GetFunctionProto(JSContext *ctx) return js_dup(ctx->function_proto); } +void JS_SetDebugTraceHandler(JSContext *ctx, JSDebugTraceFunc *cb) +{ + ctx->debug_trace = cb; +} + +/* Debug API: Get stack frame at specific level */ +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; +} + +/* Get the call stack depth */ +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; +} + +/* Get local variables at a specific stack level */ +JSDebugLocalVar *JS_GetLocalVariablesAtLevel(JSContext *ctx, int level, int *pcount) +{ + if (pcount) + *pcount = 0; + + JSStackFrame *sf = js_get_stack_frame_at_level(ctx, level); + if (sf == NULL) + return NULL; + + JSValue func = sf->cur_func; + if (JS_VALUE_GET_TAG(func) != JS_TAG_OBJECT) + return NULL; + + JSObject *p = JS_VALUE_GET_OBJ(func); + if (p->class_id != JS_CLASS_BYTECODE_FUNCTION) + return NULL; + + JSFunctionBytecode *b = p->u.func.function_bytecode; + int total_vars = b->arg_count + b->var_count; + + if (total_vars == 0) + return NULL; + + JSDebugLocalVar *vars = js_malloc(ctx, sizeof(JSDebugLocalVar) * total_vars); + if (!vars) + return NULL; + + int idx = 0; + + /* First, get arguments */ + for (int i = 0; i < b->arg_count; i++, idx++) { + JSVarDef *vd = &b->vardefs[i]; + vars[idx].name = JS_AtomToCString(ctx, vd->var_name); + vars[idx].value = js_dup(sf->arg_buf[i]); + vars[idx].is_arg = 1; + vars[idx].scope_level = vd->scope_level; + } + + /* Then, get local variables */ + for (int i = 0; i < b->var_count; i++, idx++) { + JSVarDef *vd = &b->vardefs[b->arg_count + i]; + vars[idx].name = JS_AtomToCString(ctx, vd->var_name); + vars[idx].value = js_dup(sf->var_buf[i]); + vars[idx].is_arg = 0; + vars[idx].scope_level = vd->scope_level; + } + + if (pcount) + *pcount = total_vars; + return vars; +} + +/* Free local variables array */ +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); +} + typedef enum JSFreeModuleEnum { JS_FREE_MODULE_ALL, JS_FREE_MODULE_NOT_RESOLVED, @@ -17551,6 +17651,24 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, JSValue *call_argv; SWITCH(pc) { + CASE(OP_debug): + 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); + line_num = find_line_num(ctx, b, pc_index, &col_num); + + char filename[ATOM_GET_STR_BUF_SIZE]; + JS_AtomGetStr(ctx, filename, sizeof(filename), b->filename); + char funcname[ATOM_GET_STR_BUF_SIZE]; + JS_AtomGetStr(ctx, funcname, sizeof(funcname), b->func_name); + int ret = ctx->debug_trace(ctx, filename, funcname, + line_num, col_num); + + if (ret != 0) + goto exception; + } + BREAK; CASE(OP_push_i32): *sp++ = js_int32(get_u32(pc)); pc += 4; @@ -22424,6 +22542,7 @@ static __exception int next_token(JSParseState *s) if (JS_VALUE_IS_NAN(ret) || lre_js_is_ident_next(utf8_decode(p, &p1))) { JS_FreeValue(s->ctx, ret); + s->col_num = max_int(1, s->mark - s->eol); js_parse_error(s, "invalid number literal"); goto fail; } @@ -23136,6 +23255,14 @@ static void emit_source_loc(JSParseState *s) dbuf_putc(bc, OP_source_loc); dbuf_put_u32(bc, s->token.line_num); dbuf_put_u32(bc, s->token.col_num); + if (unlikely(s->ctx->debug_trace)) + dbuf_putc(bc, OP_debug); +} + +static void emit_source_loc_debug(JSParseState *s) +{ + if (unlikely(s->ctx->debug_trace)) + emit_source_loc(s); } static void emit_op(JSParseState *s, uint8_t val) @@ -28253,6 +28380,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; @@ -28290,12 +28418,14 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, case TOK_LET: case TOK_CONST: haslet: + emit_source_loc_debug(s); if (!(decl_mask & DECL_MASK_OTHER)) { js_parse_error(s, "lexical declarations can't appear in single-statement context"); goto fail; } /* 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)) @@ -28306,6 +28436,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(){}` */ @@ -28414,6 +28545,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, int tok, bits; bool is_async; + emit_source_loc_debug(s); if (next_token(s)) goto fail; @@ -28572,6 +28704,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, } if (js_parse_expect_semi(s)) goto fail; + emit_source_loc_debug(s); } break; case TOK_SWITCH: @@ -28580,6 +28713,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; @@ -28788,6 +28922,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, js_parse_error(s, "expecting catch or finally"); goto fail; } + emit_source_loc_debug(s); emit_label(s, label_finally); if (s->token.val == TOK_FINALLY) { int saved_eval_ret_idx = 0; /* avoid warning */ @@ -28823,6 +28958,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, } pop_break_entry(s->cur_func); } + emit_source_loc_debug(s); emit_op(s, OP_ret); emit_label(s, label_end); } @@ -33286,6 +33422,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) { + pos = pos_next; } else { break; } @@ -33573,6 +33711,9 @@ static int get_label_pos(JSFunctionDef *s, int label) case OP_source_loc: pos += 9; continue; + case OP_debug: + pos += 1; + continue; case OP_label: pos += 5; continue; @@ -34031,6 +34172,10 @@ static bool code_has_label(CodeContext *s, int pos, int label) pos += 9; continue; } + if (op == OP_debug) { + pos += 1; + continue; + } if (op == OP_label) { int lab = get_u32(s->bc_buf + pos + 1); if (lab == label) @@ -34063,6 +34208,7 @@ static int find_jump_target(JSFunctionDef *s, int label, int *pop) switch(op = s->byte_code.buf[pos]) { case OP_source_loc: case OP_label: + case OP_debug: pos += opcode_info[op].size; continue; case OP_goto: @@ -34278,6 +34424,13 @@ 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_label: { label = get_u32(bc_buf + pos + 1); diff --git a/quickjs.h b/quickjs.h index f87b06d26..2c012e4b1 100644 --- a/quickjs.h +++ b/quickjs.h @@ -543,6 +543,44 @@ 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, non-zero to raise an exception. + OP_debug opcodes are always emitted at statement boundaries. The + callback is only invoked when one has been set via + JS_SetDebugTraceHandler. */ +typedef int JSDebugTraceFunc(JSContext *ctx, + const char *filename, + const char *funcname, + int line, + int col); + +/* Set (or clear) the debug trace handler on a context. When the + interpreter hits an OP_debug opcode and a handler is set, it is + called. Pass NULL to disable. Works with any context, including + those created with JS_NewContextRaw. */ +JS_EXTERN void JS_SetDebugTraceHandler(JSContext *ctx, + JSDebugTraceFunc *cb); + +/* Debug API: Get local variables in stack frames */ +typedef struct JSDebugLocalVar { + const char *name; /* variable name */ + JSValue value; /* variable value */ + int is_arg; /* 1 if argument, 0 if local variable */ + int scope_level; /* scope level of the variable */ +} 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.) + *pcount: output, number of variables returned + Returns allocated array of JSDebugLocalVar (must be freed with JS_FreeLocalVariables), + or NULL on error. */ +JS_EXTERN JSDebugLocalVar *JS_GetLocalVariablesAtLevel(JSContext *ctx, int level, int *pcount); + +/* Free local variables array returned by JS_GetLocalVariablesAtLevel */ +JS_EXTERN void JS_FreeLocalVariables(JSContext *ctx, JSDebugLocalVar *vars, int count); + /* the following functions are used to select the intrinsic object to save memory */ JS_EXTERN JSContext *JS_NewContextRaw(JSRuntime *rt);