-
Notifications
You must be signed in to change notification settings - Fork 383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Goja debugger #294
Comments
This looks really cool! The first two questions that came to mind for me was whether the debugger would support source maps (and potentially from non-JS sources, e.g. TS), and whether it would be fully controllable programmatically via an API. For the latter I'm thinking of cases where goja is embedded in a larger application where debugging the running program via a CLI application might not be possible. |
Yesterday I experimented a bit with
I also thought about having a websocket server to control the debugger remotely, but I think it's a bit too early, but is still a topic for discussion. |
The changes are on the debugger branch, which contains:
Caveats:
|
I might be misinterpreting things, but if |
@nwidger Apparently an old version of the sourcemap of my script was causing trouble. I updated it with the latest source code and now it works as expected. Update: |
For reference:
|
Current supported commands:
|
@mostafa I just played around with the demo in the I do wonder if in the long run it might not be a bad idea for func (r *Runtime) EnableDebugMode() *Debugger {}
type Debugger struct{}
func (d *Debugger) Wait() *Break {}
func (d *Debugger) SetBreakpoint(file string, line int) error {}
func (d *Debugger) ClearBreakpoint(file string, line int) error {}
func (d *Debugger) Breakpoints() ([]Breakpoint, error) {}
func (d *Debugger) Next() error {}
func (d *Debugger) Continue() error {}
func (d *Debugger) StepIn() error {}
func (d *Debugger) StepOut() error {}
func (d *Debugger) Quit() error {}
func (d *Debugger) Eval(expr string) (Value, error) {}
func (d *Debugger) Print(expr string) (string, error) {}
type Break struct{}
func (b *Break) Filename() string {}
func (b *Break) Line() int {}
func (b *Break) Source() string {}
func (b *Break) LocalVariables() ([]LocalVariable, error) {}
func (b *Break) CallStack() ([]StackFrame, error) {} with a CLI debugger using the API maybe looking something like: vm := goja.New()
d := vm.EnableDebugMode()
go func() {
var prev *Break
for b := d.Wait(); b != nil; b, prev = d.Wait(), b {
if b != prev {
fmt.Printf("Break at %s:%d", b.Filename(), b.Line())
}
fmt.Println("> ")
cmd := parseCmdFromStdin()
switch cmd.Name {
case "cont", "c":
d.Continue()
case "next", "n":
d.Next()
case "list", "l":
fmt.Println(b.Source())
}
}
}()
_, err := vm.RunScript(filename, string(content))
if err != nil {
log.Fatal(err)
} The advantage I see to getting a public interface like this into main goja repo would be to allow a CLI frontend, a DAP frontend, a GUI frontend, a frontend driven by some sort of REST API, etc. to all be developed separately. The development of these frontends could occur outside of the main goja repo, allowing fast development without needing to bring the goja maintainer into the picture with PR's that ask him to sign up for the potential maintenance of the frontend code for years to come. An interface like this might also be more useful for people embedding goja into a larger application where debugging the running JavaScript via a CLI frontend that expects to get commands from stdin might prove difficult to use. Anyways, these are all Saturday morning post-too-much-coffee thoughts, and I realize you probably have your own goals and priorities with this work so please feel free to ignore me if they don't line up with your own. :D |
Thanks for the inspiration. I actually like your idea and the fact that I refactor the debugger with the command pattern yesterday was a step forward for implementing the API you just mentioned, to have a good separation of concerns. For the backtrace, I'm actually investigating it and I drafted some code that doesn't work as I want right now, but eventually it might. 😄 |
I implemented and exposed a new API (mostafa@f15bde1) you proposed, yet it needs more changes to make it work, that is the new event-loop, possibly based on Go channels. I also reverted the changes to the compile API (mostafa@0ebe3ef and mostafa@83f1d32), so the changes to goja_nodejs is no longer relevant and the original project can be used. |
@mostafa Nice ! It looks like you're close to being able to rip the CLI frontend code out into a separate repo, if that's what you're eventually shooting for. I think removing the changes to the compile API was a good idea. If you don't want debugger statements to do anything, don't call EnableDebugMode on the runtime, and a breaking change on the Compile methods was probably a no-no anyways. I left a couple comments on a few of the Debugger methods, please let me know if what I said didn't make sense. |
@nwidger Thanks! That's actually what I am trying to achieve. Also thanks for the comments, they make sense now that I am separating CLI frontend from Goja. |
Current progress
WIPI extracted REPL and some commands ( |
The latest code in my fork of Goja includes an API that goja_debugger taps into to control execution using an stand-alone Debugger in Goja. So, any frontend (CLI, DAP, etc.) is now able to control the stand-alone Debugger in Goja. The example code for a CLI debugger (REPL) is the goja_debugger project. Feel free to test it and report any issues. Your feedback is highly appreciated. |
It's been really fun watching you and @mstoykov poking away at this. It looks great, I just noticed a few small things that I wasn't sure about:
I'll let you know if I think of anything else. Nice work! |
@mostafa No problem! I just noticed one minor issue, if you don't include the
|
@nwidger |
@mostafa I think on-the-fly sourcemap generation might be possible with esbuild's Transform API: package main
import (
"fmt"
"github.com/evanw/esbuild/pkg/api"
)
func main() {
result := api.Transform("var x = 1", api.TransformOptions{
Sourcemap: api.SourceMapInline,
})
if len(result.Errors) > 0 {
fmt.Println(result.Errors)
}
fmt.Printf("%s", result.Code)
} Running this prints:
Here're some doc links that might prove useful: https://esbuild.github.io/api/#transform-api |
@nwidger |
@mstoykov added dynamic variable resolution, which results in local scoped variables to be visible to I added |
A small demo to brighten up your day on top of the POC by @mstoykov: |
Are there things still missing before it gets merged? |
@faisalraja Like every other piece of code, it isn't perfect. There are some issues that need to be addressed, plus it should conform to TC39 if there are any for debugging (look at Goja debugger roadmap). The biggest challenge we had was the correct mapping of the program counter (PC) to the exact line of code. It seemed to have been fixed using sourcemaps, but some tests proved otherwise. For example, the PC is reset inside a function, so one should also account for that. The main functionality heavily relies on this seemingly simple challenge. If one can fix this, the rest of the functionality, like step-out and a few things, can be easily fixed and possibly merged. |
Is this exposed in k6 ? I raised this which is support for js and wasm in k6. grafana/k6#2562 |
@gedw99 Please have a look at this POC: |
This seems stale, so let's close it until we have a consensus and time to do it. |
Hi,
I just wanted to inform the community that I am working on a POC of a debugger for Goja and I made some progress. While talking with @mstoykov from the k6 team, he suggested that I inform the community and especially @dop251, in case there are any suggestions, recommendations, or early feedback. I'll make a PR soon, but in the meantime, this is a preview:
These are what I worked on so far:
debugger
statement implementation that just incrementsvm.pc
and enablesdebugMode
ifruntime.EnableDebugMode()
function is called in advance to enable compiling and emittingdebugger
commands. It involved modifications to compiler, runtime and vm structs and might be backward-incompatible.next
,continue
andrun
are implemented. There are some bugs (and caveats) to be fixed before the initial PR. The REPL has abbreviated commands (n
,c
andr
) and remembers the last commands that was used, so that the next Enter would end up executing the last command. Thenext
command currently acts as a step command somehow until I figured out how to jump lines. Thecontinue
command work as expected, continuing execution until the nextdebugger
command. The REPL mimics the behavior ofnode inspect
.run
command in REPL evaluates and executes any function or variable up to the point of the current stack and program counter in the local context. The process starts by parsing the line, compiling it and then inserting the compiled instructions after the current program counter at thevm.prg.code
and then executing the line. Before any insertions, the context and the state is saved, and after the execution, both will be restored.I'd be happy to hear your feedback, although I haven't made a PR yet.
UPDATE:
This is a terminal recording from the latest changes and updates on 1 June 2021: https://asciinema.org/a/423306
Disclaimer: I work at @k6io, but this is voluntary work I do in my free time.
The text was updated successfully, but these errors were encountered: