While you're reading this, keep in mind that I'm available for hire stupid!
Otto is a JavaScript interpreter
written in Go. Recently, along with Steven
Hartland, I volunteered to help maintain it. One
of the features I’ve added is the ability to use debugger
statements in the
interpreter. I don’t think this feature is very well known, but it’s
incredibly handy, so I’d like to tell you about it.
There are really two parts to this functionality. The first is the ability to
hook into the interpreter and run some code when it encounters a debugger
statement. The second is an interactive REPL for running code in the context
of the interpreter at that time.
Otto.SetDebuggerHandler
This is the first part of the feature. Each Otto instance has a method called
SetDebuggerHandler
, which takes a func(*otto.Otto)
parameter. When the
interpreter hits a debugger
statement, this function will be called.
Here’s an example of how to use it:
package main
import (
"fmt"
"sort"
"github.com/robertkrimen/otto"
)
func main() {
// Create a new otto interpreter instance.
vm := otto.New()
// This is where the magic happens!
vm.SetDebuggerHandler(func(o *otto.Otto) {
// The `Context` function is another hidden gem - I'll talk about that in
// another post.
c := o.Context()
// Here, we go through all the symbols in scope, adding their names to a
// list.
var a []string
for k := range c.Symbols {
a = append(a, k)
}
sort.Strings(a)
// Print out the symbols in scope.
fmt.Printf("symbols in scope: %v\n", a)
})
// Here's our script - very simple.
s := `
var a = 1;
var b = 2;
debugger;
`
// When we run this, we should see all the symbols printed out (including
// `a` and `b`).
if _, err := vm.Run(s); err != nil {
panic(err)
}
}
So those are the basics. Now you know how to trigger a function to run when
the interpreter hits a debugger
statement. But on its own, that’s not
incredibly useful. Enter the REPL.
otto/repl
The otto/repl package
is another feature I added recently. At the same time as SetDebuggerHandler
in fact. It’s a very simple REPL that you can attach to an existing
*otto.Otto
and use to interactively evaluate statements.
The repl package itself exposes a selection of methods, mostly for customising
the prompt, and the text that is printed when the REPL is initially opened.
Additionally, however, it provides a function which matches the
func(*otto.Otto)
signature of the SetDebuggerHandler
function. This means
that you can use it as, you guessed it, a debugger
handler.
Here’s an example of using the REPL on its own.
package main
import (
"github.com/robertkrimen/otto"
"github.com/robertkrimen/otto/repl"
)
func main() {
// Create a new otto interpreter instance.
vm := otto.New()
// Here's our script - very simple.
s := `
var a = 1;
var b = 2;
`
// This will run our script above, setting `a` to `1`, and `b` to `2`.
if _, err := vm.Run(s); err != nil {
panic(err)
}
// This will enter the REPL. It will block until we hit ctrl+c, or until we
// manage to make the interpreter really, really unhappy.
if err := repl.Run(vm); err != nil {
panic(err)
}
}
Very easy stuff, and already really fun!
Otto.SetDebuggerHandler + otto/repl
I think you can see where this is going. If you looked at the godoc page for
the otto/repl
package, you’ll already know there’s a function called
DebuggerHandler
. I’ll give you zero guesses as to how it’s intended to be
used. Let’s take a look.
package main
import (
"github.com/robertkrimen/otto"
"github.com/robertkrimen/otto/repl"
)
func main() {
// Create a new otto interpreter instance.
vm := otto.New()
// This attaches a debugger handler that pops open a REPL on the console
// when the interpreter hits a `debugger` statement.
vm.SetDebuggerHandler(repl.DebuggerHandler)
// Here's our script - very simple.
s := `
var a = 1;
var b = 2;
debugger;
`
// This will run our script above, setting `a` to `1`, then `b` to `2`, then
// triggering the debugger.
if _, err := vm.Run(s); err != nil {
panic(err)
}
}
So easy. So, so easy. But so useful.
Now, you wouldn’t want to have this kind of thing enabled in a production
system. The fact of the matter is that you can’t (or, well, you shouldn’t)
have multiple REPLs on the one console. It just won’t end well. It’s great for
debugging though. In one project, I have a command line flag
--enable_debugger
, and it’s this easy to enable the debugger repl:
enableDebugger := flag.Bool("enable_debugger", false, "Enable otto debugger.")
// ...
if enableDebugger {
vm.SetDebuggerHandler(repl.DebuggerHandler)
}
What’s next?
The current REPL has a long way to go before it’s as useful as, for example, the node REPL. It’ll probably never be quite that fully-featured, but there are some things I’d really like to add.
- tab-complete symbols in scope
- better formatting
- customisable input/output streams (otto REPL over TCP, anyone?)
Hopefully I’ll be able to put those features together sometime soon.