A Cooperative REPL Server for Guile 2.0.10

January 24, 2014

The next release of GNU Guile, 2.0.10, is to be released “real soon now”. My contribution to this release is the new (system repl coop-server) module. This module introduces a useful variant of the REPL server that I’ve named the “cooperative” REPL server. It is cooperative because it can be integrated with single-threaded programs without the thread synchronization issues present in the ordinary REPL server.

Using delimited continuations and mvars (another new feature coming in Guile 2.0.10), it was possible to create a REPL server whose clients all ran in the context of a single thread. The cooperative server puts the user in control of when it is safe to evaluate expressions entered at the REPL prompt.

Here’s an example of how to use it:

(use-modules (system repl coop-server))

(define server (spawn-coop-repl-server))

(while #t
  (poll-coop-repl-server server)
  (sleep 1))

That’s all it takes. There are only 2 public procedures! spawn-coop-repl-server creates a new cooperative REPL server object and starts listening for clients. poll-coop-repl-server applies pending operations for the server.

Now that I’ve explained the API, onto the implementation details! Although the REPLs run in the context of a single thread, there are other threads needed to make it all work. To avoid thinking too much about thread synchronization issues, I used the new (ice-9 mvars) module to provide thread-safe objects for read/write operations. I used delimited continuations in order to yield control back to the user from inside the loop.

When the server is spawned, a new thread is created to listen for client connections. When a client connects, a request to create a new REPL is put into the server’s "evaluation" mvar, the storage place for pending server operations. The job of poll-coop-repl-server is to attempt to take from the evaluation mvar and execute the operation stored within, if any. In this case, an operation called new-repl is hanging out along with the client socket. The contents of the mvar are removed, and a new REPL is started in the main thread.

This new REPL is run within a "prompt", Guile’s implementation of delimited continuations. The prompt allows the REPL to be paused temporarily while waiting for user input. Just before the prompt is aborted, a thunk containing the logic to read user input is stored within the client’s "read" mvar for yet another thread to use. Without this additional thread and the use of a prompt, the main thread would block while waiting for input, defeating the purpose of the cooperative REPL. The reader thread waits for this thunk to appear in the read mvar. When it appears, the thunk is applied and the resulting expression is stored as an eval operation within the evaluation mvar. When poll-coop-repl-server is called, the REPL prompt is resumed. The input expression is evaluated in the context of the main thread, the result is printed, and the process repeats itself.

That’s all, folks. Thanks for following along. I’m very excited to use the cooperative REPL server in guile-2d and I hope that others find it useful as well. Many thanks to Mark Weaver for helping me out when I got stuck and for all of the helpful code review.