Add Debugging Interface#1421
Conversation
添加位置变动(操作变动)回调接口,为外部实现调试功能实现可能
Introduces JS_SetOPChangedHandler to allow setting a callback for operation changes in the JSContext. Also adds calls to emit_source_loc in various statement parsing locations to improve source location tracking during parsing.
假如没有,位置跟踪会发生异常。
解决在函数内出现静态错误时,返回的堆栈信息中的列号错误的bug。
Introduces functions to get stack depth and retrieve local variables at a specific stack frame level, along with a struct for local variable info and a function to free the allocated array. Also updates the JSOPChangedHandler signature to include JSContext for improved debugging capabilities.
假如采用旧的代码,会发生下面的错误:
function add(a, b){
return a + b;
var b // OP_return会出现在这里
while(1){}
}
add(1, 2)
|
At a quick glance, this looks better than the other approaches I've seen, kudos! Now, since this will have a performance impact, I'd say we want it gated with a compile time time macro. @bnoordhuis thoughts? |
|
Thanks for the feedback @saghul! I've added a compile-time macro Compile-time gating (
|
|
@bnoordhuis WDYT? |
bnoordhuis
left a comment
There was a problem hiding this comment.
Behind a compile-time flag would be good. The diff mostly looks good to me but there are a couple of changes I'd like to see:
-
the API functions should always be available but return an error when compiled without debugger support
-
can you rename the new emit_source_loc calls to something like emit_source_loc_debug and turn that into a no-op when there's no debugger support?
-
I don't love the name JSOPChangedHandler. Suggestions for a better one? Something like JSBytecodeTraceFunc perhaps?
I'm assuming this good enough as a building block to assemble a functional debugger from? I can see how it lets you single-step through code, inspect stack frames, set breakpoints, etc., so I'm guessing... yes?
Rename the old operation_changed/JSOPChangedHandler to bytecode_trace/JSBytecodeTraceFunc and replace JS_SetOPChangedHandler with JS_SetBytecodeTraceHandler. Add conditional compilation guards so debugger-related code is compiled only when QJS_ENABLE_DEBUGGER is set (including stack depth, local-variable APIs, and freeing logic). Introduce emit_source_loc_debug no-op macro when debugger is disabled and make JS_GetStackDepth return -1 without the debugger. Update public header comments to reflect the new API and behavior.
|
I didn't quite understand why emit_source_loc_debug was needed before. Now I get it a little bit— the newly added emit_source_loc calls are debug-specific (for stepping through if/for/return/etc.), unlike the original 5 upstream calls which are always needed for error reporting. Having emit_source_loc_debug as a no-op without debugger support keeps the bytecode identical to upstream and avoids the performance overhead. |
* fix: wrap debug_trace test with #ifdef QJS_ENABLE_DEBUGGER Agent-Logs-Url: https://github.com/G-Yong/quickjs/sessions/c47f60f2-9eab-4e86-a75e-3755d5e81466 Co-authored-by: G-Yong <21030893+G-Yong@users.noreply.github.com> * chore: remove accidentally committed build-debug directory Agent-Logs-Url: https://github.com/G-Yong/quickjs/sessions/c47f60f2-9eab-4e86-a75e-3755d5e81466 Co-authored-by: G-Yong <21030893+G-Yong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: G-Yong <21030893+G-Yong@users.noreply.github.com>
Yes, exactly. :-) The reason I suggested it is because it lets the debugger be a runtime option instead of a compile-time option. |
…nstead of a compile-time option
|
Thanks @bnoordhuis, I now understand the intent! I've refactored the implementation to make the debugger a runtime option instead of a compile-time option: Key changes:
This means a single QuickJS binary can serve both as a normal engine and a debug-capable engine, controlled entirely at runtime by whether |
|
@dblnz A note about the The earlier approach (from my fork's restart:
for(;;) {
#ifdef QJS_ENABLE_DEBUGGER
if (b && ctx->bytecode_trace != NULL) {
// callback here...
}
#endif
SWITCH(pc) {
CASE(OP_push_i32): ...This had a fundamental problem with #if defined(EMSCRIPTEN) || defined(_MSC_VER) || defined(QJS_ENABLE_DEBUGGER)
#define DIRECT_DISPATCH 0
The current This is why this PR's approach is superior to what I originally proposed in #119. |
This sounds great, thanks! |
|
@G-Yong looks like there are some conflicts with master now, could you please resolve them? 🙏 |
|
Asked Claude for a review: At a quick glance, some of these are legit. |
Apply fixes for the issues raised in the latest adversarial review on the "Add Debugging Interface" PR (quickjs-ng#1421): * OP_debug callback (JS_CallInternal): - Use JS_AtomToCString instead of a 64-byte stack buffer so filename and funcname are passed to the trace handler at full length. - When the callback returns non-zero without raising an exception, throw a default InternalError("aborted by debugger") so JS_GetException() never returns the JS_UNINITIALIZED sentinel. - When the callback calls JS_Throw* but returns 0, also unwind to the exception handler so a pending exception is never silently carried over into the next opcode / next eval. * JS_GetLocalVariablesAtLevel: - Skip compiler-generated names starting with '<' (e.g. <ret>, <this_active_func>) so they no longer appear in debugger listings. - Substitute JS_UNINITIALIZED slots (let/const TDZ) with JS_UNDEFINED so the internal sentinel never escapes to C callers. - On JS_AtomToCString OOM, free what was allocated, clear the pending exception, set *pcount = 0 and return NULL instead of leaving a half-built array and a leaked exception state. - Forward-declare JS_AtomGetStr so the helper macro can use it (it is defined later in the file). * emit_source_loc / emit_source_loc_debug: - Stop appending OP_debug from inside emit_source_loc; that function is invoked from expression parsers and was emitting multiple OP_debug per statement. OP_debug is now confined to emit_source_loc_debug, which is only called at statement boundaries. - Move the emit_source_loc_debug for TOK_LET / TOK_CONST out of the 'haslet:' label so the let/const fall-through into TOK_VAR no longer fires the trace twice. - Add emit_source_loc_debug at the top of throw and default expression statements where it was missing. - Drop dead emit_source_loc_debug calls after emit_break, before label_finally and before the final OP_ret of a try/catch/finally. * find_jump_target / code_has_label: - Stop skipping OP_debug while resolving jump targets / scanning for labels. OP_debug is an executable opcode with a side effect (the user callback) and must not be jumped over by the optimizer. * next_token: replace a stray hard-tab indent with spaces. quickjs.h: rewrite the JSDebugTraceFunc / JS_SetDebugTraceHandler doc comments to match the implementation contract: * return non-zero OR raising via JS_Throw* both abort execution; the engine synthesizes a default exception when needed; * the filename/funcname pointers are only valid for the duration of the callback invocation; * OP_debug is only emitted for code parsed AFTER JS_SetDebugTraceHandler has been installed, so the handler must be set before evaluating any application code.
|
Thanks for the thorough review @saghul (and Claude)! I've pushed 1011587 on top of the branch addressing the issues raised. Summary: OP_debug callback (JS_CallInternal)
JS_GetLocalVariablesAtLevel
Duplicate / stray OP_debug emission
Optimizer
Misc
quickjs.h
I have not yet tackled the broader "handler attached after parse-time is silently a no-op" point as a behavior change — that one is now explicitly documented in the header. Happy to also implement option (a) (always emit OP_debug at statement boundaries, gate the dispatch at runtime) if you'd prefer that over the documented contract. |
|
Before changing the design I ran the web-tooling-benchmark v0.5.3 suite to compare both options. Setup: Windows / MSVC 2019 Release / x64, upstream c707cf5, three back-to-back runs each, geomean of runs/s. Option (a) — always emit OP_source_loc + OP_debug, gate at runtime in OP_debug case
Current PR design — parse-time gate (emit_source_loc_debug checks ctx->debug_trace once, at parse time)
|
… warning (#10) Agent-Logs-Url: https://github.com/G-Yong/quickjs/sessions/d47f1173-4bb5-49cb-895c-0ee2e898fddc Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: G-Yong <21030893+G-Yong@users.noreply.github.com>
…Handler) JSDebugTraceFunc now receives a `void *opaque` parameter and JS_SetDebugTraceHandler accepts an additional `void *opaque` argument that is stored in the context and forwarded to every callback invocation. This mirrors the design of JS_SetInterruptHandler and allows callers to pass arbitrary user data without needing global state.
|
Can you rebase one last time please? |
|
I’ve used the ‘Sync fork’ option on GitHub. It seems to be just a merge operation. You could ‘merge --squash’ my pull request, as there are too many changes to make a rebase feasible. 😂 |
|
👍 can you take another look to see if more changes are necessary after explicit context management landed? |
For the time being, we have made all the changes we could think of and had them reviewed by the AI. To the best of our ability and experience, the revisions are now largely complete. |
| 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); |
There was a problem hiding this comment.
[NOTE] this change looks unrelated to debugging, is it fixing a problem with column numbers that exists today?
There was a problem hiding this comment.
You’re right; this doesn’t fix a debug-related issue, but rather an exception error. If you don’t include this line, when running the following code:
function myFunc(){
var a = 0;
var b = 1 + 2 + 3 + 4 + 5 + 6 + 12abc;
}
myFunc()the exception message thrown is: "SyntaxError: invalid number literal", indicating that the exception occurred at line:col=3:9
but in reality it is 3:35.
There was a problem hiding this comment.
could you please split it into a separate commit?
There was a problem hiding this comment.
This bug was discovered and fixed by my colleague @uxxca1;
I’ll ask him to create another pull request later.
api-test.c: - Align debug_trace_cb argument indentation with the opening parenthesis. - Update JS_GetLocalVariablesAtLevel call sites to the new signature (returns int, takes JSDebugLocalVar **pvars out parameter). - Add an out-of-callback test: calling JS_GetLocalVariablesAtLevel when no frame is active must succeed and set pvars=NULL, count=0. - Drop comments that merely restate what the code already says. quickjs-opcode.h: - Drop trailing inline comment on DEF(debug, ...). quickjs.c: - Move the JS_AtomGetStr forward declaration to the main forward- declaration block instead of placing it next to its first use. - Remove comments above js_get_stack_frame_at_level and JS_GetStackDepth that add no information beyond the function names. - Change JS_GetLocalVariablesAtLevel to return int (0 = ok, -1 = exception) and add a JSDebugLocalVar **pvars out parameter so callers can distinguish "no variables / no active frame" (returns 0, NULL) from a real OOM error (returns -1, exception pending). The OOM path no longer clears the pending exception so the caller can inspect it. - Pass true/false instead of 1/0 for the is_arg argument of APPEND_VAR. - Free the vars allocation when all entries were filtered out (idx==0) so the array is never returned with a zero count. - Extract emit_debug() from emit_source_loc_debug(). At the throw statement and the default expression-statement path, where emit_source_loc() is already called unconditionally, use emit_source_loc() + emit_debug() instead of emit_source_loc_debug() to avoid emitting a duplicate OP_source_loc opcode. quickjs.h: - Change JSDebugLocalVar::is_arg from int to bool. - Remove field comments that just restate the field names. - Update JS_GetLocalVariablesAtLevel declaration and its doc comment to match the new int-return / out-parameter contract.
Add a debugging interface to the existing architecture.

Using the added debugging interface, we implemented debugging for QuickJS in VSCode. The project address is:[QuickJS-Debugger]