Using Debugger Statements in the Otto Javascript Interpreter
Apr 29, 2016
4 minutes read

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.

  1. tab-complete symbols in scope
  2. better formatting
  3. customisable input/output streams (otto REPL over TCP, anyone?)

Hopefully I’ll be able to put those features together sometime soon.


Back to posts