Time travel debugging in Spritely Goblins, previewed through Terminal Phase

By Christopher Lemmer Webber on Thu 23 January 2020

Time travel in Spritely Goblins shown through Terminal Phase

Okay, by now pretty much everyone is probably sick of hearing about Terminal Phase. Terminal Phase this, and Terminal Phase that! Weren't you getting back to other hacking on Spritely Goblins, Chris? And in fact I am, I just decided it was a good idea to demo one of the things that makes Goblins interesting.

What you're seeing above is from the experimental tt-debugger branch of Terminal Phase (not committed yet because it's a proof-of-concept, and not as clean as I'd like it to be, and also you need the "dev" branch of Goblins currently). When the user presses the "t" key, they are presented with a menu by which they can travel backwards and forwards in time. The player can select a previous state of the game from every two seconds and switch to that.

Here's the cool part: I didn't change a single line of game code to make this occur. I just added some code around the game loop that snapshotted the state as it currently existed and exposed it to the programmer.

What kind of time sorcery is this?

Dr. Who/Dr. Sussman fez comparison

Well, we're less the time-lord kind, more the functional programmer kind. Except, quasi-functional.

If you watched the part of the recent Terminal Phase video I made that shows off Goblins you'll remember that the way that objects work is that a reference to a Goblins object/actor is actually a reference that indirectly refers to a procedure for handling immediate calls and asynchronous messages. Relative to themselves (and in true actor fashion), objects specify first their initial version of themselves, and later can use a special "become" capability to specify a future version of themselves. From the perspective of the actor, this looks very functional. But from the perspective of one object/actor performing a call against another object/actor, it appears that things change.

Here is the simplest example of such an object, a cell that holds a single value:

;; Constructor for a cell.  Takes an optional initial value, defaults
;; to false.
(define (^cell bcom [val #f])
  (case-lambda
    ;; Called with no arguments; return the current value
    [() val]
    ;; Called with one argument, we become a version of ourselves
    ;; with this new value
    [(new-val)
     (bcom (^cell bcom new-val))]))

If you can't read Racket/Scheme, not a big deal; I'll just tell you that this cell can be called with no arguments to get the current value, and with one argument to set a value. But you'll see that in the former case, the value we would like to return to the caller is returned; in the latter case, we return the handler we would like to be for handling future messages (wrapped up in that bcom capability). In both cases, we aren't performing side effects, just returning something.. but in the latter case the kernel observes this and updates the current transaction's delta reflecting that this is the "new us". (Not shown here but supported: both becoming a new handler and returning a value.)

Without going into details, this makes it extremely easy to accomplish several things in Goblins:

  • Transactionality: Each "turn" of an event loop in Goblins is transactional. Rather than being applied immediately, a transaction is returned. Whether we choose to commit this or not is up to us; we will probably not, for instance, if an exception occurs, but we can record the exception (a default event loop is provided that does the default right-thing for you).
  • Snapshotting time: We can, as shown above, snapshot history and actually run code against previous state (assuming, again, that state is updated through the usual Goblins actor "become" means).
  • Time-travel debugging: Yeah, not just for Elm! I haven't built a nice interface for it in the demo above, but it's absolutely possible to expose a REPL at each snapshot in time in the game to "play around with" what's happening to debug difficult problems.

This is only a small portion of what makes Spritely Goblins interesting. The really cool stuff will come soon in the distributed programming stuff. But I realized that this is one of the more obviously cool aspects of Spritely Goblins, and before I start showing off a bunch of other interesting new things, I should show off a cool feature that exists in the code we already have!

Anyway, that's it... I hope I gave you a good sense that I'm up to interesting things. If you're excited by this stuff and you aren't already, consider donating to keep this work advancing.

Whew! I guess it's time I start writing some docs for Goblins, eh?