As a hobby I've been considering writing a debugger for go. Since talk is cheap, I'll lay out here some thoughts behind writing a debugger for Go.
There are two extremes one places in the overall build process that can hook into and each has its own benefits and disadvantages.
At one extreme, one can run "go build" (possibly with some additional options) and debug the final code using whatever symbol-table or debugger information that is available in the executable. This has the advantage of having the program act most like what gets run when the debugger is not around. From the programmer's standpoint, this is also the most desirable as it reduces the chance of the debugger causing a Heisenbug.
But depending on the level of debugger expressive power provided and compiler optimization used in generating the program, this can be the hardest to do. Source code can get transformed all sorts of ways that the casual go programmer may not be aware of. As a result, a Go expression might map to different non-contiguous regions of the generated code, or might not appear in the code at all. And on the other hand, a single instruction might map to fragments of several different distinct locations in the source code or no identifiable place in the code. The value of a variable might be ether in storage or in a register or might not exist explicitly but instead might be derivable from other values. Or maybe it's not available at all.
Although this sounds dire, in practice I've debugged optimized C code using gdb and it's generally not too bad.
Before leaving this extreme, I'd like to ask those who have used the go Windows debugger what it's capabilities are. I assume one can set breakpoints at specific lines or functions in the program? And step statements? How about the ability to evaluate expressions, look at or modify variables? Can one get a call stack trace with the names of parameters and values? Or see view the state of all goroutines? Does going into the debugger pause goroutines?
At the other extreme, a debugger can hook into AST and provide an interpreter for that. The AST most closely resembles the source code.
In practice I think having debuggers at both ends is ideal. But let me start with at the AST side, since for me that's more fun.
Having written debuggers for many scripting languages -- POSIX Shell (bash, ksh, zsh), Python, Perl, and Ruby -- the way all of these types of debuggers work is that at various "events" in the course of executing the debugged program, a routine is called. That routine handles debugging, profiling, execution coverage, or tracing. And rather than have some fixed code, what is typically done is to provide a function to register a callback function.
The most common "events" are before
- starting a user-specified breakpoint location
- starting to execute a new statement
- entering a function,
- leaving a function,
- accepting a message,
- a fatal exception,
One could also add things like before evaluation of the expression part of an if, a loop test, and so on.
As others have mentioned, right now there is a pretty usable interpreter in go.tools. Although that works off of SSA rather than AST, to start off with that's probably okay for a debugger. But it would be useful to turn off the lifting code, and use the "Naive SSA form" which is not on by default. And right now the "emit" part of the SSA builder lacks these kinds of marks and callbacks, but that is easily added.
So one trepidation I have here is that the stated goal of the interpreter seems to be a more high-performance interpreter which for reasons I've stated is might be a hindrance in a debugger. In fact, one of the problems I've had in trying to get better debugger run-time support into programming languages is the necessary concern for degrading performance. My own take when this occurs is to provide two run-time environments, possibly with a way to switch back and forth.
The rubinius implementation of Ruby is kind of interesting in this regard. It does optimization by JIT'ing. So it always keeps a simple, stupid but close-to-high-level representation of the program which uses whenever it detects that a debugger is used.
And this last section, let me close with some aspects that Go currently lacks that would make it easier to write a debugger but might also be of use in go otherwise.
The first thing is adding an "eval()" function. This for example allows the debugger to simply see or set values of variables and expressions.
In an interpreter such as the one using SSA, I think this is pretty straightforward to add.
Since Go is normally strongly typed, I guess one should provide a strongly-typed version in addition to one that returns a generic interface{} or unsafe Pointer that one has to reflect on or use a type cast. An alternative to an eval which returns interface{} or an unsafe Pointer, the eval() could just assume whatever needs to be done is handled inside the string it is passed. That is, if you want to set a variable then do that inside the eval.
For the purpose of a debugger, it is possible to work up other mechanisms to set and see variables. In a debugger towards the gdb-spectrum, such a debugger will probably still have to do this.
But for an AST-like interpreter, I think eval() will add lots of power with very little mechanism. And one might imagine a gdb-like debugger somehow using it's more cumbersome variable access mechanism hooked into an embedded AST/SSA-like interpreter.
- - - -
The last aspect I want to bring up is dealing with imports in an AST interpreter and mixed-mode interpreter, compiled go/C code interaction.
An AST-style Go interpreter of the kind I've been describing *can* easily provide the ability to dynamically import code. To do so it would pulling in the source and compiling it on the fly.
I believe there's also a way to indicate in a limited way using the compiled versions of some of the imported code that is known to the interpreter, perhaps because it needs to import it too. So here one can give up I guess the ability to debug such compiled code in favor of code speed.
In a gdb-level debugger, dynamic imports become more important to be able to run eval code.